Compare commits

..

4 Commits

Author SHA1 Message Date
centdix
373864e586 Update backend/windmill-api/src/lib.rs
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-06-03 16:13:25 +02:00
centdix
25c8e6e090 update ref 2025-06-03 16:04:10 +02:00
centdix
9246512561 add route 2025-06-03 16:00:42 +02:00
centdix
40712ecf7b add inkeep file 2025-06-03 15:57:12 +02:00
740 changed files with 19968 additions and 36936 deletions

View File

@@ -51,7 +51,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,nats,sqs_trigger,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages_windows,mcp,private
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,nats,sqs_trigger,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,mcp,private
- name: Rename binary with corresponding architecture
run: |
Rename-Item -Path ".\backend\target\release\windmill.exe" -NewName "windmill-ee.exe"

View File

@@ -12,12 +12,12 @@ jobs:
- name: Check organization membership
id: check-membership
env:
ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMENTER: ${{ github.event.comment.user.login }}
run: |
ORG="windmill-labs"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $ORG_ACCESS_TOKEN" \
-H "Authorization: token $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/orgs/$ORG/members/$COMMENTER")

View File

@@ -17,11 +17,8 @@ jobs:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_STATUS: "opened"
PR_NUMBER: ${{ github.event.pull_request.number }}
DISCORD_CHANNEL_ID: "1372204995868491786"
DISCORD_GUILD_ID: "930051556043276338"
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_PR_REVIEWS_WEBHOOK }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
merge_success_emoji:
if: github.event.pull_request.merged == true

View File

@@ -13,7 +13,7 @@ jobs:
uses: actions/checkout@v3
with:
repository: windmill-labs/windmill-helm-charts
token: ${{ secrets.HELM_CHART_TOKEN }}
token: ${{ secrets.DOCS_TOKEN }}
- name: Get version
id: get_version
@@ -57,7 +57,7 @@ jobs:
- name: Create PR
env:
GH_TOKEN: ${{ secrets.HELM_CHART_TOKEN }}
GH_TOKEN: ${{ secrets.DOCS_TOKEN }}
run: |
gh pr create \
--title "helm: bump version to ${{ env.VERSION }}" \

View File

@@ -53,7 +53,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages_windows,mcp,private
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages,mcp,private
- name: Rename binary with corresponding architecture
run: |
Rename-Item -Path ".\backend\target\release\windmill.exe" -NewName "windmill-ee.exe"

View File

@@ -1,19 +0,0 @@
name: Publish rust-client to crates.io on release
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
build_rust_and_publish_to_crates_io:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v20
with:
extra_nix_config: |
experimental-features = nix-command flakes
- run: cd rust-client && nix develop ../ --command ./dev.nu --check --publish
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}

View File

@@ -38,45 +38,24 @@ jobs:
- name: Send Discord notification and start a thread
env:
WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
CHANNEL_ID: ${{ inputs.DISCORD_CHANNEL_ID }}
GUILD_ID: ${{ inputs.DISCORD_GUILD_ID }}
PR_TITLE: ${{ inputs.PR_TITLE }}
PR_NUMBER: ${{ inputs.PR_NUMBER }}
PR_URL: ${{ inputs.PR_URL }}
PR_AUTHOR: ${{ inputs.PR_AUTHOR }}
run: |
# Check if thread already exists
thread_exists=false
if threads=$(curl -s -H "Authorization: Bot $BOT_TOKEN" "https://discord.com/api/v10/guilds/${GUILD_ID}/threads/active"); then
if thread_id=$(echo "$threads" | jq -r --arg cid "$CHANNEL_ID" --arg pref "#${PR_NUMBER}:" '.threads[] | select(.parent_id == $cid and (.name | startswith($pref))) | .id' 2>/dev/null); then
if [ -n "$thread_id" ]; then
thread_exists=true
echo "Thread already exists, skipping creation"
fi
fi
else
echo "Failed to check for existing threads, will create new thread"
fi
# Create thread if it doesn't exist or if check failed
if [ "$thread_exists" = false ]; then
echo "Creating new thread"
THREAD_TITLE="#${PR_NUMBER}: ${PR_TITLE} by \`${PR_AUTHOR}\`"
payload=$(jq -n \
--arg content "${PR_URL}" \
--arg thread "${THREAD_TITLE:0:99}" \
'{
content: $content,
thread_name: $thread,
auto_archive_duration: 10080
}'
)
curl -H "Content-Type: application/json" \
-X POST \
-d "$payload" \
"$WEBHOOK_URL"
fi
payload=$(jq -n \
--arg content "${PR_URL}" \
--arg thread "#${PR_NUMBER}: $PR_TITLE by \`${PR_AUTHOR}\`" \
'{
content: $content,
thread_name: $thread,
auto_archive_duration: 10080
}'
)
curl -H "Content-Type: application/json" \
-X POST \
-d "$payload" \
"$WEBHOOK_URL"
merge_success_emoji:
runs-on: ubuntu-latest

View File

