Compare commits
4 Commits
v1.497.0
...
inkeep-end
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
373864e586 | ||
|
|
25c8e6e090 | ||
|
|
9246512561 | ||
|
|
40712ecf7b |
2
.github/workflows/build_windows_worker_.yml
vendored
2
.github/workflows/build_windows_worker_.yml
vendored
@@ -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"
|
||||
|
||||
4
.github/workflows/create-docs.yml
vendored
4
.github/workflows/create-docs.yml
vendored
@@ -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")
|
||||
|
||||
3
.github/workflows/discord-notification.yml
vendored
3
.github/workflows/discord-notification.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/helmchart_on_release.yml
vendored
4
.github/workflows/helmchart_on_release.yml
vendored
@@ -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 }}" \
|
||||
|
||||
2
.github/workflows/publish_windows_worker.yml
vendored
2
.github/workflows/publish_windows_worker.yml
vendored
@@ -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"
|
||||
|
||||
19
.github/workflows/rust_on_release.yml
vendored
19
.github/workflows/rust_on_release.yml
vendored
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
83
CHANGELOG.md
83
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -61,8 +61,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
15
backend/.sqlx/query-1d87f41fd1abb9361d795a899120e6b77e24bf5a9044fdc5284d0d7f1e14eafa.json
generated
Normal file
15
backend/.sqlx/query-1d87f41fd1abb9361d795a899120e6b77e24bf5a9044fdc5284d0d7f1e14eafa.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
22
backend/.sqlx/query-2e9b3e718440f3c5269e9217a13076c565f3add98b6768b5476bd3afed11ea31.json
generated
Normal file
22
backend/.sqlx/query-2e9b3e718440f3c5269e9217a13076c565f3add98b6768b5476bd3afed11ea31.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -70,8 +70,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,8 +137,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
15
backend/.sqlx/query-553108ba3c0b8d579800bc8b5a4f887d79fb4c13b60b19c4913a8db18521958c.json
generated
Normal file
15
backend/.sqlx/query-553108ba3c0b8d579800bc8b5a4f887d79fb4c13b60b19c4913a8db18521958c.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -61,8 +61,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
22
backend/.sqlx/query-621e9a2a53187dac3ebed62f0d645b692815f1594bf302dbebd5f80d5d22b98e.json
generated
Normal file
22
backend/.sqlx/query-621e9a2a53187dac3ebed62f0d645b692815f1594bf302dbebd5f80d5d22b98e.json
generated
Normal 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"
|
||||
}
|
||||
@@ -147,8 +147,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -36,8 +36,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
15
backend/.sqlx/query-c3025cdb6e421e1225d420e8b1efd18d1dd3bb2fac53c1f2df648b61fb7488aa.json
generated
Normal file
15
backend/.sqlx/query-c3025cdb6e421e1225d420e8b1efd18d1dd3bb2fac53c1f2df648b61fb7488aa.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -70,8 +70,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
38
backend/.sqlx/query-cce991f582bc9d2ba28a5b2b41c679366bb07bc6a100727721a787160ac6910c.json
generated
Normal file
38
backend/.sqlx/query-cce991f582bc9d2ba28a5b2b41c679366bb07bc6a100727721a787160ac6910c.json
generated
Normal 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"
|
||||
}
|
||||
@@ -42,8 +42,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
15
backend/.sqlx/query-e38240e6d50bfe60e1c2b649588eb41dcef121ed161db04b2568ac2d990aed7c.json
generated
Normal file
15
backend/.sqlx/query-e38240e6d50bfe60e1c2b649588eb41dcef121ed161db04b2568ac2d990aed7c.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -147,8 +147,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -65,8 +65,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
3
backend/.vscode/settings.json
vendored
3
backend/.vscode/settings.json
vendored
@@ -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
1073
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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" }
|
||||
|
||||
@@ -1 +1 @@
|
||||
2c3e21f4573486628e0b8969ff478c237bd0283f
|
||||
70895a4a8f8891032c5b478a37ab6fafd0d4a9d0
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
>(
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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("ed_publication_name);
|
||||
|
||||
pg_connection
|
||||
.execute(&query, &[])
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
sqlx::query(&query).execute(pg_connection).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(", "))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -63,7 +63,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.497.0";
|
||||
export const VERSION = "1.494.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
22
flake.nix
22
flake.nix
@@ -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
0
frontend/npm
Normal file
235
frontend/package-lock.json
generated
235
frontend/package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user