Compare commits
1 Commits
v1.630.1
...
plan-2-scr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78bc6b498c |
6
.github/workflows/claude.yml
vendored
6
.github/workflows/claude.yml
vendored
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: install xmlsec1 and gssapi
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-dev libxmlsec1-dev libkrb5-dev libsasl2-dev libcurl4-openssl-dev mold clang
|
||||
sudo apt-get install -y libxml2-dev libxmlsec1-dev libkrb5-dev libsasl2-dev libcurl4-openssl-dev
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
working-directory: ./backend
|
||||
timeout-minutes: 16
|
||||
run: |
|
||||
SQLX_OFFLINE=true cargo check --features all_sqlx_features
|
||||
SQLX_OFFLINE=true cargo check --features $(./all_features_oss.sh)
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@v1
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
- Fix all warnings and errors before proceeding
|
||||
|
||||
**Backend Changes:**
|
||||
- Run: \`cargo check --features all_sqlx_features\` in the backend directory
|
||||
- Run: \`cargo check --features $(./all_features_oss.sh)\` in the backend directory
|
||||
- Fix all warnings and errors before proceeding
|
||||
|
||||
**Pull Request Creation:**
|
||||
|
||||
10
.github/workflows/git-commands.yaml
vendored
10
.github/workflows/git-commands.yaml
vendored
@@ -62,17 +62,17 @@ jobs:
|
||||
path: windmill-ee-private
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
|
||||
# Setup Rust toolchain
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
# Cache rust dependencies
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.90.0
|
||||
workspaces: "./backend -> target"
|
||||
|
||||
- name: Install xmlsec and gssapi build-time deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
pkg-config libxml2-dev libssl-dev libkrb5-dev libsasl2-dev libcurl4-openssl-dev mold clang \
|
||||
pkg-config libxml2-dev libssl-dev libkrb5-dev \
|
||||
xmlsec1 libxmlsec1-dev libxmlsec1-openssl
|
||||
|
||||
- name: Run update-sqlx script
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,30 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.630.1](https://github.com/windmill-labs/windmill/compare/v1.630.0...v1.630.1) (2026-02-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enforce self-approval check on flow resume owner endpoint ([#7886](https://github.com/windmill-labs/windmill/issues/7886)) ([7147dde](https://github.com/windmill-labs/windmill/commit/7147dde5118d7b3a179e4b310c74b148838b5afe))
|
||||
|
||||
## [1.630.0](https://github.com/windmill-labs/windmill/compare/v1.629.1...v1.630.0) (2026-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add workspace search and runnable details tools to AI chat modes ([#7874](https://github.com/windmill-labs/windmill/issues/7874)) ([a7e269f](https://github.com/windmill-labs/windmill/commit/a7e269f9f3c82db0d7e6a70e174ac19d3df730d2))
|
||||
* **aiagent:** add prompt caching for Anthropic models ([#7878](https://github.com/windmill-labs/windmill/issues/7878)) ([6272cd1](https://github.com/windmill-labs/windmill/commit/6272cd17a4f1300e22e7f0ae27b1a57571deb203))
|
||||
* download encrypted usage ([#7804](https://github.com/windmill-labs/windmill/issues/7804)) ([8363ff1](https://github.com/windmill-labs/windmill/commit/8363ff1eeef06f284e6d165fbf2dfb190ead573d))
|
||||
* **mcp:** add endpoint tools for scripts, flows, apps, and jobs ([#7859](https://github.com/windmill-labs/windmill/issues/7859)) ([03eb16a](https://github.com/windmill-labs/windmill/commit/03eb16a7c6c3cd9411840814940d09e22ce23305))
|
||||
* restriction rulesets for workspaces ([#7879](https://github.com/windmill-labs/windmill/issues/7879)) ([2851b6b](https://github.com/windmill-labs/windmill/commit/2851b6b7caac4a55f5202ace82aba68fd157c52a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** correct early return with stream + prevent delta miss ([#7872](https://github.com/windmill-labs/windmill/issues/7872)) ([1150eec](https://github.com/windmill-labs/windmill/commit/1150eec7571d5828d10b295cb61cca8edfbdffe0))
|
||||
* gate Permissions import behind #[cfg(unix)] for Windows build ([cf596f3](https://github.com/windmill-labs/windmill/commit/cf596f370ae7cc232ca63f4752d7727a74cd449b))
|
||||
* retry js eval up to 3 times on timeout from slow DB ([#7890](https://github.com/windmill-labs/windmill/issues/7890)) ([4c87e7a](https://github.com/windmill-labs/windmill/commit/4c87e7ac2e09ec83cfb998a1cebcb9b9c5ef8027))
|
||||
|
||||
## [1.629.1](https://github.com/windmill-labs/windmill/compare/v1.629.0...v1.629.1) (2026-02-10)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -67,7 +68,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -51,7 +52,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_result_stream_v2\n WHERE job_id NOT IN (SELECT id FROM v2_job_queue)\n AND job_id NOT IN (\n SELECT id FROM v2_job_completed\n WHERE completed_at > NOW() - INTERVAL '60 seconds'\n )\n RETURNING job_id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "454a611a5a162b2ace137c139bd5383bc7fe142c515dac3edd47991839485e51"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT j.id AS \"id!\", COALESCE(s.flow_status, s.workflow_as_code_status) as flow_status, q.suspend AS \"suspend!\", j.runnable_path as script_path, j.permissioned_as_email as email\n FROM v2_job_queue q JOIN v2_job j USING (id) LEFT JOIN v2_job_status s USING (id)\n WHERE j.id = $1\n ",
|
||||
"query": "\n SELECT j.id AS \"id!\", COALESCE(s.flow_status, s.workflow_as_code_status) as flow_status, q.suspend AS \"suspend!\", j.runnable_path as script_path\n FROM v2_job_queue q JOIN v2_job j USING (id) LEFT JOIN v2_job_status s USING (id)\n WHERE j.id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -22,11 +22,6 @@
|
||||
"ordinal": 3,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -38,9 +33,8 @@
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5c6e158aee5db3c4bf41a0aedfa5d6c73f5e04ebfd8703dc631bc49f2e797dd4"
|
||||
"hash": "485dc289a61a06595acae28d4968f3b6b2aeb6a8aee863dc999d6c8d58397814"
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n workspace_id,\n name,\n rules as \"rules: ProtectionRules\",\n bypass_groups,\n bypass_users\n FROM workspace_protection_rule\n WHERE workspace_id = $1\n ORDER BY name\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "rules: ProtectionRules",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "bypass_groups",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "bypass_users",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4cfb35e423a75ca2701f03d5a30a7c0778af5e548254f3e0f29004d7f2058eef"
|
||||
}
|
||||
@@ -37,7 +37,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -70,7 +71,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +245,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE workspace_protection_rule\n SET rules = $1, bypass_groups = $2, bypass_users = $3\n WHERE workspace_id = $4 AND name = $5\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "65b12bed9438900518b20dc268d71a2dba6ec66aee2971faef76b6ed56a05b6f"
|
||||
}
|
||||
@@ -35,7 +35,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH job_info AS (\n SELECT id, kind::text AS kind, parent_job\n FROM v2_job\n WHERE id = $1\n )\n SELECT\n q.id AS \"id!\",\n s.flow_status,\n q.suspend AS \"suspend!\",\n j.runnable_path AS script_path,\n j.permissioned_as_email AS email,\n (ji.kind IN ('flow', 'flowpreview')) AS \"is_flow_level!\"\n FROM job_info ji\n JOIN v2_job_queue q ON q.id = CASE\n WHEN ji.kind IN ('flow', 'flowpreview') THEN ji.id\n ELSE ji.parent_job\n END\n JOIN v2_job j ON j.id = q.id\n JOIN v2_job_status s ON s.id = q.id\n FOR UPDATE OF q\n ",
|
||||
"query": "\n WITH job_info AS (\n SELECT id, kind::text AS kind, parent_job\n FROM v2_job\n WHERE id = $1\n )\n SELECT\n q.id AS \"id!\",\n s.flow_status,\n q.suspend AS \"suspend!\",\n j.runnable_path AS script_path,\n (ji.kind IN ('flow', 'flowpreview')) AS \"is_flow_level!\"\n FROM job_info ji\n JOIN v2_job_queue q ON q.id = CASE\n WHEN ji.kind IN ('flow', 'flowpreview') THEN ji.id\n ELSE ji.parent_job\n END\n JOIN v2_job j ON j.id = q.id\n JOIN v2_job_status s ON s.id = q.id\n FOR UPDATE OF q\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,11 +25,6 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_flow_level!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -44,9 +39,8 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1a0ab65bbf2751f702fc696c1e32a7dd9524cdd806be1ad8e9ab88d4c88d3f82"
|
||||
"hash": "66e66da2ed6eace5d7ec2a41a7b11ae255f5dc212d1ff41c2905b303c8c13b18"
|
||||
}
|
||||
@@ -29,7 +29,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM workspace_protection_rule WHERE workspace_id = $1 AND name = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7ddc2c3f1ce74f9b7acdd51b94431386e7782c52251cf094e06ddb198a958b43"
|
||||
}
|
||||
@@ -27,7 +27,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
20
backend/.sqlx/query-a3e75f0309be42aca0fd74834f34b3f18dbb388bd8b9bc88b99aebedae9c3fec.json
generated
Normal file
20
backend/.sqlx/query-a3e75f0309be42aca0fd74834f34b3f18dbb388bd8b9bc88b99aebedae9c3fec.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_result_stream_v2 WHERE job_id NOT IN (SELECT id FROM v2_job_queue) RETURNING job_id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a3e75f0309be42aca0fd74834f34b3f18dbb388bd8b9bc88b99aebedae9c3fec"
|
||||
}
|
||||
@@ -185,7 +185,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return,\n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor,\n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled,\n flow.tag,\n flow.dedicated_worker,\n flow.on_behalf_of_email,\n flow.edited_by\n FROM\n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE\n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ",
|
||||
"query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return, \n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, \n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, \n flow.tag, \n flow.dedicated_worker, \n flow.on_behalf_of_email, \n flow.edited_by\n FROM \n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE \n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -62,5 +62,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "209dc4c1b91eeab1c12ffcd9f9e16f315c689ca772c736b333dcdf07c8086087"
|
||||
"hash": "a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM workspace_protection_rule WHERE workspace_id = $1 AND name = $2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "b1b26cb02dcc0748c63411c933e5e6bdfe82466134ba28408a530e22c66656de"
|
||||
}
|
||||
@@ -160,7 +160,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,7 @@
|
||||
"failure",
|
||||
"command",
|
||||
"approval",
|
||||
"preprocessor",
|
||||
"schedule_handler_old",
|
||||
"dynamic_skip"
|
||||
"preprocessor"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -71,7 +72,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO workspace_protection_rule (workspace_id, name, rules, bypass_groups, bypass_users)\n VALUES ($1, $2, $3, $4, $5)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dbf2982f43577999dec0c488dfb67f56e5bd0e5fbf8da5132f6fc5a282d2b0e1"
|
||||
}
|
||||
@@ -185,7 +185,8 @@
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -71,7 +72,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mqtt",
|
||||
"gcp",
|
||||
"default_email",
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -71,7 +72,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
197
backend/Cargo.lock
generated
197
backend/Cargo.lock
generated
@@ -890,7 +890,7 @@ dependencies = [
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-http 0.63.3",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
@@ -914,8 +914,8 @@ dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-json 0.62.4",
|
||||
"aws-smithy-http 0.63.3",
|
||||
"aws-smithy-json 0.62.3",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -980,16 +980,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-rds"
|
||||
version = "1.125.0"
|
||||
version = "1.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6897e8c3b3621b5616ccb838b1d30b8e18fb0c0699cea8e235e5005f902f12a"
|
||||
checksum = "24627b8374f8f242f351aa8a69adae8a416826a89819a7bfd010fc48b81f51e8"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-json 0.62.4",
|
||||
"aws-smithy-http 0.63.3",
|
||||
"aws-smithy-json 0.62.3",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-query",
|
||||
"aws-smithy-runtime",
|
||||
@@ -1036,8 +1036,8 @@ dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-json 0.62.4",
|
||||
"aws-smithy-http 0.63.3",
|
||||
"aws-smithy-json 0.62.3",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1060,8 +1060,8 @@ dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-json 0.62.4",
|
||||
"aws-smithy-http 0.63.3",
|
||||
"aws-smithy-json 0.62.3",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1106,7 +1106,7 @@ checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-http 0.63.3",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
@@ -1123,9 +1123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-async"
|
||||
version = "1.2.12"
|
||||
version = "1.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cba48474f1d6807384d06fec085b909f5807e16653c5af5c45dfe89539f0b70"
|
||||
checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
@@ -1134,9 +1134,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-eventstream"
|
||||
version = "0.60.19"
|
||||
version = "0.60.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c0b3e587fbaa5d7f7e870544508af8ce82ea47cd30376e69e1e37c4ac746f79"
|
||||
checksum = "35b9c7354a3b13c66f60fe4616d6d1969c9fd36b1b5333a5dfb3ee716b33c588"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
@@ -1167,9 +1167,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.63.4"
|
||||
version = "0.63.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af4a8a5fe3e4ac7ee871237c340bbce13e982d37543b65700f4419e039f5d78e"
|
||||
checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
@@ -1188,9 +1188,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http-client"
|
||||
version = "1.1.10"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0709f0083aa19b704132684bc26d3c868e06bd428ccc4373b0b55c3e8748a58b"
|
||||
checksum = "12fb0abf49ff0cab20fd31ac1215ed7ce0ea92286ba09e2854b42ba5cabe7525"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1227,27 +1227,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-json"
|
||||
version = "0.62.4"
|
||||
version = "0.62.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb"
|
||||
checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-observability"
|
||||
version = "0.2.5"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b"
|
||||
checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-query"
|
||||
version = "0.60.14"
|
||||
version = "0.60.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0"
|
||||
checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"urlencoding",
|
||||
@@ -1255,12 +1255,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime"
|
||||
version = "1.10.1"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd3dfc18c1ce097cf81fced7192731e63809829c6cbf933c1ec47452d08e1aa"
|
||||
checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-http 0.63.3",
|
||||
"aws-smithy-http-client",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1280,9 +1280,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime-api"
|
||||
version = "1.11.4"
|
||||
version = "1.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c55e0837e9b8526f49e0b9bfa9ee18ddee70e853f5bc09c5d11ebceddcb0fec"
|
||||
checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
@@ -1297,9 +1297,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "1.4.4"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "576b0d6991c9c32bc14fc340582ef148311f924d41815f641a308b5d11e8e7cd"
|
||||
checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33"
|
||||
dependencies = [
|
||||
"base64-simd 0.8.0",
|
||||
"bytes",
|
||||
@@ -1323,9 +1323,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types-convert"
|
||||
version = "0.60.13"
|
||||
version = "0.60.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62e5e75fed51b75fbc63966c039ca403aa980ba637c0ccce86c3c3144629303"
|
||||
checksum = "059deaa8583331f9f610b44c7cbc005d0cccec6dec3a7b387de096dbe6c06b8a"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"chrono",
|
||||
@@ -1333,9 +1333,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-xml"
|
||||
version = "0.60.14"
|
||||
version = "0.60.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa"
|
||||
checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57"
|
||||
dependencies = [
|
||||
"xmlparser",
|
||||
]
|
||||
@@ -8960,9 +8960,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.3"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
|
||||
checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
@@ -11317,9 +11317,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "0.15.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bef41ebc9ebed2c1b1d90203e9d1756091e8a00bbc3107676151f39868ca0ee"
|
||||
checksum = "0a621b37a548ff6ab6292d57841eb25785a7f146d89391a19c9f199414bd13da"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum 0.8.4",
|
||||
@@ -11352,9 +11352,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "0.15.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e88ad84b8b6237a934534a62b379a5be6388915663c0cc598ceb9b3292bbbfe"
|
||||
checksum = "6b79ed92303f9262db79575aa8c3652581668e9d136be6fd0b9ededa78954c95"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"proc-macro2",
|
||||
@@ -14451,9 +14451,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.7+spec-1.1.0"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1"
|
||||
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||
dependencies = [
|
||||
"winnow 0.7.14",
|
||||
]
|
||||
@@ -15725,7 +15725,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15783,7 +15783,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15907,7 +15907,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -15933,7 +15933,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -15943,7 +15943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -15966,7 +15966,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15986,7 +15986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16011,7 +16011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16040,7 +16040,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -16052,7 +16052,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.7.9",
|
||||
@@ -16075,7 +16075,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16088,7 +16088,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"sqlx",
|
||||
"strum 0.27.2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
@@ -16104,7 +16103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -16118,7 +16117,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16137,7 +16136,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -16236,7 +16235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -16255,7 +16254,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -16270,7 +16269,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -16294,7 +16293,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16311,7 +16310,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -16327,7 +16326,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16348,7 +16347,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16379,7 +16378,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-oauth2",
|
||||
@@ -16403,7 +16402,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -16412,7 +16411,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16424,7 +16423,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16436,7 +16435,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -16448,7 +16447,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16460,7 +16459,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16472,7 +16471,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -16483,7 +16482,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16494,7 +16493,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16507,7 +16506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16531,7 +16530,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16545,7 +16544,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -16562,7 +16561,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16576,7 +16575,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16595,7 +16594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -16606,7 +16605,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16643,7 +16642,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -16681,7 +16680,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -16691,7 +16690,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16720,7 +16719,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16753,7 +16752,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16773,7 +16772,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16807,7 +16806,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16841,7 +16840,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16864,7 +16863,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16888,7 +16887,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16912,7 +16911,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16947,7 +16946,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16975,7 +16974,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16998,7 +16997,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -17015,7 +17014,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -59,7 +59,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.630.1"
|
||||
version = "1.629.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -469,7 +469,7 @@ nkeys = "0.4.4"
|
||||
nu-parser = { version = "0.101.0", default-features = false }
|
||||
globset = "0.4.16"
|
||||
croner = "2.2.0"
|
||||
rmcp = { version = "=0.15.0", features = ["client", "transport-streamable-http-client", "transport-streamable-http-client-reqwest"] }
|
||||
rmcp = { version = "^0", features = ["client", "transport-streamable-http-client", "transport-streamable-http-client-reqwest"] }
|
||||
rquickjs = { version = "0.8", features = ["futures", "parallel", "macro"] }
|
||||
process-wrap = { version = "8.2.1", features = ["tokio1"] }
|
||||
|
||||
|
||||
21
backend/all_features_oss.sh
Executable file
21
backend/all_features_oss.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script outputs all features except private. Usage :
|
||||
# > cargo build --features $(./all_features_oss.sh)
|
||||
|
||||
# Path to the Cargo.toml file
|
||||
CARGO_TOML_PATH="./Cargo.toml"
|
||||
|
||||
# Extract features from Cargo.toml and output them separated by commas
|
||||
if [[ -f "$CARGO_TOML_PATH" ]]; then
|
||||
grep -A 100 '\[features\]' "$CARGO_TOML_PATH" | \
|
||||
sed -n '/\[features\]/,/^\[/p' | \
|
||||
grep -E '^[a-zA-Z0-9_-]+' | \
|
||||
grep -v 'private' | \
|
||||
grep -v 'benchmark' | \
|
||||
cut -d' ' -f1 | \
|
||||
paste -sd ',' -
|
||||
else
|
||||
echo "Cargo.toml not found at $CARGO_TOML_PATH"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1 +1 @@
|
||||
de9ff0d258f5a1a3739d9693531bad3972ddfd22
|
||||
7596cefdba81482c0b0c0b61be26369f112d8009
|
||||
|
||||
@@ -29,39 +29,7 @@ def load_openapi_spec(file_path: str) -> Dict[str, Any]:
|
||||
print(f"Error loading OpenAPI spec: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def flatten_allof_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Flatten an allOf schema into a single object schema by merging all properties."""
|
||||
if 'allOf' not in schema:
|
||||
return schema
|
||||
|
||||
merged = {"type": "object", "properties": {}, "required": []}
|
||||
|
||||
def collect_from(s: Dict[str, Any]):
|
||||
if 'allOf' in s:
|
||||
for item in s['allOf']:
|
||||
if isinstance(item, dict):
|
||||
collect_from(item)
|
||||
if 'properties' in s:
|
||||
merged['properties'].update(s['properties'])
|
||||
if 'required' in s and isinstance(s['required'], list):
|
||||
merged['required'].extend(s['required'])
|
||||
if 'description' in s and 'description' not in merged:
|
||||
merged['description'] = s['description']
|
||||
|
||||
collect_from(schema)
|
||||
|
||||
# Preserve additional top-level keys from the original schema
|
||||
preserved_keys = {'additionalProperties', 'title', 'nullable', 'default', 'example'}
|
||||
for key in preserved_keys:
|
||||
if key in schema and key not in merged:
|
||||
merged[key] = schema[key]
|
||||
|
||||
if not merged['required']:
|
||||
del merged['required']
|
||||
|
||||
return merged
|
||||
|
||||
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None, base_path: str = "", include_fields: Optional[List[str]] = None, opaque_fields: Optional[List[str]] = None, include_query_params: Optional[List[str]] = None) -> tuple:
|
||||
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None, base_path: str = "") -> tuple:
|
||||
"""Extract separate schemas for path parameters, query parameters, and request body."""
|
||||
path_params_schema = {
|
||||
"type": "object",
|
||||
@@ -104,7 +72,7 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
|
||||
path_params_schema['properties'][param_name] = param_schema
|
||||
if param_required:
|
||||
path_params_schema['required'].append(param_name)
|
||||
elif param_in == 'query' and (include_query_params is None or param_name in include_query_params):
|
||||
elif param_in == 'query':
|
||||
query_params_schema['properties'][param_name] = param_schema
|
||||
if param_required:
|
||||
query_params_schema['required'].append(param_name)
|
||||
@@ -112,28 +80,7 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
|
||||
# Process request body if present
|
||||
if request_body:
|
||||
body_schema = extract_request_body_schema(request_body, spec, base_path)
|
||||
|
||||
# Flatten allOf schemas into a single object schema for filtering
|
||||
if body_schema and (include_fields is not None or opaque_fields):
|
||||
body_schema = flatten_allof_schema(body_schema)
|
||||
|
||||
# Apply include_fields filter: only keep listed top-level properties
|
||||
if body_schema and include_fields is not None and 'properties' in body_schema:
|
||||
body_schema['properties'] = {
|
||||
k: v for k, v in body_schema['properties'].items()
|
||||
if k in include_fields
|
||||
}
|
||||
if 'required' in body_schema:
|
||||
body_schema['required'] = [
|
||||
r for r in body_schema['required'] if r in include_fields
|
||||
]
|
||||
|
||||
# Apply opaque_fields: simplify listed properties to {"type": "object"}
|
||||
if body_schema and opaque_fields and 'properties' in body_schema:
|
||||
for field in opaque_fields:
|
||||
if field in body_schema['properties']:
|
||||
body_schema['properties'][field] = {"type": "object"}
|
||||
|
||||
|
||||
# If we have required fields specified and a body schema, update the required array
|
||||
if body_schema and required_fields:
|
||||
if 'required' not in body_schema:
|
||||
@@ -148,60 +95,11 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
|
||||
# Log warning when a required field is missing from schema properties
|
||||
print(f"Warning: Required field '{field}' not found in body schema properties", file=sys.stderr)
|
||||
|
||||
# Sanitize empty schemas for JSON Schema draft 2020-12 compliance
|
||||
path_params_schema = sanitize_empty_schemas(path_params_schema)
|
||||
query_params_schema = sanitize_empty_schemas(query_params_schema)
|
||||
body_schema = sanitize_empty_schemas(body_schema)
|
||||
|
||||
# Convert enums to descriptions for client compatibility
|
||||
path_params_schema = convert_enums_to_descriptions(path_params_schema)
|
||||
query_params_schema = convert_enums_to_descriptions(query_params_schema)
|
||||
body_schema = convert_enums_to_descriptions(body_schema)
|
||||
|
||||
# Detect overlapping property names across schemas and rename with suffixes
|
||||
path_keys = set(path_params_schema['properties'].keys()) if path_params_schema and path_params_schema.get('properties') else set()
|
||||
query_keys = set(query_params_schema['properties'].keys()) if query_params_schema and query_params_schema.get('properties') else set()
|
||||
body_keys = set(body_schema['properties'].keys()) if body_schema and body_schema.get('properties') else set()
|
||||
|
||||
conflicts = (path_keys & query_keys) | (path_keys & body_keys) | (query_keys & body_keys)
|
||||
|
||||
path_field_renames = {}
|
||||
query_field_renames = {}
|
||||
body_field_renames = {}
|
||||
|
||||
for field in conflicts:
|
||||
schemas_and_renames = [
|
||||
(path_params_schema, path_keys, '__path', path_field_renames),
|
||||
(query_params_schema, query_keys, '__query', query_field_renames),
|
||||
(body_schema, body_keys, '__body', body_field_renames),
|
||||
]
|
||||
for schema, keys, suffix, renames_map in schemas_and_renames:
|
||||
if field in keys and schema and 'properties' in schema:
|
||||
new_name = field + suffix
|
||||
# Rename in properties
|
||||
schema['properties'][new_name] = schema['properties'].pop(field)
|
||||
# Update description to clarify the renamed field
|
||||
if 'description' not in schema['properties'][new_name]:
|
||||
schema['properties'][new_name] = dict(schema['properties'][new_name])
|
||||
prop = schema['properties'][new_name]
|
||||
if isinstance(prop, dict):
|
||||
existing_desc = prop.get('description', '')
|
||||
location = suffix.lstrip('_')
|
||||
if not existing_desc:
|
||||
prop['description'] = f"({location} parameter)"
|
||||
else:
|
||||
prop['description'] = f"{existing_desc} ({location} parameter)"
|
||||
# Rename in required array
|
||||
if 'required' in schema and field in schema['required']:
|
||||
schema['required'] = [new_name if r == field else r for r in schema['required']]
|
||||
# Store the reverse mapping: renamed -> original
|
||||
renames_map[new_name] = field
|
||||
|
||||
# Return None for empty schemas
|
||||
path_params_schema = path_params_schema if path_params_schema and path_params_schema.get('properties') else None
|
||||
query_params_schema = query_params_schema if query_params_schema and query_params_schema.get('properties') else None
|
||||
|
||||
return (path_params_schema, query_params_schema, body_schema, path_field_renames, query_field_renames, body_field_renames)
|
||||
path_params_schema = path_params_schema if path_params_schema['properties'] else None
|
||||
query_params_schema = query_params_schema if query_params_schema['properties'] else None
|
||||
|
||||
return (path_params_schema, query_params_schema, body_schema)
|
||||
|
||||
# Cache for loaded external files
|
||||
_external_file_cache: Dict[str, Dict[str, Any]] = {}
|
||||
@@ -262,25 +160,18 @@ def resolve_ref(ref_path: str, spec: Dict[str, Any], base_path: str = "") -> tup
|
||||
|
||||
return (current if isinstance(current, dict) else None), spec
|
||||
|
||||
def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path: str = "", _visited_refs: Optional[set] = None) -> Dict[str, Any]:
|
||||
def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path: str = "") -> Dict[str, Any]:
|
||||
"""Recursively resolve all $ref references in a schema."""
|
||||
if _visited_refs is None:
|
||||
_visited_refs = set()
|
||||
|
||||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
# If this is a $ref, resolve it
|
||||
if '$ref' in schema:
|
||||
ref_path = schema['$ref']
|
||||
if ref_path in _visited_refs:
|
||||
# Circular reference detected - return empty object to break the cycle
|
||||
return {"type": "object"}
|
||||
_visited_refs = _visited_refs | {ref_path}
|
||||
resolved, resolved_spec = resolve_ref(ref_path, spec, base_path)
|
||||
if resolved:
|
||||
# Recursively resolve any refs in the resolved schema using the appropriate spec
|
||||
return resolve_schema_refs(resolved, resolved_spec, base_path, _visited_refs)
|
||||
return resolve_schema_refs(resolved, resolved_spec, base_path)
|
||||
else:
|
||||
print(f"Warning: Could not resolve $ref: {ref_path}")
|
||||
return schema
|
||||
@@ -289,10 +180,10 @@ def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path:
|
||||
resolved_schema = {}
|
||||
for key, value in schema.items():
|
||||
if isinstance(value, dict):
|
||||
resolved_schema[key] = resolve_schema_refs(value, spec, base_path, _visited_refs)
|
||||
resolved_schema[key] = resolve_schema_refs(value, spec, base_path)
|
||||
elif isinstance(value, list):
|
||||
resolved_schema[key] = [
|
||||
resolve_schema_refs(item, spec, base_path, _visited_refs) if isinstance(item, dict) else item
|
||||
resolve_schema_refs(item, spec, base_path) if isinstance(item, dict) else item
|
||||
for item in value
|
||||
]
|
||||
else:
|
||||
@@ -300,56 +191,6 @@ def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path:
|
||||
|
||||
return resolved_schema
|
||||
|
||||
def convert_enums_to_descriptions(schema: Any) -> Any:
|
||||
"""Recursively convert enum arrays into description text to avoid client compatibility issues."""
|
||||
if isinstance(schema, list):
|
||||
return [convert_enums_to_descriptions(item) for item in schema]
|
||||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
result = {}
|
||||
enum_value = None
|
||||
# First pass: copy all non-enum keys so 'description' is available before enum processing
|
||||
for key, value in schema.items():
|
||||
if key == 'enum':
|
||||
enum_value = value
|
||||
else:
|
||||
result[key] = convert_enums_to_descriptions(value)
|
||||
|
||||
# Second pass: process enum using the already-copied description
|
||||
if enum_value is not None:
|
||||
values_str = ', '.join(str(v) for v in enum_value)
|
||||
existing = result.get('description', '')
|
||||
enum_desc = f"Possible values: {values_str}"
|
||||
result['description'] = f"{existing}. {enum_desc}" if existing else enum_desc
|
||||
|
||||
return result
|
||||
|
||||
def sanitize_empty_schemas(schema: Any) -> Any:
|
||||
"""Replace empty {} schemas with valid JSON Schema draft 2020-12 equivalents.
|
||||
|
||||
In OpenAPI, {} means 'any value' but strict JSON Schema validators (e.g. Claude's API)
|
||||
reject empty objects. This converts them to proper schemas.
|
||||
"""
|
||||
if isinstance(schema, list):
|
||||
return [sanitize_empty_schemas(item) for item in schema]
|
||||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
result = {}
|
||||
for key, value in schema.items():
|
||||
if key == 'additionalProperties' and isinstance(value, dict) and len(value) == 0:
|
||||
result[key] = True
|
||||
elif key == 'properties' and isinstance(value, dict):
|
||||
# properties is a map of name -> schema; sanitize each property schema
|
||||
result[key] = {
|
||||
k: {"type": "object"} if isinstance(v, dict) and len(v) == 0 else sanitize_empty_schemas(v)
|
||||
for k, v in value.items()
|
||||
}
|
||||
else:
|
||||
result[key] = sanitize_empty_schemas(value)
|
||||
return result
|
||||
|
||||
def extract_request_body_schema(request_body: Dict[str, Any], spec: Dict[str, Any], base_path: str = "") -> Optional[Dict[str, Any]]:
|
||||
"""Extract request body schema from OpenAPI requestBody definition and resolve refs."""
|
||||
if not request_body:
|
||||
@@ -402,9 +243,6 @@ def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
'parameters': operation.get('parameters', []),
|
||||
'requestBody': operation.get('requestBody'),
|
||||
'required_fields': operation.get('x-mcp-required-fields', []),
|
||||
'include_fields': operation.get('x-mcp-tool-include-fields'),
|
||||
'opaque_fields': operation.get('x-mcp-tool-opaque-fields'),
|
||||
'include_query_params': operation.get('x-mcp-tool-include-query-params'),
|
||||
}
|
||||
tools.append(tool)
|
||||
|
||||
@@ -440,18 +278,14 @@ export const mcpEndpointTools: EndpointTool[] = [];
|
||||
method = tool['method'].upper()
|
||||
|
||||
# Generate separate schemas
|
||||
path_params_schema, query_params_schema, body_schema, path_field_renames, query_field_renames, body_field_renames = extract_separate_schemas(
|
||||
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path,
|
||||
tool.get('include_fields'), tool.get('opaque_fields'), tool.get('include_query_params')
|
||||
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
|
||||
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path
|
||||
)
|
||||
|
||||
# Convert schemas to TypeScript - use 'as const' for better type inference
|
||||
path_params_ts = json.dumps(path_params_schema, indent=8) if path_params_schema else "undefined"
|
||||
query_params_ts = json.dumps(query_params_schema, indent=8) if query_params_schema else "undefined"
|
||||
body_schema_ts = json.dumps(body_schema, indent=8) if body_schema else "undefined"
|
||||
path_field_renames_ts = json.dumps(path_field_renames, indent=8) if path_field_renames else "undefined"
|
||||
query_field_renames_ts = json.dumps(query_field_renames, indent=8) if query_field_renames else "undefined"
|
||||
body_field_renames_ts = json.dumps(body_field_renames, indent=8) if body_field_renames else "undefined"
|
||||
|
||||
# Generate tool definition
|
||||
tool_def = f""" {{
|
||||
@@ -462,10 +296,7 @@ export const mcpEndpointTools: EndpointTool[] = [];
|
||||
method: "{method}",
|
||||
pathParamsSchema: {path_params_ts},
|
||||
queryParamsSchema: {query_params_ts},
|
||||
bodySchema: {body_schema_ts},
|
||||
pathFieldRenames: {path_field_renames_ts},
|
||||
queryFieldRenames: {query_field_renames_ts},
|
||||
bodyFieldRenames: {body_field_renames_ts}
|
||||
bodySchema: {body_schema_ts}
|
||||
}}"""
|
||||
tool_definitions.append(tool_def)
|
||||
|
||||
@@ -484,9 +315,6 @@ export interface EndpointTool {{
|
||||
pathParamsSchema?: object;
|
||||
queryParamsSchema?: object;
|
||||
bodySchema?: object;
|
||||
pathFieldRenames?: Record<string, string>;
|
||||
queryFieldRenames?: Record<string, string>;
|
||||
bodyFieldRenames?: Record<string, string>;
|
||||
}}
|
||||
|
||||
export const mcpEndpointTools: EndpointTool[] = [
|
||||
@@ -516,17 +344,13 @@ pub fn all_tools() -> Vec<EndpointTool> {{
|
||||
method = tool['method'].upper()
|
||||
|
||||
# Generate separate schemas
|
||||
path_params_schema, query_params_schema, body_schema, path_field_renames, query_field_renames, body_field_renames = extract_separate_schemas(
|
||||
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path,
|
||||
tool.get('include_fields'), tool.get('opaque_fields'), tool.get('include_query_params')
|
||||
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
|
||||
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path
|
||||
)
|
||||
|
||||
path_params_rust = schema_to_rust_value(path_params_schema)
|
||||
query_params_rust = schema_to_rust_value(query_params_schema)
|
||||
body_schema_rust = schema_to_rust_value(body_schema)
|
||||
path_field_renames_rust = schema_to_rust_value(path_field_renames if path_field_renames else None)
|
||||
query_field_renames_rust = schema_to_rust_value(query_field_renames if query_field_renames else None)
|
||||
body_field_renames_rust = schema_to_rust_value(body_field_renames if body_field_renames else None)
|
||||
|
||||
# Generate tool definition
|
||||
tool_def = f""" EndpointTool {{
|
||||
@@ -538,9 +362,6 @@ pub fn all_tools() -> Vec<EndpointTool> {{
|
||||
path_params_schema: {path_params_rust},
|
||||
query_params_schema: {query_params_rust},
|
||||
body_schema: {body_schema_rust},
|
||||
path_field_renames: {path_field_renames_rust},
|
||||
query_field_renames: {query_field_renames_rust},
|
||||
body_field_renames: {body_field_renames_rust},
|
||||
}}"""
|
||||
tool_definitions.append(tool_def)
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Drop the workspace_protection_rule table and its indexes
|
||||
DROP TABLE IF EXISTS workspace_protection_rule;
|
||||
@@ -1,10 +0,0 @@
|
||||
-- Add workspace_protection_rule table for fine-grained access control
|
||||
CREATE TABLE workspace_protection_rule (
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
rules INTEGER NOT NULL,
|
||||
bypass_groups TEXT[] NOT NULL DEFAULT '{}',
|
||||
bypass_users TEXT[] NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (workspace_id, name)
|
||||
);
|
||||
@@ -1,159 +0,0 @@
|
||||
use windmill_common::{
|
||||
error::{self, Error},
|
||||
get_database_url, DatabaseUrl,
|
||||
};
|
||||
|
||||
pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
|
||||
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 5;
|
||||
pub const DEFAULT_MAX_CONNECTIONS_INDEXER: u32 = 5;
|
||||
|
||||
pub async fn initial_connection() -> Result<sqlx::Pool<sqlx::Postgres>, error::Error> {
|
||||
let connect_options = get_database_url().await?.connect_options().await?;
|
||||
sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(2)
|
||||
.connect_with(connect_options)
|
||||
.await
|
||||
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))
|
||||
}
|
||||
|
||||
pub async fn connect_db(
|
||||
server_mode: bool,
|
||||
indexer_mode: bool,
|
||||
worker_mode: bool,
|
||||
#[cfg(feature = "private")] mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> anyhow::Result<sqlx::Pool<sqlx::Postgres>> {
|
||||
use anyhow::Context;
|
||||
|
||||
let database_url = get_database_url().await?;
|
||||
|
||||
let max_connections = match std::env::var("DATABASE_CONNECTIONS") {
|
||||
Ok(n) => n.parse::<u32>().context("invalid DATABASE_CONNECTIONS")?,
|
||||
Err(_) => {
|
||||
if server_mode {
|
||||
DEFAULT_MAX_CONNECTIONS_SERVER
|
||||
} else if indexer_mode {
|
||||
DEFAULT_MAX_CONNECTIONS_INDEXER
|
||||
} else {
|
||||
DEFAULT_MAX_CONNECTIONS_WORKER
|
||||
+ std::env::var("NUM_WORKERS")
|
||||
.ok()
|
||||
.map(|x| x.parse().ok())
|
||||
.flatten()
|
||||
.unwrap_or(1)
|
||||
- 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let pool = connect(database_url.clone(), max_connections, worker_mode).await?;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
let pool2 = pool.clone();
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
if let DatabaseUrl::IamRds(database_url) = database_url {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = killpill_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
|
||||
let needs_refresh = {
|
||||
let read_guard = database_url.read().await;
|
||||
read_guard.needs_refresh()
|
||||
};
|
||||
if needs_refresh {
|
||||
let new_url = tokio::time::timeout(std::time::Duration::from_secs(10), get_database_url()).await;
|
||||
match new_url {
|
||||
Ok(Ok(new_url)) => {
|
||||
match new_url.connect_options().await {
|
||||
Ok(connect_options) => {
|
||||
pool2.set_connect_options(connect_options);
|
||||
tracing::info!("Refreshed IAM RDS URL successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting IAM RDS connect options, retrying in 10s: {}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!("Error refreshing IAM RDS URL, trying again in 10s: {}", e);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Timeout after 10s refreshing IAM RDS URL, trying again in 10 seconds: {}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
database_url: DatabaseUrl,
|
||||
max_connections: u32,
|
||||
worker_mode: bool,
|
||||
) -> Result<sqlx::Pool<sqlx::Postgres>, error::Error> {
|
||||
use sqlx::Executor;
|
||||
use std::time::Duration;
|
||||
let mut pool_options = sqlx::postgres::PgPoolOptions::new()
|
||||
.min_connections((max_connections / 5).clamp(1, max_connections))
|
||||
.max_connections(max_connections)
|
||||
.max_lifetime(Duration::from_secs(30 * 60)); // 30 mins
|
||||
if worker_mode {
|
||||
pool_options = pool_options.idle_timeout(Duration::from_secs(60));
|
||||
}
|
||||
pool_options
|
||||
.after_connect(move |conn, _| {
|
||||
if worker_mode {
|
||||
Box::pin(async move {
|
||||
if let Err(e) = conn
|
||||
.execute(
|
||||
r#"
|
||||
SET enable_seqscan = OFF;
|
||||
SET statement_timeout = '5min';
|
||||
SET idle_in_transaction_session_timeout = '10min';
|
||||
SET tcp_keepalives_idle = 300;
|
||||
SET tcp_keepalives_interval = 60;
|
||||
SET tcp_keepalives_count = 10;"#,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error setting postgres settings: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Box::pin(async move {
|
||||
if let Err(e) = conn
|
||||
.execute(
|
||||
r#"
|
||||
SET statement_timeout = '5min';
|
||||
SET idle_in_transaction_session_timeout = '10min';
|
||||
SET tcp_keepalives_idle = 300;
|
||||
SET tcp_keepalives_interval = 60;
|
||||
SET tcp_keepalives_count = 10;"#,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error setting postgres settings: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
})
|
||||
.connect_with(
|
||||
database_url
|
||||
.connect_options()
|
||||
.await?
|
||||
.statement_cache_capacity(400),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))
|
||||
}
|
||||
@@ -117,7 +117,6 @@ const BIND_ADDR_ENV: &str = "SERVER_BIND_ADDR";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod cgroups;
|
||||
mod db_connect;
|
||||
#[cfg(feature = "private")]
|
||||
pub mod ee;
|
||||
mod ee_oss;
|
||||
@@ -666,7 +665,7 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
} else {
|
||||
println!("Connecting to database...");
|
||||
|
||||
let db = crate::db_connect::initial_connection().await?;
|
||||
let db = windmill_common::initial_connection().await?;
|
||||
|
||||
let num_version = sqlx::query_scalar!("SELECT version()").fetch_one(&db).await;
|
||||
|
||||
@@ -771,12 +770,8 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
let conn = if mode == Mode::Agent {
|
||||
conn
|
||||
} else {
|
||||
// Drop the initial connection pool before creating the main one.
|
||||
// With low PostgreSQL max_connections, both pools existing simultaneously
|
||||
// can exhaust all available connection slots, causing connect_db to hang.
|
||||
drop(conn);
|
||||
|
||||
let db = crate::db_connect::connect_db(
|
||||
// This time we use a pool of connections
|
||||
let db = windmill_common::connect_db(
|
||||
server_mode,
|
||||
indexer_mode,
|
||||
worker_mode,
|
||||
|
||||
@@ -3092,13 +3092,7 @@ RETURNING job_id"
|
||||
|
||||
async fn cleanup_job_result_stream_orphaned_jobs(db: &DB) -> error::Result<()> {
|
||||
let result = sqlx::query!(
|
||||
"DELETE FROM job_result_stream_v2
|
||||
WHERE job_id NOT IN (SELECT id FROM v2_job_queue)
|
||||
AND job_id NOT IN (
|
||||
SELECT id FROM v2_job_completed
|
||||
WHERE completed_at > NOW() - INTERVAL '60 seconds'
|
||||
)
|
||||
RETURNING job_id",
|
||||
"DELETE FROM job_result_stream_v2 WHERE job_id NOT IN (SELECT id FROM v2_job_queue) RETURNING job_id",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
|
||||
@@ -227,52 +227,6 @@ impl RunJob {
|
||||
uuid
|
||||
}
|
||||
|
||||
/// Push the job as a specific user (for testing permissions)
|
||||
pub async fn push_as(self, db: &Pool<Postgres>, username: &str, email: &str) -> Uuid {
|
||||
let RunJob { payload, args, scheduled_for_o, .. } = self;
|
||||
let mut hm_args = std::collections::HashMap::new();
|
||||
for (k, v) in args {
|
||||
hm_args.insert(k, windmill_common::worker::to_raw_value(&v));
|
||||
}
|
||||
|
||||
let tx = PushIsolationLevel::IsolatedRoot(db.clone());
|
||||
let (uuid, tx) = windmill_queue::push(
|
||||
db,
|
||||
tx,
|
||||
"test-workspace",
|
||||
payload,
|
||||
windmill_queue::PushArgs::from(&hm_args),
|
||||
username,
|
||||
email,
|
||||
format!("u/{}", username),
|
||||
/* token_prefix */ None,
|
||||
scheduled_for_o,
|
||||
/* schedule_path */ None,
|
||||
/* parent_job */ None,
|
||||
/* root job */ None,
|
||||
/* flow_innermost_root_job */ None,
|
||||
/* job_id */ None,
|
||||
/* is_flow_step */ false,
|
||||
/* same_worker */ false,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("push has to succeed");
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
uuid
|
||||
}
|
||||
|
||||
/// push the job, spawn a worker, wait until the job is in completed_job
|
||||
pub async fn run_until_complete(
|
||||
self,
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
//! Integration tests for workspace protection rulesets.
|
||||
//!
|
||||
//! Tests verify that DisableDirectDeployment protection rules correctly
|
||||
//! block/allow operations based on user permissions.
|
||||
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::workspaces::invalidate_protection_rules_cache;
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
fn client() -> reqwest::Client {
|
||||
reqwest::Client::new()
|
||||
}
|
||||
|
||||
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
|
||||
builder.header("Authorization", format!("Bearer {}", token))
|
||||
}
|
||||
|
||||
fn new_script(path: &str, summary: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"path": path,
|
||||
"summary": summary,
|
||||
"description": "",
|
||||
"content": "export async function main() { return 42; }",
|
||||
"language": "deno",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn new_flow(path: &str, summary: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"path": path,
|
||||
"summary": summary,
|
||||
"description": "",
|
||||
"value": { "modules": [] },
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Comprehensive test for protection rules functionality.
|
||||
/// Tests all essential cases in a single test to avoid cache interference.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_protection_rules(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
invalidate_protection_rules_cache("test-workspace");
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let base = format!("http://localhost:{port}/api/w/test-workspace");
|
||||
|
||||
// ========================================
|
||||
// 1. Without protection rule, non-admin can create scripts and flows
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().post(format!("{base}/scripts/create")), "SECRET_TOKEN_2")
|
||||
.json(&new_script("u/test-user-2/script_no_rule", "No rule"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 201, "Should create script without rule: {}", resp.text().await?);
|
||||
|
||||
let resp = authed(client().post(format!("{base}/flows/create")), "SECRET_TOKEN_2")
|
||||
.json(&new_flow("u/test-user-2/flow_no_rule", "No rule"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 201, "Should create flow without rule: {}", resp.text().await?);
|
||||
|
||||
// ========================================
|
||||
// 2. Non-admin cannot create protection rules
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().post(format!("{base}/workspaces/protection_rules")), "SECRET_TOKEN_2")
|
||||
.json(&json!({
|
||||
"name": "unauthorized-rule",
|
||||
"rules": ["DisableDirectDeployment"],
|
||||
"bypass_users": [],
|
||||
"bypass_groups": []
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(!resp.status().is_success(), "Non-admin should not create rules: {}", resp.status());
|
||||
|
||||
// ========================================
|
||||
// 3. Admin creates protection rule
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().post(format!("{base}/workspaces/protection_rules")), "SECRET_TOKEN")
|
||||
.json(&json!({
|
||||
"name": "test-rule",
|
||||
"rules": ["DisableDirectDeployment"],
|
||||
"bypass_users": [],
|
||||
"bypass_groups": []
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "Admin should create rule: {}", resp.text().await?);
|
||||
|
||||
// ========================================
|
||||
// 4. With rule, non-admin is blocked from creating scripts/flows
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().post(format!("{base}/scripts/create")), "SECRET_TOKEN_2")
|
||||
.json(&new_script("u/test-user-2/blocked_script", "Blocked"))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(!resp.status().is_success(), "Non-admin should be blocked from scripts: {}", resp.status());
|
||||
let body = resp.text().await?;
|
||||
assert!(body.contains("blocked") || body.contains("Blocked"), "Error should mention blocking: {}", body);
|
||||
|
||||
let resp = authed(client().post(format!("{base}/flows/create")), "SECRET_TOKEN_2")
|
||||
.json(&new_flow("u/test-user-2/blocked_flow", "Blocked"))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(!resp.status().is_success(), "Non-admin should be blocked from flows: {}", resp.status());
|
||||
|
||||
// ========================================
|
||||
// 5. Admin bypasses protection rule
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().post(format!("{base}/scripts/create")), "SECRET_TOKEN")
|
||||
.json(&new_script("u/test-user/admin_script", "Admin"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 201, "Admin should bypass rule: {}", resp.text().await?);
|
||||
|
||||
// ========================================
|
||||
// 6. Update rule to bypass test-user-2
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().post(format!("{base}/workspaces/protection_rules/test-rule")), "SECRET_TOKEN")
|
||||
.json(&json!({
|
||||
"rules": ["DisableDirectDeployment"],
|
||||
"bypass_users": ["test-user-2"],
|
||||
"bypass_groups": []
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "Should update rule: {}", resp.text().await?);
|
||||
|
||||
// Invalidate cache to pick up the update
|
||||
invalidate_protection_rules_cache("test-workspace");
|
||||
|
||||
// ========================================
|
||||
// 7. Bypassed user can now create
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().post(format!("{base}/scripts/create")), "SECRET_TOKEN_2")
|
||||
.json(&new_script("u/test-user-2/bypassed_script", "Bypassed"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 201, "Bypassed user should create: {}", resp.text().await?);
|
||||
|
||||
// ========================================
|
||||
// 8. Non-bypassed user (test-user-3) is still blocked
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().post(format!("{base}/scripts/create")), "SECRET_TOKEN_3")
|
||||
.json(&new_script("u/test-user-3/still_blocked", "Blocked"))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(!resp.status().is_success(), "Non-bypassed user should be blocked: {}", resp.status());
|
||||
|
||||
// ========================================
|
||||
// 9. Delete rule
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().delete(format!("{base}/workspaces/protection_rules/test-rule")), "SECRET_TOKEN")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "Should delete rule: {}", resp.text().await?);
|
||||
|
||||
// Invalidate cache to pick up the deletion
|
||||
invalidate_protection_rules_cache("test-workspace");
|
||||
|
||||
// ========================================
|
||||
// 10. After deletion, non-admin can create again
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().post(format!("{base}/scripts/create")), "SECRET_TOKEN_3")
|
||||
.json(&new_script("u/test-user-3/after_delete", "After delete"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 201, "Should create after rule deletion: {}", resp.text().await?);
|
||||
|
||||
// ========================================
|
||||
// 11. Verify rule list is empty
|
||||
// ========================================
|
||||
|
||||
let resp = authed(client().get(format!("{base}/workspaces/protection_rules")), "SECRET_TOKEN")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
let rules: Vec<serde_json::Value> = resp.json().await?;
|
||||
assert!(rules.is_empty(), "Should have no rules after deletion");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -237,331 +237,6 @@ mod suspend_resume {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that self-approval is blocked when self_approval_disabled is true.
|
||||
///
|
||||
/// This test verifies that when a flow has an approval step with self_approval_disabled=true,
|
||||
/// the user who triggered the flow cannot approve it themselves via the owner endpoint
|
||||
/// (POST /jobs/flow/resume/:id).
|
||||
///
|
||||
/// Bug context: The owner endpoint was missing the approval condition check, allowing
|
||||
/// users to bypass self-approval restrictions by using the UI resume button instead
|
||||
/// of the HMAC-signed approval link.
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_self_approval_disabled_blocks_owner_resume(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// Flow with self_approval_disabled=true on the approval step
|
||||
let flow_with_self_approval_disabled: FlowValue = serde_json::from_value(json!({
|
||||
"modules": [{
|
||||
"id": "a",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "deno",
|
||||
"content": "export function main() { return 'step1'; }"
|
||||
},
|
||||
"suspend": {
|
||||
"required_events": 1,
|
||||
"user_auth_required": true,
|
||||
"self_approval_disabled": true
|
||||
}
|
||||
}, {
|
||||
"id": "b",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "deno",
|
||||
"content": "export function main() { return 'step2 - after approval'; }"
|
||||
}
|
||||
}]
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
// Push flow as NON-ADMIN user (test-user-2) - admins bypass self-approval check
|
||||
// Use a path owned by test-user-2 so require_owner_of_path succeeds
|
||||
let flow = RunJob::from(JobPayload::RawFlow {
|
||||
value: flow_with_self_approval_disabled,
|
||||
path: Some("u/test-user-2/test_approval".to_string()),
|
||||
restarted_from: None,
|
||||
})
|
||||
.push_as(&db, "test-user-2", "test2@windmill.dev")
|
||||
.await;
|
||||
|
||||
let queue = listen_for_queue(&db).await;
|
||||
let db_ = db.clone();
|
||||
|
||||
in_test_worker(
|
||||
&db,
|
||||
async move {
|
||||
let db = db_;
|
||||
|
||||
// Wait for flow to suspend at approval step
|
||||
wait_until_flow_suspends(flow, queue, &db).await;
|
||||
|
||||
// Create a token for the same non-admin user who triggered the flow
|
||||
// This simulates clicking "Resume" in the UI as the flow owner
|
||||
// Args: db, w_id, owner, label, expires_in, email, job_id, perms, audit_span
|
||||
let token = windmill_common::auth::create_token_for_owner(
|
||||
&db,
|
||||
"test-workspace",
|
||||
"u/test-user-2",
|
||||
"test-token",
|
||||
100,
|
||||
"test2@windmill.dev",
|
||||
&Uuid::nil(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to resume via the owner endpoint (POST /jobs/flow/resume/:id)
|
||||
// This should FAIL because self_approval_disabled=true and the user
|
||||
// is the same as the one who triggered the flow
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/jobs/flow/resume/{flow}"
|
||||
))
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.header("Content-Type", "application/json")
|
||||
.body("{}")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let status = response.status();
|
||||
|
||||
// The request should be rejected with 403 Forbidden
|
||||
// (currently this test FAILS because the bug allows self-approval)
|
||||
assert!(
|
||||
status == reqwest::StatusCode::FORBIDDEN,
|
||||
"Self-approval should be blocked when self_approval_disabled=true. \
|
||||
Expected 403 Forbidden, got {}. Response: {}",
|
||||
status,
|
||||
response.text().await.unwrap_or_default()
|
||||
);
|
||||
},
|
||||
port,
|
||||
)
|
||||
.await;
|
||||
|
||||
server.close().await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that self-approval WORKS when self_approval_disabled is false (default behavior).
|
||||
///
|
||||
/// This is the complementary test to test_self_approval_disabled_blocks_owner_resume.
|
||||
/// When self_approval_disabled is NOT set, the flow owner should be able to approve
|
||||
/// their own flow via the owner endpoint.
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_self_approval_allowed_when_not_disabled(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// Flow with user_auth_required but WITHOUT self_approval_disabled
|
||||
let flow_without_self_approval_disabled: FlowValue = serde_json::from_value(json!({
|
||||
"modules": [{
|
||||
"id": "a",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "deno",
|
||||
"content": "export function main() { return 'step1'; }"
|
||||
},
|
||||
"suspend": {
|
||||
"required_events": 1,
|
||||
"user_auth_required": true
|
||||
// self_approval_disabled is NOT set (defaults to false)
|
||||
}
|
||||
}, {
|
||||
"id": "b",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "deno",
|
||||
"content": "export function main() { return 'step2 - after approval'; }"
|
||||
}
|
||||
}]
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
// Push flow as non-admin user
|
||||
let flow = RunJob::from(JobPayload::RawFlow {
|
||||
value: flow_without_self_approval_disabled,
|
||||
path: Some("u/test-user-2/test_approval_allowed".to_string()),
|
||||
restarted_from: None,
|
||||
})
|
||||
.push_as(&db, "test-user-2", "test2@windmill.dev")
|
||||
.await;
|
||||
|
||||
let queue = listen_for_queue(&db).await;
|
||||
let db_ = db.clone();
|
||||
|
||||
in_test_worker(
|
||||
&db,
|
||||
async move {
|
||||
let db = db_;
|
||||
|
||||
// Wait for flow to suspend at approval step
|
||||
wait_until_flow_suspends(flow, queue, &db).await;
|
||||
|
||||
// Create a token for the same user who triggered the flow
|
||||
let token = windmill_common::auth::create_token_for_owner(
|
||||
&db,
|
||||
"test-workspace",
|
||||
"u/test-user-2",
|
||||
"test-token",
|
||||
100,
|
||||
"test2@windmill.dev",
|
||||
&Uuid::nil(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to resume via the owner endpoint - this SHOULD succeed
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/jobs/flow/resume/{flow}"
|
||||
))
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.header("Content-Type", "application/json")
|
||||
.body("{}")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let status = response.status();
|
||||
|
||||
// Self-approval should be allowed when self_approval_disabled is not set
|
||||
assert!(
|
||||
status.is_success(),
|
||||
"Self-approval should be allowed when self_approval_disabled is not set. \
|
||||
Expected 2xx, got {}. Response: {}",
|
||||
status,
|
||||
response.text().await.unwrap_or_default()
|
||||
);
|
||||
},
|
||||
port,
|
||||
)
|
||||
.await;
|
||||
|
||||
server.close().await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that a DIFFERENT user can approve a flow even when self_approval_disabled is true.
|
||||
///
|
||||
/// This verifies that the self_approval_disabled setting only blocks the flow trigger,
|
||||
/// not other users. A different user should always be able to approve.
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_different_user_can_approve_when_self_approval_disabled(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// Flow with self_approval_disabled=true
|
||||
let flow_with_self_approval_disabled: FlowValue = serde_json::from_value(json!({
|
||||
"modules": [{
|
||||
"id": "a",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "deno",
|
||||
"content": "export function main() { return 'step1'; }"
|
||||
},
|
||||
"suspend": {
|
||||
"required_events": 1,
|
||||
"user_auth_required": true,
|
||||
"self_approval_disabled": true
|
||||
}
|
||||
}, {
|
||||
"id": "b",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "deno",
|
||||
"content": "export function main() { return 'step2 - after approval'; }"
|
||||
}
|
||||
}]
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
// Push flow as test-user-2
|
||||
let flow = RunJob::from(JobPayload::RawFlow {
|
||||
value: flow_with_self_approval_disabled,
|
||||
path: Some("u/test-user-2/test_approval_by_other".to_string()),
|
||||
restarted_from: None,
|
||||
})
|
||||
.push_as(&db, "test-user-2", "test2@windmill.dev")
|
||||
.await;
|
||||
|
||||
let queue = listen_for_queue(&db).await;
|
||||
let db_ = db.clone();
|
||||
|
||||
in_test_worker(
|
||||
&db,
|
||||
async move {
|
||||
let db = db_;
|
||||
|
||||
// Wait for flow to suspend at approval step
|
||||
wait_until_flow_suspends(flow, queue, &db).await;
|
||||
|
||||
// Create a token for a DIFFERENT user (test-user, who is admin)
|
||||
// This simulates a different person approving the flow
|
||||
let token = windmill_common::auth::create_token_for_owner(
|
||||
&db,
|
||||
"test-workspace",
|
||||
"u/test-user",
|
||||
"test-token",
|
||||
100,
|
||||
"test@windmill.dev",
|
||||
&Uuid::nil(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to resume via the owner endpoint as a different user - this SHOULD succeed
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/jobs/flow/resume/{flow}"
|
||||
))
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.header("Content-Type", "application/json")
|
||||
.body("{}")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let status = response.status();
|
||||
|
||||
// A different user should be able to approve even with self_approval_disabled
|
||||
assert!(
|
||||
status.is_success(),
|
||||
"Different user should be able to approve even with self_approval_disabled=true. \
|
||||
Expected 2xx, got {}. Response: {}",
|
||||
status,
|
||||
response.text().await.unwrap_or_default()
|
||||
);
|
||||
},
|
||||
port,
|
||||
)
|
||||
.await;
|
||||
|
||||
server.close().await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn cancel_after_suspend(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
|
||||
@@ -10,11 +10,22 @@ if [[ "$(uname)" == "Darwin" ]]; then
|
||||
# Uncomment the git-based samael dependency
|
||||
sed -i '' 's/^# \(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/\1/' Cargo.toml
|
||||
|
||||
# Run cargo sqlx prepare with deno_core_mac
|
||||
# Run cargo sqlx prepare with deno_core_mac
|
||||
echo "Running cargo sqlx prepare with deno_core_mac..."
|
||||
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,private,deno_core_mac
|
||||
else
|
||||
# Run cargo sqlx prepare
|
||||
echo "Running cargo sqlx prepare..."
|
||||
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,ee
|
||||
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,private
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# Undo the samael changes on macOS
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
echo "Reverting samael changes..."
|
||||
# Uncomment the version-based samael dependency
|
||||
sed -i '' 's/^#samael = { version="0.0.14", features = \["xmlsec"\] }/samael = { version="0.0.14", features = ["xmlsec"] }/' Cargo.toml
|
||||
# Comment out the git-based samael dependency
|
||||
sed -i '' 's/^\(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/# \1/' Cargo.toml
|
||||
fi
|
||||
|
||||
BIN
backend/v8.snap
Normal file
BIN
backend/v8.snap
Normal file
Binary file not shown.
@@ -16,9 +16,9 @@ use axum::{
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use windmill_api_auth::{check_scopes, ApiAuthed, AuthCache, Tokened};
|
||||
use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::{error::Error, webhook::{WebhookMessage, WebhookShared}, workspaces::{check_user_against_rule, ProtectionRuleKind, RuleCheckResult}};
|
||||
use windmill_common::webhook::{WebhookMessage, WebhookShared};
|
||||
use windmill_common::DB;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
@@ -174,19 +174,6 @@ async fn create_folder(
|
||||
Path(w_id): Path<String>,
|
||||
Json(ng): Json<NewFolder>,
|
||||
) -> Result<String> {
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
let mut tx = user_db.clone().begin(&authed).await?;
|
||||
|
||||
if !VALID_FOLDER_NAME.is_match(&ng.name) {
|
||||
@@ -328,19 +315,6 @@ async fn update_folder(
|
||||
) -> Result<String> {
|
||||
use sql_builder::prelude::*;
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
let mut sqlb = SqlBuilder::update_table("folder");
|
||||
sqlb.and_where_eq("name", "?".bind(&name));
|
||||
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
||||
@@ -630,18 +604,6 @@ async fn delete_folder(
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
) -> Result<String> {
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
|
||||
|
||||
@@ -18,7 +18,6 @@ use windmill_api_auth::{
|
||||
auth::{list_tokens_internal, TruncatedTokenWithEmail},
|
||||
check_scopes, maybe_refresh_folders, require_owner_of_path, ApiAuthed,
|
||||
};
|
||||
use windmill_common::workspaces::{check_user_against_rule, ProtectionRuleKind, RuleCheckResult};
|
||||
use windmill_common::{
|
||||
utils::{WithStarredInfoQuery, HTTP_CLIENT},
|
||||
webhook::{WebhookMessage, WebhookShared},
|
||||
@@ -30,7 +29,7 @@ use hyper::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sql_builder::prelude::*;
|
||||
use sqlx::{FromRow, Postgres, Transaction};
|
||||
use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::assets::{clear_static_asset_usage, AssetUsageKind};
|
||||
use windmill_common::min_version::{
|
||||
@@ -430,20 +429,6 @@ async fn create_flow(
|
||||
));
|
||||
}
|
||||
check_scopes(&authed, || format!("flows:write:{}", nf.path))?;
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
validate_flow(&nf).await?;
|
||||
if *CLOUD_HOSTED {
|
||||
let nb_flows =
|
||||
@@ -878,20 +863,6 @@ async fn update_flow(
|
||||
}
|
||||
let flow_path = flow_path.to_path();
|
||||
check_scopes(&authed, || format!("flows:write:{}", flow_path))?;
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
validate_flow(&nf).await?;
|
||||
|
||||
let authed = maybe_refresh_folders(&flow_path, &w_id, authed, &db).await;
|
||||
@@ -1446,18 +1417,6 @@ async fn archive_flow_by_path(
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("flows:write:{}", path))?;
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
sqlx::query!(
|
||||
@@ -1563,18 +1522,6 @@ async fn delete_flow_by_path(
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("flows:write:{}", path))?;
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
sqlx::query!(
|
||||
|
||||
@@ -12,7 +12,9 @@ use windmill_api_auth::{
|
||||
check_scopes, maybe_refresh_folders, require_owner_of_path, ApiAuthed,
|
||||
};
|
||||
use windmill_common::{
|
||||
utils::{BulkDeleteRequest, WithStarredInfoQuery, HTTP_CLIENT}, webhook::{WebhookMessage, WebhookShared}, workspaces::{check_user_against_rule, ProtectionRuleKind, RuleCheckResult}, DB
|
||||
utils::{BulkDeleteRequest, WithStarredInfoQuery, HTTP_CLIENT},
|
||||
webhook::{WebhookMessage, WebhookShared},
|
||||
DB,
|
||||
};
|
||||
use windmill_queue::schedule::clear_schedule;
|
||||
|
||||
@@ -33,7 +35,7 @@ use serde_json::value::RawValue;
|
||||
use sql_builder::prelude::*;
|
||||
use sqlx::{FromRow, Postgres, Transaction};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_dep_map::process_relative_imports;
|
||||
use windmill_dep_map::scoped_dependency_map::ScopedDependencyMap;
|
||||
@@ -546,18 +548,6 @@ async fn create_script(
|
||||
Path(w_id): Path<String>,
|
||||
Json(ns): Json<NewScript>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
let (hash, tx, hdm) =
|
||||
create_script_internal(ns, w_id, authed, db.clone(), user_db, webhook).await?;
|
||||
tx.commit().await?;
|
||||
@@ -1916,18 +1906,6 @@ async fn archive_script_by_path(
|
||||
}
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("scripts:write:{}", path))?;
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
require_owner_of_path(&authed, path)?;
|
||||
@@ -1995,18 +1973,6 @@ async fn archive_script_by_hash(
|
||||
"Operators cannot archive scripts for security reasons".to_string(),
|
||||
));
|
||||
}
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let script = sqlx::query_as::<_, Script<ScriptRunnableSettingsHandle>>(
|
||||
@@ -2052,20 +2018,9 @@ async fn delete_script_by_hash(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||
) -> JsonResult<Script<ScriptRunnableSettingsInline>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let script = sqlx::query_as::<_, Script<ScriptRunnableSettingsHandle>>(
|
||||
"UPDATE script SET content = '', archived = true, deleted = true, lock = '', schema = null WHERE hash = $1 AND \
|
||||
workspace_id = $2 RETURNING *",
|
||||
@@ -2123,19 +2078,6 @@ async fn delete_script_by_path(
|
||||
));
|
||||
}
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let draft_only = sqlx::query_scalar!(
|
||||
@@ -2262,19 +2204,6 @@ async fn delete_scripts_bulk(
|
||||
));
|
||||
}
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let mut deleted_paths = sqlx::query_scalar!(
|
||||
|
||||
@@ -39,4 +39,3 @@ sqlx.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
@@ -26,8 +26,7 @@ use hex;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use uuid::Uuid;
|
||||
use strum::{IntoEnumIterator};
|
||||
use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::db::UserDB;
|
||||
use windmill_common::s3_helpers::LargeFileStorage;
|
||||
@@ -39,7 +38,8 @@ use windmill_common::workspaces::GitRepositorySettings;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::workspaces::WorkspaceDeploymentUISettings;
|
||||
use windmill_common::workspaces::{
|
||||
check_user_against_rule, get_datatable_resource_from_db_unchecked, DataTable, DataTableCatalogResourceType, ProtectionRuleKind, ProtectionRules, ProtectionRuleset, RuleCheckResult, WorkspaceGitSyncSettings
|
||||
get_datatable_resource_from_db_unchecked, DataTable, DataTableCatalogResourceType,
|
||||
WorkspaceGitSyncSettings,
|
||||
};
|
||||
use windmill_common::workspaces::{Ducklake, DucklakeCatalogResourceType};
|
||||
use windmill_common::PgDatabase;
|
||||
@@ -147,12 +147,6 @@ pub fn workspaced_service() -> Router {
|
||||
post(reset_workspace_diffs),
|
||||
)
|
||||
.route("/compare/:target_workspace_id", get(compare_workspaces))
|
||||
.route("/protection_rules", get(list_protection_rules))
|
||||
.route("/protection_rules", post(create_protection_rule))
|
||||
.route(
|
||||
"/protection_rules/:rule_name",
|
||||
post(update_protection_rule).delete(delete_protection_rule),
|
||||
)
|
||||
}
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
@@ -3388,18 +3382,6 @@ async fn create_workspace_fork_branch(
|
||||
if *DISABLE_WORKSPACE_FORK {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
}
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableWorkspaceForking,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
Ok(Json(
|
||||
handle_fork_branch_creation(&authed.email, &authed.username, &db, &w_id, &nw.id).await?,
|
||||
@@ -3421,18 +3403,6 @@ async fn create_workspace_fork(
|
||||
if *DISABLE_WORKSPACE_FORK {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
}
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&parent_workspace_id,
|
||||
&ProtectionRuleKind::DisableWorkspaceForking,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
|
||||
@@ -4315,268 +4285,6 @@ async fn update_operator_settings(
|
||||
Ok("Operator settings updated successfully".to_string())
|
||||
}
|
||||
|
||||
// Protection Rules API endpoints
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateProtectionRuleRequest {
|
||||
name: String,
|
||||
rules: Vec<ProtectionRuleKind>,
|
||||
bypass_groups: Vec<String>,
|
||||
bypass_users: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdateProtectionRuleRequest {
|
||||
rules: Vec<ProtectionRuleKind>,
|
||||
bypass_groups: Vec<String>,
|
||||
bypass_users: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ProtectionRulesetResponse {
|
||||
pub workspace_id: String,
|
||||
pub name: String,
|
||||
pub rules: Vec<ProtectionRuleKind>,
|
||||
pub bypass_groups: Vec<String>,
|
||||
pub bypass_users: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<ProtectionRuleset> for ProtectionRulesetResponse {
|
||||
fn from(value: ProtectionRuleset) -> Self {
|
||||
let mut rules = vec![];
|
||||
|
||||
for rule in ProtectionRuleKind::iter() {
|
||||
if value.rules.contains(rule.flag()) {
|
||||
rules.push(rule)
|
||||
}
|
||||
}
|
||||
|
||||
ProtectionRulesetResponse {
|
||||
rules,
|
||||
workspace_id: value.workspace_id,
|
||||
name: value.name,
|
||||
bypass_groups: value.bypass_groups,
|
||||
bypass_users: value.bypass_users,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List all protection rules for a workspace
|
||||
async fn list_protection_rules(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<ProtectionRulesetResponse>> {
|
||||
let rules =
|
||||
(*windmill_common::workspaces::get_protection_rules(&w_id, &db).await?).clone();
|
||||
Ok(Json(rules.into_iter().map(ProtectionRulesetResponse::from).collect()))
|
||||
}
|
||||
|
||||
/// Create a new protection rule
|
||||
async fn create_protection_rule(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(req): Json<CreateProtectionRuleRequest>,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
// Check if rule with this name already exists
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM workspace_protection_rule WHERE workspace_id = $1 AND name = $2)",
|
||||
&w_id,
|
||||
&req.name
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if exists {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Protection rule with name '{}' already exists",
|
||||
req.name
|
||||
)));
|
||||
}
|
||||
|
||||
// Insert the new rule
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO workspace_protection_rule (workspace_id, name, rules, bypass_groups, bypass_users)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
&w_id,
|
||||
&req.name,
|
||||
ProtectionRules::from(&req.rules).bits(),
|
||||
&req.bypass_groups,
|
||||
&req.bypass_users,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"workspaces.create_protection_rule",
|
||||
ActionKind::Create,
|
||||
&w_id,
|
||||
Some(&req.name),
|
||||
Some([("name", &req.name[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Invalidate cache
|
||||
windmill_common::workspaces::invalidate_protection_rules_cache(&w_id);
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Settings { setting_type: format!("protection_rule_{}", req.name) },
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Created protection rule '{}'", req.name))
|
||||
}
|
||||
|
||||
/// Update an existing protection rule
|
||||
async fn update_protection_rule(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, rule_name)): Path<(String, String)>,
|
||||
Json(req): Json<UpdateProtectionRuleRequest>,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
// Check if rule exists
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM workspace_protection_rule WHERE workspace_id = $1 AND name = $2)",
|
||||
&w_id,
|
||||
&rule_name
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if !exists {
|
||||
return Err(Error::NotFound(format!(
|
||||
"Protection rule '{}' not found",
|
||||
rule_name
|
||||
)));
|
||||
}
|
||||
|
||||
// Update the rule
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE workspace_protection_rule
|
||||
SET rules = $1, bypass_groups = $2, bypass_users = $3
|
||||
WHERE workspace_id = $4 AND name = $5
|
||||
"#,
|
||||
ProtectionRules::from(&req.rules).bits(),
|
||||
&req.bypass_groups,
|
||||
&req.bypass_users,
|
||||
&w_id,
|
||||
&rule_name
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"workspaces.update_protection_rule",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&rule_name),
|
||||
Some([("name", &rule_name[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Invalidate cache
|
||||
windmill_common::workspaces::invalidate_protection_rules_cache(&w_id);
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Settings { setting_type: format!("protection_rule_{}", rule_name) },
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Updated protection rule '{}'", rule_name))
|
||||
}
|
||||
|
||||
/// Delete a protection rule
|
||||
async fn delete_protection_rule(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, rule_name)): Path<(String, String)>,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
// Delete the rule
|
||||
let result = sqlx::query!(
|
||||
"DELETE FROM workspace_protection_rule WHERE workspace_id = $1 AND name = $2",
|
||||
&w_id,
|
||||
&rule_name
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(Error::NotFound(format!(
|
||||
"Protection rule '{}' not found",
|
||||
rule_name
|
||||
)));
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"workspaces.delete_protection_rule",
|
||||
ActionKind::Delete,
|
||||
&w_id,
|
||||
Some(&rule_name),
|
||||
Some([("name", &rule_name[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Invalidate cache
|
||||
windmill_common::workspaces::invalidate_protection_rules_cache(&w_id);
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Settings { setting_type: format!("protection_rule_{}", rule_name) },
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Deleted protection rule '{}'", rule_name))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct WorkspaceComparison {
|
||||
pub all_ahead_items_visible: bool,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.630.1
|
||||
version: 1.629.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -4169,126 +4169,6 @@ paths:
|
||||
type: string
|
||||
example: "Updated mute critical alert UI settings for workspace: workspace_id"
|
||||
|
||||
/w/{workspace}/workspaces/protection_rules:
|
||||
get:
|
||||
summary: list all protection rules for a workspace
|
||||
operationId: listProtectionRules
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: list of protection rules
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ProtectionRuleset"
|
||||
post:
|
||||
summary: create a new protection rule
|
||||
operationId: createProtectionRule
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: New protection rule configuration
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- rules
|
||||
- bypass_groups
|
||||
- bypass_users
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Unique name for the protection rule
|
||||
example: "Production Protection"
|
||||
rules:
|
||||
$ref: "#/components/schemas/ProtectionRules"
|
||||
bypass_groups:
|
||||
$ref: "#/components/schemas/RuleBypasserGroups"
|
||||
bypass_users:
|
||||
$ref: "#/components/schemas/RuleBypasserUsers"
|
||||
responses:
|
||||
"200":
|
||||
description: protection rule created successfully
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: rule with this name already exists
|
||||
|
||||
/w/{workspace}/workspaces/protection_rules/{rule_name}:
|
||||
post:
|
||||
summary: update an existing protection rule
|
||||
operationId: updateProtectionRule
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: rule_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Name of the protection rule to update
|
||||
requestBody:
|
||||
description: Updated protection rule configuration
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- rules
|
||||
- bypass_groups
|
||||
- bypass_users
|
||||
properties:
|
||||
rules:
|
||||
$ref: "#/components/schemas/ProtectionRules"
|
||||
bypass_groups:
|
||||
$ref: "#/components/schemas/RuleBypasserGroups"
|
||||
bypass_users:
|
||||
$ref: "#/components/schemas/RuleBypasserUsers"
|
||||
responses:
|
||||
"200":
|
||||
description: protection rule updated successfully
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: protection rule not found
|
||||
delete:
|
||||
summary: delete a protection rule
|
||||
operationId: deleteProtectionRule
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: rule_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Name of the protection rule to delete
|
||||
responses:
|
||||
"200":
|
||||
description: protection rule deleted successfully
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: protection rule not found
|
||||
/w/{workspace}/workspaces/public_app_rate_limit:
|
||||
post:
|
||||
summary: Set public app rate limit for this workspace
|
||||
@@ -6052,17 +5932,6 @@ paths:
|
||||
post:
|
||||
summary: create script
|
||||
operationId: createScript
|
||||
x-mcp-tool: true
|
||||
x-mcp-instructions: "To create a script, specify the path (e.g., 'f/my_folder/my_script'), the content (source code), and the language. For TypeScript, use 'bun' unless deno-specific APIs are needed."
|
||||
x-mcp-tool-include-fields:
|
||||
- path
|
||||
- content
|
||||
- language
|
||||
- summary
|
||||
- description
|
||||
- kind
|
||||
- tag
|
||||
- deployment_message
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
@@ -6339,7 +6208,6 @@ paths:
|
||||
post:
|
||||
summary: delete script by hash (erase content but keep hash, require admin)
|
||||
operationId: deleteScriptByHash
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
@@ -6357,7 +6225,6 @@ paths:
|
||||
post:
|
||||
summary: delete script at a given path (require admin)
|
||||
operationId: deleteScriptByPath
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
@@ -6748,9 +6615,6 @@ paths:
|
||||
post:
|
||||
summary: run script by path
|
||||
operationId: runScriptByPath
|
||||
x-mcp-tool: true
|
||||
x-mcp-instructions: "You should first use getScriptByPath to retrieve the script's schema and understand what arguments are expected."
|
||||
x-mcp-tool-include-query-params: []
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
@@ -7693,18 +7557,6 @@ paths:
|
||||
post:
|
||||
summary: create flow
|
||||
operationId: createFlow
|
||||
x-mcp-tool: true
|
||||
x-mcp-tool-include-fields:
|
||||
- path
|
||||
- summary
|
||||
- description
|
||||
- value
|
||||
- schema
|
||||
- tag
|
||||
- deployment_message
|
||||
x-mcp-tool-opaque-fields:
|
||||
- value
|
||||
- schema
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
@@ -7735,18 +7587,6 @@ paths:
|
||||
post:
|
||||
summary: update flow
|
||||
operationId: updateFlow
|
||||
x-mcp-tool: true
|
||||
x-mcp-tool-include-fields:
|
||||
- path
|
||||
- summary
|
||||
- description
|
||||
- value
|
||||
- schema
|
||||
- tag
|
||||
- deployment_message
|
||||
x-mcp-tool-opaque-fields:
|
||||
- value
|
||||
- schema
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
@@ -7804,7 +7644,6 @@ paths:
|
||||
delete:
|
||||
summary: delete flow by path
|
||||
operationId: deleteFlowByPath
|
||||
x-mcp-tool: true
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
@@ -8047,16 +7886,6 @@ paths:
|
||||
post:
|
||||
summary: create app
|
||||
operationId: createApp
|
||||
x-mcp-tool: true
|
||||
x-mcp-tool-include-fields:
|
||||
- path
|
||||
- value
|
||||
- summary
|
||||
- policy
|
||||
- deployment_message
|
||||
x-mcp-tool-opaque-fields:
|
||||
- value
|
||||
- policy
|
||||
tags:
|
||||
- app
|
||||
parameters:
|
||||
@@ -8404,16 +8233,6 @@ paths:
|
||||
post:
|
||||
summary: update app
|
||||
operationId: updateApp
|
||||
x-mcp-tool: true
|
||||
x-mcp-tool-include-fields:
|
||||
- path
|
||||
- value
|
||||
- summary
|
||||
- policy
|
||||
- deployment_message
|
||||
x-mcp-tool-opaque-fields:
|
||||
- value
|
||||
- policy
|
||||
tags:
|
||||
- app
|
||||
parameters:
|
||||
@@ -8695,9 +8514,6 @@ paths:
|
||||
post:
|
||||
summary: run flow by path
|
||||
operationId: runFlowByPath
|
||||
x-mcp-tool: true
|
||||
x-mcp-instructions: "You should first use getFlowByPath to retrieve the flow's schema and understand what arguments are expected."
|
||||
x-mcp-tool-include-query-params: []
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
@@ -18093,7 +17909,7 @@ components:
|
||||
ScriptArgs:
|
||||
type: object
|
||||
description: The arguments to pass to the script or flow
|
||||
additionalProperties: true
|
||||
additionalProperties: {}
|
||||
|
||||
Input:
|
||||
type: object
|
||||
@@ -22100,7 +21916,8 @@ components:
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
value: {}
|
||||
value:
|
||||
type: object
|
||||
policy:
|
||||
$ref: "#/components/schemas/Policy"
|
||||
execution_mode:
|
||||
@@ -22649,7 +22466,8 @@ components:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
value: {}
|
||||
value:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- value
|
||||
@@ -23103,47 +22921,7 @@ components:
|
||||
kind:
|
||||
$ref: "#/components/schemas/AssetKind"
|
||||
required: [path, kind]
|
||||
ProtectionRuleset:
|
||||
type: object
|
||||
description: A workspace protection rule defining restrictions and bypass permissions
|
||||
required:
|
||||
- name
|
||||
- rules
|
||||
- bypass_groups
|
||||
- bypass_users
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Unique name for the protection rule
|
||||
example: "Production Protection"
|
||||
workspace_id:
|
||||
type: string
|
||||
rules:
|
||||
$ref: "#/components/schemas/ProtectionRules"
|
||||
bypass_groups:
|
||||
$ref: "#/components/schemas/RuleBypasserGroups"
|
||||
bypass_users:
|
||||
$ref: "#/components/schemas/RuleBypasserUsers"
|
||||
ProtectionRules:
|
||||
type: array
|
||||
description: Configuration of protection restrictions
|
||||
items:
|
||||
$ref: "#/components/schemas/ProtectionRuleKind"
|
||||
ProtectionRuleKind:
|
||||
type: string
|
||||
enum:
|
||||
- DisableDirectDeployment
|
||||
- DisableWorkspaceForking
|
||||
RuleBypasserGroups:
|
||||
type: array
|
||||
description: Groups that can bypass this ruleset
|
||||
items:
|
||||
type: string
|
||||
RuleBypasserUsers:
|
||||
type: array
|
||||
description: Users that can bypass this ruleset
|
||||
items:
|
||||
type: string
|
||||
|
||||
NativeServiceName:
|
||||
type: string
|
||||
enum:
|
||||
|
||||
@@ -49,7 +49,7 @@ use sha2::{Digest, Sha256};
|
||||
use sql_builder::{bind::Bind, SqlBuilder};
|
||||
use sqlx::{types::Uuid, FromRow};
|
||||
use std::str;
|
||||
use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::{
|
||||
apps::{AppScriptId, ListAppQuery, APP_WORKSPACED_ROUTE},
|
||||
@@ -65,7 +65,6 @@ use windmill_common::{
|
||||
},
|
||||
variables::{build_crypt, build_crypt_with_key_suffix, encrypt},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
workspaces::{check_user_against_rule, ProtectionRuleKind, RuleCheckResult},
|
||||
HUB_BASE_URL,
|
||||
};
|
||||
|
||||
@@ -1050,20 +1049,6 @@ async fn create_app_raw<'a>(
|
||||
"Operators cannot create apps for security reasons".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
let (path, _id) = process_app_multipart!(
|
||||
authed,
|
||||
user_db,
|
||||
@@ -1124,19 +1109,6 @@ async fn create_app(
|
||||
let path = app.path.clone();
|
||||
check_scopes(&authed, || format!("apps:write:{}", &path))?;
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
let (new_tx, _path, _id) = create_app_internal(authed, db, user_db, &w_id, false, app).await?;
|
||||
|
||||
new_tx.commit().await?;
|
||||
@@ -1375,19 +1347,6 @@ async fn delete_app(
|
||||
));
|
||||
}
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
// Check if it's a raw app before deletion
|
||||
let is_raw_app = sqlx::query_scalar!(
|
||||
"SELECT app_version.raw_app FROM app
|
||||
@@ -1493,21 +1452,6 @@ async fn update_app(
|
||||
// create_app_internal(authed, user_db, db, &w_id, &mut app).await?;
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("apps:write:{}", path))?;
|
||||
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
let opath = path.to_string();
|
||||
let (new_tx, npath, _v_id) =
|
||||
update_app_internal(authed, db, user_db, &w_id, path, false, ns).await?;
|
||||
@@ -1538,21 +1482,6 @@ async fn update_app_raw<'a>(
|
||||
"Operators cannot update apps for security reasons".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
if let RuleCheckResult::Blocked(msg) = check_user_against_rule(
|
||||
&w_id,
|
||||
&ProtectionRuleKind::DisableDirectDeployment,
|
||||
AuditAuthorable::username(&authed),
|
||||
&authed.groups,
|
||||
authed.is_admin,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::PermissionDenied(msg));
|
||||
}
|
||||
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("apps:write:{}", path))?;
|
||||
let opath = path.to_string();
|
||||
|
||||
@@ -114,10 +114,7 @@ use windmill_common::{
|
||||
get_latest_flow_version_info_for_path, get_script_info_for_hash, utils::empty_as_none,
|
||||
ScriptHashInfo, BASE_URL,
|
||||
};
|
||||
use windmill_queue::{
|
||||
get_result_and_success_by_id_from_flow, job_is_complete, push, PushArgs, PushArgsOwned,
|
||||
PushIsolationLevel,
|
||||
};
|
||||
use windmill_queue::{job_is_complete, push, PushArgs, PushArgsOwned, PushIsolationLevel};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
let cors = CorsLayer::new()
|
||||
@@ -2133,16 +2130,6 @@ pub async fn resume_suspended_flow_as_owner(
|
||||
require_owner_of_path(&authed, flow_path)?;
|
||||
check_scopes(&authed, || format!("jobs:run:flows:{}", flow_path))?;
|
||||
|
||||
// Check approval conditions (self-approval, required groups, etc.)
|
||||
if let Some(ref flow_status_value) = flow.flow_status {
|
||||
if let Ok(flow_status) =
|
||||
serde_json::from_value::<FlowStatus>(flow_status_value.clone())
|
||||
{
|
||||
let trigger_email = flow.email.as_deref().unwrap_or("");
|
||||
conditionally_require_authed_user(Some(authed.clone()), flow_status, trigger_email)?;
|
||||
}
|
||||
}
|
||||
|
||||
let value = value.unwrap_or(serde_json::Value::Null);
|
||||
|
||||
insert_resume_job(
|
||||
@@ -2421,7 +2408,6 @@ struct FlowInfo {
|
||||
flow_status: Option<serde_json::Value>,
|
||||
suspend: i32,
|
||||
script_path: Option<String>,
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
/// Get flow info from either a step job (by looking up its parent) or a flow job directly.
|
||||
@@ -2440,7 +2426,6 @@ async fn get_flow_info_for_resume(job_id: Uuid, db: &DB) -> error::Result<(FlowI
|
||||
s.flow_status,
|
||||
q.suspend AS "suspend!",
|
||||
j.runnable_path AS script_path,
|
||||
j.permissioned_as_email AS email,
|
||||
(ji.kind IN ('flow', 'flowpreview')) AS "is_flow_level!"
|
||||
FROM job_info ji
|
||||
JOIN v2_job_queue q ON q.id = CASE
|
||||
@@ -2462,7 +2447,6 @@ async fn get_flow_info_for_resume(job_id: Uuid, db: &DB) -> error::Result<(FlowI
|
||||
flow_status: result.flow_status,
|
||||
suspend: result.suspend,
|
||||
script_path: result.script_path,
|
||||
email: Some(result.email),
|
||||
};
|
||||
|
||||
Ok((flow_info, result.is_flow_level))
|
||||
@@ -2475,7 +2459,7 @@ async fn get_suspended_flow_info<'c>(
|
||||
let flow = sqlx::query_as!(
|
||||
FlowInfo,
|
||||
r#"
|
||||
SELECT j.id AS "id!", COALESCE(s.flow_status, s.workflow_as_code_status) as flow_status, q.suspend AS "suspend!", j.runnable_path as script_path, j.permissioned_as_email as email
|
||||
SELECT j.id AS "id!", COALESCE(s.flow_status, s.workflow_as_code_status) as flow_status, q.suspend AS "suspend!", j.runnable_path as script_path
|
||||
FROM v2_job_queue q JOIN v2_job j USING (id) LEFT JOIN v2_job_status s USING (id)
|
||||
WHERE j.id = $1
|
||||
"#,
|
||||
@@ -3487,11 +3471,8 @@ pub async fn run_workflow_as_code(
|
||||
let JobExtended { inner: job, raw_code, raw_lock, .. } = job;
|
||||
|
||||
let (_debouncing_settings, concurrency_settings) =
|
||||
windmill_common::runnable_settings::prefetch_cached_from_handle(
|
||||
job.runnable_settings_handle,
|
||||
&db,
|
||||
)
|
||||
.await?;
|
||||
windmill_common::runnable_settings::prefetch_cached_from_handle(job.runnable_settings_handle, &db)
|
||||
.await?;
|
||||
|
||||
let (job_payload, tag, _delete_after_use, timeout, on_behalf_of) = match job.job_kind {
|
||||
JobKind::Preview => (
|
||||
@@ -4251,7 +4232,7 @@ pub async fn stream_job(
|
||||
};
|
||||
|
||||
let poll_delay_ms = run_query.poll_delay_ms;
|
||||
let (uuid, early_return) = match runnable_id {
|
||||
let uuid = match runnable_id {
|
||||
RunnableId::ScriptId(ScriptId::ScriptPath(script_path))
|
||||
| RunnableId::HubScript(script_path) => {
|
||||
let (uuid, _, _) = push_script_job_by_path_into_queue(
|
||||
@@ -4266,10 +4247,10 @@ pub async fn stream_job(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
(uuid, None)
|
||||
uuid
|
||||
}
|
||||
RunnableId::ScriptId(ScriptId::ScriptHash(script_hash)) => {
|
||||
let (uuid, _) = run_job_by_hash_inner(
|
||||
run_job_by_hash_inner(
|
||||
authed.clone(),
|
||||
db.clone(),
|
||||
user_db,
|
||||
@@ -4279,11 +4260,11 @@ pub async fn stream_job(
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
(uuid, None)
|
||||
.await?
|
||||
.0
|
||||
}
|
||||
RunnableId::FlowId(FlowId::FlowPath(flow_path)) => {
|
||||
let (uuid, early_return, _) = push_flow_job_by_path_into_queue(
|
||||
push_flow_job_by_path_into_queue(
|
||||
authed.clone(),
|
||||
db.clone(),
|
||||
None,
|
||||
@@ -4294,11 +4275,11 @@ pub async fn stream_job(
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
(uuid, early_return)
|
||||
.await?
|
||||
.0
|
||||
}
|
||||
RunnableId::FlowId(FlowId::FlowVersion(version)) => {
|
||||
let (uuid, early_return) = run_flow_by_version_inner(
|
||||
run_flow_by_version_inner(
|
||||
authed.clone(),
|
||||
db.clone(),
|
||||
user_db,
|
||||
@@ -4308,8 +4289,8 @@ pub async fn stream_job(
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
(uuid, early_return)
|
||||
.await?
|
||||
.0
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4340,7 +4321,6 @@ pub async fn stream_job(
|
||||
None,
|
||||
tx,
|
||||
poll_delay_ms,
|
||||
early_return,
|
||||
);
|
||||
|
||||
let body = axum::body::Body::from_stream(stream.map(Result::<_, std::convert::Infallible>::Ok));
|
||||
@@ -4663,19 +4643,12 @@ fn register_potential_assets_on_inline_execution(
|
||||
let columns = asset.columns.as_ref().map(|cols| {
|
||||
cols.iter()
|
||||
.map(|(col_name, col_access_type)| {
|
||||
(
|
||||
col_name.clone(),
|
||||
windmill_common::assets::asset_access_type_from_parser(
|
||||
*col_access_type,
|
||||
),
|
||||
)
|
||||
(col_name.clone(), windmill_common::assets::asset_access_type_from_parser(*col_access_type))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
register_runtime_asset(InsertRuntimeAssetParams {
|
||||
access_type: asset
|
||||
.access_type
|
||||
.map(windmill_common::assets::asset_access_type_from_parser),
|
||||
access_type: asset.access_type.map(windmill_common::assets::asset_access_type_from_parser),
|
||||
asset_kind: windmill_common::assets::asset_kind_from_parser(asset.kind),
|
||||
asset_path: asset.path,
|
||||
columns,
|
||||
@@ -5852,7 +5825,6 @@ async fn get_job_update(
|
||||
no_logs,
|
||||
is_flow,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
@@ -5893,7 +5865,6 @@ async fn get_job_update_sse(
|
||||
is_flow,
|
||||
tx,
|
||||
poll_delay_ms,
|
||||
None,
|
||||
);
|
||||
|
||||
let stream = tokio_stream::wrappers::ReceiverStream::new(rx).map(|x| {
|
||||
@@ -5931,7 +5902,6 @@ pub fn start_job_update_sse_stream(
|
||||
is_flow: Option<bool>,
|
||||
tx: tokio::sync::mpsc::Sender<JobUpdateSSEStream>,
|
||||
poll_delay_ms: Option<u64>,
|
||||
early_return: Option<String>,
|
||||
) -> () {
|
||||
tokio::spawn(async move {
|
||||
let mut log_offset = initial_log_offset;
|
||||
@@ -5959,7 +5929,6 @@ pub fn start_job_update_sse_stream(
|
||||
no_logs,
|
||||
is_flow,
|
||||
flow_stream_job_id,
|
||||
early_return.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -6077,7 +6046,6 @@ pub fn start_job_update_sse_stream(
|
||||
no_logs,
|
||||
is_flow,
|
||||
flow_stream_job_id,
|
||||
early_return.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -6208,7 +6176,6 @@ async fn get_job_update_data(
|
||||
no_logs: Option<bool>,
|
||||
is_flow: Option<bool>,
|
||||
flow_stream_job_id: Option<Uuid>,
|
||||
early_return: Option<&str>,
|
||||
) -> error::Result<JobUpdate> {
|
||||
let tags = if log_view {
|
||||
log_job_view(
|
||||
@@ -6372,16 +6339,6 @@ async fn get_job_update_data(
|
||||
|
||||
let flow_stream_job_id = flow_stream_job_id.or(new_flow_stream_job_id);
|
||||
|
||||
let result = if let Some(early_return) = early_return {
|
||||
match get_result_and_success_by_id_from_flow(db, w_id, job_id, early_return, None).await
|
||||
{
|
||||
Ok((early_result, _)) => Some(early_result),
|
||||
Err(_) => result,
|
||||
}
|
||||
} else {
|
||||
result
|
||||
};
|
||||
|
||||
let flow_stream_delta =
|
||||
get_flow_stream_delta(db, flow_stream_job_id, stream_offset).await?;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -323,25 +323,16 @@ impl McpBackend for WindmillBackend {
|
||||
workspace_id,
|
||||
args_map,
|
||||
&endpoint_tool.path_params_schema,
|
||||
&endpoint_tool.path_field_renames,
|
||||
)?;
|
||||
let query_string = build_query_string(
|
||||
args_map,
|
||||
&endpoint_tool.query_params_schema,
|
||||
&endpoint_tool.query_field_renames,
|
||||
);
|
||||
let query_string = build_query_string(args_map, &endpoint_tool.query_params_schema);
|
||||
let full_url = format!(
|
||||
"{}/api{}{}",
|
||||
self.base_internal_url, path_template, query_string
|
||||
);
|
||||
|
||||
// Prepare request body
|
||||
let body_json = build_request_body(
|
||||
&endpoint_tool.method,
|
||||
args_map,
|
||||
&endpoint_tool.body_schema,
|
||||
&endpoint_tool.body_field_renames,
|
||||
);
|
||||
let body_json =
|
||||
build_request_body(&endpoint_tool.method, args_map, &endpoint_tool.body_schema);
|
||||
|
||||
// Create and execute request
|
||||
let response = create_http_request(
|
||||
|
||||
@@ -242,34 +242,19 @@ pub async fn get_hub_script_schema(path: &str, db: &DB) -> Result<Option<Schema>
|
||||
// HTTP request utilities for endpoint tools
|
||||
// ============================================================================
|
||||
|
||||
/// Look up the original field name from a field_renames map.
|
||||
/// field_renames maps renamed_key -> original_key (e.g. {"path__path": "path"}).
|
||||
fn get_original_name(renamed_key: &str, field_renames: &Option<Value>) -> String {
|
||||
field_renames
|
||||
.as_ref()
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|m| m.get(renamed_key))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| renamed_key.to_string())
|
||||
}
|
||||
|
||||
/// Substitute path parameters in the URL template
|
||||
pub fn substitute_path_params(
|
||||
path: &str,
|
||||
workspace_id: &str,
|
||||
args_map: &serde_json::Map<String, Value>,
|
||||
path_schema: &Option<Value>,
|
||||
path_field_renames: &Option<Value>,
|
||||
) -> BackendResult<String> {
|
||||
let mut path_template = path.replace("{workspace}", workspace_id);
|
||||
|
||||
if let Some(schema) = path_schema {
|
||||
if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
|
||||
for (param_name, _) in props {
|
||||
// param_name may be renamed (e.g. "path__path"), get original for URL placeholder
|
||||
let original_name = get_original_name(param_name, path_field_renames);
|
||||
let placeholder = format!("{{{}}}", original_name);
|
||||
let placeholder = format!("{{{}}}", param_name);
|
||||
match args_map.get(param_name) {
|
||||
Some(param_value) => {
|
||||
if let Some(str_val) = param_value.as_str() {
|
||||
@@ -295,7 +280,6 @@ pub fn substitute_path_params(
|
||||
pub fn build_query_string(
|
||||
args_map: &serde_json::Map<String, Value>,
|
||||
query_schema: &Option<Value>,
|
||||
query_field_renames: &Option<Value>,
|
||||
) -> String {
|
||||
let Some(schema) = query_schema else {
|
||||
return String::new();
|
||||
@@ -311,13 +295,11 @@ pub fn build_query_string(
|
||||
.get(param_name)
|
||||
.filter(|v| !v.is_null())
|
||||
.map(|value| {
|
||||
// Use the original name for the query parameter key
|
||||
let original_name = get_original_name(param_name, query_field_renames);
|
||||
let value_str = value.to_string();
|
||||
let str_val = value_str.trim_matches('"');
|
||||
format!(
|
||||
"{}={}",
|
||||
urlencoding::encode(&original_name),
|
||||
urlencoding::encode(param_name),
|
||||
urlencoding::encode(str_val)
|
||||
)
|
||||
})
|
||||
@@ -336,7 +318,6 @@ pub fn build_request_body(
|
||||
method: &str,
|
||||
args_map: &serde_json::Map<String, Value>,
|
||||
body_schema: &Option<Value>,
|
||||
body_field_renames: &Option<Value>,
|
||||
) -> Option<Value> {
|
||||
if method == "GET" {
|
||||
return None;
|
||||
@@ -348,11 +329,9 @@ pub fn build_request_body(
|
||||
let body_map: serde_json::Map<String, Value> = props
|
||||
.keys()
|
||||
.filter_map(|param_name| {
|
||||
args_map.get(param_name).map(|value| {
|
||||
// Use the original name as the key in the request body
|
||||
let original_name = get_original_name(param_name, body_field_renames);
|
||||
(original_name, value.clone())
|
||||
})
|
||||
args_map
|
||||
.get(param_name)
|
||||
.map(|value| (param_name.clone(), value.clone()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
pub use windmill_store::resources::*;
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
use axum::{ extract::{Extension, Path}, routing::get,
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
#[cfg(feature = "mcp")]
|
||||
|
||||
@@ -505,7 +505,7 @@ async fn route_job(
|
||||
match trigger.request_type {
|
||||
RequestType::SyncSse => {
|
||||
// Trigger the job (always async when streaming)
|
||||
let (uuid, _, early_return, _) = trigger_runnable_inner(
|
||||
let (uuid, _, _, _) = trigger_runnable_inner(
|
||||
&db,
|
||||
None,
|
||||
Some(user_db.clone()),
|
||||
@@ -553,7 +553,6 @@ async fn route_job(
|
||||
None,
|
||||
tx,
|
||||
None,
|
||||
early_return,
|
||||
);
|
||||
|
||||
let body = axum::body::Body::from_stream(
|
||||
|
||||
@@ -3,20 +3,7 @@ use crate::{
|
||||
DB,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::time::Instant;
|
||||
use uuid::Uuid;
|
||||
|
||||
static BENCHMARK_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
static SHARED_BENCH_ITERS: std::sync::LazyLock<Arc<AtomicU64>> =
|
||||
std::sync::LazyLock::new(|| Arc::new(AtomicU64::new(0)));
|
||||
|
||||
pub fn shared_bench_iters() -> Arc<AtomicU64> {
|
||||
SHARED_BENCH_ITERS.clone()
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PoolStats {
|
||||
@@ -49,10 +36,6 @@ pub struct BenchmarkInfo {
|
||||
pub start: Instant,
|
||||
#[serde(skip)]
|
||||
pub iters: u64,
|
||||
#[serde(skip)]
|
||||
seen_top_level: HashSet<Uuid>,
|
||||
#[serde(skip)]
|
||||
pub shared_iters: Arc<AtomicU64>,
|
||||
timings: Vec<BenchmarkIter>,
|
||||
pub iter_durations: Vec<u64>,
|
||||
pub total_duration: Option<u64>,
|
||||
@@ -60,11 +43,9 @@ pub struct BenchmarkInfo {
|
||||
}
|
||||
|
||||
impl BenchmarkInfo {
|
||||
pub fn new(shared_iters: Arc<AtomicU64>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
BenchmarkInfo {
|
||||
iters: 0,
|
||||
seen_top_level: HashSet::new(),
|
||||
shared_iters,
|
||||
timings: vec![],
|
||||
start: Instant::now(),
|
||||
iter_durations: vec![],
|
||||
@@ -83,23 +64,13 @@ impl BenchmarkInfo {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_top_level(&mut self, job_id: Uuid) -> bool {
|
||||
if self.seen_top_level.insert(job_id) {
|
||||
self.iters += 1;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn add_iter(&mut self, bench: BenchmarkIter, job_id: Uuid, is_top_level: bool) -> bool {
|
||||
let newly_counted = is_top_level && self.seen_top_level.insert(job_id);
|
||||
if newly_counted {
|
||||
pub fn add_iter(&mut self, bench: BenchmarkIter, inc_iters: bool) {
|
||||
if inc_iters {
|
||||
self.iters += 1;
|
||||
}
|
||||
let elapsed_total = bench.start.elapsed().as_nanos() as u64;
|
||||
self.timings.push(bench);
|
||||
self.iter_durations.push(elapsed_total);
|
||||
newly_counted
|
||||
}
|
||||
|
||||
pub fn write_to_file(&mut self, path: &str) -> anyhow::Result<()> {
|
||||
@@ -147,120 +118,12 @@ impl BenchmarkIter {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn benchmark_verify(benchmark_jobs: i32, db: &DB) {
|
||||
let benchmark_kind = std::env::var("BENCHMARK_KIND").unwrap_or("noop".to_string());
|
||||
|
||||
if benchmark_jobs <= 0 || benchmark_kind == "none" {
|
||||
return;
|
||||
}
|
||||
|
||||
// For flows, child jobs are created dynamically so only check top-level (parent_job IS NULL).
|
||||
// "parallelflow" inserts only 1 top-level flow regardless of benchmark_jobs.
|
||||
let expected_top_level = match benchmark_kind.as_str() {
|
||||
"parallelflow" => 1i64,
|
||||
_ => benchmark_jobs as i64,
|
||||
};
|
||||
|
||||
let row = sqlx::query!(
|
||||
"SELECT
|
||||
COUNT(*) FILTER (WHERE status = 'success') AS succeeded,
|
||||
COUNT(*) FILTER (WHERE status = 'failure') AS failed,
|
||||
COUNT(*) FILTER (WHERE status = 'canceled') AS canceled
|
||||
FROM v2_job_completed
|
||||
JOIN v2_job USING (id)
|
||||
WHERE v2_job.workspace_id = 'admins' AND v2_job.parent_job IS NULL",
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.expect("benchmark verify query failed");
|
||||
|
||||
let succeeded = row.succeeded.unwrap_or(0);
|
||||
let failed = row.failed.unwrap_or(0);
|
||||
let canceled = row.canceled.unwrap_or(0);
|
||||
let total = succeeded + failed + canceled;
|
||||
|
||||
let remaining_in_queue = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM v2_job_queue WHERE workspace_id = 'admins'",
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.expect("benchmark verify queue query failed")
|
||||
.unwrap_or(0);
|
||||
|
||||
println!("=== BENCHMARK VERIFICATION ===");
|
||||
println!(" kind: {benchmark_kind}");
|
||||
println!(" expected top-level: {expected_top_level}");
|
||||
println!(" completed total: {total}");
|
||||
println!(" succeeded: {succeeded}");
|
||||
println!(" failed: {failed}");
|
||||
println!(" canceled: {canceled}");
|
||||
println!(" still in queue: {remaining_in_queue}");
|
||||
|
||||
if failed > 0 || canceled > 0 {
|
||||
tracing::error!(
|
||||
"BENCHMARK VERIFICATION FAILED: {failed} failed, {canceled} canceled out of {total} completed"
|
||||
);
|
||||
}
|
||||
if remaining_in_queue > 0 {
|
||||
tracing::warn!(
|
||||
"BENCHMARK VERIFICATION: {remaining_in_queue} jobs still in queue after benchmark"
|
||||
);
|
||||
}
|
||||
if succeeded != expected_top_level {
|
||||
tracing::error!(
|
||||
"BENCHMARK VERIFICATION FAILED: expected {expected_top_level} succeeded top-level jobs, got {succeeded}"
|
||||
);
|
||||
} else if failed == 0 && canceled == 0 && remaining_in_queue == 0 {
|
||||
println!(" result: ALL PASSED");
|
||||
}
|
||||
println!("==============================");
|
||||
}
|
||||
|
||||
pub async fn benchmark_init(benchmark_jobs: i32, db: &DB) {
|
||||
use crate::{jobs::JobKind, scripts::ScriptLang};
|
||||
|
||||
// Only the first worker to reach this point runs init
|
||||
if BENCHMARK_INITIALIZED.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let benchmark_kind = std::env::var("BENCHMARK_KIND").unwrap_or("noop".to_string());
|
||||
|
||||
if benchmark_jobs > 0 {
|
||||
// Clean up data from previous benchmark runs
|
||||
sqlx::query!("DELETE FROM v2_job_completed WHERE workspace_id = 'admins'")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("failed to clean up v2_job_completed: {e:#}"));
|
||||
sqlx::query!("DELETE FROM v2_job_queue WHERE workspace_id = 'admins'")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("failed to clean up v2_job_queue: {e:#}"));
|
||||
sqlx::query!("DELETE FROM v2_job_status WHERE id IN (SELECT id FROM v2_job WHERE workspace_id = 'admins')")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("failed to clean up v2_job_status: {e:#}"));
|
||||
sqlx::query!("DELETE FROM v2_job_runtime WHERE id IN (SELECT id FROM v2_job WHERE workspace_id = 'admins')")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("failed to clean up v2_job_runtime: {e:#}"));
|
||||
sqlx::query("DELETE FROM job_perms WHERE workspace_id = 'admins'")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("failed to clean up job_perms: {e:#}"));
|
||||
sqlx::query!("DELETE FROM concurrency_key WHERE key LIKE 'bench_%' OR key LIKE 'u/admin/bench_%'")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("failed to clean up concurrency_key: {e:#}"));
|
||||
sqlx::query!("DELETE FROM concurrency_counter WHERE concurrency_id LIKE 'bench_%' OR concurrency_id LIKE 'u/admin/bench_%'")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("failed to clean up concurrency_counter: {e:#}"));
|
||||
sqlx::query!("DELETE FROM v2_job WHERE workspace_id = 'admins'")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("failed to clean up v2_job: {e:#}"));
|
||||
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
match benchmark_kind.as_str() {
|
||||
"dedicated" => {
|
||||
@@ -397,477 +260,6 @@ pub async fn benchmark_init(benchmark_jobs: i32, db: &DB) {
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert parallelflow jobs (4)"));
|
||||
}
|
||||
"sequentialflow" => {
|
||||
let uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_flow) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11)) RETURNING id",
|
||||
None::<i64>,
|
||||
None::<String>,
|
||||
JobKind::FlowPreview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"flow",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
serde_json::from_str::<serde_json::Value>(r#"
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "a",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"content": "export async function main() { return 'a'; }",
|
||||
"language": "deno",
|
||||
"input_transforms": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"content": "export async function main() { return 'b'; }",
|
||||
"language": "deno",
|
||||
"input_transforms": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"content": "export async function main() { return 'c'; }",
|
||||
"language": "deno",
|
||||
"input_transforms": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "d",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"content": "export async function main() { return 'd'; }",
|
||||
"language": "deno",
|
||||
"input_transforms": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"preprocessor_module": null
|
||||
}
|
||||
"#).unwrap(),
|
||||
benchmark_jobs,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert sequentialflow jobs (1)"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &uuids, "admins", "flow")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert sequentialflow jobs (2)"));
|
||||
sqlx::query!(
|
||||
"INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])",
|
||||
&uuids
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert sequentialflow jobs (3)"));
|
||||
sqlx::query!(
|
||||
"INSERT INTO v2_job_status (id, flow_status) SELECT unnest($1::uuid[]), $2",
|
||||
&uuids,
|
||||
serde_json::from_str::<serde_json::Value>(
|
||||
r#"
|
||||
{
|
||||
"step": 0,
|
||||
"modules": [
|
||||
{ "id": "a", "type": "WaitingForPriorSteps" },
|
||||
{ "id": "b", "type": "WaitingForPriorSteps" },
|
||||
{ "id": "c", "type": "WaitingForPriorSteps" },
|
||||
{ "id": "d", "type": "WaitingForPriorSteps" }
|
||||
],
|
||||
"cleanup_module": {},
|
||||
"failure_module": {
|
||||
"id": "failure",
|
||||
"type": "WaitingForPriorSteps"
|
||||
},
|
||||
"preprocessor_module": null
|
||||
}
|
||||
"#
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert sequentialflow jobs (4)"));
|
||||
}
|
||||
"scriptlogs" => {
|
||||
let uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_code) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11)) RETURNING id",
|
||||
None::<i64>,
|
||||
None::<String>,
|
||||
JobKind::Preview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"deno",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
"export async function main() { for (let i = 0; i < 1000; i++) { console.log('benchmark log line ' + i); } return 'done'; }",
|
||||
benchmark_jobs,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert scriptlogs jobs (1)"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &uuids, "admins", "deno")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert scriptlogs jobs (2)"));
|
||||
sqlx::query!(
|
||||
"INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])",
|
||||
&uuids
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert scriptlogs jobs (3)"));
|
||||
}
|
||||
"concurrencylimit" => {
|
||||
let uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_code, concurrent_limit, concurrency_time_window_s) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 FROM generate_series(1, $13)) RETURNING id",
|
||||
None::<i64>,
|
||||
Some("u/admin/bench_conclimit"),
|
||||
JobKind::Preview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"deno",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
"export async function main() { return 'done'; }",
|
||||
2i32,
|
||||
0i32,
|
||||
benchmark_jobs,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert concurrencylimit jobs (1)"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &uuids, "admins", "deno")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert concurrencylimit jobs (2)"));
|
||||
sqlx::query!(
|
||||
"INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])",
|
||||
&uuids
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert concurrencylimit jobs (3)"));
|
||||
let concurrency_id = "u/admin/bench_conclimit";
|
||||
sqlx::query!(
|
||||
"INSERT INTO concurrency_counter (concurrency_id, job_uuids) VALUES ($1, '{}'::jsonb) ON CONFLICT (concurrency_id) DO NOTHING",
|
||||
concurrency_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert concurrencylimit counter"));
|
||||
for uuid in &uuids {
|
||||
sqlx::query!(
|
||||
"INSERT INTO concurrency_key (key, ended_at, job_id) VALUES ($1, now() - INTERVAL '1 hour', $2)",
|
||||
concurrency_id,
|
||||
uuid,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert concurrencylimit key"));
|
||||
}
|
||||
}
|
||||
"concurrencykey" => {
|
||||
let uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_code, concurrent_limit, concurrency_time_window_s) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 FROM generate_series(1, $13)) RETURNING id",
|
||||
None::<i64>,
|
||||
Some("u/admin/bench_conckey"),
|
||||
JobKind::Preview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"deno",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
"export async function main() { return 'done'; }",
|
||||
1i32,
|
||||
0i32,
|
||||
benchmark_jobs,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert concurrencykey jobs (1)"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &uuids, "admins", "deno")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert concurrencykey jobs (2)"));
|
||||
sqlx::query!(
|
||||
"INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])",
|
||||
&uuids
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert concurrencykey jobs (3)"));
|
||||
let concurrency_id = "bench_shared_concurrency_key";
|
||||
sqlx::query!(
|
||||
"INSERT INTO concurrency_counter (concurrency_id, job_uuids) VALUES ($1, '{}'::jsonb) ON CONFLICT (concurrency_id) DO NOTHING",
|
||||
concurrency_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert concurrencykey counter"));
|
||||
for uuid in &uuids {
|
||||
sqlx::query!(
|
||||
"INSERT INTO concurrency_key (key, ended_at, job_id) VALUES ($1, now() - INTERVAL '1 hour', $2)",
|
||||
concurrency_id,
|
||||
uuid,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|_e| panic!("failed to insert concurrencykey key"));
|
||||
}
|
||||
}
|
||||
"mixed" => {
|
||||
let portion = benchmark_jobs / 5;
|
||||
let remainder = benchmark_jobs % 5;
|
||||
|
||||
// 1) noop jobs
|
||||
let noop_count = portion + remainder;
|
||||
let noop_uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9 FROM generate_series(1, $10)) RETURNING id",
|
||||
None::<i64>,
|
||||
None::<String>,
|
||||
JobKind::Noop as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"deno",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
noop_count,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed noop jobs"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &noop_uuids, "admins", "deno")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed noop queue"));
|
||||
sqlx::query!("INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", &noop_uuids)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed noop runtime"));
|
||||
|
||||
// 2) sequentialflow jobs
|
||||
if portion > 0 {
|
||||
let sf_uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_flow) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11)) RETURNING id",
|
||||
None::<i64>,
|
||||
None::<String>,
|
||||
JobKind::FlowPreview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"flow",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
serde_json::from_str::<serde_json::Value>(r#"{"modules":[{"id":"a","value":{"type":"rawscript","content":"export async function main() { return 'a'; }","language":"deno","input_transforms":{}}},{"id":"b","value":{"type":"rawscript","content":"export async function main() { return 'b'; }","language":"deno","input_transforms":{}}},{"id":"c","value":{"type":"rawscript","content":"export async function main() { return 'c'; }","language":"deno","input_transforms":{}}},{"id":"d","value":{"type":"rawscript","content":"export async function main() { return 'd'; }","language":"deno","input_transforms":{}}}],"preprocessor_module":null}"#).unwrap(),
|
||||
portion,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed sequentialflow jobs"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &sf_uuids, "admins", "flow")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed sequentialflow queue"));
|
||||
sqlx::query!("INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", &sf_uuids)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed sequentialflow runtime"));
|
||||
sqlx::query!(
|
||||
"INSERT INTO v2_job_status (id, flow_status) SELECT unnest($1::uuid[]), $2",
|
||||
&sf_uuids,
|
||||
serde_json::from_str::<serde_json::Value>(r#"{"step":0,"modules":[{"id":"a","type":"WaitingForPriorSteps"},{"id":"b","type":"WaitingForPriorSteps"},{"id":"c","type":"WaitingForPriorSteps"},{"id":"d","type":"WaitingForPriorSteps"}],"cleanup_module":{},"failure_module":{"id":"failure","type":"WaitingForPriorSteps"},"preprocessor_module":null}"#).unwrap()
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed sequentialflow status"));
|
||||
}
|
||||
|
||||
// 3) scriptlogs jobs
|
||||
if portion > 0 {
|
||||
let sl_uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_code) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11)) RETURNING id",
|
||||
None::<i64>,
|
||||
None::<String>,
|
||||
JobKind::Preview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"deno",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
"export async function main() { for (let i = 0; i < 1000; i++) { console.log('benchmark log line ' + i); } return 'done'; }",
|
||||
portion,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed scriptlogs jobs"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &sl_uuids, "admins", "deno")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed scriptlogs queue"));
|
||||
sqlx::query!("INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", &sl_uuids)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed scriptlogs runtime"));
|
||||
}
|
||||
|
||||
// 4) concurrencylimit jobs
|
||||
if portion > 0 {
|
||||
let cl_uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_code, concurrent_limit, concurrency_time_window_s) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 FROM generate_series(1, $13)) RETURNING id",
|
||||
None::<i64>,
|
||||
Some("u/admin/bench_conclimit"),
|
||||
JobKind::Preview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"deno",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
"export async function main() { return 'done'; }",
|
||||
2i32,
|
||||
0i32,
|
||||
portion,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencylimit jobs"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &cl_uuids, "admins", "deno")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencylimit queue"));
|
||||
sqlx::query!("INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", &cl_uuids)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencylimit runtime"));
|
||||
let cl_concurrency_id = "u/admin/bench_conclimit";
|
||||
sqlx::query!(
|
||||
"INSERT INTO concurrency_counter (concurrency_id, job_uuids) VALUES ($1, '{}'::jsonb) ON CONFLICT (concurrency_id) DO NOTHING",
|
||||
cl_concurrency_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencylimit counter"));
|
||||
for uuid in &cl_uuids {
|
||||
sqlx::query!(
|
||||
"INSERT INTO concurrency_key (key, ended_at, job_id) VALUES ($1, now() - INTERVAL '1 hour', $2)",
|
||||
cl_concurrency_id,
|
||||
uuid,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencylimit key"));
|
||||
}
|
||||
}
|
||||
|
||||
// 5) concurrencykey jobs
|
||||
if portion > 0 {
|
||||
let ck_uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_code, concurrent_limit, concurrency_time_window_s) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 FROM generate_series(1, $13)) RETURNING id",
|
||||
None::<i64>,
|
||||
Some("u/admin/bench_conckey"),
|
||||
JobKind::Preview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"deno",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
"export async function main() { return 'done'; }",
|
||||
1i32,
|
||||
0i32,
|
||||
portion,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencykey jobs"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &ck_uuids, "admins", "deno")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencykey queue"));
|
||||
sqlx::query!("INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", &ck_uuids)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencykey runtime"));
|
||||
let ck_concurrency_id = "bench_shared_concurrency_key";
|
||||
sqlx::query!(
|
||||
"INSERT INTO concurrency_counter (concurrency_id, job_uuids) VALUES ($1, '{}'::jsonb) ON CONFLICT (concurrency_id) DO NOTHING",
|
||||
ck_concurrency_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencykey counter"));
|
||||
for uuid in &ck_uuids {
|
||||
sqlx::query!(
|
||||
"INSERT INTO concurrency_key (key, ended_at, job_id) VALUES ($1, now() - INTERVAL '1 hour', $2)",
|
||||
ck_concurrency_id,
|
||||
uuid,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed concurrencykey key"));
|
||||
}
|
||||
}
|
||||
}
|
||||
"mixed_no_cc" => {
|
||||
let portion = benchmark_jobs / 3;
|
||||
let remainder = benchmark_jobs % 3;
|
||||
|
||||
// 1) noop jobs
|
||||
let noop_count = portion + remainder;
|
||||
let noop_uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9 FROM generate_series(1, $10)) RETURNING id",
|
||||
None::<i64>,
|
||||
None::<String>,
|
||||
JobKind::Noop as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"deno",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
noop_count,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc noop jobs"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &noop_uuids, "admins", "deno")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc noop queue"));
|
||||
sqlx::query!("INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", &noop_uuids)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc noop runtime"));
|
||||
|
||||
// 2) sequentialflow jobs
|
||||
if portion > 0 {
|
||||
let sf_uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_flow) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11)) RETURNING id",
|
||||
None::<i64>,
|
||||
None::<String>,
|
||||
JobKind::FlowPreview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"flow",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
serde_json::from_str::<serde_json::Value>(r#"{"modules":[{"id":"a","value":{"type":"rawscript","content":"export async function main() { return 'a'; }","language":"deno","input_transforms":{}}},{"id":"b","value":{"type":"rawscript","content":"export async function main() { return 'b'; }","language":"deno","input_transforms":{}}},{"id":"c","value":{"type":"rawscript","content":"export async function main() { return 'c'; }","language":"deno","input_transforms":{}}},{"id":"d","value":{"type":"rawscript","content":"export async function main() { return 'd'; }","language":"deno","input_transforms":{}}}],"preprocessor_module":null}"#).unwrap(),
|
||||
portion,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc sequentialflow jobs"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &sf_uuids, "admins", "flow")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc sequentialflow queue"));
|
||||
sqlx::query!("INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", &sf_uuids)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc sequentialflow runtime"));
|
||||
sqlx::query!(
|
||||
"INSERT INTO v2_job_status (id, flow_status) SELECT unnest($1::uuid[]), $2",
|
||||
&sf_uuids,
|
||||
serde_json::from_str::<serde_json::Value>(r#"{"step":0,"modules":[{"id":"a","type":"WaitingForPriorSteps"},{"id":"b","type":"WaitingForPriorSteps"},{"id":"c","type":"WaitingForPriorSteps"},{"id":"d","type":"WaitingForPriorSteps"}],"cleanup_module":{},"failure_module":{"id":"failure","type":"WaitingForPriorSteps"},"preprocessor_module":null}"#).unwrap()
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc sequentialflow status"));
|
||||
}
|
||||
|
||||
// 3) scriptlogs jobs
|
||||
if portion > 0 {
|
||||
let sl_uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_code) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11)) RETURNING id",
|
||||
None::<i64>,
|
||||
None::<String>,
|
||||
JobKind::Preview as JobKind,
|
||||
ScriptLang::Deno as ScriptLang,
|
||||
"deno",
|
||||
"admin",
|
||||
"u/admin",
|
||||
"admin@windmill.dev",
|
||||
"admins",
|
||||
"export async function main() { for (let i = 0; i < 1000; i++) { console.log('benchmark log line ' + i); } return 'done'; }",
|
||||
portion,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc scriptlogs jobs"));
|
||||
sqlx::query!("INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", &sl_uuids, "admins", "deno")
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc scriptlogs queue"));
|
||||
sqlx::query!("INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", &sl_uuids)
|
||||
.execute(&mut *tx)
|
||||
.await.unwrap_or_else(|_e| panic!("failed to insert mixed_no_cc scriptlogs runtime"));
|
||||
}
|
||||
}
|
||||
"none" => {}
|
||||
_ => {
|
||||
let uuids = sqlx::query_scalar!("INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9 FROM generate_series(1, $10)) RETURNING id",
|
||||
@@ -896,16 +288,6 @@ pub async fn benchmark_init(benchmark_jobs: i32, db: &DB) {
|
||||
.unwrap_or_else(|_e| panic!("failed to insert noop jobs (3)"));
|
||||
}
|
||||
}
|
||||
// Insert job_perms for all benchmark jobs so workers don't fall back to slow permission lookups
|
||||
sqlx::query(
|
||||
"INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, groups, folders, workspace_id)
|
||||
SELECT id, 'admin@windmill.dev', 'admin', true, false, ARRAY['all']::text[], ARRAY[]::jsonb[], 'admins'
|
||||
FROM v2_job WHERE workspace_id = 'admins'"
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("failed to insert job_perms: {e:#}"));
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ impl IntoResponse for Error {
|
||||
let status = match self {
|
||||
Self::NotFound(_) => axum::http::StatusCode::NOT_FOUND,
|
||||
Self::NotAuthorized(_) => axum::http::StatusCode::UNAUTHORIZED,
|
||||
Self::RequireAdmin(_) | Self::PermissionDenied(_) => axum::http::StatusCode::FORBIDDEN,
|
||||
Self::RequireAdmin(_) => axum::http::StatusCode::FORBIDDEN,
|
||||
Self::SqlErr { .. }
|
||||
| Self::BadRequest(_)
|
||||
| Self::AIError(_)
|
||||
|
||||
@@ -664,6 +664,153 @@ pub async fn get_database_url() -> Result<DatabaseUrl, Error> {
|
||||
Ok(database_url.clone())
|
||||
}
|
||||
|
||||
pub async fn initial_connection() -> Result<sqlx::Pool<sqlx::Postgres>, error::Error> {
|
||||
let connect_options = get_database_url().await?.connect_options().await?;
|
||||
sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(2)
|
||||
.connect_with(connect_options)
|
||||
.await
|
||||
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))
|
||||
}
|
||||
|
||||
pub async fn connect_db(
|
||||
server_mode: bool,
|
||||
indexer_mode: bool,
|
||||
worker_mode: bool,
|
||||
#[cfg(feature = "private")] mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> anyhow::Result<sqlx::Pool<sqlx::Postgres>> {
|
||||
use anyhow::Context;
|
||||
|
||||
let database_url = get_database_url().await?;
|
||||
|
||||
let max_connections = match std::env::var("DATABASE_CONNECTIONS") {
|
||||
Ok(n) => n.parse::<u32>().context("invalid DATABASE_CONNECTIONS")?,
|
||||
Err(_) => {
|
||||
if server_mode {
|
||||
DEFAULT_MAX_CONNECTIONS_SERVER
|
||||
} else if indexer_mode {
|
||||
DEFAULT_MAX_CONNECTIONS_INDEXER
|
||||
} else {
|
||||
DEFAULT_MAX_CONNECTIONS_WORKER
|
||||
+ std::env::var("NUM_WORKERS")
|
||||
.ok()
|
||||
.map(|x| x.parse().ok())
|
||||
.flatten()
|
||||
.unwrap_or(1)
|
||||
- 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let pool = connect(database_url.clone(), max_connections, worker_mode).await?;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
let pool2 = pool.clone();
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
if let DatabaseUrl::IamRds(database_url) = database_url {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = killpill_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
|
||||
let needs_refresh = {
|
||||
let read_guard = database_url.read().await;
|
||||
read_guard.needs_refresh()
|
||||
};
|
||||
if needs_refresh {
|
||||
let new_url = tokio::time::timeout(std::time::Duration::from_secs(10), get_database_url()).await;
|
||||
match new_url {
|
||||
Ok(Ok(new_url)) => {
|
||||
match new_url.connect_options().await {
|
||||
Ok(connect_options) => {
|
||||
pool2.set_connect_options(connect_options);
|
||||
tracing::info!("Refreshed IAM RDS URL successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error getting IAM RDS connect options, retrying in 10s: {}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!("Error refreshing IAM RDS URL, trying again in 10s: {}", e);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Timeout after 10s refreshing IAM RDS URL, trying again in 10 seconds: {}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
database_url: DatabaseUrl,
|
||||
max_connections: u32,
|
||||
worker_mode: bool,
|
||||
) -> Result<sqlx::Pool<sqlx::Postgres>, error::Error> {
|
||||
use sqlx::Executor;
|
||||
use std::time::Duration;
|
||||
sqlx::postgres::PgPoolOptions::new()
|
||||
.min_connections((max_connections / 5).clamp(3, max_connections))
|
||||
.max_connections(max_connections)
|
||||
.max_lifetime(Duration::from_secs(30 * 60)) // 30 mins
|
||||
.after_connect(move |conn, _| {
|
||||
if worker_mode {
|
||||
Box::pin(async move {
|
||||
if let Err(e) = conn
|
||||
.execute(
|
||||
r#"
|
||||
SET enable_seqscan = OFF;
|
||||
SET statement_timeout = '5min';
|
||||
SET idle_in_transaction_session_timeout = '10min';
|
||||
SET tcp_keepalives_idle = 300;
|
||||
SET tcp_keepalives_interval = 60;
|
||||
SET tcp_keepalives_count = 10;"#,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error setting postgres settings: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Box::pin(async move {
|
||||
if let Err(e) = conn
|
||||
.execute(
|
||||
r#"
|
||||
SET statement_timeout = '5min';
|
||||
SET idle_in_transaction_session_timeout = '10min';
|
||||
SET tcp_keepalives_idle = 300;
|
||||
SET tcp_keepalives_interval = 60;
|
||||
SET tcp_keepalives_count = 10;"#,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error setting postgres settings: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
})
|
||||
.connect_with(
|
||||
database_url
|
||||
.connect_options()
|
||||
.await?
|
||||
.statement_cache_capacity(400),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))
|
||||
}
|
||||
|
||||
type Tag = String;
|
||||
|
||||
pub use db::DB;
|
||||
@@ -705,9 +852,11 @@ impl ScriptHashInfo<ScriptRunnableSettingsHandle> {
|
||||
self,
|
||||
db: &DB,
|
||||
) -> error::Result<ScriptHashInfo<ScriptRunnableSettingsInline>> {
|
||||
let rs =
|
||||
runnable_settings::from_handle(self.runnable_settings.runnable_settings_handle, db)
|
||||
.await?;
|
||||
let rs = runnable_settings::from_handle(
|
||||
self.runnable_settings.runnable_settings_handle,
|
||||
db,
|
||||
)
|
||||
.await?;
|
||||
let (debouncing_settings, concurrency_settings) =
|
||||
runnable_settings::prefetch_cached(&rs, db).await?;
|
||||
|
||||
@@ -1027,25 +1176,25 @@ pub fn get_flow_version_info_from_version<
|
||||
_ => {
|
||||
tracing::debug!("Fetching flow version info for {version} ({path})");
|
||||
let mut conn = db.acquire().await?;
|
||||
let flow_info =
|
||||
let flow_info =
|
||||
sqlx::query_as!(
|
||||
FlowVersionInfo,
|
||||
r#"
|
||||
SELECT
|
||||
flow_version.id AS version,
|
||||
flow_version.value->>'early_return' as early_return,
|
||||
flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor,
|
||||
(flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled,
|
||||
flow.tag,
|
||||
flow.dedicated_worker,
|
||||
flow.on_behalf_of_email,
|
||||
flow_version.value->>'early_return' as early_return,
|
||||
flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor,
|
||||
(flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled,
|
||||
flow.tag,
|
||||
flow.dedicated_worker,
|
||||
flow.on_behalf_of_email,
|
||||
flow.edited_by
|
||||
FROM
|
||||
FROM
|
||||
flow_version
|
||||
INNER JOIN flow
|
||||
ON flow.path = flow_version.path AND
|
||||
flow.workspace_id = flow_version.workspace_id
|
||||
WHERE
|
||||
WHERE
|
||||
flow_version.workspace_id = $1 AND
|
||||
flow_version.path = $2 AND
|
||||
flow_version.id = $3
|
||||
|
||||
@@ -145,13 +145,6 @@ lazy_static::lazy_static! {
|
||||
tracing::info!("Mode not specified, defaulting to standalone");
|
||||
Mode::Standalone
|
||||
});
|
||||
#[cfg(feature = "benchmark")]
|
||||
let mode = {
|
||||
if mode != Mode::Worker {
|
||||
println!("Benchmark mode: forcing MODE=worker");
|
||||
}
|
||||
Mode::Worker
|
||||
};
|
||||
ModeAndAddons {
|
||||
indexer: search_addon,
|
||||
mode,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user