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
701 changed files with 19456 additions and 31808 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,88 +1,5 @@
# Changelog
## [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

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

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

@@ -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,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

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

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

@@ -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,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"]
}

1073
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.497.0"
version = "1.494.0"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.497.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" }

View File

@@ -1 +1 @@
2c3e21f4573486628e0b8969ff478c237bd0283f
70895a4a8f8891032c5b478a37ab6fafd0d4a9d0

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

@@ -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

@@ -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

@@ -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

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.497.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,31 +8444,6 @@ paths:
"201":
description: default error handler set
/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
@@ -8627,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:
@@ -14754,15 +14723,6 @@ components:
- custom_script
- signature
HttpMethod:
type: string
enum:
- get
- post
- put
- delete
- patch
HttpTrigger:
allOf:
- $ref: "#/components/schemas/TriggerExtraProperty"
@@ -14782,7 +14742,13 @@ components:
required:
- s3
http_method:
$ref: "#/components/schemas/HttpMethod"
type: string
enum:
- get
- post
- put
- delete
- patch
authentication_resource_path:
type: string
is_async:
@@ -14833,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:
@@ -14884,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

@@ -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)]
@@ -143,33 +143,17 @@ impl AIRequestConfig {
path: &str,
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 url = if is_azure {
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
.post(url)
.header("content-type", "application/json")
@@ -215,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)]

View File

@@ -26,20 +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,
@@ -47,6 +33,7 @@ pub struct ExpiringAuthCache {
}
pub struct AuthCache {
cache: Cache<(String, String), ExpiringAuthCache>,
db: DB,
superadmin_secret: Option<String>,
#[cfg(feature = "enterprise")]
@@ -60,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")]
@@ -68,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> {
@@ -76,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)
@@ -98,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(),
@@ -135,7 +123,7 @@ impl AuthCache {
username_override,
};
AUTH_CACHE.insert(
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
@@ -329,7 +317,7 @@ impl AuthCache {
}
};
if let Some(authed) = authed_o.as_ref() {
AUTH_CACHE.insert(
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),

View File

@@ -55,9 +55,12 @@ 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::{
@@ -301,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,
@@ -318,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,14 +804,6 @@ 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(())
}

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

@@ -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::{
@@ -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))
@@ -276,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(
@@ -290,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 (
@@ -332,199 +365,51 @@ async fn create_trigger_inner(
)
"#,
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.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(
@@ -533,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(),
@@ -679,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,
@@ -689,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?;
@@ -702,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?;
@@ -727,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,
@@ -737,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(
@@ -780,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#"
@@ -981,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,
);
@@ -990,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,
);
});
}
@@ -1014,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);
@@ -1063,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#"
@@ -1118,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?;
@@ -1146,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::BadRequest(format!(
"Script path of HTTP route at path: {} must not be empty",
trigger.path
))
.into_response());
}
let args = args
.process_args(
&authed,
@@ -1190,11 +1066,11 @@ 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,
>(

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;
@@ -522,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")]
@@ -659,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")]
@@ -818,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,50 +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(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))
}

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)
.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),
))
}
@@ -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

@@ -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

@@ -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

@@ -25,8 +25,7 @@ use semver::Version;
use serde::{Deserialize, Deserializer, Serialize};
use sha2::{Digest, Sha256};
use sqlx::{Pool, Postgres};
use std::{fs::DirBuilder as SyncDirBuilder, str::FromStr};
use tokio::fs::DirBuilder as AsyncDirBuilder;
use std::str::FromStr;
pub const MAX_PER_PAGE: usize = 10000;
pub const DEFAULT_PER_PAGE: usize = 1000;
@@ -34,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;
@@ -58,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;
@@ -81,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")
}
@@ -131,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,
@@ -182,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(" ", "")
@@ -192,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)
}
}
@@ -244,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)

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 =

View File

@@ -1,4 +1,3 @@
use reqwest::header::HeaderMap;
use uuid::Uuid;
use windmill_common::{agent_workers::QueueInitJob, worker::HttpClient};
use windmill_queue::{JobAndPerms, JobCompleted};
@@ -7,21 +6,14 @@ pub async fn queue_init_job(client: &HttpClient, content: &str) -> anyhow::Resul
client
.post(
"/api/agent_workers/queue_init_job",
None,
&QueueInitJob { content: content.to_string() },
)
.await
.and_then(|x: String| Uuid::parse_str(&x).map_err(|e| anyhow::anyhow!(e)))
}
pub async fn pull_job(
client: &HttpClient,
headers: Option<HeaderMap>,
body: Option<bool>,
) -> anyhow::Result<Option<JobAndPerms>> {
client
.post("/api/agent_workers/pull_job", headers, &body)
.await
pub async fn pull_job(client: &HttpClient) -> anyhow::Result<Option<JobAndPerms>> {
client.post("/api/agent_workers/pull_job", &()).await
}
pub async fn send_result(client: &HttpClient, jc: JobCompleted) -> anyhow::Result<String> {
@@ -31,7 +23,6 @@ pub async fn send_result(client: &HttpClient, jc: JobCompleted) -> anyhow::Resul
"/api/w/{}/agent_workers/send_result/{}",
jc.job.workspace_id, jc.job.id
),
None,
&jc,
)
.await

View File