@@ -1,111 +1,5 @@
# Changelog
## [1.498.0](https://github.com/windmill-labs/windmill/compare/v1.497.2...v1.498.0) (2025-06-17)
### Features
* use provider api to list available AI models in workspace settings ([#5947](https://github.com/windmill-labs/windmill/issues/5947)) ([7490e88](https://github.com/windmill-labs/windmill/commit/7490e883d747a7f65b2fefd3ec14b1cfc3d9bbd4))
* windmill http triggers and webhooks to openapi spec ([#5918](https://github.com/windmill-labs/windmill/issues/5918)) ([aba8c01](https://github.com/windmill-labs/windmill/commit/aba8c01d7f44ba4be369a3c711be9e156d6bf215))
## [1.497.2](https://github.com/windmill-labs/windmill/compare/v1.497.1...v1.497.2) (2025-06-17)
### Bug Fixes
* always rm containers in docker mode ([38eb71b](https://github.com/windmill-labs/windmill/commit/38eb71bdf55ee2f606d1d2ad2e987d5af16d88c0))
* flow steps use their tags if any specific when used as subflow ([26bec05](https://github.com/windmill-labs/windmill/commit/26bec054a3447a91c5d5f56d8b98717c06496087))
## [1.497.1](https://github.com/windmill-labs/windmill/compare/v1.497.0...v1.497.1) (2025-06-16)
### Bug Fixes
* fix mcp server initialization ([1c6a7c8](https://github.com/windmill-labs/windmill/commit/1c6a7c8cd0bd8396f158e3cb0583b927ce957f12))
## [1.497.0](https://github.com/windmill-labs/windmill/compare/v1.496.3...v1.497.0) (2025-06-16)
### Features
* add api tools to ai chat ([#5921](https://github.com/windmill-labs/windmill/issues/5921)) ([f7a83c0](https://github.com/windmill-labs/windmill/commit/f7a83c03c12b8ae70179fb228e0e2391b6ea2858))
* **backend:** use streamable http in favor of sse for MCP ([#5910](https://github.com/windmill-labs/windmill/issues/5910)) ([d47c078](https://github.com/windmill-labs/windmill/commit/d47c078bb5ab86d82d9cbbce3c55c89c0c20d809))
* better graph layout algorithm + migrate to svelte 5 almost everywhere + xyflow 1.0 ([23920ae](https://github.com/windmill-labs/windmill/commit/23920aee84fdca4a557a34ff2d66a0bb7bdca605))
* fill runnable inputs with AI chat ([#5887](https://github.com/windmill-labs/windmill/issues/5887)) ([b4a6a7e](https://github.com/windmill-labs/windmill/commit/b4a6a7e72429617d420af85a9de35bb13adfc6fb))
* **go:** local go.mod ([#5929](https://github.com/windmill-labs/windmill/issues/5929)) ([0b89260](https://github.com/windmill-labs/windmill/commit/0b89260540b307c6d614ca4275dd038fbfdac33c))
* multiple azure models support ([#5920](https://github.com/windmill-labs/windmill/issues/5920)) ([f412ede](https://github.com/windmill-labs/windmill/commit/f412ede6ed48e9a492f39582ac70a5584477529e))
* **rust:** add rust sdk ([#5909](https://github.com/windmill-labs/windmill/issues/5909)) ([332f66e](https://github.com/windmill-labs/windmill/commit/332f66e3483abbeacd4e7c1b74c94c5265314882))
### Bug Fixes
* ai chat tooltip + user settings autocomplete issue ([#5917](https://github.com/windmill-labs/windmill/issues/5917)) ([6f907c7](https://github.com/windmill-labs/windmill/commit/6f907c79b4cf6279bd52e35a3ee96e0d021422f5))
* audit logs for token refresh + consider refresh for active users ([#5930](https://github.com/windmill-labs/windmill/issues/5930)) ([cf2d09e](https://github.com/windmill-labs/windmill/commit/cf2d09e7a8c5d2472af0d483689c3fcfa2976117))
* fix input with wrong height on first render ([#5935](https://github.com/windmill-labs/windmill/issues/5935)) ([1a6283b](https://github.com/windmill-labs/windmill/commit/1a6283b42a6a514ab2e05160855cdc0f70b61d0e))
* flow step missing input warnings ([#5916](https://github.com/windmill-labs/windmill/issues/5916)) ([f077849](https://github.com/windmill-labs/windmill/commit/f077849b8f7c1916fd420e85b4844a5c5e93a139))
* **frontend:** use correct kind for flow insert module btn ([#5938](https://github.com/windmill-labs/windmill/issues/5938)) ([17c8c8a](https://github.com/windmill-labs/windmill/commit/17c8c8a5616ab8656799cea3fc5bc7cfaedc4995))
## [1.496.3](https://github.com/windmill-labs/windmill/compare/v1.496.2...v1.496.3) (2025-06-09)
### Bug Fixes
* improve concurrent job parallelism performance ([e8836a3](https://github.com/windmill-labs/windmill/commit/e8836a393a872bb91e68ba0037681caf24149470))
* Prioritize diff contexts in script mode for ai chat ([#5888](https://github.com/windmill-labs/windmill/issues/5888)) ([a47939d](https://github.com/windmill-labs/windmill/commit/a47939d13c30e2d4b41efd539f845959174d4fb1))
## [1.496.2](https://github.com/windmill-labs/windmill/compare/v1.496.1...v1.496.2) (2025-06-07)
### Bug Fixes
* add clearable by default for select ([#5900](https://github.com/windmill-labs/windmill/issues/5900)) ([b44b9c1](https://github.com/windmill-labs/windmill/commit/b44b9c1b82116ad5487af95d1f78226d56c75179))
## [1.496.1](https://github.com/windmill-labs/windmill/compare/v1.496.0...v1.496.1) (2025-06-07)
### Bug Fixes
* never consider minor version for global site packages ([#5893](https://github.com/windmill-labs/windmill/issues/5893)) ([22b2f49](https://github.com/windmill-labs/windmill/commit/22b2f4988db9314f2403508933d0aa932187c668))
## [1.496.0](https://github.com/windmill-labs/windmill/compare/v1.495.1...v1.496.0) (2025-06-06)
### Features
* generate http route triggers from openapi spec ([#5857](https://github.com/windmill-labs/windmill/issues/5857)) ([5713483](https://github.com/windmill-labs/windmill/commit/571348377b73d54b4d2a1c5775ab00b247b01910))
### Bug Fixes
* allow fileupload drag and drop in edit mode on full component without triggering file picker ([#5889](https://github.com/windmill-labs/windmill/issues/5889)) ([9ae3212](https://github.com/windmill-labs/windmill/commit/9ae3212a1e0f88a8297bf41ab53e3c1be4bcc56c))
* **python:** account instance version when cli deploy and local lockfile ([#5894](https://github.com/windmill-labs/windmill/issues/5894)) ([ec552d5](https://github.com/windmill-labs/windmill/commit/ec552d5ef6fdb5e824e453f196f9cf16629ee2ea))
* use full client side js library for route gen from openapi ([#5891](https://github.com/windmill-labs/windmill/issues/5891)) ([3c3fdbd](https://github.com/windmill-labs/windmill/commit/3c3fdbdf26a9581b815210839b91ebdedb924093))
## [1.495.0](https://github.com/windmill-labs/windmill/compare/v1.494.0...v1.495.0) (2025-06-05)
### Features
* Add ask mode to AI chat ([#5878](https://github.com/windmill-labs/windmill/issues/5878)) ([67ab469](https://github.com/windmill-labs/windmill/commit/67ab46990ad0c9fad810a64c54297419c6151c79))
* add navigator mode to AIChat and unify UI ([#5859](https://github.com/windmill-labs/windmill/issues/5859)) ([cbba829](https://github.com/windmill-labs/windmill/commit/cbba8297cd4c1caa21b96a8422bbbd5c306b8398))
* ai flow chat ([#5842](https://github.com/windmill-labs/windmill/issues/5842)) ([68ebf66](https://github.com/windmill-labs/windmill/commit/68ebf667d5c0bc306329d0b55a3cc59e5b4862cb))
* ai prompts improvements + o3/o4 support ([#5862](https://github.com/windmill-labs/windmill/issues/5862)) ([825422c](https://github.com/windmill-labs/windmill/commit/825422c48456b2c9b230e1a35914b3fbf7d1e836))
* connect fix btn in flow editor to ai chat ([#5863](https://github.com/windmill-labs/windmill/issues/5863)) ([6247d15](https://github.com/windmill-labs/windmill/commit/6247d159ce25ae13f6fbc5c105df88305ce29451))
* fix backward compatibility pg 14 for postgres trigger ([#5851](https://github.com/windmill-labs/windmill/issues/5851)) ([4cbcbdb](https://github.com/windmill-labs/windmill/commit/4cbcbdb960b469acf773d3943128b6c7d0dcb0b8))
* ssh repl like direct to workers hosts machine ([#5809](https://github.com/windmill-labs/windmill/issues/5809)) ([f252657](https://github.com/windmill-labs/windmill/commit/f2526571a3614156b2b1e5cc91b15d0c57565d99))
* use rust-postgres client instead of sqlx for postgres trigger ([#5853](https://github.com/windmill-labs/windmill/issues/5853)) ([39dbd64](https://github.com/windmill-labs/windmill/commit/39dbd646b9683e0ad8de047cca786ae468759e77))
### Bug Fixes
* broken event dispatch for simpleditor ([#5879](https://github.com/windmill-labs/windmill/issues/5879)) ([df4992a](https://github.com/windmill-labs/windmill/commit/df4992a9295ed188c2a2cb0a5dfd3e33ae2e2dcb))
* cannot parse INSTANCE_PYTHON_VERSION ([#5874](https://github.com/windmill-labs/windmill/issues/5874)) ([a0b302d](https://github.com/windmill-labs/windmill/commit/a0b302d2c58d4245260376cf280bc866be91717c))
* fix regex that extract workspaces from custom tags ([#5876](https://github.com/windmill-labs/windmill/issues/5876)) ([1551dc8](https://github.com/windmill-labs/windmill/commit/1551dc8af22f6ea41f68290ace4c58f936c47745))
* nit ai flow prompt ([#5867](https://github.com/windmill-labs/windmill/issues/5867)) ([3e769f0](https://github.com/windmill-labs/windmill/commit/3e769f0c591b80138b3a356d147228675756452f))
* **python:** assign PATCH version to python runtime only when needed ([#5866](https://github.com/windmill-labs/windmill/issues/5866)) ([50a5c1f](https://github.com/windmill-labs/windmill/commit/50a5c1f56a7e45882fa0095203de709571e149bb))
* remove duplicate tools from script ai chat ([#5880](https://github.com/windmill-labs/windmill/issues/5880)) ([fe4a767](https://github.com/windmill-labs/windmill/commit/fe4a767df0e6f46fd0c0fd21b4116c7375978bf9))
* replace crypto.randomUUID with generateRandomString for HTTP compatibility ([#5849](https://github.com/windmill-labs/windmill/issues/5849)) ([64f35d0](https://github.com/windmill-labs/windmill/commit/64f35d050fb0d1008ce7142fd62d500845e62c4a)), closes [#5847](https://github.com/windmill-labs/windmill/issues/5847)
## [1.494.0](https://github.com/windmill-labs/windmill/compare/v1.493.4...v1.494.0) (2025-05-31)

View File

@@ -1,3 +1,3 @@
To have an overview of what this app does, see @.cursor/rules/windmill-overview.mdc
For backend modifications, follow the rules mentioned here @.cursor/rules/rust-best-practices.mdc. You also have access to a summarized version of the database schema here @backend/summarized_schema.txt
For backend modifications, follow the rules mentioned here @.cursor/rules/rust-best-practices.mdc
For frontend modifications, follow the rules mentioned here @.cursor/rules/svelte5-best-practices.mdc

View File

@@ -367,11 +367,10 @@ you to have it being synced automatically everyday.
## Run a local dev setup
Using [Nix](./frontend/README_DEV.md#nix) (Recommended).
See the [./frontend/README_DEV.md](./frontend/README_DEV.md) file for all
running options.
Using [Nix](./frontend/README_DEV.md#nix).
### only Frontend

View File

@@ -61,8 +61,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false
]
},
"hash": "0de53d764bbcf44d76cd8e47f7b2cd49e4632f03f16ee6f34d9a2c0842890f05"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "CREATE INDEX CONCURRENTLY IF NOT EXISTS v2_job_queue_suspend ON v2_job_queue (workspace_id, suspend) WHERE suspend > 0;",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "19f0ccadd3ee44719a781ea0d73ea4e45f5b2c3d5c0aa5dbecf9ea9838881b74"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "1d87f41fd1abb9361d795a899120e6b77e24bf5a9044fdc5284d0d7f1e14eafa"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "2bf5f7f2cf9d85a5d23e5db2f7616fb41fece9b3d46fde2d546d70b46f9008e3"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage)\n VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)\n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "usage",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "2e9b3e718440f3c5269e9217a13076c565f3add98b6768b5476bd3afed11ea31"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -67,5 +67,5 @@
true
]
},
"hash": "5430f7728c1e9b539cc8aad29ca9e6733943278998d3df62a9486607827e59ec"
"hash": "3895cee539a24b4c6ea89fa7a835fc62bc93b0530efba09fc3c32a8f93eaabb1"
}

View File

@@ -1,170 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n summary,\n description,\n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "route_path_key",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 10,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 13,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 15,
"name": "authentication_method: _",
"type_info": {
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
}
},
{
"ordinal": 16,
"name": "static_asset_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"name": "is_static_website",
"type_info": "Bool"
},
{
"ordinal": 18,
"name": "authentication_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 19,
"name": "wrap_body",
"type_info": "Bool"
},
{
"ordinal": 20,
"name": "raw_string",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
true,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
false
]
},
"hash": "39401cb0db8d367b5beb2be0c13aa7595adae0eac4e4e3a888cb12b972d1a7ce"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false
]
},
"hash": "3aaa6b6e362b10f2f3546b8595c60ab725295e0f20cb5f85d5fddc14d503a119"
}

View File

@@ -1,60 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n summary = $16,\n description = $17,\n edited_at = now(), \n is_static_website = $18\n WHERE \n workspace_id = $19 AND \n path = $20\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bool",
"Bool",
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
},
"Varchar",
"Text",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "3f05e6186050a7ce6d8efb41067d3c5282319fe7e041f114e02fb22b91716637"
}

View File

@@ -1,169 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "route_path_key",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "authentication_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 8,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 15,
"name": "authentication_method: _",
"type_info": {
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
}
},
{
"ordinal": 16,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 17,
"name": "static_asset_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 18,
"name": "is_static_website",
"type_info": "Bool"
},
{
"ordinal": 19,
"name": "wrap_body",
"type_info": "Bool"
},
{
"ordinal": 20,
"name": "raw_string",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
false,
true,
true,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "4228b098883408323bd8413ee094454b95962047458a6927d19ac0d3e7b3f0fa"
}

View File

@@ -70,8 +70,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -137,8 +137,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path,\n summary,\n description\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "4dc21dda58758a84bbc1b7f9328efb9806223d1684c53907ff825bc0228ec18d"
}

View File

@@ -1,58 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n tag, \n script_lang AS \"script_lang!: _\"\n FROM \n v2_job\n WHERE \n id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_lang!: _",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
]
}
}
}
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true
]
},
"hash": "4e5273b9ce05f6ee2dfd5f14c8574a0cf43682480452f7dbe23012320fe7fe25"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue q SET suspend = 0\n FROM v2_job j, v2_job_status f\n WHERE parent_job = $1\n AND f.id = j.id AND q.id = j.id\n AND suspend = $2 AND (f.flow_status->'step')::int = 0",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Int4"
]
},
"nullable": []
},
"hash": "553108ba3c0b8d579800bc8b5a4f887d79fb4c13b60b19c4913a8db18521958c"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage)\n VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)\n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "usage",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "56b2326015fde12b1a4efa226518566101dd27a0f3363884781071d417f8b7e7"
}

View File

@@ -61,8 +61,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage)\n VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)\n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "usage",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "621e9a2a53187dac3ebed62f0d645b692815f1594bf302dbebd5f80d5d22b98e"
}

View File

@@ -1,55 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 2,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "workspaced_route",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"TextArray",
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "6b6aca712df657f81e74a438f6753bc72e787a3f768040c8c73ea86313badb71"
}

View File

@@ -147,8 +147,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -1,93 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route,\n summary,\n description,\n authentication_method AS \"authentication_method: _\",\n authentication_resource_path\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 2,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "authentication_method: _",
"type_info": {
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
}
},
{
"ordinal": 7,
"name": "authentication_resource_path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"TextArray",
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true,
false,
true
]
},
"hash": "714fb0f66ceb536aee8cb9ae0144757b999d25870fda37fe904e09dd5c742015"
}

View File

@@ -34,8 +34,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT distinct(path) FROM script WHERE workspace_id = $1 AND archived = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "8373b2649ab46310860adbdd7b717261771ac61d46d82d42d085ffebeb18be06"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage)\n VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)\n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "usage",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "83f64dd93b1ddc03b84681d65d9be69959987cbac1d83b64225fd1bf9ab047c9"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE worker_ping SET\nping_at = now(),\njobs_executed = 1,\ncurrent_job_id = $1,\ncurrent_job_workspace_id = 'admins'\nWHERE worker = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "997586ac14384db2c0eeee1bb3382cc6ae013695d0cda6da9ab848ca1b9a9606"
}

View File

@@ -36,8 +36,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -41,8 +41,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -65,8 +65,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE worker_ping SET \nping_at = now(), \njobs_executed = 1, \ncurrent_job_id = $1, \ncurrent_job_workspace_id = 'admins' \nWHERE worker = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "c3025cdb6e421e1225d420e8b1efd18d1dd3bb2fac53c1f2df648b61fb7488aa"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "ca3ba808e020c8c7a35eaef842b20cfeee64fd47ded72fce55cc75e0bbb291a8"
}

View File

@@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users as (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n active_authors as (SELECT distinct email FROM usr WHERE usr.operator IS false AND email IN (SELECT email FROM active_users)),\n active_authors_agg as (SELECT array_agg(email) as authors FROM active_authors),\n active_ops_agg as (SELECT array_agg(email) as operators from active_users WHERE email NOT IN (SELECT email FROM active_authors))\n SELECT active_authors_agg.authors, active_ops_agg.operators, array_length(active_authors_agg.authors, 1) as author_count, array_length(active_ops_agg.operators, 1) as operator_count FROM active_authors_agg, active_ops_agg",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "authors",
"type_info": "VarcharArray"
},
{
"ordinal": 1,
"name": "operators",
"type_info": "VarcharArray"
},
{
"ordinal": 2,
"name": "author_count",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "operator_count",
"type_info": "Int4"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null,
null
]
},
"hash": "cb3862634f18160207ee2621ddfca43f00456a27fda32583846497116f92f96c"
}

View File

@@ -70,8 +70,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users as (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n active_authors as (SELECT distinct email FROM usr WHERE usr.operator IS false AND email IN (SELECT email FROM active_users)),\n active_authors_agg as (SELECT array_agg(email) as authors FROM active_authors),\n active_ops_agg as (SELECT array_agg(email) as operators from active_users WHERE email NOT IN (SELECT email FROM active_authors))\n SELECT active_authors_agg.authors, active_ops_agg.operators, array_length(active_authors_agg.authors, 1) as author_count, array_length(active_ops_agg.operators, 1) as operator_count FROM active_authors_agg, active_ops_agg",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "authors",
"type_info": "VarcharArray"
},
{
"ordinal": 1,
"name": "operators",
"type_info": "VarcharArray"
},
{
"ordinal": 2,
"name": "author_count",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "operator_count",
"type_info": "Int4"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null,
null
]
},
"hash": "cce991f582bc9d2ba28a5b2b41c679366bb07bc6a100727721a787160ac6910c"
}

View File

@@ -42,8 +42,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path,\n summary,\n description\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "d41df114e78873111e409e1182bccc0024acf17ce740f70d2243e5e2a2b25163"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "e38240e6d50bfe60e1c2b649588eb41dcef121ed161db04b2568ac2d990aed7c"
}

View File

@@ -1,59 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n summary,\n description,\n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Bool",
"Bool",
"Varchar",
"Varchar",
"Text",
"Bool",
"Bool",
{
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
},
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool"
]
},
"nullable": []
},
"hash": "ed99d4d088d0fd0c01f29803b12e99ae0a53d0b1feaa67737da409c51c1b6751"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue q SET suspend = 0\n FROM v2_job j, v2_job_status f\n WHERE q.workspace_id = $1 AND q.suspend = $3 AND j.parent_job = $2\n AND f.id = j.id AND q.id = j.id\n AND (f.flow_status->'step')::int = 0",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Int4"
]
},
"nullable": []
},
"hash": "f1dbcb6e6d82d17c19eb88c0e67dc1cb8baf5bd40b75a2a9cd3ebac440fda632"
}

View File

@@ -147,8 +147,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -1,22 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM script WHERE workspace_id = $1 AND archived = false",
"query": "SELECT tag FROM v2_job WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "3e244a5057d4f1b4a18c0edac52cdf695c7e7aa0468d2686255de3d83719e6d0"
"hash": "faf2c77242e0ab39b33886edf3b742531bf1351d0be1c3631bde0adfe375497a"
}

View File

@@ -65,8 +65,7 @@
"csharp",
"oracledb",
"nu",
"java",
"duckdb"
"java"
]
}
}

View File

@@ -12,6 +12,5 @@
"conventionalCommits.scopes": [
"restructring triggers, decoding trigger message on work"
],
"files.exclude": { "**/*ee.rs": false },
"search.exclude": { "**/*ee.rs": false }
"rust-analyzer.cargo.features": ["postgres_trigger"]
}

1106
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.498.0"
version = "1.494.0"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.498.0"
version = "1.494.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -96,9 +96,7 @@ csharp = ["windmill-worker/csharp"]
nu = ["windmill-worker/nu"]
java = ["windmill-worker/java"]
all_languages = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
# For windows we have another set of languages enabled
# NOTE: DuckDB is ignored because of compilation problems
all_languages_windows = ["python", "deno_core", "rust", "mysql", "oracledb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
[patch.crates-io]
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7" }
@@ -130,7 +128,6 @@ prometheus = { workspace = true, optional = true }
uuid.workspace = true
gethostname.workspace = true
serde_json.workspace = true
serde_yml.workspace = true
serde.workspace = true
deno_core = { workspace = true, optional = true }
object_store = { workspace = true, optional = true }
@@ -202,7 +199,6 @@ tower-http = { version = "^0.6", features = ["trace", "cors"] }
tower-cookies = "^0.10"
serde = "^1"
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
serde_yml = "0.0.12"
uuid = { version = "^1", features = ["serde", "v4"] }
thiserror = "^2"
anyhow = "^1"
@@ -232,7 +228,7 @@ php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb
cron = "^0"
mail-send = { version = "0.4.0", features = ["builder"], default-features=false }
urlencoding = "^2"
url = { version = "^2" , features = ["serde"]}
url = "^2"
async-oauth2 = "^0"
reqwest = { version = "^0.12", features = ["json", "stream", "gzip"] }
time = "^0"

View File

@@ -1 +1 @@
67e727c618cf673850a0887931c803241abfcfe8
70895a4a8f8891032c5b478a37ab6fafd0d4a9d0

View File

@@ -4,4 +4,4 @@ DROP TYPE http_method;
ALTER TABLE script DROP COLUMN has_preprocessor;
DROP FUNCTION prevent_route_path_change();
DROP FUNCTION prevent_route_path_change();

View File

@@ -1,4 +0,0 @@
-- Remove token invalidation notification trigger
DROP TRIGGER IF EXISTS token_invalidation_trigger ON token;
DROP FUNCTION IF EXISTS notify_token_invalidation();

View File

@@ -1,17 +0,0 @@
-- Add token invalidation notification trigger
CREATE OR REPLACE FUNCTION notify_token_invalidation()
RETURNS TRIGGER AS $$
BEGIN
-- Only notify for session token deletions when the invalidation settings are enabled
IF OLD.label = 'session' AND OLD.email IS NOT NULL THEN
PERFORM pg_notify('notify_token_invalidation', OLD.token);
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER token_invalidation_trigger
AFTER DELETE ON token
FOR EACH ROW
EXECUTE FUNCTION notify_token_invalidation();

View File

@@ -1,4 +0,0 @@
-- Add down migration script here
ALTER TABLE http_trigger
DROP COLUMN summary,
DROP COLUMN description;

View File

@@ -1,5 +0,0 @@
-- Add up migration script here
ALTER TABLE http_trigger
ADD COLUMN summary VARCHAR(512) NULL,
ADD COLUMN description TEXT NULL;

View File

@@ -53,10 +53,7 @@ use windmill_common::{
scripts::ScriptLang,
stats_oss::schedule_stats,
triggers::TriggerKind,
utils::{
create_default_worker_suffix, create_ssh_agent_worker_suffix, worker_name_with_suffix,
Mode, GIT_VERSION, HOSTNAME, MODE_AND_ADDONS,
},
utils::{hostname, rd_string, Mode, GIT_VERSION, MODE_AND_ADDONS},
worker::{
reload_custom_tags_setting, Connection, HUB_CACHE_DIR, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP,
},
@@ -80,7 +77,8 @@ use windmill_worker::{
get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, CSHARP_CACHE_DIR,
DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM, GO_BIN_CACHE_DIR, GO_CACHE_DIR,
JAVA_CACHE_DIR, NU_CACHE_DIR, POWERSHELL_CACHE_DIR, PY310_CACHE_DIR, PY311_CACHE_DIR,
PY312_CACHE_DIR, PY313_CACHE_DIR, RUST_CACHE_DIR, TAR_JAVA_CACHE_DIR, UV_CACHE_DIR,
PY312_CACHE_DIR, PY313_CACHE_DIR, RUST_CACHE_DIR, TAR_JAVA_CACHE_DIR, TAR_PY310_CACHE_DIR,
TAR_PY311_CACHE_DIR, TAR_PY312_CACHE_DIR, TAR_PY313_CACHE_DIR, UV_CACHE_DIR,
};
use crate::monitor::{
@@ -159,7 +157,6 @@ lazy_static::lazy_static! {
.ok()
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or(3600 * 12);
}
pub fn main() -> anyhow::Result<()> {
@@ -268,7 +265,7 @@ async fn windmill_main() -> anyhow::Result<()> {
tracing::error!("Failed to install rustls crypto provider");
}
let hostname = HOSTNAME.to_owned();
let hostname = hostname();
let mode_and_addons = MODE_AND_ADDONS.clone();
let mode = mode_and_addons.mode;
@@ -347,7 +344,7 @@ async fn windmill_main() -> anyhow::Result<()> {
"Creating http client for cluster using base internal url {}",
std::env::var("BASE_INTERNAL_URL").unwrap_or_default()
);
let suffix = create_ssh_agent_worker_suffix(&hostname);
let suffix = windmill_common::utils::worker_suffix(&hostname, &rd_string(5));
(
Connection::Http(build_agent_http_client(&suffix)),
Some(suffix),
@@ -682,21 +679,19 @@ Windmill Community Edition {GIT_VERSION}
let base_internal_url = base_internal_rx.await?;
if worker_mode {
let mut workers = vec![];
for i in 0..num_workers {
let suffix = if i == 0 && first_suffix.is_some() {
let suffix: String = if i == 0 && first_suffix.as_ref().is_some() {
first_suffix.as_ref().unwrap().clone()
} else {
create_default_worker_suffix(&hostname)
windmill_common::utils::worker_suffix(&hostname, &rd_string(5))
};
let worker_conn = WorkerConn {
conn: if i == 0 || mode != Mode::Agent {
conn.clone()
} else {
Connection::Http(build_agent_http_client(&suffix))
},
worker_name: worker_name_with_suffix(
worker_name: windmill_common::utils::worker_name_with_suffix(
mode == Mode::Agent,
WORKER_GROUP.as_str(),
&suffix,
@@ -861,11 +856,6 @@ Windmill Community Edition {GIT_VERSION}
}
};
},
"notify_token_invalidation" => {
let token = n.payload();
tracing::info!("Token invalidation detected for token: {}...", &token[..token.len().min(8)]);
windmill_api::auth::invalidate_token_from_cache(token);
},
"notify_global_setting_change" => {
tracing::info!("Global setting change detected: {}", n.payload());
match n.payload() {
@@ -898,7 +888,7 @@ Windmill Community Edition {GIT_VERSION}
if let Err(e) = load_tag_per_workspace_workspaces(&db).await {
tracing::error!("Error loading default tag per workspace workspaces: {e:#}");
}
},
}
SMTP_SETTING => {
reload_smtp_config(&db).await;
},
@@ -1015,6 +1005,7 @@ Windmill Community Edition {GIT_VERSION}
tracing::error!(error = %e, "Could not reload critical alert UI setting");
}
},
a @_ => {
tracing::info!("Unrecognized Global Setting Change Payload: {:?}", a);
}
@@ -1066,9 +1057,7 @@ Windmill Community Edition {GIT_VERSION}
}
if server_mode {
if !*windmill_common::QUIET_LOGS {
tracing::info!("monitor task started");
}
tracing::info!("monitor task started");
}
monitor_db(
&conn,
@@ -1080,9 +1069,7 @@ Windmill Community Edition {GIT_VERSION}
)
.await;
if server_mode {
if !*windmill_common::QUIET_LOGS {
tracing::info!("monitor task finished");
}
tracing::info!("monitor task finished");
}
},
}
@@ -1191,7 +1178,6 @@ async fn listen_pg(url: &str) -> Option<PgListener> {
"notify_webhook_change",
"notify_workspace_envs_change",
"notify_runnable_version_change",
"notify_token_invalidation",
];
#[cfg(feature = "http_trigger")]
@@ -1285,6 +1271,10 @@ pub async fn run_workers(
PY311_CACHE_DIR,
PY312_CACHE_DIR,
PY313_CACHE_DIR,
TAR_PY310_CACHE_DIR,
TAR_PY311_CACHE_DIR,
TAR_PY312_CACHE_DIR,
TAR_PY313_CACHE_DIR,
BUN_BUNDLE_CACHE_DIR,
GO_CACHE_DIR,
GO_BIN_CACHE_DIR,

View File

@@ -1324,6 +1324,7 @@ pub async fn monitor_db(
initial_load: bool,
_killpill_tx: KillpillSender,
) {
tracing::info!("Starting periodic monitor task");
let zombie_jobs_f = async {
if server_mode && !initial_load && !*DISABLE_ZOMBIE_JOBS_MONITORING {
if let Some(db) = conn.as_sql() {
@@ -1421,6 +1422,7 @@ pub async fn monitor_db(
apply_autoscaling_f,
update_min_worker_version_f,
);
tracing::info!("Periodic monitor task completed");
}
pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
@@ -1912,12 +1914,12 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
.await
.expect("could not create job token");
let client = AuthedClient::new(
base_internal_url.to_string(),
job.workspace_id.to_string(),
let client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
token,
None,
);
workspace: job.workspace_id.to_string(),
force_client: None,
};
let last_ping = job.last_ping.clone();
let error_message = format!(
@@ -1936,7 +1938,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
None,
error::Error::ExecutionErr(error_message),
true,
Some(&same_worker_tx_never_used),
same_worker_tx_never_used,
"",
worker_name,
send_result_never_used,

View File

@@ -69,7 +69,7 @@ if [ "$REVERT" == "YES" ]; then
for ee_file in $(find ${EE_CODE_DIR} -name "*ee.rs"); do
ce_file="${ee_file/${EE_CODE_DIR}/}"
ce_file="${root_dirpath}/backend/${ce_file}"
rm ${ce_file} || true
rm ${ce_file}
done
elif [ "$MOVE_NEW_FILES" == "NO" ]; then
# This replaces all files in current repo with alternative EE files in windmill-ee-private
@@ -80,7 +80,7 @@ elif [ "$MOVE_NEW_FILES" == "NO" ]; then
cp "${ee_file}" "${ce_file}"
echo "File copied '${ee_file}' -->> '${ce_file}'"
else
ln -s "${ee_file}" "${ce_file}" || true
ln -s "${ee_file}" "${ce_file}"
echo "Symlink created '${ee_file}' -->> '${ce_file}'"
fi
done

View File

@@ -1,154 +0,0 @@
# This script is used to summarize the database schema.
# You can use pg_dump to dump the schema to a file.
# pg_dump --file "schema.sql" --host "localhost" --port "5432" --username "postgres" --no-password --format=c --large-objects --schema-only --no-owner --no-privileges --no-tablespaces --no-unlogged-table-data --no-comments --no-publications --no-subscriptions --no-security-labels --no-toast-compression --no-table-access-method --verbose --schema "public" "windmill"
# Then you can run python summarize_schema.py schema.sql to get the summarized schema.
import re
import sys
from collections import defaultdict
def summarize_schema(file_path):
"""
Parses a PostgreSQL dump file and extracts a summarized schema.
"""
tables = defaultdict(lambda: {'columns': [], 'pks': set(), 'fks': [], 'indexes': []})
enums = defaultdict(list)
# Use state variables to parse multi-line definitions
current_table = None
current_enum = None
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
# --- State Resets ---
if line.startswith(');'):
current_table = None
current_enum = None
continue
# --- Parse ENUM definitions ---
match_enum = re.match(r"CREATE TYPE public\.(\w+) AS ENUM \($", line)
if match_enum:
current_enum = match_enum.group(1)
continue
if current_enum:
# Extract enum values, which are typically like 'value',
value = line.strip("',")
if value and not value.startswith('--'):
enums[current_enum].append(value)
continue
# --- Parse TABLE definitions ---
match_table = re.match(r"CREATE TABLE public\.(\w+) \($", line)
if match_table:
current_table = match_table.group(1)
continue
if current_table:
# Parse columns within a CREATE TABLE block
# e.g., "column_name type NOT NULL,"
# e.g., "id bigint NOT NULL,"
match_column = re.match(r'^"?(\w+)"?\s+([\w\d\.\[\]\(\)]+)', line)
if match_column:
col_name = match_column.group(1)
col_type = match_column.group(2)
tables[current_table]['columns'].append(f"{col_name} ({col_type})")
# Parse PRIMARY KEY defined inside the table
match_pk = re.search(r"CONSTRAINT \w+ PRIMARY KEY \((.+)\)", line)
if match_pk:
# Handle multiple PK columns: "col1, col2, col3"
pk_cols = [p.strip().strip('"') for p in match_pk.group(1).split(',')]
tables[current_table]['pks'].update(pk_cols)
continue
# --- Parse Foreign Keys (defined outside CREATE TABLE) ---
match_fk = re.match(r"ALTER TABLE ONLY public\.(\w+)\s+ADD CONSTRAINT \w+ FOREIGN KEY \(([\w,\s\"]+)\) REFERENCES public\.(\w+)\(([\w,\s\"]+)\);", line)
if match_fk:
from_table, from_cols, to_table, to_cols = match_fk.groups()
# Clean up column names
from_cols_clean = ', '.join([c.strip().strip('"') for c in from_cols.split(',')])
to_cols_clean = ', '.join([c.strip().strip('"') for c in to_cols.split(',')])
fk_string = f"({from_cols_clean}) -> {to_table}({to_cols_clean})"
tables[from_table]['fks'].append(fk_string)
# --- Parse Index definitions ---
match_index = re.match(r"CREATE (UNIQUE )?INDEX (\w+) ON public\.(\w+) USING (\w+) \((.+)\);", line)
if match_index:
is_unique = match_index.group(1) is not None
index_name = match_index.group(2)
table_name = match_index.group(3)
index_type = match_index.group(4)
columns = match_index.group(5)
# Clean up column expressions
columns_clean = columns.replace('"', '')
unique_str = "UNIQUE " if is_unique else ""
index_string = f"{unique_str}INDEX {index_name} ({index_type}) ON ({columns_clean})"
tables[table_name]['indexes'].append(index_string)
return enums, tables
def format_output(enums, tables):
"""
Formats the parsed schema data into a clean, readable string.
"""
output = []
output.append("### Simplified Database Schema ###")
output.append("\n--- Custom Data Types (ENUMs) ---\n")
if not enums:
output.append("No custom ENUM types found.")
else:
for name, values in sorted(enums.items()):
output.append(f"{name}:")
for v in values:
output.append(f" - {v}")
output.append("")
output.append("\n--- Tables and Relationships ---\n")
if not tables:
output.append("No tables found.")
else:
for name, data in sorted(tables.items()):
output.append(f"TABLE: {name}")
for col in data['columns']:
col_name = col.split(' ')[0]
marker = " (PK)" if col_name in data['pks'] else ""
output.append(f" - {col}{marker}")
if data['fks']:
output.append(" Relationships:")
for fk in data['fks']:
output.append(f" - {fk}")
if data['indexes']:
output.append(" Indexes:")
for idx in data['indexes']:
output.append(f" - {idx}")
output.append("-" * 20)
return "\n".join(output)
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: python {sys.argv[0]} <path_to_dump.sql>")
sys.exit(1)
input_file = sys.argv[1]
try:
enums_data, tables_data = summarize_schema(input_file)
formatted_summary = format_output(enums_data, tables_data)
print(formatted_summary)
except FileNotFoundError:
print(f"Error: The file '{input_file}' was not found.")
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred: {e}")
sys.exit(1)

File diff suppressed because it is too large Load Diff

View File

@@ -3969,7 +3969,8 @@ async fn assert_lockfile(
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_requirements_python(db: Pool<Postgres>) {
let content = r#"# py: ==3.11.11
let content = r#"
# py: 3.11.11
# requirements:
# tiny==0.1.3
@@ -3996,7 +3997,8 @@ def main():
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_extra_requirements_python(db: Pool<Postgres>) {
{
let content = r#"# py: ==3.11.11
let content = r#"
# py: ==3.11.11
# extra_requirements:
# tiny
@@ -4023,7 +4025,8 @@ def main():
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_extra_requirements_python2(db: Pool<Postgres>) {
let content = r#"# py: ==3.11.11
let content = r#"
# py: ==3.11.11
# extra_requirements:
# tiny==0.1.3
@@ -4045,7 +4048,8 @@ def main():
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_pins_python(db: Pool<Postgres>) {
let content = r#"# py: ==3.11.11
let content = r#"
# py: ==3.11.11
# extra_requirements:
# tiny==0.1.3
# bottle==0.13.2

View File

@@ -9,7 +9,8 @@ if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' 's/^# \(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/\1/' Cargo.toml
fi
cargo sqlx prepare --workspace -- --all-targets --all-features
cargo sqlx prepare --workspace -- --all-targets --features $(./all_features_oss.sh)
./substitute_ee_code.sh -r --dir ../windmill-ee-private
# Undo the samael changes on macOS
if [[ "$(uname)" == "Darwin" ]]; then

View File

@@ -17,7 +17,7 @@ agent_worker_server = []
enterprise_saml = ["dep:samael", "dep:libxml"]
benchmark = []
embedding = ["dep:tinyvector", "dep:hf-hub", "dep:tokenizers", "dep:candle-core", "dep:candle-transformers", "dep:candle-nn"]
parquet = ["dep:datafusion", "dep:object_store", "windmill-common/parquet", "windmill-worker/parquet"]
parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/parquet", "windmill-worker/parquet"]
prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus", "windmill-worker/prometheus"]
openidconnect = ["dep:openidconnect", "windmill-common/openidconnect"]
tantivy = ["dep:windmill-indexer"]
@@ -40,7 +40,7 @@ mcp = ["dep:rmcp"]
python = []
[dependencies]
rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true }
rmcp = { git = "https://github.com/windmill-labs/rust-sdk", features = ["transport-sse-server"], optional = true }
windmill-queue.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
@@ -76,7 +76,6 @@ hex.workspace = true
base64.workspace = true
base32.workspace = true
serde_urlencoded.workspace = true
serde_yml.workspace = true
cron.workspace = true
mime_guess.workspace = true
rust-embed = { workspace = true, optional = true }
@@ -103,7 +102,6 @@ prometheus = { workspace = true, optional = true }
async_zip = { workspace = true, optional = true }
regex.workspace = true
bytes.workspace = true
url.workspace = true
samael = { workspace = true, optional = true }
libxml = { workspace = true, optional = true }
async-recursion.workspace = true
@@ -118,6 +116,7 @@ candle-nn = { workspace = true, optional = true}
datafusion = { workspace = true, optional = true}
object_store = { workspace = true, optional = true}
openidconnect = { workspace = true, optional = true}
url = { workspace = true, optional = true}
jsonwebtoken = { workspace = true }
matchit = { workspace = true, optional = true }
tokio-tungstenite = { workspace = true, optional = true}
@@ -127,7 +126,6 @@ nkeys = { workspace = true, optional = true }
const_format.workspace = true
pin-project.workspace = true
http.workspace = true
indexmap.workspace = true
async-stream.workspace = true
ulid.workspace = true
rust-postgres = { workspace = true, optional = true }

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.498.0
version: 1.494.0
title: Windmill API
contact:
@@ -4501,13 +4501,6 @@ paths:
in: query
schema:
type: boolean
- name: languages
in: query
description: |
Filter to only include scripts written in the given languages.
Accepts multiple values as a comma-separated list.
schema:
type: string
responses:
"200":
description: All scripts
@@ -8451,76 +8444,6 @@ paths:
"201":
description: default error handler set
/w/{workspace}/openapi/generate:
post:
summary: generate openapi spec from http routes/webhook
operationId: generateOpenapiSpec
tags:
- openapi
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: openapi spec info and url
content:
application/json:
schema:
$ref: "#/components/schemas/GenerateOpenapiSpec"
responses:
"200":
description: openapi spec
content:
text/plain:
schema:
type: string
/w/{workspace}/openapi/download:
post:
summary: Download the OpenAPI v3.1 spec as a file
operationId: DownloadOpenapiSpec
tags:
- openapi
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: openapi spec info and url
content:
application/json:
schema:
$ref: "#/components/schemas/GenerateOpenapiSpec"
responses:
"200":
description: Downloaded OpenAPI spec
content:
application/octet-stream:
schema:
type: string
format: binary
/w/{workspace}/http_triggers/create_many:
post:
summary: create many HTTP triggers
operationId: createHttpTriggers
tags:
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: new http trigger
required: true
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/NewHttpTrigger"
responses:
"201":
description: http trigger created
content:
text/plain:
schema:
type: string
/w/{workspace}/http_triggers/create:
post:
summary: create http trigger
@@ -8672,7 +8595,8 @@ paths:
route_path:
type: string
http_method:
$ref: "#/components/schemas/HttpMethod"
type: string
enum: ["get", "post", "put", "delete", "patch"]
trigger_path:
type: string
workspaced_route:
@@ -14799,115 +14723,6 @@ components:
- custom_script
- signature
RunnableKind:
type: string
enum:
- script
- flow
OpenapiSpecFormat:
type: string
enum:
- yaml
- json
OpenapiHttpRouteFilters:
type: object
properties:
folder_regex:
type: string
path_regex:
type: string
route_path_regex:
type: string
required:
- folder_regex
- path_regex
- route_path_regex
WebhookFilters:
type: object
properties:
user_or_folder_regex:
type: string
enum:
- "*"
- u
- f
user_or_folder_regex_value:
type: string
path:
type: string
runnable_kind:
$ref: "#/components/schemas/RunnableKind"
required:
- user_or_folder_regex
- user_or_folder_regex_value
- path
- runnable_kind
OpenapiV3Info:
type: object
properties:
title:
type: string
version:
type: string
description:
type: string
terms_of_service:
type: string
contact:
type: object
properties:
name:
type: string
url:
type: string
email:
type: string
license:
type: object
properties:
name:
type: string
identifier:
type: string
url:
type: string
required:
- name
required:
- title
- version
GenerateOpenapiSpec:
type: object
properties:
info:
$ref: "#/components/schemas/OpenapiV3Info"
url:
type: string
openapi_spec_format:
$ref: "#/components/schemas/OpenapiSpecFormat"
http_route_filters:
type: array
items:
$ref: "#/components/schemas/OpenapiHttpRouteFilters"
webhook_filters:
type: array
items:
$ref: "#/components/schemas/WebhookFilters"
HttpMethod:
type: string
enum:
- get
- post
- put
- delete
- patch
HttpTrigger:
allOf:
- $ref: "#/components/schemas/TriggerExtraProperty"
@@ -14927,13 +14742,15 @@ components:
required:
- s3
http_method:
$ref: "#/components/schemas/HttpMethod"
type: string
enum:
- get
- post
- put
- delete
- patch
authentication_resource_path:
type: string
summary:
type: string
description:
type: string
is_async:
type: boolean
authentication_method:
@@ -14968,10 +14785,6 @@ components:
type: string
workspaced_route:
type: boolean
summary:
type: string
description:
type: string
static_asset_config:
type: object
properties:
@@ -14986,7 +14799,13 @@ components:
is_flow:
type: boolean
http_method:
$ref: "#/components/schemas/HttpMethod"
type: string
enum:
- get
- post
- put
- delete
- patch
authentication_resource_path:
type: string
is_async:
@@ -15019,10 +14838,6 @@ components:
type: string
route_path:
type: string
summary:
type: string
description:
type: string
workspaced_route:
type: boolean
static_asset_config:
@@ -15041,7 +14856,13 @@ components:
is_flow:
type: boolean
http_method:
$ref: "#/components/schemas/HttpMethod"
type: string
enum:
- get
- post
- put
- delete
- patch
is_async:
type: boolean
authentication_method:

View File

@@ -4,7 +4,7 @@ use crate::{
};
use axum::{body::Bytes, extract::Path, response::IntoResponse, routing::post, Extension, Router};
use http::{HeaderMap, Method};
use http::HeaderMap;
use quick_cache::sync::Cache;
use reqwest::{Client, RequestBuilder};
use serde::{Deserialize, Serialize};
@@ -24,7 +24,7 @@ lazy_static::lazy_static! {
pub static ref AI_REQUEST_CACHE: Cache<(String, AIProvider), ExpiringAIRequestConfig> = Cache::new(500);
}
const AZURE_API_VERSION: &str = "2025-04-01-preview";
const AZURE_API_VERSION: &str = "2024-10-21";
const OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
#[derive(Deserialize, Debug)]
@@ -141,49 +141,23 @@ impl AIRequestConfig {
self,
provider: &AIProvider,
path: &str,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> Result<RequestBuilder> {
let url = format!("{}/{}", self.base_url, path);
let body = if let Some(user) = self.user {
Self::add_user_to_body(body, user)?
} else {
body
};
let base_url = self.base_url.trim_end_matches('/');
let is_azure = matches!(provider, AIProvider::OpenAI) && base_url != OPENAI_BASE_URL
let is_azure = matches!(provider, AIProvider::OpenAI) && self.base_url != OPENAI_BASE_URL
|| matches!(provider, AIProvider::AzureOpenAI);
let is_anthropic = matches!(provider, AIProvider::Anthropic);
let url = if is_azure && method != Method::GET {
if base_url.ends_with("/deployments") {
let model = Self::get_azure_model(&body)?;
format!("{}/{}/{}", base_url, model, path)
} else if base_url.ends_with("/openai") {
let model = Self::get_azure_model(&body)?;
format!("{}/deployments/{}/{}", base_url, model, path)
} else {
format!("{}/{}", base_url, path)
}
} else {
format!("{}/{}", base_url, path)
};
tracing::debug!("AI request URL: {}", url);
let mut request = HTTP_CLIENT
.request(method, url)
.header("content-type", "application/json");
for (header_name, header_value) in headers.iter() {
if header_name.to_string().starts_with("anthropic-") {
request = request.header(header_name, header_value);
}
}
request = request.body(body);
.post(url)
.header("content-type", "application/json")
.body(body);
if is_azure {
request = request.query(&[("api-version", AZURE_API_VERSION)])
@@ -191,12 +165,9 @@ impl AIRequestConfig {
if let Some(api_key) = self.api_key {
if is_azure {
request = request.header("api-key", api_key.clone())
request = request.header("api-key", api_key)
} else {
request = request.header("authorization", format!("Bearer {}", api_key.clone()))
}
if is_anthropic {
request = request.header("X-API-Key", api_key);
request = request.header("authorization", format!("Bearer {}", api_key))
}
}
@@ -228,18 +199,6 @@ impl AIRequestConfig {
.map_err(|e| Error::internal_err(format!("Failed to reserialize request body: {}", e)))?
.into())
}
fn get_azure_model(body: &Bytes) -> Result<String> {
#[derive(Deserialize, Debug)]
struct AzureModel {
model: String,
}
let azure_model: AzureModel = serde_json::from_slice(body)
.map_err(|e| Error::internal_err(format!("Failed to parse request body: {}", e)))?;
Ok(azure_model.model)
}
}
#[derive(Clone, Debug)]
@@ -352,18 +311,17 @@ pub struct AIConfig {
}
pub fn global_service() -> Router {
Router::new().route("/proxy/*ai", post(global_proxy).get(global_proxy))
Router::new().route("/proxy/*ai", post(global_proxy))
}
pub fn workspaced_service() -> Router {
Router::new().route("/proxy/*ai", post(proxy).get(proxy))
Router::new().route("/proxy/*ai", post(proxy))
}
async fn global_proxy(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(ai_path): Path<String>,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
@@ -388,7 +346,7 @@ async fn global_proxy(
let url = format!("{}/{}", base_url, ai_path);
let request = HTTP_CLIENT
.request(method, url)
.post(url)
.header("content-type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(body);
@@ -424,7 +382,6 @@ async fn proxy(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path((w_id, ai_path)): Path<(String, String)>,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
@@ -507,7 +464,7 @@ async fn proxy(
}
};
let request = request_config.prepare_request(&provider, &ai_path, method, headers, body)?;
let request = request_config.prepare_request(&provider, &ai_path, body)?;
let response = request.send().await.map_err(to_anyhow)?;

View File

@@ -12,7 +12,7 @@ use crate::{
db::{ApiAuthed, DB},
resources::get_resource_value_interpolated_internal,
users::{require_owner_of_path, OptAuthed},
utils::WithStarredInfoQuery,
utils::{RunnableKind, WithStarredInfoQuery},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
@@ -59,7 +59,7 @@ use windmill_common::{
users::username_to_permissioned_as,
utils::{
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin,
Pagination, RunnableKind, StripPath,
Pagination, StripPath,
},
variables::{build_crypt, build_crypt_with_key_suffix, encrypt},
worker::{to_raw_value, CLOUD_HOSTED},

View File

@@ -26,21 +26,6 @@ use windmill_common::{
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
};
lazy_static::lazy_static! {
// Global auth cache accessible from main.rs for direct invalidation
pub static ref AUTH_CACHE: Cache<(String, String), ExpiringAuthCache> = Cache::new(300);
}
// Global function to invalidate a specific token from cache
pub fn invalidate_token_from_cache(token: &str) {
// Remove all cache entries for this token (across all workspaces)
AUTH_CACHE.retain(|(_workspace_id, cached_token), _cached_value| cached_token != token);
tracing::info!(
"Invalidated token from auth cache: {}...",
&token[..token.len().min(8)]
);
}
#[derive(Clone)]
pub struct ExpiringAuthCache {
pub authed: ApiAuthed,
@@ -48,6 +33,7 @@ pub struct ExpiringAuthCache {
}
pub struct AuthCache {
cache: Cache<(String, String), ExpiringAuthCache>,
db: DB,
superadmin_secret: Option<String>,
#[cfg(feature = "enterprise")]
@@ -61,6 +47,7 @@ impl AuthCache {
#[cfg(feature = "enterprise")] ext_jwks: Option<Arc<RwLock<ExternalJwks>>>,
) -> Self {
AuthCache {
cache: Cache::new(300),
db,
superadmin_secret,
#[cfg(feature = "enterprise")]
@@ -69,7 +56,7 @@ impl AuthCache {
}
pub async fn invalidate(&self, w_id: &str, token: String) {
AUTH_CACHE.remove(&(w_id.to_string(), token));
self.cache.remove(&(w_id.to_string(), token));
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<ApiAuthed> {
@@ -77,7 +64,7 @@ impl AuthCache {
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
token.to_string(),
);
let s = AUTH_CACHE.get(&key).map(|c| c.to_owned());
let s = self.cache.get(&key).map(|c| c.to_owned());
match s {
Some(ExpiringAuthCache { authed, expiry }) if expiry > chrono::Utc::now() => {
Some(authed)
@@ -99,7 +86,7 @@ impl AuthCache {
};
if let Some((authed, exp)) = authed_and_exp.clone() {
AUTH_CACHE.insert(
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
@@ -136,7 +123,7 @@ impl AuthCache {
username_override,
};
AUTH_CACHE.insert(
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
@@ -330,7 +317,7 @@ impl AuthCache {
}
};
if let Some(authed) = authed_o.as_ref() {
AUTH_CACHE.insert(
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
@@ -483,27 +470,6 @@ where
}
}
fn maybe_get_workspace_id_from_path(path_vec: &[&str]) -> Option<String> {
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[1] == "api"
&& path_vec[2] == "mcp"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_owned())
} else {
if path_vec.len() >= 5 && path_vec[0] == "" && path_vec[2] == "srch" && path_vec[3] == "w" {
Some(path_vec[4].to_owned())
} else {
None
}
};
workspace_id
}
#[async_trait]
impl<S> FromRequestParts<S> for ApiAuthed
where
@@ -516,62 +482,85 @@ where
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(ApiAuthed::default());
return Ok(ApiAuthed {
email: "".to_owned(),
username: "".to_owned(),
is_admin: false,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
});
};
let already_authed = parts.extensions.get::<ApiAuthed>();
if let Some(authed) = already_authed {
return Ok(authed.clone());
}
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
Ok(authed.clone())
} else {
extract_token(parts, state).await
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(parts, state).await
};
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[1] == "api"
&& path_vec[2] == "mcp"
&& path_vec[3] == "w"
{
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = maybe_get_workspace_id_from_path(&path_vec);
Some(path_vec[4].to_string())
} else {
if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[2] == "srch"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_string())
} else {
None
}
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
parts.extensions.insert(authed.clone());
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
}
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
}
parts.extensions.insert(authed.clone());
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
}
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}

View File

@@ -55,15 +55,19 @@ use crate::mqtt_triggers::{MqttClientVersion, MqttV3Config, MqttV5Config, Subscr
use crate::nats_triggers_oss::NatsTriggerConfigConnection;
#[cfg(feature = "postgres_trigger")]
use crate::postgres_triggers::{
create_logical_replication_slot, create_pg_publication, generate_random_string,
get_default_pg_connection, PublicationData,
use {
crate::postgres_triggers::{
create_logical_replication_slot, create_pg_publication, generate_random_string,
get_pg_connection, PublicationData,
},
sqlx::Connection,
};
use crate::{
args::RawWebhookArgs,
db::{ApiAuthed, DB},
users::fetch_api_authed,
utils::RunnableKind,
};
use axum::{
@@ -81,7 +85,7 @@ use windmill_common::{
db::UserDB,
error::{JsonResult, Result},
triggers::{RunnableFormat, RunnableFormatVersion, TriggerKind},
utils::{not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
utils::{not_found_if_none, paginate, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};
@@ -300,15 +304,13 @@ async fn set_postgres_trigger_config(
user_db: UserDB,
mut capture_config: NewCaptureConfig,
) -> Result<NewCaptureConfig> {
use windmill_common::error::to_anyhow;
let Some(TriggerConfig::Postgres(postgres_config)) = capture_config.trigger_config.as_mut()
else {
return Err(Error::BadRequest("Invalid postgres config".to_string()));
};
if postgres_config.basic_mode.unwrap_or(false) {
let mut pg_connection = get_default_pg_connection(
let mut pg_connection = get_pg_connection(
authed,
Some(user_db),
&db,
@@ -317,26 +319,22 @@ async fn set_postgres_trigger_config(
)
.await?;
let tx = pg_connection.transaction().await.map_err(to_anyhow)?;
let mut tx = pg_connection.begin().await?;
let publication_name = format!("windmill_capture_{}", generate_random_string());
let replication_slot_name = publication_name.clone();
create_logical_replication_slot(tx.client(), &replication_slot_name)
.await
.map_err(to_anyhow)?;
create_logical_replication_slot(&mut tx, &replication_slot_name).await?;
create_pg_publication(
tx.client(),
&mut tx,
&publication_name,
postgres_config.publication.table_to_track.as_deref(),
&postgres_config.publication.transaction_to_track,
)
.await
.map_err(to_anyhow)?;
tx.commit().await.map_err(to_anyhow)?;
.await?;
tx.commit().await?;
postgres_config.publication_name = Some(publication_name);
postgres_config.replication_slot_name = Some(replication_slot_name);
} else {

View File

@@ -804,18 +804,10 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
.execute(db)
.await?;
});
run_windmill_migration!("v2_job_queue_suspend", db, |tx| {
sqlx::query!(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS v2_job_queue_suspend ON v2_job_queue (workspace_id, suspend) WHERE suspend > 0;"
)
.execute(db)
.await?;
});
Ok(())
}
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq)]
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct ApiAuthed {
pub email: String,
pub username: String,

View File

@@ -12,7 +12,7 @@ use crate::db::ApiAuthed;
use crate::triggers::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
};
use crate::utils::WithStarredInfoQuery;
use crate::utils::{RunnableKind, WithStarredInfoQuery};
use crate::{
db::DB,
schedule::clear_schedule,
@@ -43,7 +43,7 @@ use windmill_common::{
jobs::JobPayload,
schedule::Schedule,
scripts::Schema,
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, StripPath},
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel};
@@ -477,7 +477,7 @@ async fn create_flow(
false,
None,
true,
None,
nf.tag,
None,
None,
None,

View File

@@ -21,7 +21,7 @@ use crate::{
pub struct RawHttpTriggerArgs(pub RawWebhookArgs);
#[derive(Serialize, Deserialize, sqlx::Type, Debug, Clone, Copy, Hash, Eq, PartialEq)]
#[derive(Serialize, Deserialize, sqlx::Type, Debug, Clone, Hash, Eq, PartialEq)]
#[sqlx(type_name = "HTTP_METHOD", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum HttpMethod {

View File

@@ -441,8 +441,8 @@ pub struct BasicAuthAuthentication {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ApiKeyAuthentication {
pub api_key_header: String,
pub api_key_secret: String,
api_key_header: String,
api_key_secret: String,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize)]

View File

@@ -14,7 +14,6 @@ use crate::{
},
users::fetch_api_authed,
};
use anyhow::anyhow;
use axum::response::Response;
use axum::{
extract::{Path, Query},
@@ -29,14 +28,13 @@ use quick_cache::sync::Cache;
use serde::{Deserialize, Serialize};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::prelude::FromRow;
use sqlx::PgConnection;
use sqlx::PgTransaction;
use std::borrow::Cow;
use std::collections::HashSet;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::{RwLock, RwLockReadGuard};
use tower_http::cors::CorsLayer;
use windmill_audit::{audit_oss::audit_log, ActionKind};
use windmill_common::error::{Error, Result as WindmillResult};
use windmill_common::error::Error;
#[cfg(feature = "parquet")]
use windmill_common::s3_helpers::build_object_store_client;
use windmill_common::{
@@ -44,7 +42,7 @@ use windmill_common::{
error::{self, JsonResult},
s3_helpers::S3Object,
triggers::TriggerKind,
utils::{empty_as_none, not_found_if_none, paginate, require_admin, Pagination, StripPath},
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::CLOUD_HOSTED,
};
use windmill_git_sync::handle_deployment_metadata;
@@ -81,7 +79,6 @@ pub fn routes_global_service() -> Router {
pub fn workspaced_service() -> Router {
Router::new()
.route("/create", post(create_trigger))
.route("/create_many", post(create_many_http_trigger))
.route("/list", get(list_triggers))
.route("/get/*path", get(get_trigger))
.route("/update/*path", post(update_trigger))
@@ -114,8 +111,6 @@ struct NewTrigger {
static_asset_config: Option<sqlx::types::Json<S3Object>>,
http_method: HttpMethod,
workspaced_route: Option<bool>,
summary: Option<String>,
description: Option<String>,
is_static_website: bool,
wrap_body: Option<bool>,
raw_string: Option<bool>,
@@ -136,8 +131,6 @@ pub struct HttpTrigger {
pub is_async: bool,
pub authentication_method: AuthenticationMethod,
pub http_method: HttpMethod,
pub summary: Option<String>,
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub static_asset_config: Option<sqlx::types::Json<S3Object>>,
pub is_static_website: bool,
@@ -157,8 +150,6 @@ struct EditTrigger {
authentication_method: AuthenticationMethod,
#[serde(deserialize_with = "non_empty_str")]
authentication_resource_path: Option<String>,
summary: Option<String>,
description: Option<String>,
http_method: HttpMethod,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
workspaced_route: Option<bool>,
@@ -173,7 +164,6 @@ pub struct ListTriggerQuery {
pub per_page: Option<usize>,
pub path: Option<String>,
pub is_flow: Option<bool>,
#[serde(default, deserialize_with = "empty_as_none")]
pub path_start: Option<String>,
}
@@ -195,8 +185,6 @@ async fn list_triggers(
"wrap_body",
"raw_string",
"script_path",
"summary",
"description",
"is_flow",
"http_method",
"edited_by",
@@ -251,8 +239,6 @@ async fn get_trigger(
route_path_key,
workspaced_route,
script_path,
summary,
description,
is_flow,
http_method as "http_method: _",
edited_by,
@@ -287,7 +273,7 @@ async fn get_trigger(
fn validate_authentication_method(
authentication_method: AuthenticationMethod,
raw_string: Option<bool>,
) -> WindmillResult<()> {
) -> error::Result<()> {
match (authentication_method, raw_string) {
(AuthenticationMethod::CustomScript, raw) if !raw.unwrap_or(false) == true => {
return Err(Error::BadRequest(
@@ -301,21 +287,57 @@ fn validate_authentication_method(
Ok(())
}
async fn increase_trigger_version(tx: &mut PgConnection) -> WindmillResult<()> {
async fn increase_trigger_version_and_commit(mut tx: PgTransaction<'_>) -> error::Result<()> {
sqlx::query!("SELECT nextval('http_trigger_version_seq')",)
.fetch_one(tx)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
async fn create_trigger_inner(
tx: &mut PgConnection,
w_id: &str,
authed: &ApiAuthed,
new_http_trigger: &NewTrigger,
route_path_key: &str,
) -> WindmillResult<()> {
async fn create_trigger(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(ct): Json<NewTrigger>,
) -> error::Result<(StatusCode, String)> {
require_admin(authed.is_admin, &authed.username)?;
if !VALID_ROUTE_PATH_RE.is_match(&ct.route_path) {
return Err(error::Error::BadRequest("Invalid route path".to_string()));
}
validate_authentication_method(ct.authentication_method, ct.raw_string)?;
// route path key is extracted from the route path to check for uniqueness
// it replaces /?:{key} with :key
// it will also remove the leading / if present, not an issue as we only allow : after slashes
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&ct.route_path, ":key");
let exists = route_path_key_exists(
&route_path_key,
&ct.http_method,
&w_id,
None,
ct.workspaced_route,
&db,
)
.await?;
if exists {
return Err(error::Error::BadRequest(
"A route already exists with this path".to_string(),
));
}
if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) {
return Err(error::Error::BadRequest(
"Static website and static asset are not supported on cloud".to_string(),
));
}
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
r#"
INSERT INTO http_trigger (
@@ -328,8 +350,6 @@ async fn create_trigger_inner(
wrap_body,
raw_string,
script_path,
summary,
description,
is_flow,
is_async,
authentication_method,
@@ -341,209 +361,55 @@ async fn create_trigger_inner(
is_static_website
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17
)
"#,
w_id,
new_http_trigger.path,
new_http_trigger.route_path,
ct.path,
ct.route_path,
&route_path_key,
new_http_trigger.workspaced_route,
new_http_trigger.authentication_resource_path,
new_http_trigger.wrap_body.unwrap_or(false),
new_http_trigger.raw_string.unwrap_or(false),
new_http_trigger.script_path,
new_http_trigger.summary,
new_http_trigger.description,
new_http_trigger.is_flow,
new_http_trigger.is_async,
new_http_trigger.authentication_method as _,
new_http_trigger.http_method as _,
new_http_trigger.static_asset_config as _,
ct.workspaced_route,
ct.authentication_resource_path,
ct.wrap_body.unwrap_or(false),
ct.raw_string.unwrap_or(false),
ct.script_path,
ct.is_flow,
ct.is_async,
ct.authentication_method as _,
ct.http_method as _,
ct.static_asset_config as _,
&authed.username,
&authed.email,
new_http_trigger.is_static_website
ct.is_static_website
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
authed,
&authed,
"http_triggers.create",
ActionKind::Create,
&w_id,
Some(new_http_trigger.path.as_str()),
Some(ct.path.as_str()),
None,
)
.await?;
increase_trigger_version(tx).await?;
Ok(())
}
fn check_no_duplicates<'trigger>(
new_http_triggers: &[NewTrigger],
route_path_key: &[Cow<'trigger, str>],
) -> Result<(), Error> {
let mut seen = HashSet::with_capacity(new_http_triggers.len());
for (i, trigger) in new_http_triggers.iter().enumerate() {
if !seen.insert((
&route_path_key[i],
trigger.http_method,
trigger.workspaced_route,
)) {
return Err(Error::BadRequest(format!(
"Duplicate HTTP route detected: '{}'. Each HTTP route must have a unique 'route_path'.",
&trigger.route_path
)));
}
}
Ok(())
}
async fn create_many_http_trigger(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(new_http_triggers): Json<Vec<NewTrigger>>,
) -> WindmillResult<(StatusCode, String)> {
require_admin(authed.is_admin, &authed.username)?;
let error_wrapper = |path: &str, error: Error| -> Error {
anyhow!(
"Error occurred for HTTP route at route path: {}, error: {}",
path,
error
)
.into()
};
let mut route_path_keys = Vec::with_capacity(new_http_triggers.len());
for new_http_trigger in new_http_triggers.iter() {
let route_path_key = validate_http_trigger(&db, &w_id, new_http_trigger)
.await
.map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?;
route_path_keys.push(route_path_key);
}
check_no_duplicates(&new_http_triggers, &route_path_keys)?;
let mut tx = user_db.begin(&authed).await?;
for (i, new_http_trigger) in new_http_triggers.iter().enumerate() {
create_trigger_inner(
&mut tx,
&w_id,
&authed,
new_http_trigger,
&route_path_keys[i],
)
.await
.map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?;
}
tx.commit().await?;
for http_trigger in new_http_triggers.into_iter() {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
windmill_git_sync::DeployedObject::HttpTrigger { path: http_trigger.path.clone() },
Some(format!("HTTP route '{}' created", http_trigger.path)),
true,
)
.await?;
}
Ok((StatusCode::CREATED, format!("Created all HTTP routes")))
}
async fn validate_http_trigger<'trigger>(
db: &DB,
w_id: &str,
new_http_trigger: &'trigger NewTrigger,
) -> WindmillResult<Cow<'trigger, str>> {
if !VALID_ROUTE_PATH_RE.is_match(&new_http_trigger.route_path) {
return Err(error::Error::BadRequest("Invalid route path".to_string()));
}
validate_authentication_method(
new_http_trigger.authentication_method,
new_http_trigger.raw_string,
)?;
// route path key is extracted from the route path to check for uniqueness
// it replaces /?:{key} with :key
// it will also remove the leading / if present, not an issue as we only allow : after slashes
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&new_http_trigger.route_path, ":key");
let exists = route_path_key_exists(
&route_path_key,
&new_http_trigger.http_method,
&w_id,
None,
new_http_trigger.workspaced_route,
db,
)
.await?;
if exists {
return Err(error::Error::BadRequest(
"A route already exists with this path".to_string(),
));
}
if *CLOUD_HOSTED
&& (new_http_trigger.is_static_website || new_http_trigger.static_asset_config.is_some())
{
return Err(error::Error::BadRequest(
"Static website and static asset are not supported on cloud".to_string(),
));
}
Ok(route_path_key)
}
async fn create_trigger(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(new_http_trigger): Json<NewTrigger>,
) -> WindmillResult<(StatusCode, String)> {
require_admin(authed.is_admin, &authed.username)?;
let route_path_key = validate_http_trigger(&db, &w_id, &new_http_trigger).await?;
let mut tx = user_db.begin(&authed).await?;
let http_trigger_path = new_http_trigger.path.clone();
create_trigger_inner(&mut tx, &w_id, &authed, &new_http_trigger, &route_path_key).await?;
tx.commit().await?;
increase_trigger_version_and_commit(tx).await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
windmill_git_sync::DeployedObject::HttpTrigger { path: new_http_trigger.path.clone() },
Some(format!("HTTP route '{}' created", new_http_trigger.path)),
windmill_git_sync::DeployedObject::HttpTrigger { path: ct.path.clone() },
Some(format!("HTTP trigger '{}' created", ct.path)),
true,
)
.await?;
Ok((StatusCode::CREATED, format!("{}", http_trigger_path)))
Ok((StatusCode::CREATED, format!("{}", ct.path)))
}
async fn update_trigger(
@@ -552,9 +418,8 @@ async fn update_trigger(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ct): Json<EditTrigger>,
) -> WindmillResult<String> {
) -> error::Result<String> {
let path = path.to_path();
if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) {
return Err(error::Error::BadRequest(
"Static website and static asset are not supported on cloud".to_string(),
@@ -613,13 +478,11 @@ async fn update_trigger(
email = $13,
is_async = $14,
authentication_method = $15,
summary = $16,
description = $17,
edited_at = now(),
is_static_website = $18
is_static_website = $16
WHERE
workspace_id = $19 AND
path = $20
workspace_id = $17 AND
path = $18
"#,
route_path,
&route_path_key,
@@ -636,8 +499,6 @@ async fn update_trigger(
&authed.email,
ct.is_async,
ct.authentication_method as _,
ct.summary,
ct.description,
ct.is_static_website,
w_id,
path,
@@ -702,9 +563,7 @@ async fn update_trigger(
)
.await?;
increase_trigger_version(&mut tx).await?;
tx.commit().await?;
increase_trigger_version_and_commit(tx).await?;
handle_deployment_metadata(
&authed.email,
@@ -712,7 +571,7 @@ async fn update_trigger(
&db,
&w_id,
windmill_git_sync::DeployedObject::HttpTrigger { path: ct.path.clone() },
Some(format!("HTTP route '{}' updated", ct.path)),
Some(format!("HTTP trigger '{}' updated", ct.path)),
true,
)
.await?;
@@ -725,7 +584,7 @@ async fn delete_trigger(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> WindmillResult<String> {
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
@@ -750,9 +609,7 @@ async fn delete_trigger(
)
.await?;
increase_trigger_version(&mut tx).await?;
tx.commit().await?;
increase_trigger_version_and_commit(tx).await?;
handle_deployment_metadata(
&authed.email,
@@ -760,12 +617,12 @@ async fn delete_trigger(
&db,
&w_id,
windmill_git_sync::DeployedObject::HttpTrigger { path: path.to_string() },
Some(format!("HTTP route '{}' deleted", path)),
Some(format!("HTTP trigger '{}' deleted", path)),
true,
)
.await?;
Ok(format!("HTTP route {path} deleted"))
Ok(format!("HTTP trigger {path} deleted"))
}
async fn exists_trigger(
@@ -803,7 +660,7 @@ async fn route_path_key_exists(
trigger_path: Option<&str>,
workspaced_route: Option<bool>,
db: &DB,
) -> WindmillResult<bool> {
) -> error::Result<bool> {
let exists = if *CLOUD_HOSTED {
sqlx::query_scalar!(
r#"
@@ -1004,7 +861,7 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
.insert(format!("{}/*wm_subpath", full_path), trigger.clone())
.unwrap_or_else(|e| {
tracing::warn!(
"Failed to consider HTTP route {}/*wm_subpath: {:?}",
"Failed to consider http trigger route {}/*wm_subpath: {:?}",
full_path,
e,
);
@@ -1013,7 +870,11 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
router
.insert(full_path.clone(), trigger.clone())
.unwrap_or_else(|e| {
tracing::warn!("Failed to consider HTTP route {}: {:?}", full_path, e,);
tracing::warn!(
"Failed to consider http trigger route {}: {:?}",
full_path,
e,
);
});
}
@@ -1037,7 +898,7 @@ async fn get_http_route_trigger(
db: &DB,
user_db: UserDB,
method: &http::Method,
) -> WindmillResult<(TriggerRoute, String, HashMap<String, String>, ApiAuthed)> {
) -> error::Result<(TriggerRoute, String, HashMap<String, String>, ApiAuthed)> {
let http_method: HttpMethod = method.try_into()?;
let requested_path = format!("/{}", route_path);
@@ -1086,11 +947,11 @@ async fn get_http_route_trigger(
);
let exists = match HTTP_ACCESS_CACHE.get(&cache_key) {
Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => {
tracing::debug!("HTTP access cache hit for route {}", trigger.path);
tracing::debug!("HTTP access cache hit for trigger {}", trigger.path);
true
}
_ => {
tracing::debug!("HTTP access cache miss for route {}", trigger.path);
tracing::debug!("HTTP access cache miss for trigger {}", trigger.path);
let mut tx = user_db.begin(&authed).await?;
let exists = sqlx::query_scalar!(
r#"
@@ -1141,7 +1002,7 @@ async fn get_http_route_trigger(
trigger.email.clone(),
&trigger.workspace_id,
&db,
Some(username_override.unwrap_or(format!("HTTP-{}", trigger.path))),
Some(username_override.unwrap_or(format!("http-{}", trigger.path))),
)
.await?;
@@ -1169,14 +1030,6 @@ async fn route_job(
.await
.map_err(|e| e.into_response())?;
if trigger.script_path.is_empty() && trigger.static_asset_config.is_none() {
return Err(Error::NotFound(format!(
"Runnable path of HTTP route at path: {}",
trigger.path
))
.into_response());
}
let args = args
.process_args(
&authed,
@@ -1213,15 +1066,15 @@ async fn route_job(
let authentication_method = match HTTP_AUTH_CACHE.get(&cache_key) {
Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => {
tracing::debug!("HTTP auth method cache hit for route {}", trigger.path);
tracing::debug!("HTTP auth method cache hit for trigger {}", trigger.path);
cache_entry.value
}
_ => {
tracing::debug!("HTTP auth method cache miss for route {}", trigger.path);
tracing::debug!("HTTP auth method cache miss for trigger {}", trigger.path);
let auth_method = try_get_resource_from_db_as::<
crate::http_trigger_auth::AuthenticationMethod,
>(
&authed,
authed.clone(),
Some(user_db.clone()),
&db,
&resource_path,

View File

@@ -26,7 +26,6 @@ use tokio::io::AsyncReadExt;
#[cfg(feature = "prometheus")]
use tokio::time::Instant;
use tower::ServiceBuilder;
use windmill_common::auth::is_super_admin_email;
use windmill_common::error::JsonResult;
use windmill_common::flow_status::{JobResult, RestartedFrom};
use windmill_common::jobs::{format_completed_job_result, format_result, ENTRYPOINT_OVERRIDE};
@@ -3141,27 +3140,28 @@ pub fn add_raw_string(
}
async fn check_tag_available_for_workspace(
db: &DB,
w_id: &str,
tag: &Option<String>,
authed: &ApiAuthed,
) -> error::Result<()> {
if let Some(tag) = tag {
if tag.is_empty() {
if tag == "" {
return Ok(());
}
let tags = get_scope_tags(authed);
let mut is_tag_available_in_workspace = None;
let mut is_tag_in_workspace_custom_tags = false;
if let Some(tags) = tags.as_ref() {
is_tag_available_in_workspace = Some(tags.contains(&tag.as_str()));
if let Some(tags) = tags {
if !tags.contains(&tag.as_str()) {
return Err(Error::BadRequest(format!(
"Tag {tag} is not available in your scope"
)));
}
}
let custom_tags_per_w = CUSTOM_TAGS_PER_WORKSPACE.read().await;
if custom_tags_per_w.0.contains(&tag.to_string()) {
is_tag_in_workspace_custom_tags = true;
Ok(())
} else if custom_tags_per_w.1.contains_key(tag)
&& custom_tags_per_w
.1
@@ -3169,33 +3169,16 @@ async fn check_tag_available_for_workspace(
.unwrap()
.contains(&w_id.to_string())
{
is_tag_in_workspace_custom_tags = true;
}
match is_tag_available_in_workspace {
Some(true) | None => {
if is_tag_in_workspace_custom_tags {
return Ok(());
}
}
_ => {}
}
if !is_super_admin_email(db, &authed.email).await? {
if tags.is_some() && is_tag_available_in_workspace.is_some() {
return Err(Error::BadRequest(format!(
"Tag {tag} is not available in your scope"
)));
}
Ok(())
} else {
return Err(error::Error::BadRequest(format!(
"Only super admins are allowed to use tags that are not included in the allowed CUSTOM_TAGS: {:?}",
"Tag {tag} cannot be used on workspace {w_id}: (CUSTOM_TAGS: {:?})",
custom_tags_per_w
)));
}
} else {
Ok(())
}
return Ok(());
}
#[cfg(feature = "enterprise")]
@@ -3526,7 +3509,7 @@ pub async fn run_flow_by_path_inner(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let (email, permissioned_as, push_authed, tx) =
@@ -3718,7 +3701,7 @@ pub async fn run_script_by_path_inner(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let (email, permissioned_as, push_authed, tx) =
if let Some(on_behalf_of) = on_behalf_of.as_ref() {
@@ -3789,7 +3772,7 @@ pub async fn run_workflow_as_code(
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
check_tag_available_for_workspace(&db, &w_id, &run_query.tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &run_query.tag, &authed).await?;
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
@@ -4386,7 +4369,7 @@ pub async fn run_wait_result_job_by_path_get(
drop(tx);
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let (email, permissioned_as, push_authed, tx) =
if let Some(on_behalf_of) = on_behalf_authed.as_ref() {
@@ -4526,7 +4509,7 @@ pub async fn run_wait_result_script_by_path_internal(
drop(tx);
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let (email, permissioned_as, push_authed, tx) =
if let Some(on_behalf_of) = on_behalf_of.as_ref() {
@@ -4627,7 +4610,7 @@ pub async fn run_wait_result_script_by_hash(
check_scopes(&authed, || format!("run:script/{path}"))?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let (email, permissioned_as, push_authed, tx) = if let Some(email) = on_behalf_of_email.as_ref()
{
@@ -4747,7 +4730,7 @@ pub async fn run_wait_result_flow_by_path_internal(
drop(tx);
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let (email, permissioned_as, push_authed, tx) =
if let Some(on_behalf_of_email) = on_behalf_of_email.as_ref() {
@@ -4821,7 +4804,7 @@ async fn run_preview_script(
}
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(preview.tag.clone());
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into());
let (uuid, tx) = push(
@@ -4903,7 +4886,7 @@ async fn run_bundle_preview_script(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(preview.tag.clone());
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let ltx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into());
let args = preview.args.unwrap_or_default();
@@ -5487,7 +5470,7 @@ async fn run_preview_flow_job(
}
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(raw_flow.tag.clone());
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into());
let (uuid, tx) = push(
@@ -5582,7 +5565,7 @@ pub async fn run_job_by_hash_inner(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let (email, permissioned_as, push_authed, tx) = if let Some(email) = on_behalf_of_email.as_ref()
{

View File

@@ -19,16 +19,14 @@ use crate::oauth2_oss::SlackVerifier;
use crate::smtp_server_oss::SmtpServer;
#[cfg(feature = "mcp")]
use crate::mcp::{extract_and_store_workspace_id, setup_mcp_server, shutdown_mcp_server};
#[cfg(feature = "mcp")]
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use crate::mcp::{setup_mcp_server, Runner as McpRunner};
use crate::tracing_init::MyOnFailure;
use crate::{
tracing_init::{MyMakeSpan, MyOnResponse},
users::OptAuthed,
webhook_util::WebhookShared,
};
#[cfg(feature = "agent_worker_server")]
use agent_workers_oss::AgentCache;
@@ -71,7 +69,7 @@ mod ai;
mod apps;
pub mod args;
mod audit;
pub mod auth;
mod auth;
mod capture;
mod concurrency_groups;
mod configs;
@@ -103,8 +101,6 @@ mod integration;
#[cfg(feature = "postgres_trigger")]
mod postgres_triggers;
pub mod openapi;
mod approvals;
#[cfg(all(feature = "enterprise", feature = "private"))]
pub mod apps_ee;
@@ -524,18 +520,21 @@ pub async fn run_server(
// Setup MCP server
#[allow(unused_variables)]
let (mcp_router, mcp_session_manager) = {
let (mcp_router, mcp_main_ct, mcp_service_ct) = {
#[cfg(feature = "mcp")]
if server_mode || mcp_mode {
let (mcp_router, mcp_session_manager) = setup_mcp_server().await?;
let mcp_middleware = axum::middleware::from_fn(extract_and_store_workspace_id);
(mcp_router.layer(mcp_middleware), Some(mcp_session_manager))
let (mcp_sse_server, mcp_router) = setup_mcp_server(addr, "/api/mcp/w/:workspace_id")?;
#[cfg(feature = "mcp")]
let mcp_main_ct = mcp_sse_server.config.ct.clone(); // Token to signal shutdown *to* MCP
#[cfg(feature = "mcp")]
let mcp_service_ct = mcp_sse_server.with_service(McpRunner::new); // Token to wait for MCP *service* shutdown
(mcp_router, Some(mcp_main_ct), Some(mcp_service_ct))
} else {
(Router::new(), Option::<Arc<LocalSessionManager>>::None)
(Router::new(), None, None)
}
#[cfg(not(feature = "mcp"))]
(Router::new(), Option::<()>::None)
(Router::new(), None::<()>, None::<()>)
};
#[cfg(feature = "agent_worker_server")]
@@ -597,7 +596,6 @@ pub async fn run_server(
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc_oss::workspaced_service())
.nest("/openapi", openapi::openapi_service())
.nest("/http_triggers", http_triggers_service)
.nest("/websocket_triggers", websocket_triggers_service)
.nest("/kafka_triggers", kafka_triggers_service)
@@ -662,7 +660,7 @@ pub async fn run_server(
.layer(from_extractor::<OptAuthed>())
.layer(cors.clone()),
)
.nest("/mcp/w/:workspace_id/sse", mcp_router)
.nest("/mcp/w/:workspace_id", mcp_router)
.layer(from_extractor::<OptAuthed>())
.nest("/agent_workers", {
#[cfg(feature = "agent_worker_server")]
@@ -821,9 +819,16 @@ pub async fn run_server(
tracing::info!("Graceful shutdown of server");
#[cfg(feature = "mcp")]
if let Some(mcp_session_manager) = mcp_session_manager {
shutdown_mcp_server(mcp_session_manager).await;
tracing::info!("MCP server shutdown");
{
if let Some(mcp_main_ct) = mcp_main_ct {
tracing::info!("Received shutdown signal, cancelling MCP server...");
mcp_main_ct.cancel();
}
if let Some(mcp_service_ct) = mcp_service_ct {
tracing::info!("Waiting for MCP service cancellation...");
mcp_service_ct.cancelled().await;
tracing::info!("MCP service cancelled.");
}
}
});

View File

@@ -1,10 +1,11 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use axum::body::to_bytes;
use axum::Router;
use axum::{extract::Path, http::Request, middleware::Next, response::Response};
use rmcp::transport::sse_server::{SseServer, SseServerConfig};
use rmcp::{
handler::server::ServerHandler,
model::*,
@@ -16,6 +17,7 @@ use serde_json::Value;
use sql_builder::prelude::*;
use sqlx::FromRow;
use tokio::try_join;
use tokio_util::sync::CancellationToken;
use windmill_common::db::UserDB;
use windmill_common::worker::to_raw_value;
use windmill_common::{DB, HUB_BASE_URL};
@@ -27,9 +29,6 @@ use crate::jobs::{
run_wait_result_flow_by_path_internal, run_wait_result_script_by_path_internal, RunJobQuery,
};
use crate::HTTP_CLIENT;
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager, SessionManager, StreamableHttpService,
};
use windmill_common::utils::{query_elems_from_hub, StripPath};
/// Transforms the path for workspace scripts/flows.
@@ -857,44 +856,28 @@ impl ServerHandler for Runner {
})
};
let http_parts = context
.extensions
.get::<axum::http::request::Parts>()
.ok_or_else(|| {
tracing::error!("http::request::Parts not found");
Error::internal_error("http::request::Parts not found", None)
})?;
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
tracing::error!("ApiAuthed Axum extension not found");
Error::internal_error("ApiAuthed Axum extension not found", None)
})?;
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
tracing::error!("DB Axum extension not found");
Error::internal_error("DB Axum extension not found", None)
})?;
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
tracing::error!("UserDB Axum extension not found");
Error::internal_error("UserDB Axum extension not found", None)
})?;
let authed = context
.req_extensions
.get::<ApiAuthed>()
.ok_or_else(|| Error::internal_error("ApiAuthed not found", None))?;
let db = context
.req_extensions
.get::<DB>()
.ok_or_else(|| Error::internal_error("DB not found", None))?;
let user_db = context
.req_extensions
.get::<UserDB>()
.ok_or_else(|| Error::internal_error("UserDB not found", None))?;
let args = parse_args(request.arguments)?;
let workspace_id = http_parts
.extensions
.get::<WorkspaceId>()
.ok_or_else(|| {
tracing::error!("WorkspaceId not found");
Error::internal_error("WorkspaceId not found", None)
})
.map(|w_id| w_id.0.clone())?;
let (tool_type, path, is_hub) =
Runner::reverse_transform(&request.name).unwrap_or_default();
let item_schema = if is_hub {
Runner::get_hub_script_schema(&format!("hub/{}", path), db).await?
} else {
Runner::get_item_schema(&path, user_db, authed, &workspace_id, &tool_type).await?
Runner::get_item_schema(&path, user_db, authed, &context.workspace_id, &tool_type)
.await?
};
let schema_obj = if let Some(ref s) = item_schema {
@@ -923,6 +906,8 @@ impl ServerHandler for Runner {
} else {
windmill_queue::PushArgsOwned::default()
};
let w_id = context.workspace_id.clone();
let script_or_flow_path = if is_hub {
StripPath(format!("hub/{}", path))
} else {
@@ -937,7 +922,7 @@ impl ServerHandler for Runner {
script_or_flow_path,
authed.clone(),
user_db.clone(),
workspace_id.clone(),
w_id.clone(),
push_args,
)
.await
@@ -949,7 +934,7 @@ impl ServerHandler for Runner {
authed.clone(),
user_db.clone(),
push_args,
workspace_id.clone(),
w_id.clone(),
)
.await
};
@@ -993,38 +978,19 @@ impl ServerHandler for Runner {
_request: Option<PaginatedRequestParam>,
mut _context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, Error> {
let http_parts = _context
.extensions
.get::<axum::http::request::Parts>()
.ok_or_else(|| {
tracing::error!("http::request::Parts not found");
Error::internal_error("http::request::Parts not found", None)
})?;
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
tracing::error!("DB Axum extension not found");
Error::internal_error("DB Axum extension not found", None)
})?;
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
tracing::error!("UserDB Axum extension not found");
Error::internal_error("UserDB Axum extension not found", None)
})?;
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
tracing::error!("ApiAuthed Axum extension not found");
Error::internal_error("ApiAuthed Axum extension not found", None)
})?;
let workspace_id = http_parts
.extensions
.get::<WorkspaceId>()
.ok_or_else(|| {
tracing::error!("WorkspaceId not found");
Error::internal_error("WorkspaceId not found", None)
})
.map(|w_id| w_id.0.clone())?;
let workspace_id = _context.workspace_id.clone();
let db = _context
.req_extensions
.get::<DB>()
.ok_or_else(|| Error::internal_error("DB not found", None))?;
let user_db = _context
.req_extensions
.get::<UserDB>()
.ok_or_else(|| Error::internal_error("UserDB not found", None))?;
let authed = _context
.req_extensions
.get::<ApiAuthed>()
.ok_or_else(|| Error::internal_error("ApiAuthed not found", None))?;
let owned_scope = authed.scopes.as_ref().and_then(|scopes| {
scopes
.iter()
@@ -1161,54 +1127,15 @@ impl ServerHandler for Runner {
}
}
#[derive(Clone, Debug)]
pub struct WorkspaceId(pub String);
pub async fn extract_and_store_workspace_id(
Path(params): Path<String>,
mut request: Request<axum::body::Body>,
next: Next,
) -> Response {
let workspace_id = params;
request.extensions_mut().insert(WorkspaceId(workspace_id));
next.run(request).await
}
pub async fn setup_mcp_server() -> anyhow::Result<(Router, Arc<LocalSessionManager>)> {
let session_manager = Arc::new(LocalSessionManager::default());
let service_config = Default::default();
let service = StreamableHttpService::new(
|| Ok(Runner::new()),
session_manager.clone(),
service_config,
);
let router = axum::Router::new().nest_service("/", service);
Ok((router, session_manager))
}
pub async fn shutdown_mcp_server(session_manager: Arc<LocalSessionManager>) {
let session_ids_to_close = {
let sessions_map = session_manager.sessions.read().await;
sessions_map.keys().cloned().collect::<Vec<_>>()
pub fn setup_mcp_server(addr: SocketAddr, path: &str) -> anyhow::Result<(SseServer, Router)> {
let config = SseServerConfig {
bind: addr,
sse_path: "/sse".to_string(),
post_path: "/message".to_string(),
full_message_path: path.to_string(),
ct: CancellationToken::new(),
sse_keep_alive: None,
};
if !session_ids_to_close.is_empty() {
tracing::info!(
"Closing {} active MCP session(s)...",
session_ids_to_close.len()
);
let close_futures = session_ids_to_close
.iter()
.map(|session_id| {
let manager_clone = session_manager.clone();
async move {
if let Err(_) = manager_clone.close_session(session_id).await {
tracing::warn!("Error closing MCP session");
}
}
})
.collect::<Vec<_>>();
futures::future::join_all(close_futures).await;
}
Ok(SseServer::new(config))
}

View File

@@ -478,7 +478,7 @@ pub async fn test_mqtt_connection(
test_postgres;
let mqtt_resource = try_get_resource_from_db_as::<MqttResource>(
&authed,
authed,
Some(user_db),
&db,
&mqtt_resource_path,
@@ -1253,7 +1253,7 @@ impl MqttConfig {
}
}
let mqtt_resource = try_get_resource_from_db_as::<MqttResource>(
&authed,
authed,
Some(UserDB::new(db.clone())),
db,
mqtt_resource_path,

View File

@@ -1,984 +0,0 @@
use std::{
collections::{HashMap, HashSet},
fmt::Display,
};
use anyhow::anyhow;
use axum::{
body::Body, extract::Path, http, response::Response, routing::post, Extension, Json, Router,
};
use http::{header, HeaderValue, Method, StatusCode};
use indexmap::IndexMap;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::{to_value, Map, Value};
use sqlx::PgConnection;
use url::Url;
use windmill_common::{
db::UserDB,
error::{Error, Result},
utils::{deserialize_url, empty_as_none, is_empty, RunnableKind},
DB,
};
use crate::db::ApiAuthed;
#[cfg(feature = "http_trigger")]
use crate::{
http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication,
http_triggers::AuthenticationMethod, resources::try_get_resource_from_db_as,
};
lazy_static::lazy_static! {
static ref DEFAULT_OPENAPI_INFO_OBJECT: Info = Info {
title: "Windmill API".to_string(),
version: "1.0.0".to_string(),
..Default::default()
};
}
const DEFAULT_OPENAPI_GENERATED_VERSION: &'static str = "3.1.0";
const JWT_SECURITY_SCHEME: &'static str = "JwtAuth";
const BASIC_HTTP_AUTH_SCHEME: &'static str = "BasicHttp";
const DEFAULT_REQUEST_KEY: &'static str = "defaultRequest";
const DEFAULT_ASYNC_RESPONSE_KEY: &'static str = "AsyncResponse";
const DEFAULT_SYNC_RESPONSE_KEY: &'static str = "SyncResponse";
const DEFAULT_PAYLOAD_PARAM_KEY: &'static str = "PayloadParam";
pub fn openapi_service() -> Router {
Router::new()
.route("/generate", post(generate_openapi_spec))
.route("/download", post(download_spec))
}
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum Format {
JSON,
YAML,
}
impl Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let format = match self {
Format::JSON => "json",
Format::YAML => "yaml",
};
write!(f, "{}", format)
}
}
impl Default for Format {
fn default() -> Self {
Self::YAML
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
struct Contact {
#[serde(skip_serializing_if = "is_empty")]
name: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_url",
skip_serializing_if = "Option::is_none"
)]
url: Option<Url>,
#[serde(skip_serializing_if = "is_empty")]
email: Option<String>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
struct License {
name: String,
#[serde(skip_serializing_if = "is_empty")]
identifier: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_url",
skip_serializing_if = "Option::is_none"
)]
url: Option<Url>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Info {
title: String,
version: String,
#[serde(skip_serializing_if = "is_empty")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
contact: Option<Contact>,
#[serde(skip_serializing_if = "Option::is_none")]
license: Option<License>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Server {
url: String,
#[serde(skip_serializing_if = "is_empty")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
variables: Option<HashMap<String, Value>>,
}
#[derive(Debug)]
pub enum SecurityScheme {
BearerJwt,
BasicHttp,
ApiKey(String),
}
#[derive(Debug)]
pub struct WebhookConfig {
runnable_kind: RunnableKind,
}
impl WebhookConfig {
pub fn new(runnable_kind: RunnableKind) -> Self {
Self { runnable_kind }
}
}
#[derive(Debug)]
pub struct HttpRouteConfig {
method: Method,
}
impl HttpRouteConfig {
pub fn new(method: Method) -> Self {
Self { method }
}
}
#[derive(Debug)]
pub enum Kind {
Webhook(WebhookConfig),
HttpRoute(HttpRouteConfig),
}
#[derive(Debug)]
pub struct FuturePath {
route_path: String,
kind: Kind,
is_async: Option<bool>,
summary: Option<String>,
description: Option<String>,
security_scheme: Option<SecurityScheme>,
}
impl FuturePath {
pub fn new(
route_path: String,
kind: Kind,
is_async: Option<bool>,
summary: Option<String>,
description: Option<String>,
security_scheme: Option<SecurityScheme>,
) -> FuturePath {
FuturePath { route_path, kind, is_async, summary, description, security_scheme }
}
}
fn from_route_path_to_openapi_path(
route_path: &str,
kind: &Kind,
) -> Result<(Vec<String>, Option<Value>)> {
let mut openapi_path = String::new();
let mut parameters = Vec::new();
for segment in route_path.split('/') {
if segment.starts_with(':') {
let param_name = &segment[1..];
if param_name.is_empty() {
return Err(anyhow!("Empty parameter name in path: {}", route_path).into());
}
openapi_path.push_str(&format!("/{{{}}}", param_name));
parameters.push(serde_json::json!({
"name": param_name,
"in": "path",
"required": true,
"schema": { "type": "string" }
}));
} else if !segment.is_empty() {
openapi_path.push('/');
openapi_path.push_str(segment);
} else {
openapi_path.push('/');
}
}
let parameters_json = if parameters.is_empty() {
None
} else {
Some(Value::Array(parameters))
};
let prefix = match kind {
Kind::HttpRoute(_) => "",
Kind::Webhook(WebhookConfig { runnable_kind }) => match runnable_kind {
RunnableKind::Script => "p",
RunnableKind::Flow => "f",
},
};
let normalized_path = if openapi_path.starts_with('/') {
format!("{prefix}{openapi_path}")
} else {
format!("{}/{}", prefix, openapi_path)
};
let route_paths = if prefix.is_empty() {
vec![normalized_path]
} else {
vec![
format!("/run/{}", &normalized_path),
format!("/run_wait_result/{}", &normalized_path),
]
};
Ok((route_paths, parameters_json))
}
fn get_servers_component(url: &str, kind: &Kind) -> Server {
let url = url.trim_end_matches('/');
let server = match kind {
Kind::HttpRoute(_) => {
Server { url: format!("{}/api/r", url), description: None, variables: None }
}
Kind::Webhook(_) => Server {
url: format!("{}/api/w/{{workspace}}/jobs", url),
variables: Some(HashMap::from([(
"workspace".to_string(),
serde_json::json!({
"default": "test",
"description": "Workspace identifier"
}),
)])),
description: None,
},
};
server
}
fn generate_paths(
paths: Vec<FuturePath>,
url: Option<&Url>,
) -> Result<IndexMap<String, IndexMap<String, Value>>> {
let mut map: IndexMap<String, IndexMap<String, Value>> = IndexMap::new();
let generate_default_request = || {
serde_json::json!({
"$ref": format!("#/components/requestBodies/{DEFAULT_REQUEST_KEY}")
})
};
let generate_response = |is_async: bool| {
let responses = if is_async {
serde_json::json!({
"200": {
"$ref": format!("#/components/responses/{DEFAULT_ASYNC_RESPONSE_KEY}")
}
})
} else {
serde_json::json!(serde_json::json!({
"200": {
"$ref": format!("#/components/responses/{DEFAULT_SYNC_RESPONSE_KEY}")
}
}))
};
responses
};
let get_security_scheme = |security_scheme: Option<&SecurityScheme>| -> Vec<Value> {
if let Some(security_scheme) = security_scheme {
let scheme = match security_scheme {
SecurityScheme::ApiKey(api_key) => header_to_pascal_case(&api_key),
SecurityScheme::BearerJwt => JWT_SECURITY_SCHEME.to_owned(),
SecurityScheme::BasicHttp => BASIC_HTTP_AUTH_SCHEME.to_owned(),
};
vec![serde_json::json!({
scheme: []
})]
} else {
vec![]
}
};
let mut duplicate_webhooks = HashSet::new();
for path in paths {
let (route_paths, parameters) =
from_route_path_to_openapi_path(&path.route_path, &path.kind)?;
for route_path in route_paths {
let path_object = map.entry(route_path.clone()).or_insert_with(|| {
let mut path_object = IndexMap::new();
if let Some(url) = url {
let servers = get_servers_component(url.as_str(), &path.kind);
path_object.insert("servers".to_string(), to_value(vec![servers]).unwrap());
}
if parameters.is_some() {
path_object.insert(
"parameters".to_string(),
to_value(parameters.clone()).unwrap(),
);
}
path_object
});
let is_async;
let (methods, is_webhook) = match &path.kind {
Kind::Webhook(_) => {
if !duplicate_webhooks.insert(route_path.clone()) {
return Err(anyhow!("Found duplicate webhook: {}", path.route_path).into());
}
is_async = route_path.starts_with("/run/");
let methods = if is_async {
vec![Method::POST]
} else {
vec![Method::GET, Method::POST]
};
(methods, true)
}
Kind::HttpRoute(HttpRouteConfig { method }) => {
if path_object.get(&method.to_string()).is_some() {
return Err(anyhow!("Found duplicate route: {}", path.route_path).into());
}
is_async = path.is_async.unwrap_or(true);
(vec![method.to_owned()], false)
}
};
for method in methods {
let mut method_map = IndexMap::new();
if let Some(summary) = path.summary.as_ref().filter(|s| !s.is_empty()) {
method_map.insert("summary", Value::String(summary.to_owned()));
}
if let Some(description) = path.description.as_ref().filter(|s| !s.is_empty()) {
method_map.insert("description", Value::String(description.to_owned()));
}
method_map.insert(
"security",
to_value(get_security_scheme(path.security_scheme.as_ref()))?,
);
if method != Method::GET {
method_map.insert("requestBody", generate_default_request());
} else if is_webhook {
method_map.insert(
"parameters",
Value::Array(vec![serde_json::json!({
"$ref": format!("#/components/parameters/{DEFAULT_PAYLOAD_PARAM_KEY}")
})]),
);
}
method_map.insert("responses", generate_response(is_async));
path_object.insert(method.to_string().to_lowercase(), to_value(&method_map)?);
}
}
}
return Ok(map);
}
pub fn transform_to_minified_postgres_regex(glob: &str) -> String {
let mut regex = String::from("^");
for ch in glob.chars() {
match ch {
'*' => regex.push_str(".*"),
'.' | '+' | '(' | ')' | '|' | '^' | '$' | '{' | '}' | '[' | ']' | '\\' => {
regex.push('\\');
regex.push(ch);
}
_ => regex.push(ch),
}
}
regex.push('$');
regex
}
#[derive(Debug, Default)]
pub struct ServerToSet {
pub http_route: bool,
pub webhook_flow: bool,
pub webhook_script: bool,
}
impl ServerToSet {
pub fn new(http_route: bool, webhook_flow: bool, webhook_script: bool) -> ServerToSet {
ServerToSet { http_route, webhook_flow, webhook_script }
}
}
fn header_to_pascal_case(header: &str) -> String {
header
.split(|c: char| c == '-' || c == '_' || c == ' ')
.filter(|s| !s.is_empty())
.map(|s| {
let mut chars = s.chars();
match chars.next() {
Some(first) => {
first.to_ascii_uppercase().to_string()
+ chars.as_str().to_ascii_lowercase().as_str()
}
None => String::new(),
}
})
.collect::<String>()
}
#[derive(Debug, Default)]
struct SecuritySchemeToAdd {
basic_http: bool,
bearer_jwt: bool,
api_keys: Vec<(String, Value)>,
}
fn generate_all_security_schemes(future_paths: &[FuturePath]) -> SecuritySchemeToAdd {
let mut to_add = SecuritySchemeToAdd::default();
let mut set = HashSet::new();
for future_path in future_paths {
if !to_add.basic_http
&& matches!(future_path.security_scheme, Some(SecurityScheme::BasicHttp))
{
to_add.basic_http = true
} else if !to_add.bearer_jwt
&& matches!(future_path.security_scheme, Some(SecurityScheme::BearerJwt))
{
to_add.bearer_jwt = true
} else if let Some(SecurityScheme::ApiKey(api_key)) = future_path.security_scheme.as_ref() {
let pascal_case_header = header_to_pascal_case(&api_key);
if !set.insert(pascal_case_header.clone()) {
continue;
}
let scheme = serde_json::json!({
"type": "apiKey",
"in": "header",
"name": api_key
});
to_add.api_keys.push((pascal_case_header, scheme));
}
}
to_add
}
fn generate_components(future_paths: &[FuturePath]) -> Map<String, Value> {
let mut components = Map::new();
if future_paths
.iter()
.any(|path| matches!(path.kind, Kind::Webhook(_)))
{
components.insert(
"parameters".to_owned(),
serde_json::json!({
"PayloadParam": {
"name": "payload",
"in": "query",
"required": true,
"description": "A URL-safe base64-encoded JSON string payload.",
"schema": {
"type": "string"
}
}
}),
);
}
{
let mut security_scheme = Map::new();
let SecuritySchemeToAdd { basic_http, bearer_jwt, api_keys } =
generate_all_security_schemes(future_paths);
if basic_http {
security_scheme.insert(
BASIC_HTTP_AUTH_SCHEME.to_owned(),
serde_json::json!({
"type": "http",
"scheme": "basic"
}),
);
}
if bearer_jwt {
security_scheme.insert(
JWT_SECURITY_SCHEME.to_owned(),
serde_json::json!({
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}),
);
}
for (key, value) in api_keys {
security_scheme.insert(key, value);
}
components.insert("securitySchemes".to_owned(), Value::Object(security_scheme));
}
components.insert("requestBodies".to_owned(), serde_json::json!({
DEFAULT_REQUEST_KEY: {
"description": "This route may or may not require a request body, but its structure and content type are unknown.",
"required": false,
"content": {
"application/json": {}
}
}
}));
components.insert("responses".to_owned(), serde_json::json!({
DEFAULT_ASYNC_RESPONSE_KEY: {
"description": "Returns a job ID as a UUID string.",
"content": {
"text/plain": {
"schema": {
"type": "string",
"format": "uuid",
"examples": [ "550e8400-e29b-41d4-a716-446655440000" ]
}
}
}
},
DEFAULT_SYNC_RESPONSE_KEY: {
"description": "This route may return a response, but its structure and content type are unknown.",
"content": {
"application/octet-stream": {}
}
},
}));
components
}
pub fn generate_openapi_document(
info: Option<&Info>,
url: Option<&Url>,
paths: Vec<FuturePath>,
format: Format,
) -> Result<String> {
let mut openapi_doc: IndexMap<&'static str, Value> = IndexMap::new();
openapi_doc.insert("openapi", to_value(&DEFAULT_OPENAPI_GENERATED_VERSION)?);
openapi_doc.insert(
"info",
to_value(info.unwrap_or(&DEFAULT_OPENAPI_INFO_OBJECT))?,
);
openapi_doc.insert("components", Value::Object(generate_components(&paths)));
openapi_doc.insert("paths", to_value(generate_paths(paths, url)?)?);
let openapi_document = match format {
Format::YAML => serde_yml::to_string(&openapi_doc).map_err(|err| {
anyhow!(
"Could not generate OpenAPI document in YAML format: {}",
err
)
})?,
Format::JSON => serde_json::to_string_pretty(&openapi_doc).map_err(|err| {
anyhow!(
"Could not generate OpenAPI document in JSON format: {}",
err
)
})?,
};
Ok(openapi_document)
}
#[allow(unused)]
#[derive(Debug, Deserialize)]
struct HttpRouteFilter {
folder_regex: String,
path_regex: String,
route_path_regex: String,
}
#[derive(Debug, Deserialize)]
struct WebhookFilter {
user_or_folder_regex: String,
user_or_folder_regex_value: String,
path: String,
runnable_kind: RunnableKind,
}
#[derive(Debug, Deserialize)]
struct GenerateOpenAPI {
info: Option<Info>,
url: Option<Url>,
#[serde(default, deserialize_with = "empty_as_none")]
http_route_filters: Option<Vec<HttpRouteFilter>>,
#[serde(default, deserialize_with = "empty_as_none")]
webhook_filters: Option<Vec<WebhookFilter>>,
#[serde(default)]
openapi_spec_format: Format,
}
#[cfg(feature = "http_trigger")]
async fn http_routes_to_future_paths(
db: &DB,
user_db: UserDB,
authed: &ApiAuthed,
pg_pool: &mut PgConnection,
http_route_filters: Option<&[HttpRouteFilter]>,
w_id: &str,
) -> Result<Vec<FuturePath>> {
let mut http_routes = Vec::new();
if let Some(http_route_filters) = http_route_filters {
let path_regex = http_route_filters
.iter()
.map(|filter| {
transform_to_minified_postgres_regex(&format!(
"f/{}/{}",
filter.folder_regex, filter.path_regex
))
})
.collect_vec();
let route_path_regex = http_route_filters
.iter()
.map(|filter| transform_to_minified_postgres_regex(&filter.route_path_regex))
.collect_vec();
#[derive(Debug, Deserialize)]
struct MinifiedHttpTrigger {
route_path: String,
http_method: HttpMethod,
is_async: bool,
workspaced_route: bool,
summary: Option<String>,
description: Option<String>,
authentication_method: AuthenticationMethod,
authentication_resource_path: Option<String>,
}
http_routes = sqlx::query_as!(
MinifiedHttpTrigger,
r#"
SELECT
route_path,
http_method AS "http_method: _",
is_async,
workspaced_route,
summary,
description,
authentication_method AS "authentication_method: _",
authentication_resource_path
FROM
http_trigger
WHERE
path ~ ANY($1) AND
route_path ~ ANY($2) AND
workspace_id = $3
"#,
&path_regex,
&route_path_regex,
&w_id
)
.fetch_all(pg_pool)
.await?;
}
let mut openapi_future_paths = Vec::with_capacity(http_routes.len());
for http_route in http_routes {
let auth_method = match http_route.authentication_method {
AuthenticationMethod::BasicHttp => Some(SecurityScheme::BasicHttp),
AuthenticationMethod::Windmill => Some(SecurityScheme::BearerJwt),
AuthenticationMethod::ApiKey => {
let resource_path = match http_route.authentication_resource_path {
Some(resource_path) => resource_path,
None => {
return Err(Error::BadRequest(
"Missing authentication resource path".to_string(),
));
}
};
let api = try_get_resource_from_db_as::<ApiKeyAuthentication>(
authed,
Some(user_db.clone()),
db,
&resource_path,
w_id,
)
.await?;
Some(SecurityScheme::ApiKey(api.api_key_header))
}
_ => None,
};
let route_path = if http_route.workspaced_route {
format!("{}/{}", w_id, http_route.route_path.trim_start_matches('/'))
} else {
http_route.route_path.clone()
};
let method = match http_route.http_method {
HttpMethod::Get => Method::GET,
HttpMethod::Post => Method::POST,
HttpMethod::Put => Method::PUT,
HttpMethod::Patch => Method::PATCH,
HttpMethod::Delete => Method::DELETE,
};
let future_path = FuturePath::new(
route_path,
Kind::HttpRoute(HttpRouteConfig::new(method)),
Some(http_route.is_async),
http_route.summary,
http_route.description,
auth_method,
);
openapi_future_paths.push(future_path);
}
Ok(openapi_future_paths)
}
#[cfg(not(feature = "http_trigger"))]
async fn http_routes_to_future_paths(
_db: &DB,
_user_db: UserDB,
_authed: &ApiAuthed,
_pg_pool: &mut PgConnection,
_http_route_filters: Option<&[HttpRouteFilter]>,
_w_id: &str,
) -> Result<Vec<FuturePath>> {
Ok(Vec::new())
}
async fn webhook_to_future_paths(
pg_pool: &mut PgConnection,
webhook_filters: Option<&[WebhookFilter]>,
w_id: &str,
) -> Result<Vec<FuturePath>> {
let mut openapi_future_paths = Vec::new();
if let Some(webhook_filters) = webhook_filters {
let mut script_webhook_filter = Vec::new();
let mut flow_webhook_filter = Vec::new();
for webhook in webhook_filters {
let full_regex = transform_to_minified_postgres_regex(&format!(
"{}/{}/{}",
&webhook.user_or_folder_regex, &webhook.user_or_folder_regex_value, &webhook.path
));
match webhook.runnable_kind {
RunnableKind::Script => {
script_webhook_filter.push(full_regex);
}
RunnableKind::Flow => {
flow_webhook_filter.push(full_regex);
}
}
}
#[derive(Debug, Deserialize, Clone, Hash)]
struct MinifiedWebhook {
path: String,
description: Option<String>,
summary: Option<String>,
}
impl PartialEq for MinifiedWebhook {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for MinifiedWebhook {}
let webhook_scripts = sqlx::query_as!(
MinifiedWebhook,
r#"SELECT
path,
summary,
description
FROM
script
WHERE
path ~ ANY($1) AND
workspace_id = $2
"#,
&script_webhook_filter,
&w_id
)
.fetch_all(&mut *pg_pool)
.await?
.into_iter()
.unique()
.collect_vec();
let webhook_flows = sqlx::query_as!(
MinifiedWebhook,
r#"SELECT
path,
summary,
description
FROM
flow
WHERE
path ~ ANY($1) AND
workspace_id = $2
"#,
&flow_webhook_filter,
&w_id
)
.fetch_all(&mut *pg_pool)
.await?
.into_iter()
.unique()
.collect_vec();
openapi_future_paths.reserve_exact(webhook_scripts.len() + webhook_flows.len());
for webhook in webhook_scripts {
openapi_future_paths.push(FuturePath::new(
webhook.path,
Kind::Webhook(WebhookConfig::new(RunnableKind::Script)),
None,
webhook.summary,
webhook.description,
Some(SecurityScheme::BearerJwt),
));
}
for webhook in webhook_flows {
openapi_future_paths.push(FuturePath::new(
webhook.path,
Kind::Webhook(WebhookConfig::new(RunnableKind::Flow)),
None,
webhook.summary,
webhook.description,
Some(SecurityScheme::BearerJwt),
));
}
}
Ok(openapi_future_paths)
}
async fn generate_openapi_future_path(
db: &DB,
user_db: UserDB,
authed: &ApiAuthed,
http_route_filters: Option<&[HttpRouteFilter]>,
webhook_filters: Option<&[WebhookFilter]>,
w_id: &str,
) -> Result<Vec<FuturePath>> {
if http_route_filters.is_none() && webhook_filters.is_none() {
return Err(Error::BadRequest(
"Expected http route filter and/or webhook filters".to_string(),
));
}
let mut tx = user_db.clone().begin(authed).await?;
let mut openapi_future_paths =
http_routes_to_future_paths(db, user_db, authed, &mut tx, http_route_filters, w_id).await?;
openapi_future_paths
.append(&mut webhook_to_future_paths(&mut tx, webhook_filters, w_id).await?);
tx.commit().await?;
if openapi_future_paths.is_empty() {
return Err(Error::NotFound(
"No match for the current filter".to_string(),
));
}
Ok(openapi_future_paths)
}
async fn generate_openapi_spec(
Extension(authed): Extension<ApiAuthed>,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(generate_openapi): Json<GenerateOpenAPI>,
) -> Result<String> {
let openapi_future_paths = generate_openapi_future_path(
&db,
user_db,
&authed,
generate_openapi.http_route_filters.as_deref(),
generate_openapi.webhook_filters.as_deref(),
&w_id,
)
.await?;
let openapi_document = generate_openapi_document(
generate_openapi.info.as_ref(),
generate_openapi.url.as_ref(),
openapi_future_paths,
generate_openapi.openapi_spec_format,
);
openapi_document
}
async fn download_spec(
Extension(authed): Extension<ApiAuthed>,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(generate_openapi): Json<GenerateOpenAPI>,
) -> Result<Response> {
let openapi_future_paths = generate_openapi_future_path(
&db,
user_db,
&authed,
generate_openapi.http_route_filters.as_deref(),
generate_openapi.webhook_filters.as_deref(),
&w_id,
)
.await?;
let openapi_document = generate_openapi_document(
generate_openapi.info.as_ref(),
generate_openapi.url.as_ref(),
openapi_future_paths,
generate_openapi.openapi_spec_format,
)?;
let response = Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"),
)
.body(Body::from(openapi_document))
.unwrap();
Ok(response)
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,13 +7,15 @@ use crate::{
};
use chrono::Utc;
use itertools::Itertools;
use native_tls::{Certificate, TlsConnector};
use pg_escape::quote_identifier;
use pg_escape::{quote_identifier, quote_literal};
use rand::Rng;
use rust_postgres::{config::SslMode, Client, Config, NoTls};
use rust_postgres_native_tls::MakeTlsConnector;
use serde_json::value::RawValue;
use sqlx::{
postgres::{PgConnectOptions, PgSslMode},
Connection, PgConnection,
};
use std::collections::HashMap;
use std::str::FromStr;
use axum::{
routing::{delete, get, post},
@@ -31,7 +33,7 @@ use handler::{
};
use windmill_common::{
db::UserDB,
error::{to_anyhow, Error, Result},
error::{Error, Result},
utils::StripPath,
};
mod bool;
@@ -50,144 +52,76 @@ const ERROR_REPLICATION_SLOT_NOT_EXISTS: &str = r#"The replication slot associat
const ERROR_PUBLICATION_NAME_NOT_EXISTS: &str = r#"The publication associated with this trigger no longer exists. Recreate a new publication or select an existing one in the advanced tab, or delete and recreate a new trigger"#;
fn build_tls_connector(
ssl_mode: SslMode,
root_certificate_pem: Option<&String>,
) -> Result<Option<MakeTlsConnector>> {
let get_tls_builder_for_verify = |root_certificate: Option<&String>| {
let mut builder = TlsConnector::builder();
if let Some(root_certificate) = root_certificate {
let root_certificate_pem =
Certificate::from_pem(root_certificate.as_bytes()).map_err(to_anyhow)?;
builder.add_root_certificate(root_certificate_pem);
}
Ok::<_, Error>(builder)
};
let connector = match ssl_mode {
SslMode::Disable => return Ok(None),
SslMode::Require | SslMode::Prefer => {
let mut builder = TlsConnector::builder();
builder.danger_accept_invalid_certs(true);
builder.danger_accept_invalid_hostnames(true);
builder
}
SslMode::VerifyCa => {
let mut builder = get_tls_builder_for_verify(root_certificate_pem)?;
builder.danger_accept_invalid_hostnames(true);
builder
}
SslMode::VerifyFull => {
let builder = get_tls_builder_for_verify(root_certificate_pem)?;
builder
}
_ => unreachable!(),
};
Ok(Some(MakeTlsConnector::new(
connector.build().map_err(to_anyhow)?,
)))
}
pub async fn get_raw_postgres_connection(
database: &Postgres,
logical_mode: bool,
) -> Result<Client> {
let ssl_mode = match database.sslmode.as_ref() {
"disable" => SslMode::Disable,
"" | "prefer" | "allow" => SslMode::Prefer,
"require" => SslMode::Require,
"verify-ca" => SslMode::VerifyCa,
"verify-full" => SslMode::VerifyFull,
ssl_mode => {
return Err(Error::BadRequest(
format!("Invalid ssl mode for postgres: {}, please put a valid ssl_mode among the following available ssl mode: ['disable', 'allow', 'prefer', 'verify-ca', 'verify-full']", ssl_mode),
))
}
};
let mut config = Config::new();
config
.dbname(&database.dbname)
.host(&database.host)
.user(&database.user)
.ssl_mode(ssl_mode);
if logical_mode {
config.replication_mode(rust_postgres::config::ReplicationMode::Logical);
}
if let Some(port) = database.port {
config.port(port);
};
if !database.password.is_empty() {
config.password(&database.password);
}
let connector = build_tls_connector(ssl_mode, database.root_certificate_pem.as_ref())?;
let client = if let Some(connector) = connector {
let (client, connection) = config.connect(connector).await.map_err(to_anyhow)?;
tokio::spawn(async move {
if let Err(e) = connection.await {
tracing::debug!("{:#?}", e);
};
tracing::info!("Successfully Connected into database");
});
client
} else {
let (client, connection) = config.connect(NoTls).await.map_err(to_anyhow)?;
tokio::spawn(async move {
if let Err(e) = connection.await {
tracing::debug!("{:#?}", e);
};
tracing::info!("Successfully Connected into database");
});
client
};
Ok(client)
}
pub async fn get_pg_connection(
authed: ApiAuthed,
user_db: Option<UserDB>,
db: &DB,
postgres_resource_path: &str,
w_id: &str,
logical_mode: bool,
) -> Result<Client> {
) -> Result<PgConnection> {
let database =
try_get_resource_from_db_as::<Postgres>(&authed, user_db, db, postgres_resource_path, w_id)
try_get_resource_from_db_as::<Postgres>(authed, user_db, db, postgres_resource_path, w_id)
.await?;
Ok(get_raw_postgres_connection(&database, logical_mode).await?)
Ok(get_raw_postgres_connection(&database).await?)
}
pub async fn get_default_pg_connection(
authed: ApiAuthed,
user_db: Option<UserDB>,
db: &DB,
postgres_resource_path: &str,
w_id: &str,
) -> Result<Client> {
get_pg_connection(authed, user_db, db, postgres_resource_path, w_id, false).await
pub async fn get_raw_postgres_connection(db: &Postgres) -> Result<PgConnection> {
let options = {
let sslmode = if !db.sslmode.is_empty() {
PgSslMode::from_str(&db.sslmode)?
} else {
PgSslMode::Prefer
};
let options = {
let inner_options = PgConnectOptions::new()
.host(&db.host)
.database(&db.dbname)
.ssl_mode(sslmode)
.username(&db.user);
if let Some(port) = db.port {
inner_options.port(port)
} else {
inner_options
}
};
let options = if let Some(root_certificate_pem) = &db.root_certificate_pem {
options.ssl_root_cert_from_pem(root_certificate_pem.as_bytes().to_vec())
} else {
options
};
if !db.password.is_empty() {
options.password(&db.password)
} else {
options
}
};
Ok(PgConnection::connect_with(&options).await?)
}
pub async fn create_logical_replication_slot(tx: &Client, slot_name: &str) -> Result<()> {
tx.execute(
&format!("SELECT * FROM pg_create_logical_replication_slot($1, 'pgoutput')"),
&[&slot_name],
)
.await
.map_err(to_anyhow)?;
pub async fn create_logical_replication_slot(
pg_connection: &mut PgConnection,
name: &str,
) -> Result<()> {
let query = format!(
r#"
SELECT
*
FROM
pg_create_logical_replication_slot({}, 'pgoutput');"#,
quote_literal(&name)
);
sqlx::query(&query).execute(pg_connection).await?;
Ok(())
}
async fn check_if_valid_publication_for_postgres_version(
pg_connection: &Client,
pg_connection: &mut PgConnection,
table_to_track: Option<&[Relations]>,
) -> Result<bool> {
let postgres_version = get_postgres_version_internal(pg_connection).await?;
@@ -221,7 +155,7 @@ async fn check_if_valid_publication_for_postgres_version(
}
pub async fn create_pg_publication(
pg_connection: &Client,
pg_connection: &mut PgConnection,
publication_name: &str,
table_to_track: Option<&[Relations]>,
transaction_to_track: &[String],
@@ -243,7 +177,7 @@ pub async fn create_pg_publication(
} else {
if pg_14 && first {
query.push_str(" TABLE ONLY ");
first = false
first = false;
} else if !pg_14 {
query.push_str(" TABLE ONLY ");
}
@@ -290,22 +224,20 @@ pub async fn create_pg_publication(
query.push_str("');");
}
pg_connection
.execute(&query, &[])
.await
.map_err(to_anyhow)?;
sqlx::query(&query).execute(pg_connection).await?;
Ok(())
}
pub async fn drop_publication(pg_connection: &Client, publication_name: &str) -> Result<()> {
pub async fn drop_publication(
pg_connection: &mut PgConnection,
publication_name: &str,
) -> Result<()> {
let mut query = String::from("DROP PUBLICATION IF EXISTS ");
let quoted_publication_name = quote_identifier(publication_name);
query.push_str(&quoted_publication_name);
pg_connection
.execute(&query, &[])
.await
.map_err(to_anyhow)?;
sqlx::query(&query).execute(pg_connection).await?;
Ok(())
}

View File

@@ -19,27 +19,25 @@ use crate::{
use bytes::{BufMut, Bytes, BytesMut};
use chrono::TimeZone;
use futures::{pin_mut, SinkExt, StreamExt};
use native_tls::{Certificate, TlsConnector};
use pg_escape::{quote_identifier, quote_literal};
use rand::seq::SliceRandom;
use rust_postgres::{Client, CopyBothDuplex, SimpleQueryMessage};
use rust_postgres::{config::SslMode, Client, Config, CopyBothDuplex, NoTls, SimpleQueryMessage};
use rust_postgres_native_tls::MakeTlsConnector;
use serde::Deserialize;
use serde_json::value::RawValue;
use sqlx::types::Json as SqlxJson;
use windmill_common::{
db::UserDB,
error::{self, to_anyhow},
triggers::TriggerKind,
utils::report_critical_error,
worker::to_raw_value,
db::UserDB, error, triggers::TriggerKind, utils::report_critical_error, worker::to_raw_value,
INSTANCE_NAME,
};
use super::{
drop_publication, get_default_pg_connection, get_raw_postgres_connection,
drop_publication, get_pg_connection,
handler::{drop_logical_replication_slot, Postgres, PostgresTrigger},
replication_message::PrimaryKeepAliveBody,
Error, ERROR_PUBLICATION_NAME_NOT_EXISTS, ERROR_REPLICATION_SLOT_NOT_EXISTS,
ERROR_PUBLICATION_NAME_NOT_EXISTS, ERROR_REPLICATION_SLOT_NOT_EXISTS,
};
pub struct LogicalReplicationSettings {
@@ -71,11 +69,109 @@ impl RowExist for Vec<SimpleQueryMessage> {
}
}
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("Error from database: {0}")]
Postgres(#[from] rust_postgres::Error),
#[error("Error : {0}")]
Common(#[from] windmill_common::error::Error),
#[error("Tls Error: {0}")]
Tls(#[from] native_tls::Error),
}
fn build_tls_connector(
ssl_mode: SslMode,
root_certificate_pem: Option<&String>,
) -> Result<Option<MakeTlsConnector>, Error> {
let get_tls_builder_for_verify = |root_certificate: Option<&String>| {
let mut builder = TlsConnector::builder();
if let Some(root_certificate) = root_certificate {
let root_certificate_pem = Certificate::from_pem(root_certificate.as_bytes()).map_err(|e| {
Error::Common(error::Error::BadConfig(format!("Invalid Certs: {e:#}")))
})?;
builder.add_root_certificate(root_certificate_pem);
}
Ok::<_, Error>(builder)
};
let connector = match ssl_mode {
SslMode::Disable => return Ok(None),
SslMode::Require | SslMode::Prefer => {
let mut builder = TlsConnector::builder();
builder.danger_accept_invalid_certs(true);
builder.danger_accept_invalid_hostnames(true);
builder
}
SslMode::VerifyCa => {
let mut builder = get_tls_builder_for_verify(root_certificate_pem)?;
builder.danger_accept_invalid_hostnames(true);
builder
}
SslMode::VerifyFull => {
let builder = get_tls_builder_for_verify(root_certificate_pem)?;
builder
}
_ => unreachable!(),
};
Ok(Some(MakeTlsConnector::new(connector.build()?)))
}
pub struct PostgresSimpleClient(Client);
impl PostgresSimpleClient {
async fn new(database: &Postgres) -> Result<Self, Error> {
let client = get_raw_postgres_connection(database, true).await?;
let ssl_mode = match database.sslmode.as_ref() {
"disable" => SslMode::Disable,
"" | "prefer" | "allow" => SslMode::Prefer,
"require" => SslMode::Require,
"verify-ca" => SslMode::VerifyCa,
"verify-full" => SslMode::VerifyFull,
ssl_mode => {
return Err(Error::Common(windmill_common::error::Error::BadRequest(
format!("Invalid ssl mode for postgres: {}, please put a valid ssl_mode among the following avalible ssl mode: ['disable', 'allow', 'prefer', 'verify-ca', 'verify-full']", ssl_mode),
)))
}
};
let mut config = Config::new();
config
.dbname(&database.dbname)
.host(&database.host)
.user(&database.user)
.ssl_mode(ssl_mode)
.replication_mode(rust_postgres::config::ReplicationMode::Logical);
if let Some(port) = database.port {
config.port(port);
};
if !database.password.is_empty() {
config.password(&database.password);
}
let connector = build_tls_connector(ssl_mode, database.root_certificate_pem.as_ref())?;
let client = if let Some(connector) = connector {
let (client, connection) = config.connect(connector).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
tracing::debug!("{:#?}", e);
};
tracing::info!("Successfully Connected into database");
});
client
} else {
let (client, connection) = config.connect(NoTls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
tracing::debug!("{:#?}", e);
};
tracing::info!("Successfully Connected into database");
});
client
};
Ok(PostgresSimpleClient(client))
}
@@ -106,8 +202,7 @@ impl PostgresSimpleClient {
Ok((
self.0
.copy_both_simple::<bytes::Bytes>(query.as_str())
.await
.map_err(to_anyhow)?,
.await?,
LogicalReplicationSettings::new(false),
))
}
@@ -417,7 +512,7 @@ impl PostgresConfig {
};
let database = try_get_resource_from_db_as::<Postgres>(
&authed,
authed,
Some(UserDB::new(db.clone())),
&db,
postgres_resource_path,
@@ -432,13 +527,12 @@ impl PostgresConfig {
"SELECT pubname FROM pg_publication WHERE pubname = {}",
quote_literal(&publication_name)
))
.await
.map_err(to_anyhow)?;
.await?;
if !publication.row_exist() {
return Err(Error::BadConfig(
return Err(Error::Common(error::Error::BadConfig(
ERROR_PUBLICATION_NAME_NOT_EXISTS.to_string(),
));
)));
}
let replication_slot = client
@@ -446,19 +540,17 @@ impl PostgresConfig {
"SELECT slot_name FROM pg_replication_slots WHERE slot_name = {}",
quote_literal(&replication_slot_name)
))
.await
.map_err(to_anyhow)?;
.await?;
if !replication_slot.row_exist() {
return Err(Error::BadConfig(
return Err(Error::Common(error::Error::BadConfig(
ERROR_REPLICATION_SLOT_NOT_EXISTS.to_string(),
));
)));
}
let (logical_replication_stream, logical_replication_settings) = client
.get_logical_replication_stream(&publication_name, &replication_slot_name)
.await
.map_err(to_anyhow)?;
.await?;
Ok((logical_replication_stream, logical_replication_settings))
}
@@ -493,7 +585,7 @@ impl PostgresConfig {
let user_db = UserDB::new(db.clone());
let mut pg_connection = get_default_pg_connection(
let mut pg_connection = get_pg_connection(
authed.clone(),
Some(user_db.clone()),
&db,

View File

@@ -1217,7 +1217,7 @@ async fn update_resource_type(
)
))]
pub async fn try_get_resource_from_db_as<T>(
authed: &ApiAuthed,
authed: ApiAuthed,
user_db: Option<UserDB>,
db: &DB,
resource_path: &str,

View File

@@ -206,6 +206,7 @@ async fn list_scripts(
Query(lq): Query<ListScriptQuery>,
) -> JsonResult<Vec<ListableScript>> {
let (per_page, offset) = paginate(pagination);
let mut sqlb = SqlBuilder::select_from("script as o")
.fields(&[
"hash",
@@ -320,16 +321,6 @@ async fn list_scripts(
.fields(&["dm.deployment_msg"]);
}
if let Some(languages) = lq.languages {
sqlb.and_where_in(
"language",
&languages
.iter()
.map(|language| quote(language.as_str()))
.collect_vec(),
);
}
let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, ListableScript>(&sql)
@@ -1289,11 +1280,6 @@ async fn raw_script_by_path_unpinned(
raw_script_by_path_internal(path, user_db, db, authed, w_id, true).await
}
lazy_static::lazy_static! {
static ref DEBUG_RAW_SCRIPT_ENDPOINTS: bool =
std::env::var("DEBUG_RAW_SCRIPT_ENDPOINTS").is_ok();
}
async fn raw_script_by_path_internal(
path: StripPath,
user_db: UserDB,
@@ -1346,26 +1332,6 @@ async fn raw_script_by_path_internal(
"Script {path} exists but {} does not have permissions to access it",
authed.username
)));
} else {
if *DEBUG_RAW_SCRIPT_ENDPOINTS {
let other_script_o = sqlx::query_scalar!(
"SELECT path FROM script WHERE workspace_id = $1 AND archived = false",
w_id
)
.fetch_all(&db)
.await?;
let other_script_archived = sqlx::query_scalar!(
"SELECT distinct(path) FROM script WHERE workspace_id = $1 AND archived = true",
w_id
)
.fetch_all(&db)
.await?;
tracing::warn!(
"Script {path} does not exist in workspace {w_id} but these paths do, non-archived: {:?} | archived: {:?}",
other_script_o.join(", "),
other_script_archived.join(", ")
)
}
}
}

View File

@@ -542,7 +542,7 @@ async fn list_users_as_super_admin(
let rows = if active_only.is_some_and(|x| x) {
sqlx::query_as!(
GlobalUserInfo,
"WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),
"WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),
authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)
SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username
FROM password
@@ -1750,22 +1750,7 @@ async fn refresh_token(
.await?
.unwrap_or(false);
let new_token = create_session_token(&authed.email, super_admin, &mut tx, cookies).await?;
audit_log(
&mut *tx,
&AuditAuthor {
email: authed.email.to_string(),
username: authed.email.to_string(),
username_override: None,
},
"users.token.refresh",
ActionKind::Create,
&"global",
Some(&truncate_token(&new_token)),
None,
)
.await?;
let _ = create_session_token(&authed.email, super_admin, &mut tx, cookies).await?;
tx.commit().await?;
Ok("token refreshed".to_string())

View File

@@ -6,6 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::fmt::Display;
use axum::{body::Body, response::Response};
use regex::Regex;
use serde::{Deserialize, Deserializer};
@@ -29,6 +31,23 @@ pub struct WithStarredInfoQuery {
pub with_starred_info: Option<bool>,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RunnableKind {
Script,
Flow,
}
impl Display for RunnableKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let runnable_kind = match self {
RunnableKind::Script => "script",
RunnableKind::Flow => "flow"
};
write!(f, "{}", runnable_kind)
}
}
pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
let is_admin = is_super_admin_email(db, email).await?;

View File

@@ -548,8 +548,6 @@ pub(crate) async fn tarball_workspace(
authentication_resource_path,
script_path,
is_flow,
summary,
description,
edited_by,
edited_at,
email,

View File

@@ -80,7 +80,6 @@ backon.workspace = true
openidconnect = { workspace = true, optional = true }
strum.workspace = true
strum_macros.workspace = true
url.workspace = true
semver.workspace = true
croner = "2.0.6"

View File

@@ -36,7 +36,6 @@ pub struct AgentAuth {
}
pub const AGENT_JWT_PREFIX: &str = "jwt_agent_";
pub fn build_agent_http_client(worker_suffix: &str) -> HttpClient {
let client = ClientBuilder::new(
reqwest::Client::builder()

View File

@@ -17,15 +17,6 @@ pub struct AuthedClient {
}
impl AuthedClient {
pub fn new(
base_internal_url: String,
workspace: String,
token: String,
force_client: Option<reqwest::Client>,
) -> AuthedClient {
AuthedClient { base_internal_url, workspace, token, force_client }
}
pub async fn get(&self, url: &str, query: Vec<(&str, String)>) -> anyhow::Result<Response> {
self.force_client
.as_ref()

View File

@@ -149,8 +149,6 @@ lazy_static::lazy_static! {
pub static ref DEPLOYED_SCRIPT_INFO_CACHE: Cache<(String, i64), ScriptHashInfo> = Cache::new(1000);
pub static ref FLOW_INFO_CACHE: Cache<(String, i64), FlowVersionInfo> = Cache::new(1000);
pub static ref QUIET_LOGS: bool = std::env::var("QUIET_LOGS").map(|s| s.parse::<bool>().unwrap_or(false)).unwrap_or(false);
}
const LATEST_VERSION_ID_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(60);

View File

@@ -9,7 +9,6 @@
use std::{
fmt::{self, Display},
hash::{Hash, Hasher},
str::FromStr,
};
use crate::{
@@ -22,7 +21,6 @@ use crate::worker::HUB_CACHE_DIR;
use anyhow::Context;
use backon::ConstantBuilder;
use backon::{BackoffBuilder, Retryable};
use itertools::Itertools;
use serde::de::Error as _;
use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize};
@@ -87,40 +85,6 @@ impl ScriptLang {
}
}
impl FromStr for ScriptLang {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let language = match s.to_lowercase().as_str() {
"bun" => ScriptLang::Bun,
"bunnative" => ScriptLang::Bunnative,
"nativets" => ScriptLang::Nativets,
"deno" => ScriptLang::Deno,
"python3" => ScriptLang::Python3,
"go" => ScriptLang::Go,
"bash" => ScriptLang::Bash,
"powershell" => ScriptLang::Powershell,
"postgresql" => ScriptLang::Postgresql,
"mysql" => ScriptLang::Mysql,
"bigquery" => ScriptLang::Bigquery,
"snowflake" => ScriptLang::Snowflake,
"mssql" => ScriptLang::Mssql,
"graphql" => ScriptLang::Graphql,
"oracledb" => ScriptLang::OracleDB,
"php" => ScriptLang::Php,
"rust" => ScriptLang::Rust,
"ansible" => ScriptLang::Ansible,
"csharp" => ScriptLang::CSharp,
"nu" => ScriptLang::Nu,
"java" => ScriptLang::Java,
language => {
return Err(anyhow::anyhow!("{} is currently not supported", language).into())
}
};
Ok(language)
}
}
#[derive(Eq, PartialEq, Debug, Hash, Clone, Copy, sqlx::Type)]
#[sqlx(transparent)]
pub struct ScriptHash(pub i64);
@@ -403,7 +367,7 @@ where
deserializer.deserialize_any(StringOrArrayVisitor)
}
#[derive(Debug, Deserialize)]
#[derive(Deserialize)]
pub struct ListScriptQuery {
pub path_start: Option<String>,
pub path_exact: Option<String>,
@@ -420,29 +384,6 @@ pub struct ListScriptQuery {
pub include_without_main: Option<bool>,
pub include_draft_only: Option<bool>,
pub with_deployment_msg: Option<bool>,
#[serde(default, deserialize_with = "from_seq")]
pub languages: Option<Vec<ScriptLang>>,
}
fn from_seq<'de, D>(deserializer: D) -> Result<Option<Vec<ScriptLang>>, D::Error>
where
D: Deserializer<'de>,
{
let s = <String>::deserialize(deserializer)?;
let languages: Vec<ScriptLang> = s
.split(",")
.map(ScriptLang::from_str)
.try_collect()
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
let languages = if languages.is_empty() {
None
} else {
Some(languages)
};
Ok(languages)
}
pub fn to_i64(s: &str) -> crate::error::Result<i64> {

View File

@@ -22,14 +22,10 @@ use croner::Cron;
use rand::{distr::Alphanumeric, rng, Rng};
use reqwest::Client;
use semver::Version;
use serde::{de::Error as SerdeDeserializerError, Deserialize, Deserializer, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use sha2::{Digest, Sha256};
use sqlx::{Pool, Postgres};
use std::borrow::Cow;
use std::fmt::Display;
use std::{fs::DirBuilder as SyncDirBuilder, str::FromStr};
use tokio::fs::DirBuilder as AsyncDirBuilder;
use url::Url;
use std::str::FromStr;
pub const MAX_PER_PAGE: usize = 10000;
pub const DEFAULT_PER_PAGE: usize = 1000;
@@ -37,10 +33,6 @@ pub const DEFAULT_PER_PAGE: usize = 1000;
pub const GIT_VERSION: &str =
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
pub const AGENT_JWT_PREFIX: &str = "jwt_agent_";
pub const WORKER_NAME_PREFIX: &str = "wk";
pub const AGENT_WORKER_NAME_PREFIX: &str = "ag";
use crate::CRITICAL_ALERT_MUTE_UI_ENABLED;
use std::panic::{self, AssertUnwindSafe, Location};
use std::sync::atomic::Ordering;
@@ -61,12 +53,6 @@ lazy_static::lazy_static! {
}
).unwrap_or(Version::new(0, 1, 0));
pub static ref HOSTNAME :String = std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| {
gethostname()
.to_str()
.map(|x| x.to_string())
.unwrap_or_else(|| rd_string(5))
});
pub static ref MODE_AND_ADDONS: ModeAndAddons = {
let mut search_addon = false;
@@ -84,7 +70,7 @@ lazy_static::lazy_static! {
}
Mode::Worker
} else if &x == "agent" {
println!("Binary is in 'agent' mode with BASE_INTERNAL_URL={}", std::env::var("BASE_INTERNAL_URL").unwrap_or_default());
println!("Binary is in 'agent' mode");
if std::env::var("BASE_INTERNAL_URL").is_err() {
panic!("BASE_INTERNAL_URL is required in agent mode")
}
@@ -134,10 +120,6 @@ lazy_static::lazy_static! {
};
}
lazy_static::lazy_static! {
pub static ref AGENT_TOKEN: String = std::env::var("AGENT_TOKEN").unwrap_or_default();
}
#[derive(Clone)]
pub struct ModeAndAddons {
pub indexer: bool,
@@ -185,6 +167,15 @@ pub async fn require_admin_or_devops(
Ok(())
}
pub fn hostname() -> String {
std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| {
gethostname()
.to_str()
.map(|x| x.to_string())
.unwrap_or_else(|| rd_string(5))
})
}
fn instance_name(hostname: &str) -> String {
hostname
.replace(" ", "")
@@ -195,30 +186,15 @@ fn instance_name(hostname: &str) -> String {
.to_string()
}
const DEFAULT_WORKER_SUFFIX_LEN: usize = 5;
pub const SSH_AGENT_WORKER_SUFFIX: &'static str = "/ssh";
pub fn create_worker_suffix(hostname: &str, rd_string_len: usize, ssh_ag_worker: bool) -> String {
let mut wk_suffix = format!("{}-{}", instance_name(hostname), rd_string(rd_string_len));
if ssh_ag_worker {
wk_suffix.push_str(SSH_AGENT_WORKER_SUFFIX);
}
wk_suffix
}
pub fn create_ssh_agent_worker_suffix(hostname: &str) -> String {
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, true)
}
pub fn create_default_worker_suffix(hostname: &str) -> String {
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, false)
pub fn worker_suffix(hostname: &str, rd_string: &str) -> String {
format!("{}-{}", instance_name(hostname), rd_string)
}
pub fn worker_name_with_suffix(is_agent: bool, worker_group: &str, suffix: &str) -> String {
if is_agent {
format!("{}-{}-{}", AGENT_WORKER_NAME_PREFIX, worker_group, suffix)
format!("ag-{}-{}", worker_group, suffix)
} else {
format!("{}-{}-{}", WORKER_NAME_PREFIX, worker_group, suffix)
format!("wk-{}-{}", worker_group, suffix)
}
}
@@ -247,21 +223,6 @@ pub async fn now_from_db<'c, E: sqlx::PgExecutor<'c>>(
.unwrap())
}
pub async fn create_directory_async(directory_path: &str) {
AsyncDirBuilder::new()
.recursive(true)
.create(directory_path)
.await
.expect("could not create dir");
}
pub fn create_directory_sync(directory_path: &str) {
SyncDirBuilder::new()
.recursive(true)
.create(directory_path)
.expect("could not create dir");
}
pub fn not_found_if_none<T, U: AsRef<str>>(opt: Option<T>, kind: &str, name: U) -> Result<T> {
if let Some(o) = opt {
Ok(o)
@@ -526,18 +487,6 @@ impl<T> IsEmpty for Vec<T> {
}
}
impl<T> IsEmpty for Option<T>
where
T: IsEmpty,
{
fn is_empty(&self) -> bool {
match self {
Some(v) => v.is_empty(),
None => true,
}
}
}
pub fn empty_as_none<'de, D, T>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
@@ -547,26 +496,6 @@ where
Ok(option.filter(|s| !s.is_empty()))
}
pub fn is_empty<T>(value: &T) -> bool
where
T: IsEmpty,
{
value.is_empty()
}
pub fn deserialize_url<'de, D: Deserializer<'de>>(
de: D,
) -> std::result::Result<Option<Url>, D::Error> {
let intermediate = <Option<Cow<'de, str>>>::deserialize(de)?;
match intermediate.as_deref() {
None | Some("") => Ok(None),
Some(non_empty_string) => Url::parse(non_empty_string)
.map(Some)
.map_err(D::Error::custom),
}
}
pub async fn fetch_mute_workspace(_db: &DB, workspace_id: &str) -> Result<bool> {
match sqlx::query!(
"SELECT mute_critical_alerts FROM workspace_settings WHERE workspace_id = $1",
@@ -818,20 +747,3 @@ impl<F: Future> Future for WarnAfterFuture<F> {
}
}
}
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum RunnableKind {
Script,
Flow,
}
impl Display for RunnableKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let runnable_kind = match self {
RunnableKind::Script => "script",
RunnableKind::Flow => "flow",
};
write!(f, "{}", runnable_kind)
}
}

View File

@@ -1,5 +1,4 @@
use anyhow::anyhow;
use axum::http::HeaderMap;
use bytes::Bytes;
use const_format::concatcp;
use itertools::Itertools;
@@ -35,6 +34,7 @@ use crate::{
pub const DEFAULT_CLOUD_TIMEOUT: u64 = 900;
pub const DEFAULT_SELFHOSTED_TIMEOUT: u64 = 604800; // 7 days
lazy_static::lazy_static! {
pub static ref WORKER_GROUP: String = std::env::var("WORKER_GROUP").unwrap_or_else(|_| {
#[cfg(not(feature = "enterprise"))]
@@ -129,7 +129,7 @@ lazy_static::lazy_static! {
pub static ref ALL_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
static ref CUSTOM_TAG_REGEX: Regex = Regex::new(r"^([\w-]+)\(((?:[\w-]+\+)*[\w-]+)\)$").unwrap();
static ref CUSTOM_TAG_REGEX: Regex = Regex::new(r"^([\w-]+)\(((?:[\w-])+\+?)+\)$").unwrap();
pub static ref DISABLE_BUNDLING: bool = std::env::var("DISABLE_BUNDLING")
.ok()
@@ -159,20 +159,12 @@ impl HttpClient {
pub async fn post<T: Serialize, R: DeserializeOwned>(
&self,
url: &str,
headers: Option<HeaderMap>,
body: &T,
) -> anyhow::Result<R> {
let response_builder = self
let response = self
.0
.post(format!("{}{}", *BASE_INTERNAL_URL, url))
.json(body);
let response_builder = match headers {
Some(headers) => response_builder.headers(headers),
None => response_builder,
};
let response = response_builder
.json(body)
.send()
.await
.map_err(|e| anyhow::anyhow!(e))?;
@@ -267,7 +259,7 @@ fn format_pull_query(peek: String) -> String {
id, workspace_id, parent_job, created_by, created_at, runnable_id,
runnable_path, args, kind, trigger, trigger_kind,
permissioned_as, permissioned_as_email, script_lang,
flow_innermost_root_job, flow_step_id,
flow_innermost_root_job, flow_step_id,
same_worker, pre_run_error, visible_to_owner, tag, concurrent_limit,
concurrency_time_window_s, timeout, cache_ttl, priority, raw_code, raw_lock,
raw_flow, script_entrypoint_override, preprocessed
@@ -275,13 +267,13 @@ fn format_pull_query(peek: String) -> String {
WHERE id = (SELECT id FROM peek)
) SELECT j.id, j.workspace_id, j.parent_job, j.created_by, started_at, scheduled_for,
j.runnable_id, j.runnable_path, j.args, canceled_by,
canceled_reason, j.kind, j.trigger, j.trigger_kind, j.permissioned_as,
canceled_reason, j.kind, j.trigger, j.trigger_kind, j.permissioned_as,
flow_status, j.script_lang,
j.same_worker, j.pre_run_error, j.visible_to_owner,
j.same_worker, j.pre_run_error, j.visible_to_owner,
j.tag, j.concurrent_limit, j.concurrency_time_window_s, j.flow_innermost_root_job,
j.timeout, j.flow_step_id, j.cache_ttl, j.priority, j.raw_code, j.raw_lock, j.raw_flow,
j.script_entrypoint_override, j.preprocessed, pj.runnable_path as parent_runnable_path,
COALESCE(p.email, j.permissioned_as_email) as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin,
COALESCE(p.email, j.permissioned_as_email) as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin,
p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders
FROM q, j
LEFT JOIN v2_job_status f USING (id)
@@ -316,7 +308,7 @@ pub async fn store_suspended_pull_query(wc: &WorkerConfig) {
}
pub fn make_pull_query(tags: &[String]) -> String {
let query = format_pull_query(format!(
format_pull_query(format!(
"SELECT id
FROM v2_job_queue
WHERE running = false AND tag IN ({}) AND scheduled_for <= now()
@@ -324,8 +316,7 @@ pub fn make_pull_query(tags: &[String]) -> String {
FOR UPDATE SKIP LOCKED
LIMIT 1",
tags.iter().map(|x| format!("'{x}'")).join(", ")
));
query
))
}
pub async fn store_pull_query(wc: &WorkerConfig) {
@@ -1037,7 +1028,7 @@ pub async fn update_ping_http(
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct JobCancelled {
pub canceled_by: String,
pub reason: String,
@@ -1069,11 +1060,11 @@ pub async fn update_ping_for_failed_init_script_query(
db: &DB,
) -> anyhow::Result<()> {
sqlx::query!(
"UPDATE worker_ping SET
ping_at = now(),
jobs_executed = 1,
current_job_id = $1,
current_job_workspace_id = 'admins'
"UPDATE worker_ping SET
ping_at = now(),
jobs_executed = 1,
current_job_id = $1,
current_job_workspace_id = 'admins'
WHERE worker = $2",
last_job_id,
worker_name
@@ -1519,7 +1510,7 @@ pub struct WorkerConfig {
impl std::fmt::Debug for WorkerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, init_bash: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?} }}",
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, init_bash: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?} }}",
self.worker_tags, self.priority_tags_sorted, self.dedicated_worker, self.init_bash, self.cache_clear, self.additional_python_paths, self.pip_local_dependencies, self.env_vars.iter().map(|(k, v)| format!("{}: {}{} ({} chars)", k, &v[..3.min(v.len())], "***", v.len())).collect::<Vec<String>>().join(", "))
}
}

View File

@@ -391,7 +391,6 @@ pub async fn append_logs(
if let Err(e) = client
.post::<_, String>(
&format!("/api/w/{}/agent_workers/push_logs/{}", workspace.as_ref(), job_id),
None,
&logs.as_ref(),
)
.await {
@@ -1011,43 +1010,33 @@ pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
#[cfg(feature = "cloud")]
if *CLOUD_HOSTED && !queued_job.is_flow() && _duration > 1000 {
let db = db.clone();
let w_id = queued_job.workspace_id.clone();
let email = queued_job.permissioned_as_email.clone();
let w_id2 = w_id.clone();
let email2 = email.clone();
tokio::task::spawn(async move {
let additional_usage = _duration / 1000;
let premium_workspace = windmill_common::workspaces::is_premium_workspace(&db, &w_id).await;
tokio::time::timeout(std::time::Duration::from_secs(10), async move {
let additional_usage = _duration / 1000;
let w_id = &queued_job.workspace_id;
let premium_workspace = windmill_common::workspaces::is_premium_workspace(db, w_id).await;
let _ = sqlx::query!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
w_id,
additional_usage as i32
)
.execute(db)
.await
.map_err(|e| Error::internal_err(format!("updating usage: {e:#}")));
if !premium_workspace {
let _ = sqlx::query!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2)
VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
w_id,
queued_job.permissioned_as_email,
additional_usage as i32
)
.execute(&db)
.execute(db)
.await
.map_err(|e| Error::internal_err(format!("updating usage: {e:#}")));
if !premium_workspace {
let _ = sqlx::query!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), $2)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2",
email,
additional_usage as i32
)
.execute(&db)
.await
.map_err(|e| Error::internal_err(format!("updating usage: {e:#}")));
}}).await.unwrap_or_else(|_| {
tracing::error!("Could not update usage for workspace {w_id2} and permissioned as {email2}, stopped after 10s");
});
});
}
}
#[cfg(feature = "enterprise")]
if !success {
@@ -2138,7 +2127,7 @@ pub struct PulledJob {
// NOTE:
// Precomputed by the server
// Used to offload work from agent workers to server
#[derive(Debug, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
pub enum PrecomputedAgentInfo {
Bun { local: String, remote: String },
Python {
@@ -2149,7 +2138,7 @@ pub enum PrecomputedAgentInfo {
requirements: Option<String> },
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct JobAndPerms {
pub job: MiniPulledJob,
pub raw_code: Option<String>,
@@ -2301,20 +2290,16 @@ pub async fn pull(
db: &Pool<Postgres>,
suspend_first: bool,
worker_name: &str,
query_o: Option<&(String, String)>,
query_o: Option<(String, String)>,
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
) -> windmill_common::error::Result<PulledJobResult> {
loop {
if let Some((query_suspended, query_no_suspend)) = query_o {
if let Some((query_suspended, query_no_suspend)) = query_o.as_ref() {
let njob = {
let job = if query_suspended.is_empty() {
None
} else {
sqlx::query_as::<_, PulledJob>(query_suspended)
let job = sqlx::query_as::<_, PulledJob>(query_suspended)
.bind(worker_name)
.fetch_optional(db)
.await?
};
.await?;
if let Some(job) = job {
PulledJobResult { job: Some(job), suspended: true }
} else {
@@ -2564,6 +2549,7 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<'c>(
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
return Ok((None, false));
}
let r = if suspend_first {
// tracing::info!("Pulling job with query: {}", query);
sqlx::query_as::<_, PulledJob>(&query)
@@ -2716,7 +2702,7 @@ pub async fn get_result_by_id(
.await
{
Ok(res) => Ok(res),
Err(e) => {
Err(_) => {
let root = sqlx::query!(
"SELECT
id As \"id!\",
@@ -2733,7 +2719,7 @@ pub async fn get_result_by_id(
let restarted_from_id = not_found_if_none(
root.restarted_from,
"Id not found in the result's mapping of the root job and root job had no restarted from information",
format!("parent: {}, root: {}, id: {}, error: {e:#}", flow_id, root.id, node_id),
format!("parent: {}, root: {}, id: {}", flow_id, root.id, node_id),
)?;
get_result_by_id_from_original_flow(
@@ -3279,40 +3265,35 @@ pub async fn push<'c, 'd>(
job_payload,
JobPayload::Flow { .. } | JobPayload::RawFlow { .. }
) {
tokio::time::timeout(std::time::Duration::from_secs(10), async move {
let workspace_usage = sqlx::query_scalar!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1
RETURNING usage.usage",
workspace_id
)
.fetch_one(_db)
.await
.map_err(|e| Error::internal_err(format!("updating usage: {e:#}")))?;
let workspace_usage = sqlx::query_scalar!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, TRUE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1
RETURNING usage.usage",
workspace_id
)
.fetch_one(_db)
.await
.map_err(|e| Error::internal_err(format!("updating usage: {e:#}")))?;
let user_usage = if !premium_workspace {
Some(sqlx::query_scalar!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1
RETURNING usage.usage",
email
)
.fetch_one(_db)
.await
.map_err(|e| Error::internal_err(format!("updating usage: {e:#}")))?)
} else {
None
};
Ok((Some(workspace_usage), user_usage))
}).await.unwrap_or_else(|e| {
tracing::error!("Could not update usage for workspace {workspace_id} and permissioned as {email}, stopped after 10s: {e:#}");
Err(Error::internal_err(format!("Could not update usage for workspace {workspace_id} and permissioned as {email}, stopped after 10s: {e:#}")))
})
let user_usage = if !premium_workspace {
Some(sqlx::query_scalar!(
"INSERT INTO usage (id, is_workspace, month_, usage)
VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)
ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1
RETURNING usage.usage",
email
)
.fetch_one(_db)
.await
.map_err(|e| Error::internal_err(format!("updating usage: {e:#}")))?)
} else {
None
};
(Some(workspace_usage), user_usage)
} else {
Ok((None, None))
}?;
(None, None)
};
if !premium_workspace {
let is_super_admin =

Some files were not shown because too many files have changed in this diff Show More