refactor: extract 12 leaf crates from windmill-api (#7899)

* feat(backend): extract 12 leaf crates from windmill-api to improve incremental compilation

Extract independent modules from windmill-api (90k LOC monolith) into
separate leaf crates to reduce incremental compilation times. Modules
extracted: assets, configs, debug, flow-conversations, inputs,
npm-proxy, openapi, schedule, settings, workers, agent-workers, and
alerting (from windmill-common).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add windmill-api-settings dep to root crate, make ee_oss public

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add agent_workers integration tests

8 tests covering agent worker lifecycle:
- Simple script execution (bun)
- Script with arguments
- Script with logs (verified in job_logs table)
- Script failure handling
- Complex result (nested objects/arrays)
- Agent token creation via API
- Token creation + Initial/MainLoop ping cycle
- Multiple sequential job execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-02-11 11:31:03 +01:00
committed by GitHub
parent 9e8d77436b
commit e26e437dd1
40 changed files with 1203 additions and 381 deletions
+1
View File
@@ -19,3 +19,4 @@ backend/target
frontend/node_modules
typescript-client/node_modules
frontend/.svelte-kit
backend/chrome_profiler.json
@@ -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"
}
@@ -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"
}
+5 -1
View File
@@ -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
+230
View File
@@ -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"
+30 -4
View File
@@ -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" }
+1 -1
View File
@@ -1 +1 @@
0eaf364f5421db5710318eb36c062114ab6ca081
b2bd490cd14e7794bebbd8737c2833ea671370b3
+269
View File
@@ -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<Postgres>) -> 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<Postgres>) -> 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<Postgres>) -> 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<Postgres>) -> 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<Postgres>) -> 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<Postgres>) -> 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<Postgres>) -> 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<Postgres>) -> 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(())
}
+1 -1
View File
@@ -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()],
+18
View File
@@ -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
+208
View File
@@ -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<chrono::Utc>,
acknowledged: Option<bool>,
workspace_id: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct AlertQueryParams {
pub page: Option<i32>,
pub page_size: Option<i32>,
pub acknowledged: Option<bool>,
}
pub async fn get_critical_alerts(
db: DB,
params: AlertQueryParams,
workspace_id: Option<String>,
) -> JsonResult<serde_json::Value> {
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<String>,
id: i32,
) -> error::Result<String> {
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<String>,
) -> error::Result<String> {
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())
}
@@ -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 }
@@ -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;
+18
View File
@@ -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
@@ -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()
+29
View File
@@ -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
@@ -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()
+28
View File
@@ -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
@@ -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;
+2 -7
View File
@@ -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<RwLock<Option<EmbeddingsDb>>> = Arc::new(RwLock::new(None));
pub static ref MODEL_INSTANCE: Arc<RwLock<Option<Arc<ModelInstance>>>> = 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::<u64>().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")]
@@ -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
@@ -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},
+19
View File
@@ -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
@@ -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},
+21
View File
@@ -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
@@ -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 {
+26
View File
@@ -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
@@ -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! {
+29
View File
@@ -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
@@ -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 {
+38
View File
@@ -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 }
@@ -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"))
}
@@ -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<String> {
pub async fn get_critical_alerts(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Query(params): Query<crate::utils::AlertQueryParams>,
Query(params): Query<windmill_alerting::AlertQueryParams>,
) -> JsonResult<serde_json::Value> {
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<i32>,
) -> error::Result<String> {
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<String> {
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"))]
+19
View File
@@ -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
@@ -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()
+20 -8
View File
@@ -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
+22 -44
View File
@@ -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);
+6 -265
View File
@@ -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<chrono::Utc>,
acknowledged: Option<bool>,
workspace_id: Option<String>,
}
#[cfg(feature = "enterprise")]
#[derive(Deserialize, Debug)]
pub struct AlertQueryParams {
pub page: Option<i32>,
pub page_size: Option<i32>,
pub acknowledged: Option<bool>,
}
#[cfg(feature = "enterprise")]
pub async fn get_critical_alerts(
db: DB,
params: AlertQueryParams,
workspace_id: Option<String>,
) -> JsonResult<serde_json::Value> {
// 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<String>,
id: i32,
) -> error::Result<String> {
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<String>,
) -> error::Result<String> {
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;
+37
View File
@@ -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,
+6
View File
@@ -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..]