@@ -492,7 +492,6 @@ pub async fn update_worker_ping_for_failed_init_script(
if let Err(e) = client
.post::<_, ()>(
UPDATE_PING_URL,
None,
&Ping {
last_job_executed: Some(last_job_id),
last_job_workspace_id: None,

View File

@@ -3,11 +3,7 @@ use std::{collections::HashMap, fs::DirBuilder, process::Stdio};
use itertools::Itertools;
use serde_json::value::RawValue;
use tokio::{
fs::{self, File},
io::AsyncReadExt,
process::Command,
};
use tokio::{fs::File, io::AsyncReadExt, process::Command};
use uuid::Uuid;
use windmill_common::{
error::{self, Error},
@@ -95,7 +91,6 @@ pub async fn handle_go_job(
true,
skip_go_mod,
skip_tidy,
false,
worker_name,
&job.workspace_id,
occupation_metrics,
@@ -361,21 +356,11 @@ pub async fn install_go_dependencies(
non_dep_job: bool,
skip_go_mod: bool,
has_sum: bool,
raw_deps: bool,
worker_name: &str,
w_id: &str,
occupation_metrics: &mut OccupancyMetrics,
) -> error::Result<String> {
if raw_deps {
let go_mod =
if let Some(module) = code.lines().find(|l| l.trim_start().starts_with("module ")) {
code.replace(module, "module mymod")
} else {
format!("module mymod\n{code}")
};
fs::write(format!("{job_dir}/go.mod"), go_mod).await?;
}
if !raw_deps && !skip_go_mod {
if !skip_go_mod {
gen_go_mymod(code, job_dir).await?;
let mut child_cmd = Command::new(GO_PATH.as_str());
child_cmd
@@ -415,9 +400,7 @@ pub async fn install_go_dependencies(
let mut new_lockfile = false;
let hash = if raw_deps {
calculate_hash(code)
} else if !has_sum {
let hash = if !has_sum {
calculate_hash(parse_go_imports(&code)?.iter().join("\n").as_str())
} else {
"".to_string()
@@ -446,15 +429,7 @@ pub async fn install_go_dependencies(
}
}
let mod_command = if skip_tidy ||
// If there is go.mod provided we want to use `download` only.
// Unlike `tidy` it does not modify local go.mod
raw_deps
{
"download"
} else {
"tidy"
};
let mod_command = if skip_tidy { "download" } else { "tidy" };
let mut child_cmd = Command::new(GO_PATH.as_str());
child_cmd
.current_dir(job_dir)

View File

@@ -227,7 +227,6 @@ pub async fn handle_child(
if let Err(err) = client
.post::<_, ()>(
&format!("/api/agent_workers/set_job_cancelled/{}", job_id),
None,
&JobCancelled {
canceled_by: "timeout".to_string(),
reason: format!("duration > {}", timeout_duration.as_secs()),

View File

@@ -57,9 +57,10 @@ mod worker_flow;
mod worker_lockfiles;
mod worker_utils;
pub use worker::*;
pub use worker_lockfiles::process_relative_imports;
pub use worker::*;
pub use result_processor::handle_job_error;
pub use bun_executor::{

View File

@@ -658,7 +658,7 @@ except BaseException as e:
// Add /tmp/windmill/cache/python_x_y_z/global-site-packages to PYTHONPATH.
// Usefull if certain wheels needs to be preinstalled before execution.
let global_site_packages_path = py_version.to_cache_dir(true) + "/global-site-packages";
let global_site_packages_path = py_version.to_cache_dir() + "/global-site-packages";
let additional_python_paths_folders = {
let mut paths = additional_python_paths.clone();
if std::fs::metadata(&global_site_packages_path).is_ok() {
@@ -1489,7 +1489,7 @@ pub async fn handle_python_reqs(
if req.starts_with('#') || req.starts_with('-') || req.trim().is_empty() {
continue;
}
let py_prefix = &py_version.to_cache_dir(false);
let py_prefix = &py_version.to_cache_dir();
let venv_p = format!(
"{py_prefix}/{}",
@@ -1740,7 +1740,7 @@ pub async fn handle_python_reqs(
tokio::select! {
// Cancel was called on the job
_ = kill_rx.recv() => return Err(anyhow::anyhow!("S3 pull was canceled")),
pull = pull_from_tar(os, venv_p.clone(), py_version.to_cache_dir_top_level(false), None, false) => {
pull = pull_from_tar(os, venv_p.clone(), py_version.to_cache_dir_top_level(), None, false) => {
if let Err(e) = pull {
tracing::info!(
workspace_id = %w_id,
@@ -1891,7 +1891,7 @@ pub async fn handle_python_reqs(
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
if s3_push {
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
tokio::spawn(build_tar_and_push(os, venv_p.clone(), py_version.to_cache_dir_top_level(false), None, false));
tokio::spawn(build_tar_and_push(os, venv_p.clone(), py_version.to_cache_dir_top_level(), None, false));
}
}

View File

@@ -153,18 +153,12 @@ impl PyV {
let valid = all_versions
.clone()
.into_iter()
.filter(|v| version_specifiers.iter().all(|vs| (vs).contains(&*v)))
.filter(|v| version_specifiers.iter().all(|vs| vs.contains(&*v)))
.collect_vec();
if !valid.is_empty() {
let mut result = valid[0].clone();
// Is there at least one version specifier that has PATCH digit?
let patch_vs = version_specifiers
.iter()
.any(|vs| vs.version().release().get(2).is_some());
if select_latest {
return Ok(result.clone());
return Ok(valid[0].clone());
}
// Usually INSTANCE_PYTHON_VERSION
@@ -189,10 +183,9 @@ impl PyV {
// - We will iterate until find the closest version to target.
// - If closest version has the same MINOR version, use it.
// - If it differs in MINOR version, take latest PATCH version.
//
let mut result = None;
let [major, minor, ..] = result.release() else {
return Err(Error::InternalErr(format!("Failed to parse \"{}\". Available python versions are supposed to be in SEMVER format (MAJOR.MINOR)", *result)));
};
// This represents newest version with oldest MINOR:
//
// I Iterable Newest in MINOR
@@ -202,9 +195,12 @@ impl PyV {
// 4. 3.10.2 -> 3.10.2
// 5. 3.10.1 -> 3.10.2
// 6. 3.10.0 -> 3.10.2
let mut newest_in_minor = (result.clone(), (*major, *minor));
let mut newest_in_minor = None;
for v in valid.iter() {
if result.is_none() {
result.replace(v);
}
if v < &gv {
// We will not continue if we start looking into versions older than gravity version.
break;
@@ -216,46 +212,37 @@ impl PyV {
// Since we go top to down we can assume
// the first occurence of new minor version contains the latest patch version.
if newest_in_minor.1 != (*major, *minor) {
newest_in_minor = (v.clone(), (*major, *minor));
if matches!(newest_in_minor, Some((_, mm)) if mm != (major, minor))
|| newest_in_minor.is_none()
{
newest_in_minor = Some((v.clone(), (major, minor)));
}
if gravity_matcher.contains(v) {
// return as soon as gravity matcher has first hit.
// Only in case version specifiers do specify PATCH version OR gravity version specify PATCH
if patch_vs || gv.release().get(2).is_some() {
return Ok(v.clone());
} else {
let Some(release_numbers) = v.release().get(0..=1) else {
return Err(Error::InternalErr(format!(
"Failed to get release numbers from: \"{}\". ",
**v
)));
};
return Ok(PyV(pep440_rs::Version::new(release_numbers)));
}
return Ok(v.clone());
}
// If we are still in the loop, it means that we are getting closer to gravity version
else {
result = v.clone();
result = Some(v);
}
}
let [gravity_major, gravity_minor, ..] = gv.release() else {
return Err(Error::internal_err(format!("Cannot get MAJOR nor MINOR version of python gravity version ({}). Something might be wrong with INSTANCE_PYTHON_VERSION.", &*gv)));
return Err(Error::internal_err(format!("Cannot get MAJOR or/and MINOR version of python gravity version ({}). Something might be wrong with INSTANCE_PYTHON_VERSION.", &*gv)));
};
if (*gravity_major, *gravity_minor) != newest_in_minor.1 {
// Return full version only if there is PATCH versions in version specifiers
if patch_vs {
return Ok(newest_in_minor.0);
} else {
let mm = newest_in_minor.1;
return Ok(PyV(pep440_rs::Version::new([mm.0, mm.1])));
if let Some((v, mm)) = newest_in_minor {
if (gravity_major, gravity_minor) != mm {
return Ok(v);
}
}
Ok(result)
result
.ok_or(Error::internal_err(
"No python candidates found. This is a bug!",
))
.map(ToOwned::to_owned)
} else {
Err(anyhow!(
"
@@ -278,24 +265,14 @@ impl PyV {
.into())
}
}
/// e.g.: `/tmp/windmill/cache/python_3_x_y`
pub(crate) fn to_cache_dir(&self, ignore_patch: bool) -> String {
/// e.g.: `/tmp/windmill/cache/python_3xy`
pub(crate) fn to_cache_dir(&self) -> String {
use windmill_common::worker::ROOT_CACHE_DIR;
format!(
"{ROOT_CACHE_DIR}{}",
self.to_cache_dir_top_level(ignore_patch)
)
format!("{ROOT_CACHE_DIR}{}", self.to_cache_dir_top_level())
}
/// e.g.: `python_3_x_y`
pub fn to_cache_dir_top_level(&self, ignore_patch: bool) -> String {
if ignore_patch {
if let [major, minor, ..] = self.release() {
return format!("python_{major}_{minor}");
}
tracing::warn!("failed to parse python's ({}) top level directory with no patch digit, fallback to full version.", self.to_string());
}
pub fn to_cache_dir_top_level(&self) -> String {
format!("python_{}", self.to_string().replace(".", "_"))
}
@@ -306,7 +283,6 @@ impl PyV {
) -> Self {
let mut err = None;
let pyv = match INSTANCE_PYTHON_VERSION.read().await.clone() {
Some(v) if &v == "default" => PyVAlias::default().into(),
Some(v) => pep440_rs::Version::from_str(&v).unwrap_or_else(|_| {
let v = PyVAlias::default().into();
err = Some(format!("\nCannot parse INSTANCE_PYTHON_VERSION ({:?}), fallback to latest_stable ({v:?})", *INSTANCE_PYTHON_VERSION));
@@ -599,7 +575,7 @@ impl PyV {
// For the default version directory created during startup (main.rs)
DirBuilder::new()
.recursive(true)
.create(self.to_cache_dir(false))
.create(self.to_cache_dir())
.await
.expect("could not create initial worker dir");
@@ -786,7 +762,7 @@ mod tests {
pyv("0.9.3"),
pyv("0.9.2"),
],
pyv("0.9"), //
pyv("0.9.4"), //
)
.await;
}
@@ -809,7 +785,7 @@ mod tests {
pyv("0.8.1"),
pyv("0.8.0"),
],
pyv("1.0"), //
pyv("1.0.2"), //
)
.await;
}
@@ -848,7 +824,7 @@ mod tests {
pyv("2.2.1"),
pyv("2.2.0"),
],
pyv("2.2"),
pyv("2.2.2"),
)
.await;
}
@@ -869,151 +845,4 @@ mod tests {
)
.await;
}
#[tokio::test]
async fn test_python_resolution_8() {
assert_resolution(
"2.2",
false,
vec![">2.2", ">=2.4", "<2.4.1"],
vec![
pyv("2.4.1"),
pyv("2.4.0"),
pyv("2.3.1"),
pyv("2.3.0"),
pyv("2.2.1"),
pyv("2.2.0"),
pyv("2.1.1"),
pyv("2.1.0"),
],
pyv("2.4.0"),
)
.await;
}
#[tokio::test]
async fn test_python_resolution_9() {
assert_resolution(
"2.2",
true,
vec![">2.2", ">2.3", "<2.4.1"],
vec![
pyv("2.4.1"),
pyv("2.4.0"),
pyv("2.3.1"),
pyv("2.3.0"),
pyv("2.2.1"),
pyv("2.2.0"),
pyv("2.1.1"),
pyv("2.1.0"),
],
pyv("2.4.0"),
)
.await;
}
#[tokio::test]
async fn test_python_resolution_10() {
assert_resolution(
"2.2",
false,
vec![">2.2", ">=2.4"],
vec![
pyv("2.4.1"),
pyv("2.4.0"),
pyv("2.3.1"),
pyv("2.3.0"),
pyv("2.2.1"),
pyv("2.2.0"),
pyv("2.1.1"),
pyv("2.1.0"),
],
// vec![pyv("2.4.1"), pyv("2.3"), pyv("2.2"), pyv("2.1")],
pyv("2.4"),
)
.await;
}
#[tokio::test]
async fn test_python_resolution_11() {
assert_resolution(
"2.4.1",
false,
vec![">2.2", ">=2.3", "<2.4"],
vec![
pyv("2.4.1"),
pyv("2.4.0"),
pyv("2.3.1"),
pyv("2.3.0"),
pyv("2.2.1"),
pyv("2.2.0"),
pyv("2.1.1"),
pyv("2.1.0"),
],
// vec![pyv("2.4.1"), pyv("2.3"), pyv("2.2"), pyv("2.1")],
pyv("2.3"),
)
.await;
}
#[tokio::test]
async fn test_python_resolution_12() {
assert_resolution(
"2.3",
false,
vec![">2.2", ">=2.3", "<2.4"],
vec![
pyv("2.4.1"),
pyv("2.4.0"),
pyv("2.3.1"),
pyv("2.3.0"),
pyv("2.2.1"),
pyv("2.2.0"),
pyv("2.1.1"),
pyv("2.1.0"),
],
// vec![pyv("2.4.1"), pyv("2.3"), pyv("2.2"), pyv("2.1")],
pyv("2.3"),
)
.await;
}
#[tokio::test]
async fn test_python_resolution_13() {
assert_resolution(
"2.3.1",
false,
vec![">2.2", ">=2.3", "<2.4"],
vec![
pyv("2.4.1"),
pyv("2.4.0"),
pyv("2.3.1"),
pyv("2.3.0"),
pyv("2.2.1"),
pyv("2.2.0"),
pyv("2.1.1"),
pyv("2.1.0"),
],
// vec![pyv("2.4.1"), pyv("2.3"), pyv("2.2"), pyv("2.1")],
pyv("2.3.1"),
)
.await;
}
#[tokio::test]
async fn test_python_resolution_14() {
assert_resolution(
"2.3.0",
false,
vec![],
vec![pyv("2.4.1"), pyv("2.4.0"), pyv("2.3.1")],
pyv("2.3.1"),
)
.await;
}
#[tokio::test]
async fn test_python_resolution_16() {
assert_resolution(
"2.3",
false,
vec![],
vec![pyv("2.4.1"), pyv("2.4.0"), pyv("2.3.1")],
pyv("2.3"),
)
.await;
}
}

View File

@@ -32,7 +32,7 @@ use windmill_queue::{
append_logs, get_queued_job, CanceledBy, JobCompleted, MiniPulledJob, WrappedError,
};
use serde_json::{json, value::RawValue, Value};
use serde_json::{json, value::RawValue};
use tokio::task::JoinHandle;
@@ -44,7 +44,7 @@ use crate::{
otel_oss::add_root_flow_job_to_otlp,
worker_flow::update_flow_status_after_job_completion,
JobCompletedReceiver, JobCompletedSender, SameWorkerSender, SendResult, UpdateFlow,
INIT_SCRIPT_TAG, SAME_WORKER_REQUIREMENTS,
INIT_SCRIPT_TAG,
};
use windmill_common::client::AuthedClient;
@@ -54,7 +54,7 @@ async fn process_jc(
base_internal_url: &str,
db: &DB,
worker_dir: &str,
same_worker_tx: Option<&SameWorkerSender>,
same_worker_tx: &SameWorkerSender,
job_completed_sender: &JobCompletedSender,
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
) {
@@ -104,8 +104,8 @@ async fn process_jc(
jc,
&base_internal_url,
&db,
worker_dir,
same_worker_tx,
&worker_dir,
&same_worker_tx,
&worker_name,
job_completed_sender.clone(),
#[cfg(feature = "benchmark")]
@@ -119,11 +119,6 @@ async fn process_jc(
}
}
enum JobCompletedRx {
JobCompleted(SendResult),
Killpill,
}
pub fn start_background_processor(
job_completed_rx: JobCompletedReceiver,
job_completed_sender: JobCompletedSender,
@@ -145,6 +140,10 @@ pub fn start_background_processor(
#[cfg(feature = "benchmark")]
let mut infos = BenchmarkInfo::new();
enum JobCompletedRx {
JobCompleted(SendResult),
Killpill,
}
//if we have been killed, we want to drain the queue of jobs
while let Some(sr) = {
if has_been_killed && same_worker_queue_size.load(Ordering::SeqCst) == 0 {
@@ -187,7 +186,7 @@ pub fn start_background_processor(
&base_internal_url,
&db,
&worker_dir,
Some(&same_worker_tx),
&same_worker_tx,
&job_completed_sender,
#[cfg(feature = "benchmark")]
&mut bench,
@@ -230,19 +229,19 @@ pub fn start_background_processor(
tracing::info!(parent_flow = %flow, "updating flow status");
if let Err(e) = update_flow_status_after_job_completion(
&db,
&AuthedClient::new(
base_internal_url.to_string(),
w_id.clone(),
token.clone(),
None,
),
&AuthedClient {
base_internal_url: base_internal_url.to_string(),
workspace: w_id.clone(),
token: token.clone(),
force_client: None,
},
flow,
&Uuid::nil(),
&w_id,
success,
Arc::new(result),
true,
&same_worker_tx,
same_worker_tx.clone(),
&worker_dir,
stop_early_override,
&worker_name,
@@ -387,19 +386,23 @@ pub async fn handle_receive_completed_job(
base_internal_url: &str,
db: &DB,
worker_dir: &str,
same_worker_tx: Option<&SameWorkerSender>,
same_worker_tx: &SameWorkerSender,
worker_name: &str,
job_completed_tx: JobCompletedSender,
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
) -> Option<Arc<MiniPulledJob>> {
let token = jc.token.clone();
let workspace = jc.job.workspace_id.clone();
let client = AuthedClient::new(base_internal_url.to_string(), workspace, token, None);
let client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
workspace,
token,
force_client: None,
};
let job = jc.job.clone();
let mem_peak = jc.mem_peak.clone();
let canceled_by = jc.canceled_by.clone();
let processed_completed_job = process_completed_job(
match process_completed_job(
jc,
&client,
db,
@@ -410,9 +413,8 @@ pub async fn handle_receive_completed_job(
#[cfg(feature = "benchmark")]
bench,
)
.await;
match processed_completed_job {
.await
{
Err(err) => {
handle_job_error(
db,
@@ -452,7 +454,7 @@ pub async fn process_completed_job(
client: &AuthedClient,
db: &DB,
worker_dir: &str,
same_worker_tx: Option<&SameWorkerSender>,
same_worker_tx: SameWorkerSender,
worker_name: &str,
job_completed_tx: JobCompletedSender,
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
@@ -528,7 +530,7 @@ pub async fn process_completed_job(
true,
result,
false,
&same_worker_tx.expect(SAME_WORKER_REQUIREMENTS).to_owned(),
same_worker_tx.clone(),
&worker_dir,
None,
worker_name,
@@ -568,7 +570,7 @@ pub async fn process_completed_job(
false,
Arc::new(serde_json::value::to_raw_value(&result).unwrap()),
false,
&same_worker_tx.expect(SAME_WORKER_REQUIREMENTS).to_owned(),
same_worker_tx,
&worker_dir,
None,
worker_name,
@@ -585,34 +587,6 @@ pub async fn process_completed_job(
return Ok(None);
}
async fn handle_non_flow_job_error(
db: &DB,
job: &MiniPulledJob,
mem_peak: i32,
canceled_by: Option<CanceledBy>,
err: Value,
worker_name: &str,
) -> Result<WrappedError, Error> {
append_logs(
&job.id,
&job.workspace_id,
format!("Unexpected error during job execution:\n{err:#?}"),
&db.into(),
)
.await;
add_completed_job_error(
db,
job,
mem_peak,
canceled_by,
err,
worker_name,
false,
None,
)
.await
}
#[tracing::instrument(name = "job_error", level = "info", skip_all, fields(job_id = %job.id))]
pub async fn handle_job_error(
db: &DB,
@@ -622,7 +596,7 @@ pub async fn handle_job_error(
canceled_by: Option<CanceledBy>,
err: Error,
unrecoverable: bool,
same_worker_tx: Option<&SameWorkerSender>,
same_worker_tx: SameWorkerSender,
worker_dir: &str,
worker_name: &str,
job_completed_tx: JobCompletedSender,
@@ -631,13 +605,22 @@ pub async fn handle_job_error(
let err = error_to_value(err);
let update_job_future = || async {
handle_non_flow_job_error(
append_logs(
&job.id,
&job.workspace_id,
format!("Unexpected error during job execution:\n{err:#?}"),
&db.into(),
)
.await;
add_completed_job_error(
db,
job,
mem_peak,
canceled_by.clone(),
err.clone(),
worker_name,
false,
None,
)
.await
};
@@ -666,7 +649,7 @@ pub async fn handle_job_error(
false,
Arc::new(serde_json::value::to_raw_value(&wrapped_error).unwrap()),
unrecoverable,
&same_worker_tx.expect(SAME_WORKER_REQUIREMENTS).clone(),
same_worker_tx,
worker_dir,
None,
worker_name,

View File

@@ -66,7 +66,6 @@ mount {
#[cfg(not(debug_assertions))]
const DEV_CONF_NSJAIL: &'static str = "";
#[cfg(not(windows))]
lazy_static::lazy_static! {
static ref CARGO_HOME_DEFAULT: String = format!("{}/.cargo", *HOME_DIR);
static ref RUSTUP_HOME_DEFAULT: String = format!("{}/.rustup", *HOME_DIR);

View File

@@ -18,10 +18,10 @@ use windmill_common::{
cache::{future::FutureCachedExt, ScriptData, ScriptMetadata},
schema::{should_validate_schema, SchemaValidator},
scripts::PREVIEW_IS_TAR_CODEBASE_HASH,
utils::{create_directory_async, WarnAfterExt},
utils::WarnAfterExt,
worker::{
make_pull_query, write_file, Connection, HttpClient, MAX_TIMEOUT, ROOT_CACHE_DIR,
ROOT_CACHE_NOMOUNT_DIR, TMP_DIR,
write_file, Connection, HttpClient, MAX_TIMEOUT, ROOT_CACHE_DIR, ROOT_CACHE_NOMOUNT_DIR,
TMP_DIR,
},
KillpillSender,
};
@@ -34,7 +34,7 @@ use const_format::concatcp;
#[cfg(feature = "prometheus")]
use prometheus::IntCounter;
use tracing::{field, Instrument, Span};
use tracing::{field, Instrument};
#[cfg(feature = "prometheus")]
use windmill_common::METRICS_DEBUG_ENABLED;
#[cfg(feature = "prometheus")]
@@ -44,7 +44,7 @@ use serde::{Deserialize, Serialize};
use sqlx::types::Json;
use std::{
collections::HashMap,
fmt::Display,
fs::DirBuilder,
sync::{
atomic::{AtomicBool, AtomicU16, Ordering},
Arc,
@@ -168,10 +168,15 @@ use windmill_common::bench::{benchmark_init, BenchmarkInfo, BenchmarkIter};
use windmill_common::add_time;
pub const PY310_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_10");
pub const PY311_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_11");
pub const PY312_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_12");
pub const PY313_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_13");
pub const PY310_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_310");
pub const PY311_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_311");
pub const PY312_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_312");
pub const PY313_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_313");
pub const TAR_PY310_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/python_310");
pub const TAR_PY311_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/python_311");
pub const TAR_PY312_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/python_312");
pub const TAR_PY313_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/python_313");
pub const TAR_JAVA_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/java");
@@ -205,9 +210,6 @@ const NUM_SECS_READINGS: u64 = 60;
const INCLUDE_DEPS_PY_SH_CONTENT: &str = include_str!("../nsjail/download_deps.py.sh");
const WORKER_SHELL_NAP_TIME_DURATION: u64 = 15;
const TIMEOUT_TO_RESET_WORKER_SHELL_NAP_TIME_DURATION: u64 = 2 * 60;
pub const DEFAULT_SLEEP_QUEUE: u64 = 50;
// only 1 native job so that we don't have to worry about concurrency issues on non dedicated native jobs workers
@@ -245,8 +247,6 @@ lazy_static::lazy_static! {
const DOTNET_DEFAULT_PATH: &str = "C:\\Program Files\\dotnet\\dotnet.exe";
#[cfg(unix)]
const DOTNET_DEFAULT_PATH: &str = "/usr/bin/dotnet";
pub const SAME_WORKER_REQUIREMENTS: &'static str =
"SameWorkerSender is required because this job may be part of a flow";
lazy_static::lazy_static! {
@@ -391,31 +391,6 @@ lazy_static::lazy_static! {
pub static ref WIN_ENVS: Envs = vec![];
}
#[derive(Debug)]
pub enum NextJob {
Sql(PulledJob),
Http(JobAndPerms),
}
impl NextJob {
pub fn job(self) -> MiniPulledJob {
match self {
NextJob::Sql(job) => job.job,
NextJob::Http(job) => job.job,
}
}
}
impl std::ops::Deref for NextJob {
type Target = MiniPulledJob;
fn deref(&self) -> &Self::Target {
match self {
NextJob::Sql(job) => &job.job,
NextJob::Http(job) => &job.job,
}
}
}
//only matter if CLOUD_HOSTED
pub const MAX_RESULT_SIZE: usize = 1024 * 1024 * 2; // 2MB
@@ -456,26 +431,20 @@ impl JobCompletedReceiver {
}
impl JobCompletedSender {
pub fn new_job_completed_sender_sql(buffer_size: u8) -> (Self, JobCompletedReceiver) {
let (sender, receiver) = flume::bounded::<SendResult>(buffer_size as usize);
let (unbounded_sender, unbounded_rx) = flume::unbounded::<SendResult>();
let (killpill_tx, killpill_rx) = broadcast::channel::<()>(10);
(
Self::Sql(SqlJobCompletedSender { sender, unbounded_sender, killpill_tx }),
JobCompletedReceiver { bounded_rx: receiver, killpill_rx, unbounded_rx },
)
}
pub fn new(conn: &Connection, buffer_size: u8) -> (Self, Option<JobCompletedReceiver>) {
match conn {
Connection::Sql(_) => {
let result = Self::new_job_completed_sender_sql(buffer_size);
(result.0, Some(result.1))
let (sender, receiver) = flume::bounded::<SendResult>(buffer_size as usize);
let (unbounded_sender, unbounded_rx) = flume::unbounded::<SendResult>();
let (killpill_tx, killpill_rx) = broadcast::channel::<()>(10);
(
Self::Sql(SqlJobCompletedSender { sender, unbounded_sender, killpill_tx }),
Some(JobCompletedReceiver { bounded_rx: receiver, killpill_rx, unbounded_rx }),
)
}
Connection::Http(client) => (Self::Http(client.clone()), None),
}
}
pub fn new_never_used() -> (Self, Option<Receiver<SendResult>>) {
(Self::NeverUsed, None)
}
@@ -653,233 +622,6 @@ fn add_outstanding_wait_time(
}
}
async fn extract_job_and_perms(job: NextJob, conn: &Connection) -> JobAndPerms {
match (job, conn) {
(NextJob::Sql(job), Connection::Sql(db)) => job.get_job_and_perms(db).await,
(NextJob::Sql(_), Connection::Http(_)) => panic!("sql job on http connection"),
(NextJob::Http(job), _) => job,
}
}
fn create_span(arc_job: &Arc<MiniPulledJob>, worker_name: &str, hostname: &str) -> Span {
let span = tracing::span!(tracing::Level::INFO, "job",
job_id = %arc_job.id, root_job = field::Empty, workspace_id = %arc_job.workspace_id, worker = %worker_name, hostname = %hostname, tag = %arc_job.tag,
language = field::Empty,
script_path = field::Empty, flow_step_id = field::Empty, parent_job = field::Empty,
otel.name = field::Empty);
let rj = arc_job.flow_innermost_root_job.unwrap_or(arc_job.id);
if let Some(lg) = arc_job.script_lang.as_ref() {
span.record("language", lg.as_str());
}
if let Some(step_id) = arc_job.flow_step_id.as_ref() {
span.record("otel.name", format!("job {}", step_id).as_str());
span.record("flow_step_id", step_id.as_str());
} else {
span.record("otel.name", "job");
}
if let Some(parent_job) = arc_job.parent_job.as_ref() {
span.record("parent_job", parent_job.to_string().as_str());
}
if let Some(script_path) = arc_job.runnable_path.as_ref() {
span.record("script_path", script_path.as_str());
}
if let Some(root_job) = arc_job.flow_innermost_root_job.as_ref() {
span.record("root_job", root_job.to_string().as_str());
}
windmill_common::otel_oss::set_span_parent(&span, &rj);
span
}
pub async fn handle_all_job_kind_error(
conn: &Connection,
authed_client: &AuthedClient,
job: Arc<MiniPulledJob>,
err: Error,
same_worker_tx: Option<&SameWorkerSender>,
worker_dir: &str,
worker_name: &str,
job_completed_tx: JobCompletedSender,
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
) {
match conn {
Connection::Sql(db) => {
handle_job_error(
db,
authed_client,
job.as_ref(),
0,
None,
err,
false,
same_worker_tx,
&worker_dir,
&worker_name,
job_completed_tx.clone(),
#[cfg(feature = "benchmark")]
bench,
)
.await;
}
Connection::Http(_) => {
job_completed_tx
.send_job(
JobCompleted {
preprocessed_args: None,
job: job.clone(),
result: Arc::new(windmill_common::worker::to_raw_value(&error_to_value(
err,
))),
result_columns: None,
mem_peak: 0,
canceled_by: None,
success: false,
cached_res_path: None,
token: authed_client.token.clone(),
duration: None,
},
false,
)
.await
.expect("send job completed");
}
}
}
pub fn start_interactive_worker_shell(
conn: Connection,
hostname: String,
worker_name: String,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
job_completed_tx: JobCompletedSender,
base_internal_url: String,
worker_dir: String,
) -> JoinHandle<()> {
tokio::spawn(async move {
let mut occupancy_metrics = OccupancyMetrics::new(Instant::now());
let mut last_executed_job: Option<Instant> =
Instant::now().checked_sub(Duration::from_millis(2500));
loop {
if let Ok(_) = killpill_rx.try_recv() {
break;
} else {
let pulled_job = match &conn {
Connection::Sql(db) => {
let query = ("".to_string(), make_pull_query(&[hostname.to_owned()]));
#[cfg(feature = "benchmark")]
let mut bench = windmill_common::bench::BenchmarkIter::new();
let job = pull(
&db,
false,
&worker_name,
Some(&query),
#[cfg(feature = "benchmark")]
&mut bench,
)
.await;
job.map(|x| x.job.map(NextJob::Sql))
}
Connection::Http(client) => {
crate::agent_workers::pull_job(&client, None, Some(true))
.await
.map_err(|e| error::Error::InternalErr(e.to_string()))
.map(|x| x.map(|y| NextJob::Http(y)))
}
};
match pulled_job {
Ok(Some(job)) => {
tracing::debug!(worker = %worker_name, hostname = %hostname, "started handling of job {}", job.id);
let job_dir = create_job_dir(&worker_dir, job.id).await;
#[cfg(feature = "benchmark")]
let mut bench = windmill_common::bench::BenchmarkIter::new();
let JobAndPerms {
job,
raw_code,
raw_lock,
raw_flow,
parent_runnable_path,
token,
precomputed_agent_info: precomputed_bundle,
} = extract_job_and_perms(job, &conn).await;
let authed_client = AuthedClient::new(
base_internal_url.to_owned(),
job.workspace_id.clone(),
token,
None,
);
let arc_job = Arc::new(job);
let _ = handle_queued_job(
arc_job.clone(),
raw_code,
raw_lock,
raw_flow,
parent_runnable_path,
&conn,
&authed_client,
&hostname,
&worker_name,
&worker_dir,
&job_dir,
None,
&base_internal_url,
job_completed_tx.clone(),
&mut occupancy_metrics,
&mut killpill_rx,
precomputed_bundle,
#[cfg(feature = "benchmark")]
&mut bench,
)
.await;
last_executed_job = Some(Instant::now());
}
Ok(None) => {
let now = Instant::now();
match last_executed_job {
Some(last)
if now.duration_since(last).as_secs()
> TIMEOUT_TO_RESET_WORKER_SHELL_NAP_TIME_DURATION =>
{
tokio::time::sleep(Duration::from_secs(
WORKER_SHELL_NAP_TIME_DURATION,
))
.await;
}
_ => {
tokio::time::sleep(Duration::from_millis(*SLEEP_QUEUE)).await;
}
}
}
Err(err) => {
tracing::error!(worker = %worker_name, hostname = %hostname, "Failed to pull jobs: {}", err);
}
};
}
}
})
}
async fn create_job_dir(worker_directory: &str, job_id: impl Display) -> String {
let job_dir_path = format!("{}/{}", worker_directory, job_id);
create_directory_async(&job_dir_path).await;
job_dir_path
}
pub async fn run_worker(
conn: &Connection,
hostname: &str,
@@ -944,7 +686,10 @@ pub async fn run_worker(
write_file(&HOME_ENV, ".netrc", netrc).expect("could not write netrc");
}
create_directory_async(&worker_dir).await;
DirBuilder::new()
.recursive(true)
.create(&worker_dir)
.expect("could not create initial worker dir");
if !*DISABLE_NSJAIL {
let _ = write_file(
@@ -1203,25 +948,6 @@ pub async fn run_worker(
)),
_ => None,
};
// If we're the first worker to run, we start another background process that listens for a specific tag.
// This tag is associated only with jobs using Bash as the script language.
// For agent workers, the expected tag format is the worker name suffixed with "-ssh".
// For regular workers, the tag is simply the machine's hostname and if not found the randomly generated hostname.
let interactive_shell = if i_worker == 1 {
let it_shell = start_interactive_worker_shell(
conn.clone(),
hostname.to_owned(),
worker_name.clone(),
killpill_rx.resubscribe(),
job_completed_tx.clone(),
base_internal_url.to_owned(),
worker_dir.clone(),
);
Some(it_shell)
} else {
None
};
let mut last_executed_job: Option<Instant> = None;
@@ -1332,7 +1058,7 @@ pub async fn run_worker(
if !valid_key {
tracing::error!(
worker = %worker_name, hostname = %hostname,
"Invalid license key, workers require a valid license key, sleeping for 10s waiting for valid key to be set"
"Invalid license key, workers require a valid license key, sleeping for 30s waiting for valid key to be set"
);
tokio::time::sleep(Duration::from_secs(10)).await;
continue;
@@ -1402,6 +1128,29 @@ pub async fn run_worker(
} else {
tracing::info!("benchmark not finished, still pulling jobs {}", infos.iters);
}
enum NextJob {
Sql(PulledJob),
Http(JobAndPerms),
}
impl NextJob {
pub fn job(self) -> MiniPulledJob {
match self {
NextJob::Sql(job) => job.job,
NextJob::Http(job) => job.job,
}
}
}
impl std::ops::Deref for NextJob {
type Target = MiniPulledJob;
fn deref(&self) -> &Self::Target {
match self {
NextJob::Sql(job) => &job.job,
NextJob::Http(job) => &job.job,
}
}
}
let next_job = {
// println!("2: {:?}", instant.elapsed());
@@ -1442,7 +1191,6 @@ pub async fn run_worker(
"/api/agent_workers/same_worker_job/{}",
same_worker_job.job_id
),
None,
&same_worker_job,
)
.await
@@ -1548,7 +1296,7 @@ pub async fn run_worker(
}
job.map(|x| x.job.map(NextJob::Sql))
}
Connection::Http(client) => crate::agent_workers::pull_job(&client, None, None)
Connection::Http(client) => crate::agent_workers::pull_job(&client)
.await
.map_err(|e| error::Error::InternalErr(e.to_string()))
.map(|x| x.map(|y| NextJob::Http(y))),
@@ -1596,7 +1344,6 @@ pub async fn run_worker(
}
}
}
if matches!(job.kind, JobKind::Noop) {
add_time!(bench, "send job completed START");
job_completed_tx
@@ -1678,12 +1425,20 @@ pub async fn run_worker(
// fields macro we can make a job id that only appears when
// the job is defined?
let job_dir = create_job_dir(&worker_dir, job.id).await;
let job_dir = format!("{worker_dir}/{}", job.id);
DirBuilder::new()
.recursive(true)
.create(&job_dir)
.expect("could not create job dir");
let same_worker = job.same_worker;
let folder = if job.script_lang == Some(ScriptLang::Go) {
create_directory_async(&format!("{job_dir}/go")).await;
DirBuilder::new()
.recursive(true)
.create(&format!("{job_dir}/go"))
.expect("could not create go dir");
"/go"
} else {
""
@@ -1695,22 +1450,26 @@ pub async fn run_worker(
if tokio::fs::metadata(target).await.is_err() {
let parent_flow = job.parent_job.unwrap();
let parent_shared_dir = format!("{worker_dir}/{parent_flow}/shared");
create_directory_async(&parent_shared_dir).await;
DirBuilder::new()
.recursive(true)
.create(&parent_shared_dir)
.expect("could not create parent shared dir");
symlink(&parent_shared_dir, target)
.await
.expect("could not symlink target");
}
} else {
create_directory_async(target).await;
DirBuilder::new()
.recursive(true)
.create(target)
.expect("could not create shared dir");
}
#[cfg(feature = "prometheus")]
let tag = job.tag.clone();
let is_init_script: bool = job.tag.as_str() == INIT_SCRIPT_TAG;
let is_flow = job.is_flow();
let job_id = job.id;
let JobAndPerms {
job,
raw_code,
@@ -1719,32 +1478,70 @@ pub async fn run_worker(
parent_runnable_path,
token,
precomputed_agent_info: precomputed_bundle,
} = extract_job_and_perms(job, &conn).await;
} = match (job, &conn) {
(NextJob::Sql(job), Connection::Sql(db)) => job.get_job_and_perms(db).await,
(NextJob::Sql(_), Connection::Http(_)) => {
panic!("sql job on http connection")
}
(NextJob::Http(job), _) => job,
};
let authed_client = AuthedClient::new(
base_internal_url.to_owned(),
job.workspace_id.clone(),
// let token = create_token(&db, &job, job_perms).await;
let authed_client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
token,
None,
);
workspace: job.workspace_id.to_string(),
force_client: None,
};
let arc_job = Arc::new(job);
add_time!(bench, "handle_queued_job START");
let span = create_span(&arc_job, &worker_name, hostname);
let span = tracing::span!(tracing::Level::INFO, "job",
job_id = %arc_job.id, root_job = field::Empty, workspace_id = %arc_job.workspace_id, worker = %worker_name, hostname = %hostname, tag = %arc_job.tag,
language = field::Empty,
script_path = field::Empty, flow_step_id = field::Empty, parent_job = field::Empty,
otel.name = field::Empty);
let rj = if let Some(root_job) = arc_job.flow_innermost_root_job {
root_job
} else {
arc_job.id
};
if let Some(lg) = arc_job.script_lang.as_ref() {
span.record("language", lg.as_str());
}
if let Some(step_id) = arc_job.flow_step_id.as_ref() {
span.record("otel.name", format!("job {}", step_id).as_str());
span.record("flow_step_id", step_id.as_str());
} else {
span.record("otel.name", "job");
}
if let Some(parent_job) = arc_job.parent_job.as_ref() {
span.record("parent_job", parent_job.to_string().as_str());
}
if let Some(script_path) = arc_job.runnable_path.as_ref() {
span.record("script_path", script_path.as_str());
}
if let Some(root_job) = arc_job.flow_innermost_root_job.as_ref() {
span.record("root_job", root_job.to_string().as_str());
}
let job_result = handle_queued_job(
windmill_common::otel_oss::set_span_parent(&span, &rj);
// span.context().span().add_event_with_timestamp("job created".to_string(), arc_job.created_at.into(), vec![]);
match handle_queued_job(
arc_job.clone(),
raw_code,
raw_lock,
raw_flow,
parent_runnable_path,
&conn,
conn,
&authed_client,
hostname,
&hostname,
&worker_name,
&worker_dir,
&job_dir,
Some(same_worker_tx.clone()),
same_worker_tx.clone(),
base_internal_url,
job_completed_tx.clone(),
&mut occupancy_metrics,
@@ -1754,40 +1551,74 @@ pub async fn run_worker(
&mut bench,
)
.instrument(span)
.await;
match job_result {
Ok(false) if is_init_script => {
tracing::error!("init script job failed, exiting");
update_worker_ping_for_failed_init_script(conn, &worker_name, job_id)
.await;
break;
}
.await
{
Err(err) => {
handle_all_job_kind_error(
&conn,
&authed_client,
arc_job.clone(),
err,
Some(&same_worker_tx),
&worker_dir,
&worker_name,
job_completed_tx.clone(),
#[cfg(feature = "benchmark")]
&mut bench,
)
.await;
match conn {
Connection::Sql(db) => {
handle_job_error(
db,
&authed_client,
arc_job.as_ref(),
0,
None,
err,
false,
same_worker_tx.clone(),
&worker_dir,
&worker_name,
job_completed_tx.clone(),
#[cfg(feature = "benchmark")]
&mut bench,
)
.await;
}
Connection::Http(_) => {
job_completed_tx
.send_job(
JobCompleted {
preprocessed_args: None,
job: arc_job.clone(),
result: Arc::new(
windmill_common::worker::to_raw_value(
&error_to_value(err),
),
),
result_columns: None,
mem_peak: 0,
canceled_by: None,
success: false,
cached_res_path: None,
token: authed_client.token.clone(),
duration: None,
},
false,
)
.await
.expect("send job completed");
}
}
if is_init_script {
tracing::error!("init script job failed (in handler), exiting");
update_worker_ping_for_failed_init_script(
conn,
&worker_name,
job_id,
arc_job.id,
)
.await;
break;
}
}
Ok(false) if is_init_script => {
tracing::error!("init script job failed, exiting");
update_worker_ping_for_failed_init_script(
conn,
&worker_name,
arc_job.id,
)
.await;
break;
}
_ => {}
}
@@ -1812,7 +1643,8 @@ pub async fn run_worker(
.await;
}
if !KEEP_JOB_DIR.load(Ordering::Relaxed) && !(is_flow && same_worker) {
if !KEEP_JOB_DIR.load(Ordering::Relaxed) && !(arc_job.is_flow() && same_worker)
{
let _ = tokio::fs::remove_dir_all(job_dir).await;
}
}
@@ -1894,11 +1726,6 @@ pub async fn run_worker(
tracing::error!("error in awaiting send_result process: {e:?}")
}
}
if let Some(interactive_shell) = interactive_shell {
if let Err(e) = interactive_shell.await {
tracing::error!("error in awaiting interactive_shell process: {e:?}")
}
}
tracing::info!(worker = %worker_name, hostname = %hostname, "worker {} exited", worker_name);
tracing::info!(worker = %worker_name, hostname = %hostname, "number of jobs executed: {}", jobs_executed);
}
@@ -1987,7 +1814,7 @@ pub struct PreviousResult<'a> {
pub previous_result: Option<&'a RawValue>,
}
pub async fn handle_queued_job(
async fn handle_queued_job(
job: Arc<MiniPulledJob>,
raw_code: Option<String>,
raw_lock: Option<String>,
@@ -1999,7 +1826,7 @@ pub async fn handle_queued_job(
worker_name: &str,
worker_dir: &str,
job_dir: &str,
same_worker_tx: Option<SameWorkerSender>,
same_worker_tx: SameWorkerSender,
base_internal_url: &str,
job_completed_tx: JobCompletedSender,
occupancy_metrics: &mut OccupancyMetrics,
@@ -2112,7 +1939,7 @@ pub async fn handle_queued_job(
.to_string();
append_logs(&job.id, &job.workspace_id, logs, conn).await;
}
let result = job_completed_tx
job_completed_tx
.send_job(
JobCompleted {
preprocessed_args: None,
@@ -2128,16 +1955,8 @@ pub async fn handle_queued_job(
},
true,
)
.await;
match result {
Ok(_) => {
tracing::debug!("Send job completed")
}
Err(err) => {
tracing::error!("An error occurred while sending job completed: {:#?}", err)
}
}
.await
.expect("send job completed");
return Ok(true);
}
@@ -2156,7 +1975,7 @@ pub async fn handle_queued_job(
db,
&client,
None,
&same_worker_tx.expect(SAME_WORKER_REQUIREMENTS),
same_worker_tx,
worker_dir,
job_completed_tx.clone(),
worker_name,
@@ -2325,6 +2144,7 @@ pub async fn handle_queued_job(
{
return Ok(false);
}
process_result(
job,
result.map(|x| Arc::new(x)),

View File

@@ -80,7 +80,7 @@ pub async fn update_flow_status_after_job_completion(
success: bool,
result: Arc<Box<RawValue>>,
unrecoverable: bool,
same_worker_tx: &SameWorkerSender,
same_worker_tx: SameWorkerSender,
worker_dir: &str,
stop_early_override: Option<bool>,
worker_name: &str,
@@ -89,7 +89,6 @@ pub async fn update_flow_status_after_job_completion(
) -> error::Result<Option<Arc<MiniPulledJob>>> {
// this is manual tailrecursion because async_recursion blows up the stack
potentially_crash_for_testing();
let mut rec = RecUpdateFlowStatusAfterJobCompletion {
flow,
job_id_for_status: job_id_for_status.clone(),
@@ -110,7 +109,7 @@ pub async fn update_flow_status_after_job_completion(
rec.success,
rec.result,
unrecoverable,
same_worker_tx,
same_worker_tx.clone(),
worker_dir,
rec.stop_early_override,
rec.skip_error_handler,
@@ -135,7 +134,7 @@ pub async fn update_flow_status_after_job_completion(
error: json!(e.to_string()),
}))),
true,
same_worker_tx,
same_worker_tx.clone(),
worker_dir,
rec.stop_early_override,
rec.skip_error_handler,
@@ -148,7 +147,6 @@ pub async fn update_flow_status_after_job_completion(
}
};
unrecoverable = false;
match nrec {
UpdateFlowStatusAfterJobCompletion::Done(job) => {
add_time!(bench, "update flow status internal END");
@@ -228,7 +226,7 @@ pub async fn update_flow_status_after_job_completion_internal(
mut success: bool,
result: Arc<Box<RawValue>>,
unrecoverable: bool,
same_worker_tx: &SameWorkerSender,
same_worker_tx: SameWorkerSender,
worker_dir: &str,
stop_early_override: Option<bool>,
skip_error_handler: bool,
@@ -627,10 +625,9 @@ pub async fn update_flow_status_after_job_completion_internal(
sqlx::query!(
"UPDATE v2_job_queue q SET suspend = 0
FROM v2_job j, v2_job_status f
WHERE q.workspace_id = $1 AND q.suspend = $3 AND j.parent_job = $2
WHERE parent_job = $1
AND f.id = j.id AND q.id = j.id
AND (f.flow_status->'step')::int = 0",
w_id,
AND suspend = $2 AND (f.flow_status->'step')::int = 0",
flow,
nindex
)
@@ -1292,7 +1289,7 @@ pub async fn update_flow_status_after_job_completion_internal(
db,
client,
Some(nresult.clone()),
same_worker_tx,
same_worker_tx.clone(),
worker_dir,
job_completed_tx,
worker_name,
@@ -1589,7 +1586,7 @@ pub async fn handle_flow(
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClient,
last_result: Option<Arc<Box<RawValue>>>,
same_worker_tx: &SameWorkerSender,
same_worker_tx: SameWorkerSender,
worker_dir: &str,
job_completed_tx: JobCompletedSender,
worker_name: &str,
@@ -1646,7 +1643,7 @@ pub async fn handle_flow(
db,
client,
last_result.clone(),
same_worker_tx,
same_worker_tx.clone(),
worker_dir,
worker_name,
)
@@ -1739,7 +1736,7 @@ async fn push_next_flow_job(
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClient,
last_job_result: Option<Arc<Box<RawValue>>>,
same_worker_tx: &SameWorkerSender,
same_worker_tx: SameWorkerSender,
worker_dir: &str,
worker_name: &str,
) -> error::Result<PushNextFlowJob> {

View File

@@ -1965,11 +1965,12 @@ async fn ansible_dep(
use windmill_parser_yaml::add_versions_to_requirements_yaml;
use crate::ansible_executor::{
create_ansible_cfg, get_collection_locks, get_git_ssh_cmd, get_role_locks,
install_galaxy_collections,
};
create_ansible_cfg, get_collection_locks, get_git_ssh_cmd, get_role_locks,
install_galaxy_collections,
};
use windmill_common::client::AuthedClient;
let python_lockfile = python_dep(
reqs.python_reqs.join("\n").to_string(),
job_id,
@@ -1988,12 +1989,12 @@ async fn ansible_dep(
let conn = &Connection::Sql(db.clone());
let authed_client = AuthedClient::new(
base_internal_url.to_string(),
w_id.to_string(),
token.to_string(),
None,
);
let authed_client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
token: token.to_string(),
workspace: w_id.to_string(),
force_client: None,
};
let git_ssh_cmd = get_git_ssh_cmd(&reqs, job_dir, &authed_client).await?;
@@ -2101,12 +2102,7 @@ async fn capture_dependency_job(
(
job_raw_code.to_owned(),
match crate::PyV::try_parse_from_requirements(&split_requirements(
job_raw_code,
)) {
Some(pyv) => pyv,
None => crate::PyV::gravitational_version(job_id, w_id, None).await,
},
crate::PyV::parse_from_requirements(&split_requirements(job_raw_code)),
)
} else {
let mut version_specifiers = vec![];
@@ -2193,6 +2189,11 @@ async fn capture_dependency_job(
}
}
ScriptLang::Go => {
if raw_deps {
return Err(Error::ExecutionErr(
"Raw dependencies not supported for go".to_string(),
));
}
install_go_dependencies(
job_id,
job_raw_code,
@@ -2203,7 +2204,6 @@ async fn capture_dependency_job(
false,
false,
false,
raw_deps,
worker_name,
w_id,
occupancy_metrics,

View File

@@ -125,7 +125,6 @@ async fn update_worker_ping_full_inner(
client
.post::<_, ()>(
UPDATE_PING_URL,
None,
&Ping {
last_job_executed: None,
last_job_workspace_id: None,
@@ -191,7 +190,6 @@ pub async fn insert_ping(
client
.post::<_, ()>(
UPDATE_PING_URL,
None,
&Ping {
last_job_executed: None,
last_job_workspace_id: None,
@@ -251,7 +249,6 @@ pub async fn update_worker_ping_from_job(
client
.post::<Ping, ()>(
UPDATE_PING_URL,
None,
&Ping {
last_job_executed: Some(job_id.clone()),
last_job_workspace_id: Some(w_id.to_string()),
@@ -290,7 +287,6 @@ pub async fn ping_job_status(
client
.post(
&format!("/api/agent_workers/ping_job_status/{}", job_id),
None,
&PingJobStatus { mem_peak, current_mem },
)
.await

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.497.0";
export const VERSION = "v1.494.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -63,7 +63,7 @@ export {
// }
// });
export const VERSION = "1.497.0";
export const VERSION = "1.494.0";
const command = new Command()
.name("wmill")

View File

@@ -40,7 +40,7 @@ export class LockfileGenerationError extends Error {
export async function generateAllMetadata() {}
function findClosestRawReqs(
lang: "bun" | "python3" | "php" | "go" | undefined,
lang: "bun" | "python3" | "php" | undefined,
remotePath: string,
globalDeps: GlobalDeps
): string | undefined {
@@ -72,15 +72,6 @@ function findClosestRawReqs(
bestCandidate = { k, v };
}
});
} else if (lang == "go") {
Object.entries(globalDeps.goMods).forEach(([k, v]) => {
if (
remotePath.startsWith(k) &&
k.length >= (bestCandidate?.k ?? "").length
) {
bestCandidate = { k, v };
}
});
}
// @ts-ignore
return bestCandidate?.v;
@@ -202,14 +193,14 @@ export async function generateScriptMetadataInternal(
const language = inferContentTypeFromFilePath(scriptPath, opts.defaultTs);
const rawReqs = findClosestRawReqs(
language as "bun" | "python3" | "php" | "go" | undefined,
language as "bun" | "python3" | "php" | undefined,
scriptPath,
globalDeps
);
if (rawReqs) {
log.info(
(await blueColor())(
`Found raw requirements (package.json/requirements.txt/composer.json/go.mod) for ${scriptPath}, using it`
`Found raw requirements (package.json/requirements.txt/composer.json) for ${scriptPath}, using it`
)
);
}

View File

@@ -869,13 +869,11 @@ export type GlobalDeps = {
pkgs: Record<string, string>;
reqs: Record<string, string>;
composers: Record<string, string>;
goMods: Record<string, string>;
};
export async function findGlobalDeps(): Promise<GlobalDeps> {
const pkgs: { [key: string]: string } = {};
const reqs: { [key: string]: string } = {};
const composers: { [key: string]: string } = {};
const goMods: { [key: string]: string } = {};
const els = await FSFSElement(Deno.cwd(), [], false);
for await (const entry of readDirRecursiveWithIgnore((p, isDir) => {
p = SEP + p;
@@ -884,24 +882,21 @@ export async function findGlobalDeps(): Promise<GlobalDeps> {
!(
p.endsWith(SEP + "package.json") ||
p.endsWith(SEP + "requirements.txt") ||
p.endsWith(SEP + "composer.json") ||
p.endsWith(SEP + "go.mod")
p.endsWith(SEP + "composer.json")
)
);
}, els)) {
if (entry.isDirectory || entry.ignored) continue;
const content = await entry.getContentText();
if (entry.path.endsWith("package.json")) {
pkgs[entry.path.substring(0, entry.path.length - "package.json".length)] = content;
pkgs[entry.path.substring(0, entry.path.length - 12)] = content;
} else if (entry.path.endsWith("requirements.txt")) {
reqs[entry.path.substring(0, entry.path.length - "requirements.txt".length)] = content;
reqs[entry.path.substring(0, entry.path.length - 16)] = content;
} else if (entry.path.endsWith("composer.json")) {
composers[entry.path.substring(0, entry.path.length - "composer.json".length)] = content;
} else if (entry.path.endsWith("go.mod")) {
goMods[entry.path.substring(0, entry.path.length - "go.mod".length)] = content;
composers[entry.path.substring(0, entry.path.length - 13)] = content;
}
}
return { pkgs, reqs, composers, goMods };
return { pkgs, reqs, composers };
}
async function generateMetadata(
opts: GlobalOptions & {

View File

@@ -22,15 +22,6 @@
"rustfmt"
];
};
patchedClang = pkgs.llvmPackages_18.clang.overrideAttrs (oldAttrs: {
postFixup = ''
# Copy the original postFixup logic but skip add-hardening.sh
${oldAttrs.postFixup or ""}
# Remove the line that substitutes add-hardening.sh
sed -i 's/.*source.*add-hardening\.sh.*//' $out/bin/clang
'';
});
buildInputs = with pkgs; [
openssl
openssl.dev
@@ -73,24 +64,16 @@
in {
# Enter by `nix develop .#wasm`
devShells."wasm" = pkgs.mkShell {
# Explicitly set paths for headers and linker
shellHook = ''
export CC=${patchedClang}/bin/clang
'';
buildInputs = buildInputs ++ (with pkgs; [
(rust-bin.nightly.latest.default.override {
extensions = [
"rust-src" # for rust-analyzer
"rust-analyzer"
];
targets =
[ "wasm32-unknown-unknown" "wasm32-unknown-emscripten" ];
targets = [ "wasm32-unknown-unknown" ];
})
wasm-pack
deno
emscripten
# Needed for extra dependencies
glibc_multi
]);
};
@@ -98,7 +81,6 @@
buildInputs = buildInputs ++ (with pkgs; [
# Essentials
rust
cargo-watch
cargo-sweep
git
xcaddy
@@ -215,7 +197,7 @@
ORACLE_LIB_DIR = "${pkgs.oracle-instantclient.lib}/lib";
ANSIBLE_PLAYBOOK_PATH = "${pkgs.ansible}/bin/ansible-playbook";
ANSIBLE_GALAXY_PATH = "${pkgs.ansible}/bin/ansible-galaxy";
# RUST_LOG = "debug";
RUST_LOG = "debug";
SQLX_OFFLINE = "true";
# See this issue: https://github.com/NixOS/nixpkgs/issues/370494

0
frontend/npm Normal file
View File

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.497.0",
"version": "1.494.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.497.0",
"version": "1.494.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -22,12 +22,9 @@
"@leeoniya/ufuzzy": "^1.0.8",
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"@windmill-labs/svelte-dnd-action": "^0.9.48",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^1.0.0",
"@xyflow/svelte": "^0.1.15",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.3.4",
@@ -55,7 +52,6 @@
"monaco-vim": "^0.4.1",
"ol": "^7.4.0",
"openai": "^4.87.1",
"openapi-types": "^12.1.3",
"p-limit": "^6.1.0",
"panzoom": "^9.4.3",
"pdfjs-dist": "4.8.69",
@@ -83,11 +79,9 @@
"windmill-parser-wasm-ts": "^1.486.1",
"windmill-parser-wasm-yaml": "^1.429.0",
"windmill-sql-datatype-parser-wasm": "^1.318.0",
"xterm": "^5.3.0",
"xterm-readline": "^1.1.2",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.4",
"yaml": "^2.8.0",
"yaml": "^2.3.4",
"yjs": "^13.6.7",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5"
@@ -125,6 +119,7 @@
"postcss-load-config": "^4.0.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.3.3",
"simple-svelte-autocomplete": "^2.5.1",
"style-to-object": "^0.4.1",
"stylelint-config-recommended": "^13.0.0",
"svelte": "^5.0.0",
@@ -3006,59 +3001,6 @@
"win32"
]
},
"node_modules/@scalar/openapi-parser": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@scalar/openapi-parser/-/openapi-parser-0.15.0.tgz",
"integrity": "sha512-49u1sE+P+72nbt/9p7flBS/g7wbapTGAplcQ1FZxa4La56OMuShCl4qSSdw8lPNZjUZNTLcw7ExVOGI9p1kkOA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.17.1",
"ajv-draft-04": "^1.0.0",
"ajv-formats": "^3.0.1",
"jsonpointer": "^5.0.1",
"leven": "^4.0.0",
"yaml": "^2.4.5"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@scalar/openapi-parser/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@scalar/openapi-parser/node_modules/ajv-draft-04": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
"license": "MIT",
"peerDependencies": {
"ajv": "^8.5.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/@scalar/openapi-parser/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/@smithy/types": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.2.0.tgz",
@@ -3076,11 +3018,11 @@
"integrity": "sha512-VqAAkydywPpkw63WQhPVKCD3SdwXuihCUVZbbiY3SfSTGQyHmwRoq27y4dmJdZuJwd5JIlQoMPyGvMbUPY0RKQ=="
},
"node_modules/@svelte-put/shortcut": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-4.1.0.tgz",
"integrity": "sha512-wImNEIkbxAIWFqlfuhcbC+jRPDeRa/uJGIXHMEVVD+jqL9xCwWNnkGQJ6Qb2XVszuRLHlb8SGZDL3Io/h3vs8w==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-3.1.1.tgz",
"integrity": "sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==",
"peerDependencies": {
"svelte": "^5.1.0"
"svelte": "^3.55.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/@sveltejs/acorn-typescript": {
@@ -3274,17 +3216,6 @@
"node": ">=10.13.0"
}
},
"node_modules/@tutorlatin/svelte-tiny-virtual-list": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.2.tgz",
"integrity": "sha512-Zts/GUrU6aPgmCrL6sGiD1EoiQxUU+r7Q2g9pckEUIDybNkluV2pEYWUsQLQ+28IJlSSHTopv0fhKs3qMUxqEw==",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -3865,38 +3796,23 @@
"svelte": ">=3.23.0 || ^5.0.0-next.0"
}
},
"node_modules/@xterm/addon-fit": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
},
"node_modules/@xterm/xterm": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT",
"peer": true
},
"node_modules/@xyflow/svelte": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-1.0.2.tgz",
"integrity": "sha512-biNnzWRmKF/m7t7iY9wt3/KhoTyA8XGbS0Vix3HCL9vrULDF14R1VsMIOxKZib1GJfG+f1/cD2dQ9kplfPLs/Q==",
"version": "0.1.39",
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.1.39.tgz",
"integrity": "sha512-QZ5mzNysvJeJW7DxmqI4Urhhef9tclqtPr7WAS5zQF5Gk6k9INwzey4CYNtEZo8XMj9H8lzgoJRmgMPnJEc1kw==",
"dependencies": {
"@svelte-put/shortcut": "^4.1.0",
"@xyflow/system": "0.0.61"
"@svelte-put/shortcut": "3.1.1",
"@xyflow/system": "0.0.59",
"classcat": "^5.0.4"
},
"peerDependencies": {
"svelte": "^5.25.0"
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.61",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.61.tgz",
"integrity": "sha512-TsZG/Ez8dzxX6/Ol44LvFqVZsYvyz6dpDlAQZZk6hTL7JLGO5vN3dboRJqMwU8/Qtr5IEv5YBzojjAwIqW1HCA==",
"version": "0.0.59",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.59.tgz",
"integrity": "sha512-+xgqYhoBv5F10TQx0SiKZR/DcWtuxFYR+e/LluHb7DMtX4SsMDutZWEJ4da4fDco25jZxw5G9fOlmk7MWvYd5Q==",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-selection": "^3.0.10",
@@ -4018,45 +3934,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/amator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz",
@@ -4077,6 +3954,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -4720,6 +4598,11 @@
"consola": "^3.2.3"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@@ -5603,7 +5486,8 @@
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/encoding-down": {
"version": "6.3.0",
@@ -6160,7 +6044,8 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-diff": {
"version": "1.1.2",
@@ -6219,6 +6104,7 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"dev": true,
"funding": [
{
"type": "github",
@@ -6228,7 +6114,8 @@
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
],
"peer": true
},
"node_modules/fastest-levenshtein": {
"version": "1.0.16",
@@ -7293,6 +7180,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -7474,15 +7362,6 @@
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz",
"integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ=="
},
"node_modules/jsonpointer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
"integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7662,18 +7541,6 @@
"node": ">=6"
}
},
"node_modules/leven": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-4.0.0.tgz",
"integrity": "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -9345,12 +9212,6 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"license": "MIT"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -10854,6 +10715,8 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11150,6 +11013,12 @@
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-svelte-autocomplete": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/simple-svelte-autocomplete/-/simple-svelte-autocomplete-2.5.2.tgz",
"integrity": "sha512-6+kZP3XQGb7zs0djTWoFv8tQorBZdv64YfCKrlsYIYBCFsEeikSiNsBSSYw1Om+LYmFeMEOAYSSKoEw+qpnBXQ==",
"dev": true
},
"node_modules/sirv": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
@@ -11295,6 +11164,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -11323,6 +11193,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -13159,25 +13030,6 @@
"node": ">=0.4"
}
},
"node_modules/xterm": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
"license": "MIT"
},
"node_modules/xterm-readline": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/xterm-readline/-/xterm-readline-1.1.2.tgz",
"integrity": "sha512-1+W2nVuQvCYz9OUYwFBiolrSQUui51aDDyacKXt4PuxeBHqzvabQEJ2kwdBDzsmOjz5BwlDTAjJmYpH2OGqLFA==",
"license": "MIT",
"dependencies": {
"string-width": "^4"
},
"peerDependencies": {
"@xterm/xterm": "^5.5.0"
}
},
"node_modules/y-leveldb": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz",
@@ -13278,7 +13130,6 @@
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.497.0",
"version": "1.494.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -50,6 +50,7 @@
"postcss-load-config": "^4.0.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.3.3",
"simple-svelte-autocomplete": "^2.5.1",
"style-to-object": "^0.4.1",
"stylelint-config-recommended": "^13.0.0",
"svelte": "^5.0.0",
@@ -90,11 +91,9 @@
"@leeoniya/ufuzzy": "^1.0.8",
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@windmill-labs/svelte-dnd-action": "^0.9.48",
"@xyflow/svelte": "^1.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^0.1.15",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.3.4",
@@ -122,7 +121,6 @@
"monaco-vim": "^0.4.1",
"ol": "^7.4.0",
"openai": "^4.87.1",
"openapi-types": "^12.1.3",
"p-limit": "^6.1.0",
"panzoom": "^9.4.3",
"pdfjs-dist": "4.8.69",
@@ -134,7 +132,6 @@
"svelte-exmarkdown": "^5.0.0",
"svelte-infinite-loading": "^1.4.0",
"svelte-tiny-virtual-list": "^2.0.5",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"tailwind-merge": "^1.13.2",
"vscode": "npm:@codingame/monaco-vscode-extension-api@~16.1.1",
"vscode-languageclient": "~9.0.1",
@@ -151,11 +148,9 @@
"windmill-parser-wasm-ts": "^1.486.1",
"windmill-parser-wasm-yaml": "^1.429.0",
"windmill-sql-datatype-parser-wasm": "^1.318.0",
"xterm": "^5.3.0",
"xterm-readline": "^1.1.2",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.4",
"yaml": "^2.8.0",
"yaml": "^2.3.4",
"yjs": "^13.6.7",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5"

View File

@@ -1,90 +1,85 @@
import { tick } from 'svelte'
export const action = (node) => {
/* Constants */
const update = new Event('update')
type TextArea = HTMLTextAreaElement
export const autosize = (node: TextArea) => {
/* ------------------------------------------------------------------
* Constants
* ---------------------------------------------------------------- */
const UPDATE_EVENT = new Event('update')
const MIN_HEIGHT = 30 // px
const EXTRA = 2 // px added to scrollHeight
let width = 0
/* ------------------------------------------------------------------
* Core resize routine
* ---------------------------------------------------------------- */
const resize = () => {
node.style.height = 'auto'
node.style.height = `${Math.max(node.scrollHeight, MIN_HEIGHT) + EXTRA}px`
/* Functions */
const init = () => {
addStyles()
observeElement()
addEventListeners()
setInitialHeight()
}
/* ------------------------------------------------------------------
* Patch `value` so programmatic changes trigger resize
* ---------------------------------------------------------------- */
const proto = Object.getPrototypeOf(node)
const desc = Object.getOwnPropertyDescriptor(proto, 'value')
const dispatchUpdateEvent = () => {
node.dispatchEvent(update)
}
if (desc) {
const setInitialHeight = () => {
let height = 0
if (node.value) {
height = Math.max(node.scrollHeight ?? 0, 30) + 2
} else {
if (node.placeholder) {
node.value = node.placeholder
height = Math.max(node.scrollHeight ?? 0, 30) + 2
node.value = ''
} else {
node.value = '|'
node.style.height = '0px'
height = Math.max(node.scrollHeight ?? 0, 30) + 2
node.value = ''
}
}
node.style.height = height + 'px'
}
const setHeight = () => {
node.style.height = 'auto'
node.style.height = Math.max(node.scrollHeight ?? 0, 30) + 2 + 'px'
}
const addStyles = () => {
node.style.boxSizing = 'border-box'
}
const observeElement = () => {
let elementPrototype = Object.getPrototypeOf(node)
let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, 'value')
Object.defineProperty(node, 'value', {
get() {
return desc.get?.call(this)
get: function () {
return descriptor?.get?.apply(this, arguments as any)
},
set(v: unknown) {
desc.set?.call(this, v)
node.dispatchEvent(UPDATE_EVENT)
set: function () {
descriptor?.set?.apply(this, arguments as any)
dispatchUpdateEvent()
}
})
}
/* ------------------------------------------------------------------
* Event listeners
* ---------------------------------------------------------------- */
const onInput = () => node.dispatchEvent(UPDATE_EVENT)
const addEventListeners = () => {
node.addEventListener('input', (e) => {
dispatchUpdateEvent()
})
node.addEventListener('update', setHeight)
}
node.addEventListener('input', onInput)
node.addEventListener('update', resize)
const removeEventListeners = () => {
node.removeEventListener('input', dispatchUpdateEvent)
node.removeEventListener('update', setHeight)
}
/* ------------------------------------------------------------------
* Inline styling
* ---------------------------------------------------------------- */
node.style.boxSizing = 'border-box'
if (node.tagName.toLowerCase() !== 'textarea') {
throw new Error('svelte-textarea-auto-height can only be used on textarea elements.')
} else {
init()
/* ------------------------------------------------------------------
* Wait for DOM mount, then do an initial measure.
* If the <textarea> is already visible (offsetWidth > 0) this covers it;
* otherwise the first ResizeObserver callback will.
* ---------------------------------------------------------------- */
;(async () => {
await tick()
resize()
})()
/* ------------------------------------------------------------------
* ResizeObserver handles:
* • first time the element gets a real width
* • container/window resizes afterwards
* ---------------------------------------------------------------- */
const ro = new ResizeObserver(([entry]) => {
const newWidth = entry.contentRect.width
if (newWidth !== width) {
width = newWidth
resize()
}
})
ro.observe(node)
/* ------------------------------------------------------------------
* Action lifecycle
* ---------------------------------------------------------------- */
return {
destroy() {
ro.disconnect()
node.removeEventListener('input', onInput)
node.removeEventListener('update', resize)
return {
destroy() {
removeEventListeners()
}
}
}
}
export default autosize
export default action

View File

@@ -13,8 +13,8 @@
const dispatch = createEventDispatcher()
let email: string | undefined = $state()
let username: string | undefined = $state()
let email: string
let username: string
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
@@ -24,7 +24,7 @@
}
}
let automateUsernameCreation = $state(false)
let automateUsernameCreation = false
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
((await SettingService.getGlobal({ key: 'automate_username_creation' })) as any) ?? false
@@ -35,17 +35,17 @@
await WorkspaceService.addUser({
workspace: $workspaceStore!,
requestBody: {
email: email!,
email,
username: automateUsernameCreation ? undefined : username,
is_admin: selected == 'admin',
operator: selected == 'operator'
}
})
sendUserToast(`Added ${email}`)
if (!(await UserService.existsEmail({ email: email! }))) {
if (!(await UserService.existsEmail({ email }))) {
let isSuperadmin = $superadmin
if (!isCloudHosted()) {
const emailCopy = email!
const emailCopy = email
sendUserToast(
`User ${email} is not registered yet on the instance. ${
!isSuperadmin
@@ -62,7 +62,7 @@
goto('#superadmin-settings')
}
}
]
]
: []
)
}
@@ -70,49 +70,47 @@
dispatch('new')
}
let selected: 'operator' | 'developer' | 'admin' = $state('developer')
let selected: 'operator' | 'developer' | 'admin' = 'developer'
</script>
<Popover placement="bottom-end">
{#snippet trigger()}
<svelte:fragment slot="trigger">
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: UserPlus }}>
Add new user
</Button>
{/snippet}
{#snippet content()}
</svelte:fragment>
<svelte:fragment slot="content">
<div class="flex flex-col w-72 p-4">
<span class="text-sm mb-2 leading-6 font-semibold">Add a new user</span>
<span class="text-xs mb-1 leading-6">Email</span>
<input type="email mb-1" onkeyup={handleKeyUp} placeholder="email" bind:value={email} />
<input type="email mb-1" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
{#if !automateUsernameCreation}
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
<input type="text" onkeyup={handleKeyUp} placeholder="username" bind:value={username} />
<input type="text" on:keyup={handleKeyUp} placeholder="username" bind:value={username} />
{/if}
<span class="text-xs mb-1 pt-2 leading-6">Role</span>
<ToggleButtonGroup bind:selected class="mb-4">
{#snippet children({ item })}
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
{/snippet}
<ToggleButtonGroup bind:selected class="mb-4" let:item>
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
</ToggleButtonGroup>
<Button
variant="contained"
@@ -131,5 +129,5 @@
Add
</Button>
</div>
{/snippet}
</svelte:fragment>
</Popover>

View File

@@ -6,11 +6,7 @@
import type { DurationStatus } from './graph'
import type { Writable } from 'svelte/store'
interface Props {
states: Writable<Record<string, DurationStatus>> | undefined
}
let { states }: Props = $props()
export let states: Writable<Record<string, DurationStatus>> | undefined
</script>
<div class="flex flex-col">

View File

@@ -23,43 +23,31 @@
import Popover from './meltComponents/Popover.svelte'
import Button from './common/button/Button.svelte'
import { Loader2, Github, RotateCw, Plus, Minus, Download } from 'lucide-svelte'
import { onDestroy, untrack } from 'svelte'
import { onDestroy } from 'svelte'
interface Props {
resourceType: string
resourceTypeInfo: ResourceType | undefined
args?: Record<string, any> | any
linkedSecret?: string | undefined
isValid?: boolean
linkedSecretCandidates?: string[] | undefined
description?: string | undefined
}
export let resourceType: string
export let resourceTypeInfo: ResourceType | undefined
export let args: Record<string, any> | any = {}
export let linkedSecret: string | undefined = undefined
export let isValid = true
export let linkedSecretCandidates: string[] | undefined = undefined
export let description: string | undefined = undefined
let {
resourceType,
resourceTypeInfo,
args = $bindable({}),
linkedSecret = $bindable(undefined),
isValid = $bindable(true),
linkedSecretCandidates = undefined,
description = $bindable(undefined)
}: Props = $props()
let schema = emptySchema()
let notFound = false
let schema = $state(emptySchema())
let notFound = $state(false)
let supabaseWizard = false
let supabaseWizard = $state(false)
let loadingGithubInstallations = $state(false)
let githubInstallations: GetGlobalConnectedRepositoriesResponse = $state([])
let workspaceGithubInstallations: GetGlobalConnectedRepositoriesResponse = $state([])
let selectedGHAppAccountId: string | undefined = $state(undefined)
let selectedGHAppRepository: string | undefined = $state(undefined)
let githubInstallationUrl: string | undefined = $state(undefined)
let loadingGithubInstallations = false
let githubInstallations: GetGlobalConnectedRepositoriesResponse = []
let workspaceGithubInstallations: GetGlobalConnectedRepositoriesResponse = []
let selectedGHAppAccountId: string | undefined = undefined
let selectedGHAppRepository: string | undefined = undefined
let githubInstallationUrl: string | undefined = undefined
let installationCheckInterval: number | undefined = undefined
let isCheckingInstallation = $state(false)
let importJwt = $state('')
let githubAppPopover: { open: () => void; close: () => void } | null = $state(null)
let isCheckingInstallation = false
let importJwt = ''
let githubAppPopover: { open: () => void; close: () => void } | null = null
async function loadGithubInstallations() {
if (!$enterpriseLicense) return
@@ -171,6 +159,9 @@
notFound = true
}
}
$: $workspaceStore && loadSchema()
$: notFound && rawCode && parseJson()
function parseJson() {
try {
@@ -182,9 +173,13 @@
error = e.message
}
}
let error = $state('')
let rawCode = $state('')
let viewJsonSchema = $state(false)
let error = ''
let rawCode = ''
let viewJsonSchema = false
$: rawCode && parseJson()
$: textFileContent && parseTextFileContent()
function switchTab(asJson: boolean) {
viewJsonSchema = asJson
@@ -198,8 +193,11 @@
}
}
let connectionString = $state('')
let validConnectionString = $state(true)
$: resourceType == 'postgresql' && isSupabaseAvailable()
$: resourceType == 'git_repository' && $userStore?.is_admin && loadGithubInstallations()
let connectionString = ''
let validConnectionString = true
function parseConnectionString(close: (_: any) => void) {
const regex =
/postgres(?:ql)?:\/\/(?<user>[^:@]+)(?::(?<password>[^@]+))?@(?<host>[^:\/?]+)(?::(?<port>\d+))?\/(?<dbname>[^\?]+)?(?:\?.*sslmode=(?<sslmode>[^&]+))?/
@@ -243,8 +241,8 @@
close(null)
}
let rawCodeEditor: SimpleEditor | undefined = $state(undefined)
let textFileContent: string | undefined = $state(undefined)
let rawCodeEditor: SimpleEditor | undefined = undefined
let textFileContent: string
function parseTextFileContent() {
args = {
@@ -252,6 +250,13 @@
}
}
$: githubInstallationsNotInWorkspace = githubInstallations.filter((installation) => {
return !workspaceGithubInstallations.some(
(workspaceInstallation) =>
workspaceInstallation.installation_id === installation.installation_id
)
})
async function deleteInstallation(installation_id: number) {
if (!$workspaceStore) {
sendUserToast('Failed to delete installation', true)
@@ -324,34 +329,6 @@
}
}
}
$effect(() => {
$workspaceStore && untrack(() => loadSchema())
})
$effect(() => {
notFound && rawCode && untrack(() => parseJson())
})
$effect(() => {
rawCode && untrack(() => parseJson())
})
$effect(() => {
textFileContent && untrack(() => parseTextFileContent())
})
$effect(() => {
resourceType == 'postgresql' && untrack(() => isSupabaseAvailable())
})
$effect(() => {
resourceType == 'git_repository' &&
$userStore?.is_admin &&
untrack(() => loadGithubInstallations())
})
let githubInstallationsNotInWorkspace = $derived(
githubInstallations.filter((installation) => {
return !workspaceGithubInstallations.some(
(workspaceInstallation) =>
workspaceInstallation.installation_id === installation.installation_id
)
})
)
</script>
{#if !notFound}
@@ -369,7 +346,7 @@
placement: 'bottom'
}}
>
{#snippet trigger()}
<svelte:fragment slot="trigger">
<Button
spacingSize="sm"
size="xs"
@@ -380,8 +357,8 @@
>
From connection string
</Button>
{/snippet}
{#snippet content({ close })}
</svelte:fragment>
<svelte:fragment slot="content" let:close>
<div class="block text-primary p-4">
<div class="w-[550px] flex flex-col items-start gap-1">
<div class="flex flex-row gap-1 w-full">
@@ -407,7 +384,7 @@
{/if}
</div>
</div>
{/snippet}
</svelte:fragment>
</Popover>
{/if}
{#if resourceType == 'postgresql' && supabaseWizard}
@@ -442,7 +419,7 @@
}}
disabled={!$enterpriseLicense || loadingGithubInstallations}
>
{#snippet trigger()}
<svelte:fragment slot="trigger">
<Button
color="none"
variant="border"
@@ -456,8 +433,8 @@
>
{$enterpriseLicense ? 'GitHub App' : 'GitHub App (ee only)'}
</Button>
{/snippet}
{#snippet content({ close })}
</svelte:fragment>
<svelte:fragment slot="content" let:close>
<div class="block text-primary p-4">
<div class="flex flex-col gap-4 w-[600px]">
{#if workspaceGithubInstallations.length > 0}
@@ -677,7 +654,7 @@
</div>
</div>
</div>
{/snippet}
</svelte:fragment>
</Popover>
{:else}
<Button

View File

@@ -61,31 +61,29 @@
on:refresh
express={expressOAuthSetup}
/>
{#snippet actions()}
<div class="flex gap-1">
{#if step > 1}
<Button variant="border" on:click={appConnectInner?.back ?? (() => {})}>Back</Button>
{/if}
{#if isGoogleSignin}
<button {disabled} on:click={appConnectInner?.next}>
<img
class="h-10 w-auto object-contain"
src={darkMode ? '/google_signin_dark.png' : '/google_signin_light.png'}
alt="Google sign-in"
/>
</button>
{:else}
<Button {disabled} on:click={appConnectInner?.next ?? (() => {})}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}
Next
{:else}
Save
{/if}
</Button>
{/if}
</div>
{/snippet}
<div slot="actions" class="flex gap-1">
{#if step > 1}
<Button variant="border" on:click={appConnectInner?.back}>Back</Button>
{/if}
{#if isGoogleSignin}
<button {disabled} on:click={appConnectInner?.next}>
<img
class="h-10 w-auto object-contain"
src={darkMode ? '/google_signin_dark.png' : '/google_signin_light.png'}
alt="Google sign-in"
/>
</button>
{:else}
<Button {disabled} on:click={appConnectInner?.next}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}
Next
{:else}
Save
{/if}
</Button>
{/if}
</div>
</DrawerContent>
</Drawer>

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