Compare commits
4 Commits
v1.498.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
|
||||
|
||||
106
CHANGELOG.md
106
CHANGELOG.md
@@ -1,111 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.498.0](https://github.com/windmill-labs/windmill/compare/v1.497.2...v1.498.0) (2025-06-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* use provider api to list available AI models in workspace settings ([#5947](https://github.com/windmill-labs/windmill/issues/5947)) ([7490e88](https://github.com/windmill-labs/windmill/commit/7490e883d747a7f65b2fefd3ec14b1cfc3d9bbd4))
|
||||
* windmill http triggers and webhooks to openapi spec ([#5918](https://github.com/windmill-labs/windmill/issues/5918)) ([aba8c01](https://github.com/windmill-labs/windmill/commit/aba8c01d7f44ba4be369a3c711be9e156d6bf215))
|
||||
|
||||
## [1.497.2](https://github.com/windmill-labs/windmill/compare/v1.497.1...v1.497.2) (2025-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* always rm containers in docker mode ([38eb71b](https://github.com/windmill-labs/windmill/commit/38eb71bdf55ee2f606d1d2ad2e987d5af16d88c0))
|
||||
* flow steps use their tags if any specific when used as subflow ([26bec05](https://github.com/windmill-labs/windmill/commit/26bec054a3447a91c5d5f56d8b98717c06496087))
|
||||
|
||||
## [1.497.1](https://github.com/windmill-labs/windmill/compare/v1.497.0...v1.497.1) (2025-06-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix mcp server initialization ([1c6a7c8](https://github.com/windmill-labs/windmill/commit/1c6a7c8cd0bd8396f158e3cb0583b927ce957f12))
|
||||
|
||||
## [1.497.0](https://github.com/windmill-labs/windmill/compare/v1.496.3...v1.497.0) (2025-06-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api tools to ai chat ([#5921](https://github.com/windmill-labs/windmill/issues/5921)) ([f7a83c0](https://github.com/windmill-labs/windmill/commit/f7a83c03c12b8ae70179fb228e0e2391b6ea2858))
|
||||
* **backend:** use streamable http in favor of sse for MCP ([#5910](https://github.com/windmill-labs/windmill/issues/5910)) ([d47c078](https://github.com/windmill-labs/windmill/commit/d47c078bb5ab86d82d9cbbce3c55c89c0c20d809))
|
||||
* better graph layout algorithm + migrate to svelte 5 almost everywhere + xyflow 1.0 ([23920ae](https://github.com/windmill-labs/windmill/commit/23920aee84fdca4a557a34ff2d66a0bb7bdca605))
|
||||
* fill runnable inputs with AI chat ([#5887](https://github.com/windmill-labs/windmill/issues/5887)) ([b4a6a7e](https://github.com/windmill-labs/windmill/commit/b4a6a7e72429617d420af85a9de35bb13adfc6fb))
|
||||
* **go:** local go.mod ([#5929](https://github.com/windmill-labs/windmill/issues/5929)) ([0b89260](https://github.com/windmill-labs/windmill/commit/0b89260540b307c6d614ca4275dd038fbfdac33c))
|
||||
* multiple azure models support ([#5920](https://github.com/windmill-labs/windmill/issues/5920)) ([f412ede](https://github.com/windmill-labs/windmill/commit/f412ede6ed48e9a492f39582ac70a5584477529e))
|
||||
* **rust:** add rust sdk ([#5909](https://github.com/windmill-labs/windmill/issues/5909)) ([332f66e](https://github.com/windmill-labs/windmill/commit/332f66e3483abbeacd4e7c1b74c94c5265314882))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ai chat tooltip + user settings autocomplete issue ([#5917](https://github.com/windmill-labs/windmill/issues/5917)) ([6f907c7](https://github.com/windmill-labs/windmill/commit/6f907c79b4cf6279bd52e35a3ee96e0d021422f5))
|
||||
* audit logs for token refresh + consider refresh for active users ([#5930](https://github.com/windmill-labs/windmill/issues/5930)) ([cf2d09e](https://github.com/windmill-labs/windmill/commit/cf2d09e7a8c5d2472af0d483689c3fcfa2976117))
|
||||
* fix input with wrong height on first render ([#5935](https://github.com/windmill-labs/windmill/issues/5935)) ([1a6283b](https://github.com/windmill-labs/windmill/commit/1a6283b42a6a514ab2e05160855cdc0f70b61d0e))
|
||||
* flow step missing input warnings ([#5916](https://github.com/windmill-labs/windmill/issues/5916)) ([f077849](https://github.com/windmill-labs/windmill/commit/f077849b8f7c1916fd420e85b4844a5c5e93a139))
|
||||
* **frontend:** use correct kind for flow insert module btn ([#5938](https://github.com/windmill-labs/windmill/issues/5938)) ([17c8c8a](https://github.com/windmill-labs/windmill/commit/17c8c8a5616ab8656799cea3fc5bc7cfaedc4995))
|
||||
|
||||
## [1.496.3](https://github.com/windmill-labs/windmill/compare/v1.496.2...v1.496.3) (2025-06-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve concurrent job parallelism performance ([e8836a3](https://github.com/windmill-labs/windmill/commit/e8836a393a872bb91e68ba0037681caf24149470))
|
||||
* Prioritize diff contexts in script mode for ai chat ([#5888](https://github.com/windmill-labs/windmill/issues/5888)) ([a47939d](https://github.com/windmill-labs/windmill/commit/a47939d13c30e2d4b41efd539f845959174d4fb1))
|
||||
|
||||
## [1.496.2](https://github.com/windmill-labs/windmill/compare/v1.496.1...v1.496.2) (2025-06-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add clearable by default for select ([#5900](https://github.com/windmill-labs/windmill/issues/5900)) ([b44b9c1](https://github.com/windmill-labs/windmill/commit/b44b9c1b82116ad5487af95d1f78226d56c75179))
|
||||
|
||||
## [1.496.1](https://github.com/windmill-labs/windmill/compare/v1.496.0...v1.496.1) (2025-06-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* never consider minor version for global site packages ([#5893](https://github.com/windmill-labs/windmill/issues/5893)) ([22b2f49](https://github.com/windmill-labs/windmill/commit/22b2f4988db9314f2403508933d0aa932187c668))
|
||||
|
||||
## [1.496.0](https://github.com/windmill-labs/windmill/compare/v1.495.1...v1.496.0) (2025-06-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* generate http route triggers from openapi spec ([#5857](https://github.com/windmill-labs/windmill/issues/5857)) ([5713483](https://github.com/windmill-labs/windmill/commit/571348377b73d54b4d2a1c5775ab00b247b01910))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow fileupload drag and drop in edit mode on full component without triggering file picker ([#5889](https://github.com/windmill-labs/windmill/issues/5889)) ([9ae3212](https://github.com/windmill-labs/windmill/commit/9ae3212a1e0f88a8297bf41ab53e3c1be4bcc56c))
|
||||
* **python:** account instance version when cli deploy and local lockfile ([#5894](https://github.com/windmill-labs/windmill/issues/5894)) ([ec552d5](https://github.com/windmill-labs/windmill/commit/ec552d5ef6fdb5e824e453f196f9cf16629ee2ea))
|
||||
* use full client side js library for route gen from openapi ([#5891](https://github.com/windmill-labs/windmill/issues/5891)) ([3c3fdbd](https://github.com/windmill-labs/windmill/commit/3c3fdbdf26a9581b815210839b91ebdedb924093))
|
||||
|
||||
## [1.495.0](https://github.com/windmill-labs/windmill/compare/v1.494.0...v1.495.0) (2025-06-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add ask mode to AI chat ([#5878](https://github.com/windmill-labs/windmill/issues/5878)) ([67ab469](https://github.com/windmill-labs/windmill/commit/67ab46990ad0c9fad810a64c54297419c6151c79))
|
||||
* add navigator mode to AIChat and unify UI ([#5859](https://github.com/windmill-labs/windmill/issues/5859)) ([cbba829](https://github.com/windmill-labs/windmill/commit/cbba8297cd4c1caa21b96a8422bbbd5c306b8398))
|
||||
* ai flow chat ([#5842](https://github.com/windmill-labs/windmill/issues/5842)) ([68ebf66](https://github.com/windmill-labs/windmill/commit/68ebf667d5c0bc306329d0b55a3cc59e5b4862cb))
|
||||
* ai prompts improvements + o3/o4 support ([#5862](https://github.com/windmill-labs/windmill/issues/5862)) ([825422c](https://github.com/windmill-labs/windmill/commit/825422c48456b2c9b230e1a35914b3fbf7d1e836))
|
||||
* connect fix btn in flow editor to ai chat ([#5863](https://github.com/windmill-labs/windmill/issues/5863)) ([6247d15](https://github.com/windmill-labs/windmill/commit/6247d159ce25ae13f6fbc5c105df88305ce29451))
|
||||
* fix backward compatibility pg 14 for postgres trigger ([#5851](https://github.com/windmill-labs/windmill/issues/5851)) ([4cbcbdb](https://github.com/windmill-labs/windmill/commit/4cbcbdb960b469acf773d3943128b6c7d0dcb0b8))
|
||||
* ssh repl like direct to workers hosts machine ([#5809](https://github.com/windmill-labs/windmill/issues/5809)) ([f252657](https://github.com/windmill-labs/windmill/commit/f2526571a3614156b2b1e5cc91b15d0c57565d99))
|
||||
* use rust-postgres client instead of sqlx for postgres trigger ([#5853](https://github.com/windmill-labs/windmill/issues/5853)) ([39dbd64](https://github.com/windmill-labs/windmill/commit/39dbd646b9683e0ad8de047cca786ae468759e77))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* broken event dispatch for simpleditor ([#5879](https://github.com/windmill-labs/windmill/issues/5879)) ([df4992a](https://github.com/windmill-labs/windmill/commit/df4992a9295ed188c2a2cb0a5dfd3e33ae2e2dcb))
|
||||
* cannot parse INSTANCE_PYTHON_VERSION ([#5874](https://github.com/windmill-labs/windmill/issues/5874)) ([a0b302d](https://github.com/windmill-labs/windmill/commit/a0b302d2c58d4245260376cf280bc866be91717c))
|
||||
* fix regex that extract workspaces from custom tags ([#5876](https://github.com/windmill-labs/windmill/issues/5876)) ([1551dc8](https://github.com/windmill-labs/windmill/commit/1551dc8af22f6ea41f68290ace4c58f936c47745))
|
||||
* nit ai flow prompt ([#5867](https://github.com/windmill-labs/windmill/issues/5867)) ([3e769f0](https://github.com/windmill-labs/windmill/commit/3e769f0c591b80138b3a356d147228675756452f))
|
||||
* **python:** assign PATCH version to python runtime only when needed ([#5866](https://github.com/windmill-labs/windmill/issues/5866)) ([50a5c1f](https://github.com/windmill-labs/windmill/commit/50a5c1f56a7e45882fa0095203de709571e149bb))
|
||||
* remove duplicate tools from script ai chat ([#5880](https://github.com/windmill-labs/windmill/issues/5880)) ([fe4a767](https://github.com/windmill-labs/windmill/commit/fe4a767df0e6f46fd0c0fd21b4116c7375978bf9))
|
||||
* replace crypto.randomUUID with generateRandomString for HTTP compatibility ([#5849](https://github.com/windmill-labs/windmill/issues/5849)) ([64f35d0](https://github.com/windmill-labs/windmill/commit/64f35d050fb0d1008ce7142fd62d500845e62c4a)), closes [#5847](https://github.com/windmill-labs/windmill/issues/5847)
|
||||
|
||||
## [1.494.0](https://github.com/windmill-labs/windmill/compare/v1.493.4...v1.494.0) (2025-05-31)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
To have an overview of what this app does, see @.cursor/rules/windmill-overview.mdc
|
||||
For backend modifications, follow the rules mentioned here @.cursor/rules/rust-best-practices.mdc. You also have access to a summarized version of the database schema here @backend/summarized_schema.txt
|
||||
For backend modifications, follow the rules mentioned here @.cursor/rules/rust-best-practices.mdc
|
||||
For frontend modifications, follow the rules mentioned here @.cursor/rules/svelte5-best-practices.mdc
|
||||
|
||||
@@ -367,11 +367,10 @@ you to have it being synced automatically everyday.
|
||||
|
||||
## Run a local dev setup
|
||||
|
||||
Using [Nix](./frontend/README_DEV.md#nix) (Recommended).
|
||||
|
||||
See the [./frontend/README_DEV.md](./frontend/README_DEV.md) file for all
|
||||
running options.
|
||||
|
||||
Using [Nix](./frontend/README_DEV.md#nix).
|
||||
|
||||
### only Frontend
|
||||
|
||||
|
||||
@@ -61,8 +61,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0de53d764bbcf44d76cd8e47f7b2cd49e4632f03f16ee6f34d9a2c0842890f05"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n summary,\n description,\n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "route_path_key",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "workspaced_route",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "wrap_body",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "raw_string",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "39401cb0db8d367b5beb2be0c13aa7595adae0eac4e4e3a888cb12b972d1a7ce"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3aaa6b6e362b10f2f3546b8595c60ab725295e0f20cb5f85d5fddc14d503a119"
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n summary = $16,\n description = $17,\n edited_at = now(), \n is_static_website = $18\n WHERE \n workspace_id = $19 AND \n path = $20\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3f05e6186050a7ce6d8efb41067d3c5282319fe7e041f114e02fb22b91716637"
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspaced_route",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "route_path_key",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "wrap_body",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "raw_string",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4228b098883408323bd8413ee094454b95962047458a6927d19ac0d3e7b3f0fa"
|
||||
}
|
||||
@@ -70,8 +70,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,8 +137,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path,\n summary,\n description\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "summary",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4dc21dda58758a84bbc1b7f9328efb9806223d1684c53907ff825bc0228ec18d"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "workspaced_route",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6b6aca712df657f81e74a438f6753bc72e787a3f768040c8c73ea86313badb71"
|
||||
}
|
||||
@@ -147,8 +147,7 @@
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"java"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route,\n summary,\n description,\n authentication_method AS \"authentication_method: _\",\n authentication_resource_path\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "workspaced_route",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "714fb0f66ceb536aee8cb9ae0144757b999d25870fda37fe904e09dd5c742015"
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path,\n summary,\n description\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "summary",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d41df114e78873111e409e1182bccc0024acf17ce740f70d2243e5e2a2b25163"
|
||||
}
|
||||
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,59 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n summary,\n description,\n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ed99d4d088d0fd0c01f29803b12e99ae0a53d0b1feaa67737da409c51c1b6751"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
1106
backend/Cargo.lock
generated
1106
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.498.0"
|
||||
version = "1.494.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -32,7 +32,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.498.0"
|
||||
version = "1.494.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -96,9 +96,7 @@ csharp = ["windmill-worker/csharp"]
|
||||
nu = ["windmill-worker/nu"]
|
||||
java = ["windmill-worker/java"]
|
||||
all_languages = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
|
||||
# For windows we have another set of languages enabled
|
||||
# NOTE: DuckDB is ignored because of compilation problems
|
||||
all_languages_windows = ["python", "deno_core", "rust", "mysql", "oracledb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7" }
|
||||
@@ -130,7 +128,6 @@ prometheus = { workspace = true, optional = true }
|
||||
uuid.workspace = true
|
||||
gethostname.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yml.workspace = true
|
||||
serde.workspace = true
|
||||
deno_core = { workspace = true, optional = true }
|
||||
object_store = { workspace = true, optional = true }
|
||||
@@ -202,7 +199,6 @@ tower-http = { version = "^0.6", features = ["trace", "cors"] }
|
||||
tower-cookies = "^0.10"
|
||||
serde = "^1"
|
||||
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
|
||||
serde_yml = "0.0.12"
|
||||
uuid = { version = "^1", features = ["serde", "v4"] }
|
||||
thiserror = "^2"
|
||||
anyhow = "^1"
|
||||
@@ -232,7 +228,7 @@ php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb
|
||||
cron = "^0"
|
||||
mail-send = { version = "0.4.0", features = ["builder"], default-features=false }
|
||||
urlencoding = "^2"
|
||||
url = { version = "^2" , features = ["serde"]}
|
||||
url = "^2"
|
||||
async-oauth2 = "^0"
|
||||
reqwest = { version = "^0.12", features = ["json", "stream", "gzip"] }
|
||||
time = "^0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
67e727c618cf673850a0887931c803241abfcfe8
|
||||
70895a4a8f8891032c5b478a37ab6fafd0d4a9d0
|
||||
|
||||
@@ -4,4 +4,4 @@ DROP TYPE http_method;
|
||||
|
||||
ALTER TABLE script DROP COLUMN has_preprocessor;
|
||||
|
||||
DROP FUNCTION prevent_route_path_change();
|
||||
DROP FUNCTION prevent_route_path_change();
|
||||
@@ -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();
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE http_trigger
|
||||
DROP COLUMN summary,
|
||||
DROP COLUMN description;
|
||||
@@ -1,5 +0,0 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE http_trigger
|
||||
ADD COLUMN summary VARCHAR(512) NULL,
|
||||
ADD COLUMN description TEXT NULL;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# This script is used to summarize the database schema.
|
||||
# You can use pg_dump to dump the schema to a file.
|
||||
# pg_dump --file "schema.sql" --host "localhost" --port "5432" --username "postgres" --no-password --format=c --large-objects --schema-only --no-owner --no-privileges --no-tablespaces --no-unlogged-table-data --no-comments --no-publications --no-subscriptions --no-security-labels --no-toast-compression --no-table-access-method --verbose --schema "public" "windmill"
|
||||
# Then you can run python summarize_schema.py schema.sql to get the summarized schema.
|
||||
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
def summarize_schema(file_path):
|
||||
"""
|
||||
Parses a PostgreSQL dump file and extracts a summarized schema.
|
||||
"""
|
||||
tables = defaultdict(lambda: {'columns': [], 'pks': set(), 'fks': [], 'indexes': []})
|
||||
enums = defaultdict(list)
|
||||
|
||||
# Use state variables to parse multi-line definitions
|
||||
current_table = None
|
||||
current_enum = None
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
|
||||
# --- State Resets ---
|
||||
if line.startswith(');'):
|
||||
current_table = None
|
||||
current_enum = None
|
||||
continue
|
||||
|
||||
# --- Parse ENUM definitions ---
|
||||
match_enum = re.match(r"CREATE TYPE public\.(\w+) AS ENUM \($", line)
|
||||
if match_enum:
|
||||
current_enum = match_enum.group(1)
|
||||
continue
|
||||
|
||||
if current_enum:
|
||||
# Extract enum values, which are typically like 'value',
|
||||
value = line.strip("',")
|
||||
if value and not value.startswith('--'):
|
||||
enums[current_enum].append(value)
|
||||
continue
|
||||
|
||||
# --- Parse TABLE definitions ---
|
||||
match_table = re.match(r"CREATE TABLE public\.(\w+) \($", line)
|
||||
if match_table:
|
||||
current_table = match_table.group(1)
|
||||
continue
|
||||
|
||||
if current_table:
|
||||
# Parse columns within a CREATE TABLE block
|
||||
# e.g., "column_name type NOT NULL,"
|
||||
# e.g., "id bigint NOT NULL,"
|
||||
match_column = re.match(r'^"?(\w+)"?\s+([\w\d\.\[\]\(\)]+)', line)
|
||||
if match_column:
|
||||
col_name = match_column.group(1)
|
||||
col_type = match_column.group(2)
|
||||
tables[current_table]['columns'].append(f"{col_name} ({col_type})")
|
||||
|
||||
# Parse PRIMARY KEY defined inside the table
|
||||
match_pk = re.search(r"CONSTRAINT \w+ PRIMARY KEY \((.+)\)", line)
|
||||
if match_pk:
|
||||
# Handle multiple PK columns: "col1, col2, col3"
|
||||
pk_cols = [p.strip().strip('"') for p in match_pk.group(1).split(',')]
|
||||
tables[current_table]['pks'].update(pk_cols)
|
||||
continue
|
||||
|
||||
# --- Parse Foreign Keys (defined outside CREATE TABLE) ---
|
||||
match_fk = re.match(r"ALTER TABLE ONLY public\.(\w+)\s+ADD CONSTRAINT \w+ FOREIGN KEY \(([\w,\s\"]+)\) REFERENCES public\.(\w+)\(([\w,\s\"]+)\);", line)
|
||||
if match_fk:
|
||||
from_table, from_cols, to_table, to_cols = match_fk.groups()
|
||||
# Clean up column names
|
||||
from_cols_clean = ', '.join([c.strip().strip('"') for c in from_cols.split(',')])
|
||||
to_cols_clean = ', '.join([c.strip().strip('"') for c in to_cols.split(',')])
|
||||
|
||||
fk_string = f"({from_cols_clean}) -> {to_table}({to_cols_clean})"
|
||||
tables[from_table]['fks'].append(fk_string)
|
||||
|
||||
# --- Parse Index definitions ---
|
||||
match_index = re.match(r"CREATE (UNIQUE )?INDEX (\w+) ON public\.(\w+) USING (\w+) \((.+)\);", line)
|
||||
if match_index:
|
||||
is_unique = match_index.group(1) is not None
|
||||
index_name = match_index.group(2)
|
||||
table_name = match_index.group(3)
|
||||
index_type = match_index.group(4)
|
||||
columns = match_index.group(5)
|
||||
|
||||
# Clean up column expressions
|
||||
columns_clean = columns.replace('"', '')
|
||||
|
||||
unique_str = "UNIQUE " if is_unique else ""
|
||||
index_string = f"{unique_str}INDEX {index_name} ({index_type}) ON ({columns_clean})"
|
||||
tables[table_name]['indexes'].append(index_string)
|
||||
|
||||
return enums, tables
|
||||
|
||||
def format_output(enums, tables):
|
||||
"""
|
||||
Formats the parsed schema data into a clean, readable string.
|
||||
"""
|
||||
output = []
|
||||
|
||||
output.append("### Simplified Database Schema ###")
|
||||
output.append("\n--- Custom Data Types (ENUMs) ---\n")
|
||||
if not enums:
|
||||
output.append("No custom ENUM types found.")
|
||||
else:
|
||||
for name, values in sorted(enums.items()):
|
||||
output.append(f"{name}:")
|
||||
for v in values:
|
||||
output.append(f" - {v}")
|
||||
output.append("")
|
||||
|
||||
output.append("\n--- Tables and Relationships ---\n")
|
||||
if not tables:
|
||||
output.append("No tables found.")
|
||||
else:
|
||||
for name, data in sorted(tables.items()):
|
||||
output.append(f"TABLE: {name}")
|
||||
for col in data['columns']:
|
||||
col_name = col.split(' ')[0]
|
||||
marker = " (PK)" if col_name in data['pks'] else ""
|
||||
output.append(f" - {col}{marker}")
|
||||
|
||||
if data['fks']:
|
||||
output.append(" Relationships:")
|
||||
for fk in data['fks']:
|
||||
output.append(f" - {fk}")
|
||||
|
||||
if data['indexes']:
|
||||
output.append(" Indexes:")
|
||||
for idx in data['indexes']:
|
||||
output.append(f" - {idx}")
|
||||
output.append("-" * 20)
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: python {sys.argv[0]} <path_to_dump.sql>")
|
||||
sys.exit(1)
|
||||
|
||||
input_file = sys.argv[1]
|
||||
|
||||
try:
|
||||
enums_data, tables_data = summarize_schema(input_file)
|
||||
formatted_summary = format_output(enums_data, tables_data)
|
||||
print(formatted_summary)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: The file '{input_file}' was not found.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
sys.exit(1)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@ agent_worker_server = []
|
||||
enterprise_saml = ["dep:samael", "dep:libxml"]
|
||||
benchmark = []
|
||||
embedding = ["dep:tinyvector", "dep:hf-hub", "dep:tokenizers", "dep:candle-core", "dep:candle-transformers", "dep:candle-nn"]
|
||||
parquet = ["dep:datafusion", "dep:object_store", "windmill-common/parquet", "windmill-worker/parquet"]
|
||||
parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/parquet", "windmill-worker/parquet"]
|
||||
prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus", "windmill-worker/prometheus"]
|
||||
openidconnect = ["dep:openidconnect", "windmill-common/openidconnect"]
|
||||
tantivy = ["dep:windmill-indexer"]
|
||||
@@ -40,7 +40,7 @@ mcp = ["dep:rmcp"]
|
||||
python = []
|
||||
|
||||
[dependencies]
|
||||
rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true }
|
||||
rmcp = { git = "https://github.com/windmill-labs/rust-sdk", features = ["transport-sse-server"], optional = true }
|
||||
windmill-queue.workspace = true
|
||||
windmill-common = { workspace = true, default-features = false }
|
||||
windmill-audit.workspace = true
|
||||
@@ -76,7 +76,6 @@ hex.workspace = true
|
||||
base64.workspace = true
|
||||
base32.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
serde_yml.workspace = true
|
||||
cron.workspace = true
|
||||
mime_guess.workspace = true
|
||||
rust-embed = { workspace = true, optional = true }
|
||||
@@ -103,7 +102,6 @@ prometheus = { workspace = true, optional = true }
|
||||
async_zip = { workspace = true, optional = true }
|
||||
regex.workspace = true
|
||||
bytes.workspace = true
|
||||
url.workspace = true
|
||||
samael = { workspace = true, optional = true }
|
||||
libxml = { workspace = true, optional = true }
|
||||
async-recursion.workspace = true
|
||||
@@ -118,6 +116,7 @@ candle-nn = { workspace = true, optional = true}
|
||||
datafusion = { workspace = true, optional = true}
|
||||
object_store = { workspace = true, optional = true}
|
||||
openidconnect = { workspace = true, optional = true}
|
||||
url = { workspace = true, optional = true}
|
||||
jsonwebtoken = { workspace = true }
|
||||
matchit = { workspace = true, optional = true }
|
||||
tokio-tungstenite = { workspace = true, optional = true}
|
||||
@@ -127,7 +126,6 @@ nkeys = { workspace = true, optional = true }
|
||||
const_format.workspace = true
|
||||
pin-project.workspace = true
|
||||
http.workspace = true
|
||||
indexmap.workspace = true
|
||||
async-stream.workspace = true
|
||||
ulid.workspace = true
|
||||
rust-postgres = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.498.0
|
||||
version: 1.494.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -4501,13 +4501,6 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: languages
|
||||
in: query
|
||||
description: |
|
||||
Filter to only include scripts written in the given languages.
|
||||
Accepts multiple values as a comma-separated list.
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: All scripts
|
||||
@@ -8451,76 +8444,6 @@ paths:
|
||||
"201":
|
||||
description: default error handler set
|
||||
|
||||
/w/{workspace}/openapi/generate:
|
||||
post:
|
||||
summary: generate openapi spec from http routes/webhook
|
||||
operationId: generateOpenapiSpec
|
||||
tags:
|
||||
- openapi
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: openapi spec info and url
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GenerateOpenapiSpec"
|
||||
responses:
|
||||
"200":
|
||||
description: openapi spec
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/openapi/download:
|
||||
post:
|
||||
summary: Download the OpenAPI v3.1 spec as a file
|
||||
operationId: DownloadOpenapiSpec
|
||||
tags:
|
||||
- openapi
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: openapi spec info and url
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GenerateOpenapiSpec"
|
||||
responses:
|
||||
"200":
|
||||
description: Downloaded OpenAPI spec
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
|
||||
/w/{workspace}/http_triggers/create_many:
|
||||
post:
|
||||
summary: create many HTTP triggers
|
||||
operationId: createHttpTriggers
|
||||
tags:
|
||||
- http_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: new http trigger
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/NewHttpTrigger"
|
||||
responses:
|
||||
"201":
|
||||
description: http trigger created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/http_triggers/create:
|
||||
post:
|
||||
summary: create http trigger
|
||||
@@ -8672,7 +8595,8 @@ paths:
|
||||
route_path:
|
||||
type: string
|
||||
http_method:
|
||||
$ref: "#/components/schemas/HttpMethod"
|
||||
type: string
|
||||
enum: ["get", "post", "put", "delete", "patch"]
|
||||
trigger_path:
|
||||
type: string
|
||||
workspaced_route:
|
||||
@@ -14799,115 +14723,6 @@ components:
|
||||
- custom_script
|
||||
- signature
|
||||
|
||||
RunnableKind:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
- flow
|
||||
|
||||
OpenapiSpecFormat:
|
||||
type: string
|
||||
enum:
|
||||
- yaml
|
||||
- json
|
||||
|
||||
OpenapiHttpRouteFilters:
|
||||
type: object
|
||||
properties:
|
||||
folder_regex:
|
||||
type: string
|
||||
path_regex:
|
||||
type: string
|
||||
route_path_regex:
|
||||
type: string
|
||||
required:
|
||||
- folder_regex
|
||||
- path_regex
|
||||
- route_path_regex
|
||||
|
||||
WebhookFilters:
|
||||
type: object
|
||||
properties:
|
||||
user_or_folder_regex:
|
||||
type: string
|
||||
enum:
|
||||
- "*"
|
||||
- u
|
||||
- f
|
||||
user_or_folder_regex_value:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
runnable_kind:
|
||||
$ref: "#/components/schemas/RunnableKind"
|
||||
required:
|
||||
- user_or_folder_regex
|
||||
- user_or_folder_regex_value
|
||||
- path
|
||||
- runnable_kind
|
||||
|
||||
OpenapiV3Info:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
terms_of_service:
|
||||
type: string
|
||||
contact:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
license:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
identifier:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
required:
|
||||
- title
|
||||
- version
|
||||
|
||||
GenerateOpenapiSpec:
|
||||
type: object
|
||||
properties:
|
||||
info:
|
||||
$ref: "#/components/schemas/OpenapiV3Info"
|
||||
url:
|
||||
type: string
|
||||
openapi_spec_format:
|
||||
$ref: "#/components/schemas/OpenapiSpecFormat"
|
||||
http_route_filters:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/OpenapiHttpRouteFilters"
|
||||
webhook_filters:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WebhookFilters"
|
||||
|
||||
HttpMethod:
|
||||
type: string
|
||||
enum:
|
||||
- get
|
||||
- post
|
||||
- put
|
||||
- delete
|
||||
- patch
|
||||
|
||||
HttpTrigger:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TriggerExtraProperty"
|
||||
@@ -14927,13 +14742,15 @@ components:
|
||||
required:
|
||||
- s3
|
||||
http_method:
|
||||
$ref: "#/components/schemas/HttpMethod"
|
||||
type: string
|
||||
enum:
|
||||
- get
|
||||
- post
|
||||
- put
|
||||
- delete
|
||||
- patch
|
||||
authentication_resource_path:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
is_async:
|
||||
type: boolean
|
||||
authentication_method:
|
||||
@@ -14968,10 +14785,6 @@ components:
|
||||
type: string
|
||||
workspaced_route:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
static_asset_config:
|
||||
type: object
|
||||
properties:
|
||||
@@ -14986,7 +14799,13 @@ components:
|
||||
is_flow:
|
||||
type: boolean
|
||||
http_method:
|
||||
$ref: "#/components/schemas/HttpMethod"
|
||||
type: string
|
||||
enum:
|
||||
- get
|
||||
- post
|
||||
- put
|
||||
- delete
|
||||
- patch
|
||||
authentication_resource_path:
|
||||
type: string
|
||||
is_async:
|
||||
@@ -15019,10 +14838,6 @@ components:
|
||||
type: string
|
||||
route_path:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
workspaced_route:
|
||||
type: boolean
|
||||
static_asset_config:
|
||||
@@ -15041,7 +14856,13 @@ components:
|
||||
is_flow:
|
||||
type: boolean
|
||||
http_method:
|
||||
$ref: "#/components/schemas/HttpMethod"
|
||||
type: string
|
||||
enum:
|
||||
- get
|
||||
- post
|
||||
- put
|
||||
- delete
|
||||
- patch
|
||||
is_async:
|
||||
type: boolean
|
||||
authentication_method:
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use axum::{body::Bytes, extract::Path, response::IntoResponse, routing::post, Extension, Router};
|
||||
use http::{HeaderMap, Method};
|
||||
use http::HeaderMap;
|
||||
use quick_cache::sync::Cache;
|
||||
use reqwest::{Client, RequestBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -24,7 +24,7 @@ lazy_static::lazy_static! {
|
||||
pub static ref AI_REQUEST_CACHE: Cache<(String, AIProvider), ExpiringAIRequestConfig> = Cache::new(500);
|
||||
}
|
||||
|
||||
const AZURE_API_VERSION: &str = "2025-04-01-preview";
|
||||
const AZURE_API_VERSION: &str = "2024-10-21";
|
||||
const OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -141,49 +141,23 @@ impl AIRequestConfig {
|
||||
self,
|
||||
provider: &AIProvider,
|
||||
path: &str,
|
||||
method: Method,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<RequestBuilder> {
|
||||
let url = format!("{}/{}", self.base_url, path);
|
||||
|
||||
let body = if let Some(user) = self.user {
|
||||
Self::add_user_to_body(body, user)?
|
||||
} else {
|
||||
body
|
||||
};
|
||||
|
||||
let base_url = self.base_url.trim_end_matches('/');
|
||||
|
||||
let is_azure = matches!(provider, AIProvider::OpenAI) && base_url != OPENAI_BASE_URL
|
||||
let is_azure = matches!(provider, AIProvider::OpenAI) && self.base_url != OPENAI_BASE_URL
|
||||
|| matches!(provider, AIProvider::AzureOpenAI);
|
||||
let is_anthropic = matches!(provider, AIProvider::Anthropic);
|
||||
|
||||
let url = if is_azure && method != Method::GET {
|
||||
if base_url.ends_with("/deployments") {
|
||||
let model = Self::get_azure_model(&body)?;
|
||||
format!("{}/{}/{}", base_url, model, path)
|
||||
} else if base_url.ends_with("/openai") {
|
||||
let model = Self::get_azure_model(&body)?;
|
||||
format!("{}/deployments/{}/{}", base_url, model, path)
|
||||
} else {
|
||||
format!("{}/{}", base_url, path)
|
||||
}
|
||||
} else {
|
||||
format!("{}/{}", base_url, path)
|
||||
};
|
||||
|
||||
tracing::debug!("AI request URL: {}", url);
|
||||
|
||||
let mut request = HTTP_CLIENT
|
||||
.request(method, url)
|
||||
.header("content-type", "application/json");
|
||||
|
||||
for (header_name, header_value) in headers.iter() {
|
||||
if header_name.to_string().starts_with("anthropic-") {
|
||||
request = request.header(header_name, header_value);
|
||||
}
|
||||
}
|
||||
|
||||
request = request.body(body);
|
||||
.post(url)
|
||||
.header("content-type", "application/json")
|
||||
.body(body);
|
||||
|
||||
if is_azure {
|
||||
request = request.query(&[("api-version", AZURE_API_VERSION)])
|
||||
@@ -191,12 +165,9 @@ impl AIRequestConfig {
|
||||
|
||||
if let Some(api_key) = self.api_key {
|
||||
if is_azure {
|
||||
request = request.header("api-key", api_key.clone())
|
||||
request = request.header("api-key", api_key)
|
||||
} else {
|
||||
request = request.header("authorization", format!("Bearer {}", api_key.clone()))
|
||||
}
|
||||
if is_anthropic {
|
||||
request = request.header("X-API-Key", api_key);
|
||||
request = request.header("authorization", format!("Bearer {}", api_key))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,18 +199,6 @@ impl AIRequestConfig {
|
||||
.map_err(|e| Error::internal_err(format!("Failed to reserialize request body: {}", e)))?
|
||||
.into())
|
||||
}
|
||||
|
||||
fn get_azure_model(body: &Bytes) -> Result<String> {
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct AzureModel {
|
||||
model: String,
|
||||
}
|
||||
|
||||
let azure_model: AzureModel = serde_json::from_slice(body)
|
||||
.map_err(|e| Error::internal_err(format!("Failed to parse request body: {}", e)))?;
|
||||
|
||||
Ok(azure_model.model)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -352,18 +311,17 @@ pub struct AIConfig {
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new().route("/proxy/*ai", post(global_proxy).get(global_proxy))
|
||||
Router::new().route("/proxy/*ai", post(global_proxy))
|
||||
}
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new().route("/proxy/*ai", post(proxy).get(proxy))
|
||||
Router::new().route("/proxy/*ai", post(proxy))
|
||||
}
|
||||
|
||||
async fn global_proxy(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(ai_path): Path<String>,
|
||||
method: Method,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> impl IntoResponse {
|
||||
@@ -388,7 +346,7 @@ async fn global_proxy(
|
||||
let url = format!("{}/{}", base_url, ai_path);
|
||||
|
||||
let request = HTTP_CLIENT
|
||||
.request(method, url)
|
||||
.post(url)
|
||||
.header("content-type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(body);
|
||||
@@ -424,7 +382,6 @@ async fn proxy(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, ai_path)): Path<(String, String)>,
|
||||
method: Method,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> impl IntoResponse {
|
||||
@@ -507,7 +464,7 @@ async fn proxy(
|
||||
}
|
||||
};
|
||||
|
||||
let request = request_config.prepare_request(&provider, &ai_path, method, headers, body)?;
|
||||
let request = request_config.prepare_request(&provider, &ai_path, body)?;
|
||||
|
||||
let response = request.send().await.map_err(to_anyhow)?;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
resources::get_resource_value_interpolated_internal,
|
||||
users::{require_owner_of_path, OptAuthed},
|
||||
utils::WithStarredInfoQuery,
|
||||
utils::{RunnableKind, WithStarredInfoQuery},
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
HTTP_CLIENT,
|
||||
};
|
||||
@@ -59,7 +59,7 @@ use windmill_common::{
|
||||
users::username_to_permissioned_as,
|
||||
utils::{
|
||||
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin,
|
||||
Pagination, RunnableKind, StripPath,
|
||||
Pagination, StripPath,
|
||||
},
|
||||
variables::{build_crypt, build_crypt_with_key_suffix, encrypt},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
|
||||
@@ -26,21 +26,6 @@ use windmill_common::{
|
||||
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// Global auth cache accessible from main.rs for direct invalidation
|
||||
pub static ref AUTH_CACHE: Cache<(String, String), ExpiringAuthCache> = Cache::new(300);
|
||||
}
|
||||
|
||||
// Global function to invalidate a specific token from cache
|
||||
pub fn invalidate_token_from_cache(token: &str) {
|
||||
// Remove all cache entries for this token (across all workspaces)
|
||||
AUTH_CACHE.retain(|(_workspace_id, cached_token), _cached_value| cached_token != token);
|
||||
tracing::info!(
|
||||
"Invalidated token from auth cache: {}...",
|
||||
&token[..token.len().min(8)]
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExpiringAuthCache {
|
||||
pub authed: ApiAuthed,
|
||||
@@ -48,6 +33,7 @@ pub struct ExpiringAuthCache {
|
||||
}
|
||||
|
||||
pub struct AuthCache {
|
||||
cache: Cache<(String, String), ExpiringAuthCache>,
|
||||
db: DB,
|
||||
superadmin_secret: Option<String>,
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -61,6 +47,7 @@ impl AuthCache {
|
||||
#[cfg(feature = "enterprise")] ext_jwks: Option<Arc<RwLock<ExternalJwks>>>,
|
||||
) -> Self {
|
||||
AuthCache {
|
||||
cache: Cache::new(300),
|
||||
db,
|
||||
superadmin_secret,
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -69,7 +56,7 @@ impl AuthCache {
|
||||
}
|
||||
|
||||
pub async fn invalidate(&self, w_id: &str, token: String) {
|
||||
AUTH_CACHE.remove(&(w_id.to_string(), token));
|
||||
self.cache.remove(&(w_id.to_string(), token));
|
||||
}
|
||||
|
||||
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<ApiAuthed> {
|
||||
@@ -77,7 +64,7 @@ impl AuthCache {
|
||||
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
|
||||
token.to_string(),
|
||||
);
|
||||
let s = AUTH_CACHE.get(&key).map(|c| c.to_owned());
|
||||
let s = self.cache.get(&key).map(|c| c.to_owned());
|
||||
match s {
|
||||
Some(ExpiringAuthCache { authed, expiry }) if expiry > chrono::Utc::now() => {
|
||||
Some(authed)
|
||||
@@ -99,7 +86,7 @@ impl AuthCache {
|
||||
};
|
||||
|
||||
if let Some((authed, exp)) = authed_and_exp.clone() {
|
||||
AUTH_CACHE.insert(
|
||||
self.cache.insert(
|
||||
key,
|
||||
ExpiringAuthCache {
|
||||
authed: authed.clone(),
|
||||
@@ -136,7 +123,7 @@ impl AuthCache {
|
||||
username_override,
|
||||
};
|
||||
|
||||
AUTH_CACHE.insert(
|
||||
self.cache.insert(
|
||||
key,
|
||||
ExpiringAuthCache {
|
||||
authed: authed.clone(),
|
||||
@@ -330,7 +317,7 @@ impl AuthCache {
|
||||
}
|
||||
};
|
||||
if let Some(authed) = authed_o.as_ref() {
|
||||
AUTH_CACHE.insert(
|
||||
self.cache.insert(
|
||||
key,
|
||||
ExpiringAuthCache {
|
||||
authed: authed.clone(),
|
||||
@@ -483,27 +470,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_get_workspace_id_from_path(path_vec: &[&str]) -> Option<String> {
|
||||
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
|
||||
Some(path_vec[3].to_owned())
|
||||
} else if path_vec.len() >= 5
|
||||
&& path_vec[0] == ""
|
||||
&& path_vec[1] == "api"
|
||||
&& path_vec[2] == "mcp"
|
||||
&& path_vec[3] == "w"
|
||||
{
|
||||
Some(path_vec[4].to_owned())
|
||||
} else {
|
||||
if path_vec.len() >= 5 && path_vec[0] == "" && path_vec[2] == "srch" && path_vec[3] == "w" {
|
||||
Some(path_vec[4].to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
workspace_id
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for ApiAuthed
|
||||
where
|
||||
@@ -516,62 +482,85 @@ where
|
||||
state: &S,
|
||||
) -> std::result::Result<Self, Self::Rejection> {
|
||||
if parts.method == http::Method::OPTIONS {
|
||||
return Ok(ApiAuthed::default());
|
||||
return Ok(ApiAuthed {
|
||||
email: "".to_owned(),
|
||||
username: "".to_owned(),
|
||||
is_admin: false,
|
||||
is_operator: false,
|
||||
groups: Vec::new(),
|
||||
folders: Vec::new(),
|
||||
scopes: None,
|
||||
username_override: None,
|
||||
});
|
||||
};
|
||||
let already_authed = parts.extensions.get::<ApiAuthed>();
|
||||
|
||||
if let Some(authed) = already_authed {
|
||||
return Ok(authed.clone());
|
||||
}
|
||||
|
||||
let already_tokened = parts.extensions.get::<Tokened>();
|
||||
let token_o = if let Some(token) = already_tokened {
|
||||
Some(token.token.clone())
|
||||
Ok(authed.clone())
|
||||
} else {
|
||||
extract_token(parts, state).await
|
||||
};
|
||||
|
||||
if let Some(token) = token_o {
|
||||
if let Ok(Extension(cache)) =
|
||||
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
|
||||
let already_tokened = parts.extensions.get::<Tokened>();
|
||||
let token_o = if let Some(token) = already_tokened {
|
||||
Some(token.token.clone())
|
||||
} else {
|
||||
extract_token(parts, state).await
|
||||
};
|
||||
let original_uri = OriginalUri::from_request_parts(parts, state)
|
||||
.await
|
||||
.ok()
|
||||
.map(|x| x.0)
|
||||
.unwrap_or_default();
|
||||
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
|
||||
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
|
||||
Some(path_vec[3].to_owned())
|
||||
} else if path_vec.len() >= 5
|
||||
&& path_vec[0] == ""
|
||||
&& path_vec[1] == "api"
|
||||
&& path_vec[2] == "mcp"
|
||||
&& path_vec[3] == "w"
|
||||
{
|
||||
let original_uri = OriginalUri::from_request_parts(parts, state)
|
||||
.await
|
||||
.ok()
|
||||
.map(|x| x.0)
|
||||
.unwrap_or_default();
|
||||
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
|
||||
let workspace_id = maybe_get_workspace_id_from_path(&path_vec);
|
||||
Some(path_vec[4].to_string())
|
||||
} else {
|
||||
if path_vec.len() >= 5
|
||||
&& path_vec[0] == ""
|
||||
&& path_vec[2] == "srch"
|
||||
&& path_vec[3] == "w"
|
||||
{
|
||||
Some(path_vec[4].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(token) = token_o {
|
||||
if let Ok(Extension(cache)) =
|
||||
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
|
||||
{
|
||||
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
|
||||
parts.extensions.insert(authed.clone());
|
||||
if authed.scopes.as_ref().is_some_and(|scopes| {
|
||||
scopes
|
||||
.iter()
|
||||
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
|
||||
}) && (path_vec.len() < 3
|
||||
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
|
||||
{
|
||||
BRUTE_FORCE_COUNTER.increment().await;
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("Unauthorized scoped token: {:?}", authed.scopes),
|
||||
));
|
||||
}
|
||||
Span::current().record("username", &authed.username.as_str());
|
||||
Span::current().record("email", &authed.email);
|
||||
|
||||
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
|
||||
if authed.scopes.as_ref().is_some_and(|scopes| {
|
||||
scopes
|
||||
.iter()
|
||||
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
|
||||
}) && (path_vec.len() < 3
|
||||
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
|
||||
{
|
||||
BRUTE_FORCE_COUNTER.increment().await;
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("Unauthorized scoped token: {:?}", authed.scopes),
|
||||
));
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
Span::current().record("workspace_id", &workspace_id);
|
||||
}
|
||||
return Ok(authed);
|
||||
}
|
||||
|
||||
parts.extensions.insert(authed.clone());
|
||||
|
||||
Span::current().record("username", &authed.username.as_str());
|
||||
Span::current().record("email", &authed.email);
|
||||
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
Span::current().record("workspace_id", &workspace_id);
|
||||
}
|
||||
return Ok(authed);
|
||||
}
|
||||
}
|
||||
BRUTE_FORCE_COUNTER.increment().await;
|
||||
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
|
||||
}
|
||||
BRUTE_FORCE_COUNTER.increment().await;
|
||||
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,15 +55,19 @@ use crate::mqtt_triggers::{MqttClientVersion, MqttV3Config, MqttV5Config, Subscr
|
||||
use crate::nats_triggers_oss::NatsTriggerConfigConnection;
|
||||
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
use crate::postgres_triggers::{
|
||||
create_logical_replication_slot, create_pg_publication, generate_random_string,
|
||||
get_default_pg_connection, PublicationData,
|
||||
use {
|
||||
crate::postgres_triggers::{
|
||||
create_logical_replication_slot, create_pg_publication, generate_random_string,
|
||||
get_pg_connection, PublicationData,
|
||||
},
|
||||
sqlx::Connection,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
args::RawWebhookArgs,
|
||||
db::{ApiAuthed, DB},
|
||||
users::fetch_api_authed,
|
||||
utils::RunnableKind,
|
||||
};
|
||||
|
||||
use axum::{
|
||||
@@ -81,7 +85,7 @@ use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{JsonResult, Result},
|
||||
triggers::{RunnableFormat, RunnableFormatVersion, TriggerKind},
|
||||
utils::{not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
|
||||
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
};
|
||||
|
||||
@@ -300,15 +304,13 @@ async fn set_postgres_trigger_config(
|
||||
user_db: UserDB,
|
||||
mut capture_config: NewCaptureConfig,
|
||||
) -> Result<NewCaptureConfig> {
|
||||
use windmill_common::error::to_anyhow;
|
||||
|
||||
let Some(TriggerConfig::Postgres(postgres_config)) = capture_config.trigger_config.as_mut()
|
||||
else {
|
||||
return Err(Error::BadRequest("Invalid postgres config".to_string()));
|
||||
};
|
||||
|
||||
if postgres_config.basic_mode.unwrap_or(false) {
|
||||
let mut pg_connection = get_default_pg_connection(
|
||||
let mut pg_connection = get_pg_connection(
|
||||
authed,
|
||||
Some(user_db),
|
||||
&db,
|
||||
@@ -317,26 +319,22 @@ async fn set_postgres_trigger_config(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let tx = pg_connection.transaction().await.map_err(to_anyhow)?;
|
||||
let mut tx = pg_connection.begin().await?;
|
||||
|
||||
let publication_name = format!("windmill_capture_{}", generate_random_string());
|
||||
let replication_slot_name = publication_name.clone();
|
||||
|
||||
create_logical_replication_slot(tx.client(), &replication_slot_name)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
create_logical_replication_slot(&mut tx, &replication_slot_name).await?;
|
||||
|
||||
create_pg_publication(
|
||||
tx.client(),
|
||||
&mut tx,
|
||||
&publication_name,
|
||||
postgres_config.publication.table_to_track.as_deref(),
|
||||
&postgres_config.publication.transaction_to_track,
|
||||
)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
tx.commit().await.map_err(to_anyhow)?;
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
postgres_config.publication_name = Some(publication_name);
|
||||
postgres_config.replication_slot_name = Some(replication_slot_name);
|
||||
} else {
|
||||
|
||||
@@ -804,18 +804,10 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
|
||||
.execute(db)
|
||||
.await?;
|
||||
});
|
||||
|
||||
run_windmill_migration!("v2_job_queue_suspend", db, |tx| {
|
||||
sqlx::query!(
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS v2_job_queue_suspend ON v2_job_queue (workspace_id, suspend) WHERE suspend > 0;"
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct ApiAuthed {
|
||||
pub email: String,
|
||||
pub username: String,
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::db::ApiAuthed;
|
||||
use crate::triggers::{
|
||||
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
|
||||
};
|
||||
use crate::utils::WithStarredInfoQuery;
|
||||
use crate::utils::{RunnableKind, WithStarredInfoQuery};
|
||||
use crate::{
|
||||
db::DB,
|
||||
schedule::clear_schedule,
|
||||
@@ -43,7 +43,7 @@ use windmill_common::{
|
||||
jobs::JobPayload,
|
||||
schedule::Schedule,
|
||||
scripts::Schema,
|
||||
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
|
||||
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, StripPath},
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel};
|
||||
@@ -477,7 +477,7 @@ async fn create_flow(
|
||||
false,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
nf.tag,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -441,8 +441,8 @@ pub struct BasicAuthAuthentication {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ApiKeyAuthentication {
|
||||
pub api_key_header: String,
|
||||
pub api_key_secret: String,
|
||||
api_key_header: String,
|
||||
api_key_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
|
||||
@@ -14,7 +14,6 @@ use crate::{
|
||||
},
|
||||
users::fetch_api_authed,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use axum::response::Response;
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
@@ -29,14 +28,13 @@ use quick_cache::sync::Cache;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sql_builder::{bind::Bind, SqlBuilder};
|
||||
use sqlx::prelude::FromRow;
|
||||
use sqlx::PgConnection;
|
||||
use sqlx::PgTransaction;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tower_http::cors::CorsLayer;
|
||||
use windmill_audit::{audit_oss::audit_log, ActionKind};
|
||||
use windmill_common::error::{Error, Result as WindmillResult};
|
||||
use windmill_common::error::Error;
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::s3_helpers::build_object_store_client;
|
||||
use windmill_common::{
|
||||
@@ -44,7 +42,7 @@ use windmill_common::{
|
||||
error::{self, JsonResult},
|
||||
s3_helpers::S3Object,
|
||||
triggers::TriggerKind,
|
||||
utils::{empty_as_none, not_found_if_none, paginate, require_admin, Pagination, StripPath},
|
||||
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
|
||||
worker::CLOUD_HOSTED,
|
||||
};
|
||||
use windmill_git_sync::handle_deployment_metadata;
|
||||
@@ -81,7 +79,6 @@ pub fn routes_global_service() -> Router {
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
.route("/create", post(create_trigger))
|
||||
.route("/create_many", post(create_many_http_trigger))
|
||||
.route("/list", get(list_triggers))
|
||||
.route("/get/*path", get(get_trigger))
|
||||
.route("/update/*path", post(update_trigger))
|
||||
@@ -114,8 +111,6 @@ struct NewTrigger {
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
http_method: HttpMethod,
|
||||
workspaced_route: Option<bool>,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
is_static_website: bool,
|
||||
wrap_body: Option<bool>,
|
||||
raw_string: Option<bool>,
|
||||
@@ -136,8 +131,6 @@ pub struct HttpTrigger {
|
||||
pub is_async: bool,
|
||||
pub authentication_method: AuthenticationMethod,
|
||||
pub http_method: HttpMethod,
|
||||
pub summary: Option<String>,
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
pub is_static_website: bool,
|
||||
@@ -157,8 +150,6 @@ struct EditTrigger {
|
||||
authentication_method: AuthenticationMethod,
|
||||
#[serde(deserialize_with = "non_empty_str")]
|
||||
authentication_resource_path: Option<String>,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
http_method: HttpMethod,
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
workspaced_route: Option<bool>,
|
||||
@@ -173,7 +164,6 @@ pub struct ListTriggerQuery {
|
||||
pub per_page: Option<usize>,
|
||||
pub path: Option<String>,
|
||||
pub is_flow: Option<bool>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
pub path_start: Option<String>,
|
||||
}
|
||||
|
||||
@@ -195,8 +185,6 @@ async fn list_triggers(
|
||||
"wrap_body",
|
||||
"raw_string",
|
||||
"script_path",
|
||||
"summary",
|
||||
"description",
|
||||
"is_flow",
|
||||
"http_method",
|
||||
"edited_by",
|
||||
@@ -251,8 +239,6 @@ async fn get_trigger(
|
||||
route_path_key,
|
||||
workspaced_route,
|
||||
script_path,
|
||||
summary,
|
||||
description,
|
||||
is_flow,
|
||||
http_method as "http_method: _",
|
||||
edited_by,
|
||||
@@ -287,7 +273,7 @@ async fn get_trigger(
|
||||
fn validate_authentication_method(
|
||||
authentication_method: AuthenticationMethod,
|
||||
raw_string: Option<bool>,
|
||||
) -> WindmillResult<()> {
|
||||
) -> error::Result<()> {
|
||||
match (authentication_method, raw_string) {
|
||||
(AuthenticationMethod::CustomScript, raw) if !raw.unwrap_or(false) == true => {
|
||||
return Err(Error::BadRequest(
|
||||
@@ -301,21 +287,57 @@ fn validate_authentication_method(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn increase_trigger_version(tx: &mut PgConnection) -> WindmillResult<()> {
|
||||
async fn increase_trigger_version_and_commit(mut tx: PgTransaction<'_>) -> error::Result<()> {
|
||||
sqlx::query!("SELECT nextval('http_trigger_version_seq')",)
|
||||
.fetch_one(tx)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_trigger_inner(
|
||||
tx: &mut PgConnection,
|
||||
w_id: &str,
|
||||
authed: &ApiAuthed,
|
||||
new_http_trigger: &NewTrigger,
|
||||
route_path_key: &str,
|
||||
) -> WindmillResult<()> {
|
||||
async fn create_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(ct): Json<NewTrigger>,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
if !VALID_ROUTE_PATH_RE.is_match(&ct.route_path) {
|
||||
return Err(error::Error::BadRequest("Invalid route path".to_string()));
|
||||
}
|
||||
|
||||
validate_authentication_method(ct.authentication_method, ct.raw_string)?;
|
||||
|
||||
// route path key is extracted from the route path to check for uniqueness
|
||||
// it replaces /?:{key} with :key
|
||||
// it will also remove the leading / if present, not an issue as we only allow : after slashes
|
||||
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&ct.route_path, ":key");
|
||||
let exists = route_path_key_exists(
|
||||
&route_path_key,
|
||||
&ct.http_method,
|
||||
&w_id,
|
||||
None,
|
||||
ct.workspaced_route,
|
||||
&db,
|
||||
)
|
||||
.await?;
|
||||
if exists {
|
||||
return Err(error::Error::BadRequest(
|
||||
"A route already exists with this path".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Static website and static asset are not supported on cloud".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO http_trigger (
|
||||
@@ -328,8 +350,6 @@ async fn create_trigger_inner(
|
||||
wrap_body,
|
||||
raw_string,
|
||||
script_path,
|
||||
summary,
|
||||
description,
|
||||
is_flow,
|
||||
is_async,
|
||||
authentication_method,
|
||||
@@ -341,209 +361,55 @@ async fn create_trigger_inner(
|
||||
is_static_website
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17
|
||||
)
|
||||
"#,
|
||||
w_id,
|
||||
new_http_trigger.path,
|
||||
new_http_trigger.route_path,
|
||||
ct.path,
|
||||
ct.route_path,
|
||||
&route_path_key,
|
||||
new_http_trigger.workspaced_route,
|
||||
new_http_trigger.authentication_resource_path,
|
||||
new_http_trigger.wrap_body.unwrap_or(false),
|
||||
new_http_trigger.raw_string.unwrap_or(false),
|
||||
new_http_trigger.script_path,
|
||||
new_http_trigger.summary,
|
||||
new_http_trigger.description,
|
||||
new_http_trigger.is_flow,
|
||||
new_http_trigger.is_async,
|
||||
new_http_trigger.authentication_method as _,
|
||||
new_http_trigger.http_method as _,
|
||||
new_http_trigger.static_asset_config as _,
|
||||
ct.workspaced_route,
|
||||
ct.authentication_resource_path,
|
||||
ct.wrap_body.unwrap_or(false),
|
||||
ct.raw_string.unwrap_or(false),
|
||||
ct.script_path,
|
||||
ct.is_flow,
|
||||
ct.is_async,
|
||||
ct.authentication_method as _,
|
||||
ct.http_method as _,
|
||||
ct.static_asset_config as _,
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
new_http_trigger.is_static_website
|
||||
ct.is_static_website
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
authed,
|
||||
&authed,
|
||||
"http_triggers.create",
|
||||
ActionKind::Create,
|
||||
&w_id,
|
||||
Some(new_http_trigger.path.as_str()),
|
||||
Some(ct.path.as_str()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
increase_trigger_version(tx).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_no_duplicates<'trigger>(
|
||||
new_http_triggers: &[NewTrigger],
|
||||
route_path_key: &[Cow<'trigger, str>],
|
||||
) -> Result<(), Error> {
|
||||
let mut seen = HashSet::with_capacity(new_http_triggers.len());
|
||||
|
||||
for (i, trigger) in new_http_triggers.iter().enumerate() {
|
||||
if !seen.insert((
|
||||
&route_path_key[i],
|
||||
trigger.http_method,
|
||||
trigger.workspaced_route,
|
||||
)) {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Duplicate HTTP route detected: '{}'. Each HTTP route must have a unique 'route_path'.",
|
||||
&trigger.route_path
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_many_http_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(new_http_triggers): Json<Vec<NewTrigger>>,
|
||||
) -> WindmillResult<(StatusCode, String)> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let error_wrapper = |path: &str, error: Error| -> Error {
|
||||
anyhow!(
|
||||
"Error occurred for HTTP route at route path: {}, error: {}",
|
||||
path,
|
||||
error
|
||||
)
|
||||
.into()
|
||||
};
|
||||
|
||||
let mut route_path_keys = Vec::with_capacity(new_http_triggers.len());
|
||||
|
||||
for new_http_trigger in new_http_triggers.iter() {
|
||||
let route_path_key = validate_http_trigger(&db, &w_id, new_http_trigger)
|
||||
.await
|
||||
.map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?;
|
||||
|
||||
route_path_keys.push(route_path_key);
|
||||
}
|
||||
|
||||
check_no_duplicates(&new_http_triggers, &route_path_keys)?;
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
for (i, new_http_trigger) in new_http_triggers.iter().enumerate() {
|
||||
create_trigger_inner(
|
||||
&mut tx,
|
||||
&w_id,
|
||||
&authed,
|
||||
new_http_trigger,
|
||||
&route_path_keys[i],
|
||||
)
|
||||
.await
|
||||
.map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
for http_trigger in new_http_triggers.into_iter() {
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::HttpTrigger { path: http_trigger.path.clone() },
|
||||
Some(format!("HTTP route '{}' created", http_trigger.path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((StatusCode::CREATED, format!("Created all HTTP routes")))
|
||||
}
|
||||
|
||||
async fn validate_http_trigger<'trigger>(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
new_http_trigger: &'trigger NewTrigger,
|
||||
) -> WindmillResult<Cow<'trigger, str>> {
|
||||
if !VALID_ROUTE_PATH_RE.is_match(&new_http_trigger.route_path) {
|
||||
return Err(error::Error::BadRequest("Invalid route path".to_string()));
|
||||
}
|
||||
|
||||
validate_authentication_method(
|
||||
new_http_trigger.authentication_method,
|
||||
new_http_trigger.raw_string,
|
||||
)?;
|
||||
|
||||
// route path key is extracted from the route path to check for uniqueness
|
||||
// it replaces /?:{key} with :key
|
||||
// it will also remove the leading / if present, not an issue as we only allow : after slashes
|
||||
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(&new_http_trigger.route_path, ":key");
|
||||
|
||||
let exists = route_path_key_exists(
|
||||
&route_path_key,
|
||||
&new_http_trigger.http_method,
|
||||
&w_id,
|
||||
None,
|
||||
new_http_trigger.workspaced_route,
|
||||
db,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if exists {
|
||||
return Err(error::Error::BadRequest(
|
||||
"A route already exists with this path".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if *CLOUD_HOSTED
|
||||
&& (new_http_trigger.is_static_website || new_http_trigger.static_asset_config.is_some())
|
||||
{
|
||||
return Err(error::Error::BadRequest(
|
||||
"Static website and static asset are not supported on cloud".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(route_path_key)
|
||||
}
|
||||
|
||||
async fn create_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(new_http_trigger): Json<NewTrigger>,
|
||||
) -> WindmillResult<(StatusCode, String)> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let route_path_key = validate_http_trigger(&db, &w_id, &new_http_trigger).await?;
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let http_trigger_path = new_http_trigger.path.clone();
|
||||
|
||||
create_trigger_inner(&mut tx, &w_id, &authed, &new_http_trigger, &route_path_key).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
increase_trigger_version_and_commit(tx).await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::HttpTrigger { path: new_http_trigger.path.clone() },
|
||||
Some(format!("HTTP route '{}' created", new_http_trigger.path)),
|
||||
windmill_git_sync::DeployedObject::HttpTrigger { path: ct.path.clone() },
|
||||
Some(format!("HTTP trigger '{}' created", ct.path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, format!("{}", http_trigger_path)))
|
||||
Ok((StatusCode::CREATED, format!("{}", ct.path)))
|
||||
}
|
||||
|
||||
async fn update_trigger(
|
||||
@@ -552,9 +418,8 @@ async fn update_trigger(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ct): Json<EditTrigger>,
|
||||
) -> WindmillResult<String> {
|
||||
) -> error::Result<String> {
|
||||
let path = path.to_path();
|
||||
|
||||
if *CLOUD_HOSTED && (ct.is_static_website || ct.static_asset_config.is_some()) {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Static website and static asset are not supported on cloud".to_string(),
|
||||
@@ -613,13 +478,11 @@ async fn update_trigger(
|
||||
email = $13,
|
||||
is_async = $14,
|
||||
authentication_method = $15,
|
||||
summary = $16,
|
||||
description = $17,
|
||||
edited_at = now(),
|
||||
is_static_website = $18
|
||||
is_static_website = $16
|
||||
WHERE
|
||||
workspace_id = $19 AND
|
||||
path = $20
|
||||
workspace_id = $17 AND
|
||||
path = $18
|
||||
"#,
|
||||
route_path,
|
||||
&route_path_key,
|
||||
@@ -636,8 +499,6 @@ async fn update_trigger(
|
||||
&authed.email,
|
||||
ct.is_async,
|
||||
ct.authentication_method as _,
|
||||
ct.summary,
|
||||
ct.description,
|
||||
ct.is_static_website,
|
||||
w_id,
|
||||
path,
|
||||
@@ -702,9 +563,7 @@ async fn update_trigger(
|
||||
)
|
||||
.await?;
|
||||
|
||||
increase_trigger_version(&mut tx).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
increase_trigger_version_and_commit(tx).await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
@@ -712,7 +571,7 @@ async fn update_trigger(
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::HttpTrigger { path: ct.path.clone() },
|
||||
Some(format!("HTTP route '{}' updated", ct.path)),
|
||||
Some(format!("HTTP trigger '{}' updated", ct.path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
@@ -725,7 +584,7 @@ async fn delete_trigger(
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> WindmillResult<String> {
|
||||
) -> error::Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let path = path.to_path();
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
@@ -750,9 +609,7 @@ async fn delete_trigger(
|
||||
)
|
||||
.await?;
|
||||
|
||||
increase_trigger_version(&mut tx).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
increase_trigger_version_and_commit(tx).await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
@@ -760,12 +617,12 @@ async fn delete_trigger(
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::HttpTrigger { path: path.to_string() },
|
||||
Some(format!("HTTP route '{}' deleted", path)),
|
||||
Some(format!("HTTP trigger '{}' deleted", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("HTTP route {path} deleted"))
|
||||
Ok(format!("HTTP trigger {path} deleted"))
|
||||
}
|
||||
|
||||
async fn exists_trigger(
|
||||
@@ -803,7 +660,7 @@ async fn route_path_key_exists(
|
||||
trigger_path: Option<&str>,
|
||||
workspaced_route: Option<bool>,
|
||||
db: &DB,
|
||||
) -> WindmillResult<bool> {
|
||||
) -> error::Result<bool> {
|
||||
let exists = if *CLOUD_HOSTED {
|
||||
sqlx::query_scalar!(
|
||||
r#"
|
||||
@@ -1004,7 +861,7 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
|
||||
.insert(format!("{}/*wm_subpath", full_path), trigger.clone())
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!(
|
||||
"Failed to consider HTTP route {}/*wm_subpath: {:?}",
|
||||
"Failed to consider http trigger route {}/*wm_subpath: {:?}",
|
||||
full_path,
|
||||
e,
|
||||
);
|
||||
@@ -1013,7 +870,11 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
|
||||
router
|
||||
.insert(full_path.clone(), trigger.clone())
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!("Failed to consider HTTP route {}: {:?}", full_path, e,);
|
||||
tracing::warn!(
|
||||
"Failed to consider http trigger route {}: {:?}",
|
||||
full_path,
|
||||
e,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1037,7 +898,7 @@ async fn get_http_route_trigger(
|
||||
db: &DB,
|
||||
user_db: UserDB,
|
||||
method: &http::Method,
|
||||
) -> WindmillResult<(TriggerRoute, String, HashMap<String, String>, ApiAuthed)> {
|
||||
) -> error::Result<(TriggerRoute, String, HashMap<String, String>, ApiAuthed)> {
|
||||
let http_method: HttpMethod = method.try_into()?;
|
||||
|
||||
let requested_path = format!("/{}", route_path);
|
||||
@@ -1086,11 +947,11 @@ async fn get_http_route_trigger(
|
||||
);
|
||||
let exists = match HTTP_ACCESS_CACHE.get(&cache_key) {
|
||||
Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => {
|
||||
tracing::debug!("HTTP access cache hit for route {}", trigger.path);
|
||||
tracing::debug!("HTTP access cache hit for trigger {}", trigger.path);
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!("HTTP access cache miss for route {}", trigger.path);
|
||||
tracing::debug!("HTTP access cache miss for trigger {}", trigger.path);
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let exists = sqlx::query_scalar!(
|
||||
r#"
|
||||
@@ -1141,7 +1002,7 @@ async fn get_http_route_trigger(
|
||||
trigger.email.clone(),
|
||||
&trigger.workspace_id,
|
||||
&db,
|
||||
Some(username_override.unwrap_or(format!("HTTP-{}", trigger.path))),
|
||||
Some(username_override.unwrap_or(format!("http-{}", trigger.path))),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1169,14 +1030,6 @@ async fn route_job(
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
if trigger.script_path.is_empty() && trigger.static_asset_config.is_none() {
|
||||
return Err(Error::NotFound(format!(
|
||||
"Runnable path of HTTP route at path: {}",
|
||||
trigger.path
|
||||
))
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let args = args
|
||||
.process_args(
|
||||
&authed,
|
||||
@@ -1213,15 +1066,15 @@ async fn route_job(
|
||||
|
||||
let authentication_method = match HTTP_AUTH_CACHE.get(&cache_key) {
|
||||
Some(cache_entry) if cache_entry.expiry > std::time::Instant::now() => {
|
||||
tracing::debug!("HTTP auth method cache hit for route {}", trigger.path);
|
||||
tracing::debug!("HTTP auth method cache hit for trigger {}", trigger.path);
|
||||
cache_entry.value
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!("HTTP auth method cache miss for route {}", trigger.path);
|
||||
tracing::debug!("HTTP auth method cache miss for trigger {}", trigger.path);
|
||||
let auth_method = try_get_resource_from_db_as::<
|
||||
crate::http_trigger_auth::AuthenticationMethod,
|
||||
>(
|
||||
&authed,
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
&db,
|
||||
&resource_path,
|
||||
|
||||
@@ -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;
|
||||
@@ -103,8 +101,6 @@ mod integration;
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
mod postgres_triggers;
|
||||
|
||||
pub mod openapi;
|
||||
|
||||
mod approvals;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
pub mod apps_ee;
|
||||
@@ -524,18 +520,21 @@ pub async fn run_server(
|
||||
|
||||
// Setup MCP server
|
||||
#[allow(unused_variables)]
|
||||
let (mcp_router, mcp_session_manager) = {
|
||||
let (mcp_router, mcp_main_ct, mcp_service_ct) = {
|
||||
#[cfg(feature = "mcp")]
|
||||
if server_mode || mcp_mode {
|
||||
let (mcp_router, mcp_session_manager) = setup_mcp_server().await?;
|
||||
let mcp_middleware = axum::middleware::from_fn(extract_and_store_workspace_id);
|
||||
(mcp_router.layer(mcp_middleware), Some(mcp_session_manager))
|
||||
let (mcp_sse_server, mcp_router) = setup_mcp_server(addr, "/api/mcp/w/:workspace_id")?;
|
||||
#[cfg(feature = "mcp")]
|
||||
let mcp_main_ct = mcp_sse_server.config.ct.clone(); // Token to signal shutdown *to* MCP
|
||||
#[cfg(feature = "mcp")]
|
||||
let mcp_service_ct = mcp_sse_server.with_service(McpRunner::new); // Token to wait for MCP *service* shutdown
|
||||
(mcp_router, Some(mcp_main_ct), Some(mcp_service_ct))
|
||||
} else {
|
||||
(Router::new(), Option::<Arc<LocalSessionManager>>::None)
|
||||
(Router::new(), None, None)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mcp"))]
|
||||
(Router::new(), Option::<()>::None)
|
||||
(Router::new(), None::<()>, None::<()>)
|
||||
};
|
||||
|
||||
#[cfg(feature = "agent_worker_server")]
|
||||
@@ -597,7 +596,6 @@ pub async fn run_server(
|
||||
.nest("/variables", variables::workspaced_service())
|
||||
.nest("/workspaces", workspaces::workspaced_service())
|
||||
.nest("/oidc", oidc_oss::workspaced_service())
|
||||
.nest("/openapi", openapi::openapi_service())
|
||||
.nest("/http_triggers", http_triggers_service)
|
||||
.nest("/websocket_triggers", websocket_triggers_service)
|
||||
.nest("/kafka_triggers", kafka_triggers_service)
|
||||
@@ -662,7 +660,7 @@ pub async fn run_server(
|
||||
.layer(from_extractor::<OptAuthed>())
|
||||
.layer(cors.clone()),
|
||||
)
|
||||
.nest("/mcp/w/:workspace_id/sse", mcp_router)
|
||||
.nest("/mcp/w/:workspace_id", mcp_router)
|
||||
.layer(from_extractor::<OptAuthed>())
|
||||
.nest("/agent_workers", {
|
||||
#[cfg(feature = "agent_worker_server")]
|
||||
@@ -821,9 +819,16 @@ pub async fn run_server(
|
||||
tracing::info!("Graceful shutdown of server");
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
if let Some(mcp_session_manager) = mcp_session_manager {
|
||||
shutdown_mcp_server(mcp_session_manager).await;
|
||||
tracing::info!("MCP server shutdown");
|
||||
{
|
||||
if let Some(mcp_main_ct) = mcp_main_ct {
|
||||
tracing::info!("Received shutdown signal, cancelling MCP server...");
|
||||
mcp_main_ct.cancel();
|
||||
}
|
||||
if let Some(mcp_service_ct) = mcp_service_ct {
|
||||
tracing::info!("Waiting for MCP service cancellation...");
|
||||
mcp_service_ct.cancelled().await;
|
||||
tracing::info!("MCP service cancelled.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::to_bytes;
|
||||
use axum::Router;
|
||||
use axum::{extract::Path, http::Request, middleware::Next, response::Response};
|
||||
use rmcp::transport::sse_server::{SseServer, SseServerConfig};
|
||||
use rmcp::{
|
||||
handler::server::ServerHandler,
|
||||
model::*,
|
||||
@@ -16,6 +17,7 @@ use serde_json::Value;
|
||||
use sql_builder::prelude::*;
|
||||
use sqlx::FromRow;
|
||||
use tokio::try_join;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use windmill_common::db::UserDB;
|
||||
use windmill_common::worker::to_raw_value;
|
||||
use windmill_common::{DB, HUB_BASE_URL};
|
||||
@@ -27,9 +29,6 @@ use crate::jobs::{
|
||||
run_wait_result_flow_by_path_internal, run_wait_result_script_by_path_internal, RunJobQuery,
|
||||
};
|
||||
use crate::HTTP_CLIENT;
|
||||
use rmcp::transport::streamable_http_server::{
|
||||
session::local::LocalSessionManager, SessionManager, StreamableHttpService,
|
||||
};
|
||||
use windmill_common::utils::{query_elems_from_hub, StripPath};
|
||||
|
||||
/// Transforms the path for workspace scripts/flows.
|
||||
@@ -857,44 +856,28 @@ impl ServerHandler for Runner {
|
||||
})
|
||||
};
|
||||
|
||||
let http_parts = context
|
||||
.extensions
|
||||
.get::<axum::http::request::Parts>()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("http::request::Parts not found");
|
||||
Error::internal_error("http::request::Parts not found", None)
|
||||
})?;
|
||||
|
||||
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
|
||||
tracing::error!("ApiAuthed Axum extension not found");
|
||||
Error::internal_error("ApiAuthed Axum extension not found", None)
|
||||
})?;
|
||||
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
|
||||
tracing::error!("DB Axum extension not found");
|
||||
Error::internal_error("DB Axum extension not found", None)
|
||||
})?;
|
||||
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
|
||||
tracing::error!("UserDB Axum extension not found");
|
||||
Error::internal_error("UserDB Axum extension not found", None)
|
||||
})?;
|
||||
let authed = context
|
||||
.req_extensions
|
||||
.get::<ApiAuthed>()
|
||||
.ok_or_else(|| Error::internal_error("ApiAuthed not found", None))?;
|
||||
let db = context
|
||||
.req_extensions
|
||||
.get::<DB>()
|
||||
.ok_or_else(|| Error::internal_error("DB not found", None))?;
|
||||
let user_db = context
|
||||
.req_extensions
|
||||
.get::<UserDB>()
|
||||
.ok_or_else(|| Error::internal_error("UserDB not found", None))?;
|
||||
let args = parse_args(request.arguments)?;
|
||||
|
||||
let workspace_id = http_parts
|
||||
.extensions
|
||||
.get::<WorkspaceId>()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("WorkspaceId not found");
|
||||
Error::internal_error("WorkspaceId not found", None)
|
||||
})
|
||||
.map(|w_id| w_id.0.clone())?;
|
||||
|
||||
let (tool_type, path, is_hub) =
|
||||
Runner::reverse_transform(&request.name).unwrap_or_default();
|
||||
|
||||
let item_schema = if is_hub {
|
||||
Runner::get_hub_script_schema(&format!("hub/{}", path), db).await?
|
||||
} else {
|
||||
Runner::get_item_schema(&path, user_db, authed, &workspace_id, &tool_type).await?
|
||||
Runner::get_item_schema(&path, user_db, authed, &context.workspace_id, &tool_type)
|
||||
.await?
|
||||
};
|
||||
|
||||
let schema_obj = if let Some(ref s) = item_schema {
|
||||
@@ -923,6 +906,8 @@ impl ServerHandler for Runner {
|
||||
} else {
|
||||
windmill_queue::PushArgsOwned::default()
|
||||
};
|
||||
|
||||
let w_id = context.workspace_id.clone();
|
||||
let script_or_flow_path = if is_hub {
|
||||
StripPath(format!("hub/{}", path))
|
||||
} else {
|
||||
@@ -937,7 +922,7 @@ impl ServerHandler for Runner {
|
||||
script_or_flow_path,
|
||||
authed.clone(),
|
||||
user_db.clone(),
|
||||
workspace_id.clone(),
|
||||
w_id.clone(),
|
||||
push_args,
|
||||
)
|
||||
.await
|
||||
@@ -949,7 +934,7 @@ impl ServerHandler for Runner {
|
||||
authed.clone(),
|
||||
user_db.clone(),
|
||||
push_args,
|
||||
workspace_id.clone(),
|
||||
w_id.clone(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
@@ -993,38 +978,19 @@ impl ServerHandler for Runner {
|
||||
_request: Option<PaginatedRequestParam>,
|
||||
mut _context: RequestContext<RoleServer>,
|
||||
) -> Result<ListToolsResult, Error> {
|
||||
let http_parts = _context
|
||||
.extensions
|
||||
.get::<axum::http::request::Parts>()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("http::request::Parts not found");
|
||||
Error::internal_error("http::request::Parts not found", None)
|
||||
})?;
|
||||
|
||||
let db = http_parts.extensions.get::<DB>().ok_or_else(|| {
|
||||
tracing::error!("DB Axum extension not found");
|
||||
Error::internal_error("DB Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
let user_db = http_parts.extensions.get::<UserDB>().ok_or_else(|| {
|
||||
tracing::error!("UserDB Axum extension not found");
|
||||
Error::internal_error("UserDB Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
let authed = http_parts.extensions.get::<ApiAuthed>().ok_or_else(|| {
|
||||
tracing::error!("ApiAuthed Axum extension not found");
|
||||
Error::internal_error("ApiAuthed Axum extension not found", None)
|
||||
})?;
|
||||
|
||||
let workspace_id = http_parts
|
||||
.extensions
|
||||
.get::<WorkspaceId>()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("WorkspaceId not found");
|
||||
Error::internal_error("WorkspaceId not found", None)
|
||||
})
|
||||
.map(|w_id| w_id.0.clone())?;
|
||||
|
||||
let workspace_id = _context.workspace_id.clone();
|
||||
let db = _context
|
||||
.req_extensions
|
||||
.get::<DB>()
|
||||
.ok_or_else(|| Error::internal_error("DB not found", None))?;
|
||||
let user_db = _context
|
||||
.req_extensions
|
||||
.get::<UserDB>()
|
||||
.ok_or_else(|| Error::internal_error("UserDB not found", None))?;
|
||||
let authed = _context
|
||||
.req_extensions
|
||||
.get::<ApiAuthed>()
|
||||
.ok_or_else(|| Error::internal_error("ApiAuthed not found", None))?;
|
||||
let owned_scope = authed.scopes.as_ref().and_then(|scopes| {
|
||||
scopes
|
||||
.iter()
|
||||
@@ -1161,54 +1127,15 @@ impl ServerHandler for Runner {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WorkspaceId(pub String);
|
||||
|
||||
pub async fn extract_and_store_workspace_id(
|
||||
Path(params): Path<String>,
|
||||
mut request: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let workspace_id = params;
|
||||
request.extensions_mut().insert(WorkspaceId(workspace_id));
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
pub async fn setup_mcp_server() -> anyhow::Result<(Router, Arc<LocalSessionManager>)> {
|
||||
let session_manager = Arc::new(LocalSessionManager::default());
|
||||
let service_config = Default::default();
|
||||
let service = StreamableHttpService::new(
|
||||
|| Ok(Runner::new()),
|
||||
session_manager.clone(),
|
||||
service_config,
|
||||
);
|
||||
|
||||
let router = axum::Router::new().nest_service("/", service);
|
||||
Ok((router, session_manager))
|
||||
}
|
||||
|
||||
pub async fn shutdown_mcp_server(session_manager: Arc<LocalSessionManager>) {
|
||||
let session_ids_to_close = {
|
||||
let sessions_map = session_manager.sessions.read().await;
|
||||
sessions_map.keys().cloned().collect::<Vec<_>>()
|
||||
pub fn setup_mcp_server(addr: SocketAddr, path: &str) -> anyhow::Result<(SseServer, Router)> {
|
||||
let config = SseServerConfig {
|
||||
bind: addr,
|
||||
sse_path: "/sse".to_string(),
|
||||
post_path: "/message".to_string(),
|
||||
full_message_path: path.to_string(),
|
||||
ct: CancellationToken::new(),
|
||||
sse_keep_alive: None,
|
||||
};
|
||||
|
||||
if !session_ids_to_close.is_empty() {
|
||||
tracing::info!(
|
||||
"Closing {} active MCP session(s)...",
|
||||
session_ids_to_close.len()
|
||||
);
|
||||
let close_futures = session_ids_to_close
|
||||
.iter()
|
||||
.map(|session_id| {
|
||||
let manager_clone = session_manager.clone();
|
||||
async move {
|
||||
if let Err(_) = manager_clone.close_session(session_id).await {
|
||||
tracing::warn!("Error closing MCP session");
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
futures::future::join_all(close_futures).await;
|
||||
}
|
||||
Ok(SseServer::new(config))
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ pub async fn test_mqtt_connection(
|
||||
test_postgres;
|
||||
|
||||
let mqtt_resource = try_get_resource_from_db_as::<MqttResource>(
|
||||
&authed,
|
||||
authed,
|
||||
Some(user_db),
|
||||
&db,
|
||||
&mqtt_resource_path,
|
||||
@@ -1253,7 +1253,7 @@ impl MqttConfig {
|
||||
}
|
||||
}
|
||||
let mqtt_resource = try_get_resource_from_db_as::<MqttResource>(
|
||||
&authed,
|
||||
authed,
|
||||
Some(UserDB::new(db.clone())),
|
||||
db,
|
||||
mqtt_resource_path,
|
||||
|
||||
@@ -1,984 +0,0 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
body::Body, extract::Path, http, response::Response, routing::post, Extension, Json, Router,
|
||||
};
|
||||
use http::{header, HeaderValue, Method, StatusCode};
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{to_value, Map, Value};
|
||||
use sqlx::PgConnection;
|
||||
use url::Url;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, Result},
|
||||
utils::{deserialize_url, empty_as_none, is_empty, RunnableKind},
|
||||
DB,
|
||||
};
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
|
||||
#[cfg(feature = "http_trigger")]
|
||||
use crate::{
|
||||
http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication,
|
||||
http_triggers::AuthenticationMethod, resources::try_get_resource_from_db_as,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref DEFAULT_OPENAPI_INFO_OBJECT: Info = Info {
|
||||
title: "Windmill API".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_OPENAPI_GENERATED_VERSION: &'static str = "3.1.0";
|
||||
const JWT_SECURITY_SCHEME: &'static str = "JwtAuth";
|
||||
const BASIC_HTTP_AUTH_SCHEME: &'static str = "BasicHttp";
|
||||
|
||||
const DEFAULT_REQUEST_KEY: &'static str = "defaultRequest";
|
||||
const DEFAULT_ASYNC_RESPONSE_KEY: &'static str = "AsyncResponse";
|
||||
const DEFAULT_SYNC_RESPONSE_KEY: &'static str = "SyncResponse";
|
||||
const DEFAULT_PAYLOAD_PARAM_KEY: &'static str = "PayloadParam";
|
||||
|
||||
pub fn openapi_service() -> Router {
|
||||
Router::new()
|
||||
.route("/generate", post(generate_openapi_spec))
|
||||
.route("/download", post(download_spec))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Format {
|
||||
JSON,
|
||||
YAML,
|
||||
}
|
||||
|
||||
impl Display for Format {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let format = match self {
|
||||
Format::JSON => "json",
|
||||
Format::YAML => "yaml",
|
||||
};
|
||||
write!(f, "{}", format)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Format {
|
||||
fn default() -> Self {
|
||||
Self::YAML
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
struct Contact {
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
name: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_url",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
url: Option<Url>,
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
struct License {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
identifier: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_url",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
url: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Info {
|
||||
title: String,
|
||||
version: String,
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
contact: Option<Contact>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
license: Option<License>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Server {
|
||||
url: String,
|
||||
#[serde(skip_serializing_if = "is_empty")]
|
||||
description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
variables: Option<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SecurityScheme {
|
||||
BearerJwt,
|
||||
BasicHttp,
|
||||
ApiKey(String),
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct WebhookConfig {
|
||||
runnable_kind: RunnableKind,
|
||||
}
|
||||
|
||||
impl WebhookConfig {
|
||||
pub fn new(runnable_kind: RunnableKind) -> Self {
|
||||
Self { runnable_kind }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HttpRouteConfig {
|
||||
method: Method,
|
||||
}
|
||||
|
||||
impl HttpRouteConfig {
|
||||
pub fn new(method: Method) -> Self {
|
||||
Self { method }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Kind {
|
||||
Webhook(WebhookConfig),
|
||||
HttpRoute(HttpRouteConfig),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FuturePath {
|
||||
route_path: String,
|
||||
kind: Kind,
|
||||
is_async: Option<bool>,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
security_scheme: Option<SecurityScheme>,
|
||||
}
|
||||
|
||||
impl FuturePath {
|
||||
pub fn new(
|
||||
route_path: String,
|
||||
kind: Kind,
|
||||
is_async: Option<bool>,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
security_scheme: Option<SecurityScheme>,
|
||||
) -> FuturePath {
|
||||
FuturePath { route_path, kind, is_async, summary, description, security_scheme }
|
||||
}
|
||||
}
|
||||
|
||||
fn from_route_path_to_openapi_path(
|
||||
route_path: &str,
|
||||
kind: &Kind,
|
||||
) -> Result<(Vec<String>, Option<Value>)> {
|
||||
let mut openapi_path = String::new();
|
||||
let mut parameters = Vec::new();
|
||||
|
||||
for segment in route_path.split('/') {
|
||||
if segment.starts_with(':') {
|
||||
let param_name = &segment[1..];
|
||||
|
||||
if param_name.is_empty() {
|
||||
return Err(anyhow!("Empty parameter name in path: {}", route_path).into());
|
||||
}
|
||||
|
||||
openapi_path.push_str(&format!("/{{{}}}", param_name));
|
||||
parameters.push(serde_json::json!({
|
||||
"name": param_name,
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string" }
|
||||
}));
|
||||
} else if !segment.is_empty() {
|
||||
openapi_path.push('/');
|
||||
openapi_path.push_str(segment);
|
||||
} else {
|
||||
openapi_path.push('/');
|
||||
}
|
||||
}
|
||||
|
||||
let parameters_json = if parameters.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Value::Array(parameters))
|
||||
};
|
||||
|
||||
let prefix = match kind {
|
||||
Kind::HttpRoute(_) => "",
|
||||
Kind::Webhook(WebhookConfig { runnable_kind }) => match runnable_kind {
|
||||
RunnableKind::Script => "p",
|
||||
RunnableKind::Flow => "f",
|
||||
},
|
||||
};
|
||||
|
||||
let normalized_path = if openapi_path.starts_with('/') {
|
||||
format!("{prefix}{openapi_path}")
|
||||
} else {
|
||||
format!("{}/{}", prefix, openapi_path)
|
||||
};
|
||||
|
||||
let route_paths = if prefix.is_empty() {
|
||||
vec![normalized_path]
|
||||
} else {
|
||||
vec![
|
||||
format!("/run/{}", &normalized_path),
|
||||
format!("/run_wait_result/{}", &normalized_path),
|
||||
]
|
||||
};
|
||||
|
||||
Ok((route_paths, parameters_json))
|
||||
}
|
||||
|
||||
fn get_servers_component(url: &str, kind: &Kind) -> Server {
|
||||
let url = url.trim_end_matches('/');
|
||||
|
||||
let server = match kind {
|
||||
Kind::HttpRoute(_) => {
|
||||
Server { url: format!("{}/api/r", url), description: None, variables: None }
|
||||
}
|
||||
Kind::Webhook(_) => Server {
|
||||
url: format!("{}/api/w/{{workspace}}/jobs", url),
|
||||
variables: Some(HashMap::from([(
|
||||
"workspace".to_string(),
|
||||
serde_json::json!({
|
||||
"default": "test",
|
||||
"description": "Workspace identifier"
|
||||
}),
|
||||
)])),
|
||||
description: None,
|
||||
},
|
||||
};
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
fn generate_paths(
|
||||
paths: Vec<FuturePath>,
|
||||
url: Option<&Url>,
|
||||
) -> Result<IndexMap<String, IndexMap<String, Value>>> {
|
||||
let mut map: IndexMap<String, IndexMap<String, Value>> = IndexMap::new();
|
||||
|
||||
let generate_default_request = || {
|
||||
serde_json::json!({
|
||||
"$ref": format!("#/components/requestBodies/{DEFAULT_REQUEST_KEY}")
|
||||
})
|
||||
};
|
||||
|
||||
let generate_response = |is_async: bool| {
|
||||
let responses = if is_async {
|
||||
serde_json::json!({
|
||||
"200": {
|
||||
"$ref": format!("#/components/responses/{DEFAULT_ASYNC_RESPONSE_KEY}")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
serde_json::json!(serde_json::json!({
|
||||
"200": {
|
||||
"$ref": format!("#/components/responses/{DEFAULT_SYNC_RESPONSE_KEY}")
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
responses
|
||||
};
|
||||
|
||||
let get_security_scheme = |security_scheme: Option<&SecurityScheme>| -> Vec<Value> {
|
||||
if let Some(security_scheme) = security_scheme {
|
||||
let scheme = match security_scheme {
|
||||
SecurityScheme::ApiKey(api_key) => header_to_pascal_case(&api_key),
|
||||
SecurityScheme::BearerJwt => JWT_SECURITY_SCHEME.to_owned(),
|
||||
SecurityScheme::BasicHttp => BASIC_HTTP_AUTH_SCHEME.to_owned(),
|
||||
};
|
||||
|
||||
vec![serde_json::json!({
|
||||
scheme: []
|
||||
})]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
let mut duplicate_webhooks = HashSet::new();
|
||||
|
||||
for path in paths {
|
||||
let (route_paths, parameters) =
|
||||
from_route_path_to_openapi_path(&path.route_path, &path.kind)?;
|
||||
|
||||
for route_path in route_paths {
|
||||
let path_object = map.entry(route_path.clone()).or_insert_with(|| {
|
||||
let mut path_object = IndexMap::new();
|
||||
|
||||
if let Some(url) = url {
|
||||
let servers = get_servers_component(url.as_str(), &path.kind);
|
||||
path_object.insert("servers".to_string(), to_value(vec![servers]).unwrap());
|
||||
}
|
||||
|
||||
if parameters.is_some() {
|
||||
path_object.insert(
|
||||
"parameters".to_string(),
|
||||
to_value(parameters.clone()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
path_object
|
||||
});
|
||||
|
||||
let is_async;
|
||||
|
||||
let (methods, is_webhook) = match &path.kind {
|
||||
Kind::Webhook(_) => {
|
||||
if !duplicate_webhooks.insert(route_path.clone()) {
|
||||
return Err(anyhow!("Found duplicate webhook: {}", path.route_path).into());
|
||||
}
|
||||
is_async = route_path.starts_with("/run/");
|
||||
let methods = if is_async {
|
||||
vec![Method::POST]
|
||||
} else {
|
||||
vec![Method::GET, Method::POST]
|
||||
};
|
||||
|
||||
(methods, true)
|
||||
}
|
||||
Kind::HttpRoute(HttpRouteConfig { method }) => {
|
||||
if path_object.get(&method.to_string()).is_some() {
|
||||
return Err(anyhow!("Found duplicate route: {}", path.route_path).into());
|
||||
}
|
||||
is_async = path.is_async.unwrap_or(true);
|
||||
(vec![method.to_owned()], false)
|
||||
}
|
||||
};
|
||||
|
||||
for method in methods {
|
||||
let mut method_map = IndexMap::new();
|
||||
|
||||
if let Some(summary) = path.summary.as_ref().filter(|s| !s.is_empty()) {
|
||||
method_map.insert("summary", Value::String(summary.to_owned()));
|
||||
}
|
||||
|
||||
if let Some(description) = path.description.as_ref().filter(|s| !s.is_empty()) {
|
||||
method_map.insert("description", Value::String(description.to_owned()));
|
||||
}
|
||||
|
||||
method_map.insert(
|
||||
"security",
|
||||
to_value(get_security_scheme(path.security_scheme.as_ref()))?,
|
||||
);
|
||||
|
||||
if method != Method::GET {
|
||||
method_map.insert("requestBody", generate_default_request());
|
||||
} else if is_webhook {
|
||||
method_map.insert(
|
||||
"parameters",
|
||||
Value::Array(vec![serde_json::json!({
|
||||
"$ref": format!("#/components/parameters/{DEFAULT_PAYLOAD_PARAM_KEY}")
|
||||
})]),
|
||||
);
|
||||
}
|
||||
|
||||
method_map.insert("responses", generate_response(is_async));
|
||||
|
||||
path_object.insert(method.to_string().to_lowercase(), to_value(&method_map)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(map);
|
||||
}
|
||||
|
||||
pub fn transform_to_minified_postgres_regex(glob: &str) -> String {
|
||||
let mut regex = String::from("^");
|
||||
for ch in glob.chars() {
|
||||
match ch {
|
||||
'*' => regex.push_str(".*"),
|
||||
'.' | '+' | '(' | ')' | '|' | '^' | '$' | '{' | '}' | '[' | ']' | '\\' => {
|
||||
regex.push('\\');
|
||||
regex.push(ch);
|
||||
}
|
||||
_ => regex.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
regex.push('$');
|
||||
regex
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ServerToSet {
|
||||
pub http_route: bool,
|
||||
pub webhook_flow: bool,
|
||||
pub webhook_script: bool,
|
||||
}
|
||||
|
||||
impl ServerToSet {
|
||||
pub fn new(http_route: bool, webhook_flow: bool, webhook_script: bool) -> ServerToSet {
|
||||
ServerToSet { http_route, webhook_flow, webhook_script }
|
||||
}
|
||||
}
|
||||
|
||||
fn header_to_pascal_case(header: &str) -> String {
|
||||
header
|
||||
.split(|c: char| c == '-' || c == '_' || c == ' ')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| {
|
||||
let mut chars = s.chars();
|
||||
match chars.next() {
|
||||
Some(first) => {
|
||||
first.to_ascii_uppercase().to_string()
|
||||
+ chars.as_str().to_ascii_lowercase().as_str()
|
||||
}
|
||||
None => String::new(),
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SecuritySchemeToAdd {
|
||||
basic_http: bool,
|
||||
bearer_jwt: bool,
|
||||
api_keys: Vec<(String, Value)>,
|
||||
}
|
||||
|
||||
fn generate_all_security_schemes(future_paths: &[FuturePath]) -> SecuritySchemeToAdd {
|
||||
let mut to_add = SecuritySchemeToAdd::default();
|
||||
|
||||
let mut set = HashSet::new();
|
||||
for future_path in future_paths {
|
||||
if !to_add.basic_http
|
||||
&& matches!(future_path.security_scheme, Some(SecurityScheme::BasicHttp))
|
||||
{
|
||||
to_add.basic_http = true
|
||||
} else if !to_add.bearer_jwt
|
||||
&& matches!(future_path.security_scheme, Some(SecurityScheme::BearerJwt))
|
||||
{
|
||||
to_add.bearer_jwt = true
|
||||
} else if let Some(SecurityScheme::ApiKey(api_key)) = future_path.security_scheme.as_ref() {
|
||||
let pascal_case_header = header_to_pascal_case(&api_key);
|
||||
|
||||
if !set.insert(pascal_case_header.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let scheme = serde_json::json!({
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": api_key
|
||||
});
|
||||
to_add.api_keys.push((pascal_case_header, scheme));
|
||||
}
|
||||
}
|
||||
|
||||
to_add
|
||||
}
|
||||
|
||||
fn generate_components(future_paths: &[FuturePath]) -> Map<String, Value> {
|
||||
let mut components = Map::new();
|
||||
|
||||
if future_paths
|
||||
.iter()
|
||||
.any(|path| matches!(path.kind, Kind::Webhook(_)))
|
||||
{
|
||||
components.insert(
|
||||
"parameters".to_owned(),
|
||||
serde_json::json!({
|
||||
"PayloadParam": {
|
||||
"name": "payload",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"description": "A URL-safe base64-encoded JSON string payload.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let mut security_scheme = Map::new();
|
||||
|
||||
let SecuritySchemeToAdd { basic_http, bearer_jwt, api_keys } =
|
||||
generate_all_security_schemes(future_paths);
|
||||
|
||||
if basic_http {
|
||||
security_scheme.insert(
|
||||
BASIC_HTTP_AUTH_SCHEME.to_owned(),
|
||||
serde_json::json!({
|
||||
"type": "http",
|
||||
"scheme": "basic"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if bearer_jwt {
|
||||
security_scheme.insert(
|
||||
JWT_SECURITY_SCHEME.to_owned(),
|
||||
serde_json::json!({
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (key, value) in api_keys {
|
||||
security_scheme.insert(key, value);
|
||||
}
|
||||
components.insert("securitySchemes".to_owned(), Value::Object(security_scheme));
|
||||
}
|
||||
|
||||
components.insert("requestBodies".to_owned(), serde_json::json!({
|
||||
DEFAULT_REQUEST_KEY: {
|
||||
"description": "This route may or may not require a request body, but its structure and content type are unknown.",
|
||||
"required": false,
|
||||
"content": {
|
||||
"application/json": {}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
components.insert("responses".to_owned(), serde_json::json!({
|
||||
DEFAULT_ASYNC_RESPONSE_KEY: {
|
||||
"description": "Returns a job ID as a UUID string.",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"examples": [ "550e8400-e29b-41d4-a716-446655440000" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
DEFAULT_SYNC_RESPONSE_KEY: {
|
||||
"description": "This route may return a response, but its structure and content type are unknown.",
|
||||
"content": {
|
||||
"application/octet-stream": {}
|
||||
}
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
components
|
||||
}
|
||||
|
||||
pub fn generate_openapi_document(
|
||||
info: Option<&Info>,
|
||||
url: Option<&Url>,
|
||||
paths: Vec<FuturePath>,
|
||||
format: Format,
|
||||
) -> Result<String> {
|
||||
let mut openapi_doc: IndexMap<&'static str, Value> = IndexMap::new();
|
||||
|
||||
openapi_doc.insert("openapi", to_value(&DEFAULT_OPENAPI_GENERATED_VERSION)?);
|
||||
openapi_doc.insert(
|
||||
"info",
|
||||
to_value(info.unwrap_or(&DEFAULT_OPENAPI_INFO_OBJECT))?,
|
||||
);
|
||||
|
||||
openapi_doc.insert("components", Value::Object(generate_components(&paths)));
|
||||
|
||||
openapi_doc.insert("paths", to_value(generate_paths(paths, url)?)?);
|
||||
|
||||
let openapi_document = match format {
|
||||
Format::YAML => serde_yml::to_string(&openapi_doc).map_err(|err| {
|
||||
anyhow!(
|
||||
"Could not generate OpenAPI document in YAML format: {}",
|
||||
err
|
||||
)
|
||||
})?,
|
||||
Format::JSON => serde_json::to_string_pretty(&openapi_doc).map_err(|err| {
|
||||
anyhow!(
|
||||
"Could not generate OpenAPI document in JSON format: {}",
|
||||
err
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
Ok(openapi_document)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HttpRouteFilter {
|
||||
folder_regex: String,
|
||||
path_regex: String,
|
||||
route_path_regex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WebhookFilter {
|
||||
user_or_folder_regex: String,
|
||||
user_or_folder_regex_value: String,
|
||||
path: String,
|
||||
runnable_kind: RunnableKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GenerateOpenAPI {
|
||||
info: Option<Info>,
|
||||
url: Option<Url>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
http_route_filters: Option<Vec<HttpRouteFilter>>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
webhook_filters: Option<Vec<WebhookFilter>>,
|
||||
#[serde(default)]
|
||||
openapi_spec_format: Format,
|
||||
}
|
||||
|
||||
#[cfg(feature = "http_trigger")]
|
||||
async fn http_routes_to_future_paths(
|
||||
db: &DB,
|
||||
user_db: UserDB,
|
||||
authed: &ApiAuthed,
|
||||
pg_pool: &mut PgConnection,
|
||||
http_route_filters: Option<&[HttpRouteFilter]>,
|
||||
w_id: &str,
|
||||
) -> Result<Vec<FuturePath>> {
|
||||
let mut http_routes = Vec::new();
|
||||
|
||||
if let Some(http_route_filters) = http_route_filters {
|
||||
let path_regex = http_route_filters
|
||||
.iter()
|
||||
.map(|filter| {
|
||||
transform_to_minified_postgres_regex(&format!(
|
||||
"f/{}/{}",
|
||||
filter.folder_regex, filter.path_regex
|
||||
))
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let route_path_regex = http_route_filters
|
||||
.iter()
|
||||
.map(|filter| transform_to_minified_postgres_regex(&filter.route_path_regex))
|
||||
.collect_vec();
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MinifiedHttpTrigger {
|
||||
route_path: String,
|
||||
http_method: HttpMethod,
|
||||
is_async: bool,
|
||||
workspaced_route: bool,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
authentication_method: AuthenticationMethod,
|
||||
authentication_resource_path: Option<String>,
|
||||
}
|
||||
|
||||
http_routes = sqlx::query_as!(
|
||||
MinifiedHttpTrigger,
|
||||
r#"
|
||||
SELECT
|
||||
route_path,
|
||||
http_method AS "http_method: _",
|
||||
is_async,
|
||||
workspaced_route,
|
||||
summary,
|
||||
description,
|
||||
authentication_method AS "authentication_method: _",
|
||||
authentication_resource_path
|
||||
FROM
|
||||
http_trigger
|
||||
WHERE
|
||||
path ~ ANY($1) AND
|
||||
route_path ~ ANY($2) AND
|
||||
workspace_id = $3
|
||||
"#,
|
||||
&path_regex,
|
||||
&route_path_regex,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(pg_pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut openapi_future_paths = Vec::with_capacity(http_routes.len());
|
||||
|
||||
for http_route in http_routes {
|
||||
let auth_method = match http_route.authentication_method {
|
||||
AuthenticationMethod::BasicHttp => Some(SecurityScheme::BasicHttp),
|
||||
AuthenticationMethod::Windmill => Some(SecurityScheme::BearerJwt),
|
||||
AuthenticationMethod::ApiKey => {
|
||||
let resource_path = match http_route.authentication_resource_path {
|
||||
Some(resource_path) => resource_path,
|
||||
None => {
|
||||
return Err(Error::BadRequest(
|
||||
"Missing authentication resource path".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let api = try_get_resource_from_db_as::<ApiKeyAuthentication>(
|
||||
authed,
|
||||
Some(user_db.clone()),
|
||||
db,
|
||||
&resource_path,
|
||||
w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Some(SecurityScheme::ApiKey(api.api_key_header))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let route_path = if http_route.workspaced_route {
|
||||
format!("{}/{}", w_id, http_route.route_path.trim_start_matches('/'))
|
||||
} else {
|
||||
http_route.route_path.clone()
|
||||
};
|
||||
|
||||
let method = match http_route.http_method {
|
||||
HttpMethod::Get => Method::GET,
|
||||
HttpMethod::Post => Method::POST,
|
||||
HttpMethod::Put => Method::PUT,
|
||||
HttpMethod::Patch => Method::PATCH,
|
||||
HttpMethod::Delete => Method::DELETE,
|
||||
};
|
||||
|
||||
let future_path = FuturePath::new(
|
||||
route_path,
|
||||
Kind::HttpRoute(HttpRouteConfig::new(method)),
|
||||
Some(http_route.is_async),
|
||||
http_route.summary,
|
||||
http_route.description,
|
||||
auth_method,
|
||||
);
|
||||
|
||||
openapi_future_paths.push(future_path);
|
||||
}
|
||||
|
||||
Ok(openapi_future_paths)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "http_trigger"))]
|
||||
async fn http_routes_to_future_paths(
|
||||
_db: &DB,
|
||||
_user_db: UserDB,
|
||||
_authed: &ApiAuthed,
|
||||
_pg_pool: &mut PgConnection,
|
||||
_http_route_filters: Option<&[HttpRouteFilter]>,
|
||||
_w_id: &str,
|
||||
) -> Result<Vec<FuturePath>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn webhook_to_future_paths(
|
||||
pg_pool: &mut PgConnection,
|
||||
webhook_filters: Option<&[WebhookFilter]>,
|
||||
w_id: &str,
|
||||
) -> Result<Vec<FuturePath>> {
|
||||
let mut openapi_future_paths = Vec::new();
|
||||
if let Some(webhook_filters) = webhook_filters {
|
||||
let mut script_webhook_filter = Vec::new();
|
||||
let mut flow_webhook_filter = Vec::new();
|
||||
|
||||
for webhook in webhook_filters {
|
||||
let full_regex = transform_to_minified_postgres_regex(&format!(
|
||||
"{}/{}/{}",
|
||||
&webhook.user_or_folder_regex, &webhook.user_or_folder_regex_value, &webhook.path
|
||||
));
|
||||
|
||||
match webhook.runnable_kind {
|
||||
RunnableKind::Script => {
|
||||
script_webhook_filter.push(full_regex);
|
||||
}
|
||||
RunnableKind::Flow => {
|
||||
flow_webhook_filter.push(full_regex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Hash)]
|
||||
struct MinifiedWebhook {
|
||||
path: String,
|
||||
description: Option<String>,
|
||||
summary: Option<String>,
|
||||
}
|
||||
|
||||
impl PartialEq for MinifiedWebhook {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MinifiedWebhook {}
|
||||
|
||||
let webhook_scripts = sqlx::query_as!(
|
||||
MinifiedWebhook,
|
||||
r#"SELECT
|
||||
path,
|
||||
summary,
|
||||
description
|
||||
FROM
|
||||
script
|
||||
WHERE
|
||||
path ~ ANY($1) AND
|
||||
workspace_id = $2
|
||||
"#,
|
||||
&script_webhook_filter,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *pg_pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.unique()
|
||||
.collect_vec();
|
||||
|
||||
let webhook_flows = sqlx::query_as!(
|
||||
MinifiedWebhook,
|
||||
r#"SELECT
|
||||
path,
|
||||
summary,
|
||||
description
|
||||
FROM
|
||||
flow
|
||||
WHERE
|
||||
path ~ ANY($1) AND
|
||||
workspace_id = $2
|
||||
"#,
|
||||
&flow_webhook_filter,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *pg_pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.unique()
|
||||
.collect_vec();
|
||||
|
||||
openapi_future_paths.reserve_exact(webhook_scripts.len() + webhook_flows.len());
|
||||
|
||||
for webhook in webhook_scripts {
|
||||
openapi_future_paths.push(FuturePath::new(
|
||||
webhook.path,
|
||||
Kind::Webhook(WebhookConfig::new(RunnableKind::Script)),
|
||||
None,
|
||||
webhook.summary,
|
||||
webhook.description,
|
||||
Some(SecurityScheme::BearerJwt),
|
||||
));
|
||||
}
|
||||
|
||||
for webhook in webhook_flows {
|
||||
openapi_future_paths.push(FuturePath::new(
|
||||
webhook.path,
|
||||
Kind::Webhook(WebhookConfig::new(RunnableKind::Flow)),
|
||||
None,
|
||||
webhook.summary,
|
||||
webhook.description,
|
||||
Some(SecurityScheme::BearerJwt),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(openapi_future_paths)
|
||||
}
|
||||
|
||||
async fn generate_openapi_future_path(
|
||||
db: &DB,
|
||||
user_db: UserDB,
|
||||
authed: &ApiAuthed,
|
||||
http_route_filters: Option<&[HttpRouteFilter]>,
|
||||
webhook_filters: Option<&[WebhookFilter]>,
|
||||
w_id: &str,
|
||||
) -> Result<Vec<FuturePath>> {
|
||||
if http_route_filters.is_none() && webhook_filters.is_none() {
|
||||
return Err(Error::BadRequest(
|
||||
"Expected http route filter and/or webhook filters".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut tx = user_db.clone().begin(authed).await?;
|
||||
|
||||
let mut openapi_future_paths =
|
||||
http_routes_to_future_paths(db, user_db, authed, &mut tx, http_route_filters, w_id).await?;
|
||||
|
||||
openapi_future_paths
|
||||
.append(&mut webhook_to_future_paths(&mut tx, webhook_filters, w_id).await?);
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
if openapi_future_paths.is_empty() {
|
||||
return Err(Error::NotFound(
|
||||
"No match for the current filter".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(openapi_future_paths)
|
||||
}
|
||||
|
||||
async fn generate_openapi_spec(
|
||||
Extension(authed): Extension<ApiAuthed>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(generate_openapi): Json<GenerateOpenAPI>,
|
||||
) -> Result<String> {
|
||||
let openapi_future_paths = generate_openapi_future_path(
|
||||
&db,
|
||||
user_db,
|
||||
&authed,
|
||||
generate_openapi.http_route_filters.as_deref(),
|
||||
generate_openapi.webhook_filters.as_deref(),
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let openapi_document = generate_openapi_document(
|
||||
generate_openapi.info.as_ref(),
|
||||
generate_openapi.url.as_ref(),
|
||||
openapi_future_paths,
|
||||
generate_openapi.openapi_spec_format,
|
||||
);
|
||||
|
||||
openapi_document
|
||||
}
|
||||
|
||||
async fn download_spec(
|
||||
Extension(authed): Extension<ApiAuthed>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(generate_openapi): Json<GenerateOpenAPI>,
|
||||
) -> Result<Response> {
|
||||
let openapi_future_paths = generate_openapi_future_path(
|
||||
&db,
|
||||
user_db,
|
||||
&authed,
|
||||
generate_openapi.http_route_filters.as_deref(),
|
||||
generate_openapi.webhook_filters.as_deref(),
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let openapi_document = generate_openapi_document(
|
||||
generate_openapi.info.as_ref(),
|
||||
generate_openapi.url.as_ref(),
|
||||
openapi_future_paths,
|
||||
generate_openapi.openapi_spec_format,
|
||||
)?;
|
||||
|
||||
let response = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/octet-stream"),
|
||||
)
|
||||
.body(Body::from(openapi_document))
|
||||
.unwrap();
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,15 @@ use crate::{
|
||||
};
|
||||
use chrono::Utc;
|
||||
use itertools::Itertools;
|
||||
use native_tls::{Certificate, TlsConnector};
|
||||
use pg_escape::quote_identifier;
|
||||
use pg_escape::{quote_identifier, quote_literal};
|
||||
use rand::Rng;
|
||||
use rust_postgres::{config::SslMode, Client, Config, NoTls};
|
||||
use rust_postgres_native_tls::MakeTlsConnector;
|
||||
use serde_json::value::RawValue;
|
||||
use sqlx::{
|
||||
postgres::{PgConnectOptions, PgSslMode},
|
||||
Connection, PgConnection,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use axum::{
|
||||
routing::{delete, get, post},
|
||||
@@ -31,7 +33,7 @@ use handler::{
|
||||
};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{to_anyhow, Error, Result},
|
||||
error::{Error, Result},
|
||||
utils::StripPath,
|
||||
};
|
||||
mod bool;
|
||||
@@ -50,144 +52,76 @@ const ERROR_REPLICATION_SLOT_NOT_EXISTS: &str = r#"The replication slot associat
|
||||
|
||||
const ERROR_PUBLICATION_NAME_NOT_EXISTS: &str = r#"The publication associated with this trigger no longer exists. Recreate a new publication or select an existing one in the advanced tab, or delete and recreate a new trigger"#;
|
||||
|
||||
fn build_tls_connector(
|
||||
ssl_mode: SslMode,
|
||||
root_certificate_pem: Option<&String>,
|
||||
) -> Result<Option<MakeTlsConnector>> {
|
||||
let get_tls_builder_for_verify = |root_certificate: Option<&String>| {
|
||||
let mut builder = TlsConnector::builder();
|
||||
if let Some(root_certificate) = root_certificate {
|
||||
let root_certificate_pem =
|
||||
Certificate::from_pem(root_certificate.as_bytes()).map_err(to_anyhow)?;
|
||||
builder.add_root_certificate(root_certificate_pem);
|
||||
}
|
||||
Ok::<_, Error>(builder)
|
||||
};
|
||||
let connector = match ssl_mode {
|
||||
SslMode::Disable => return Ok(None),
|
||||
SslMode::Require | SslMode::Prefer => {
|
||||
let mut builder = TlsConnector::builder();
|
||||
builder.danger_accept_invalid_certs(true);
|
||||
builder.danger_accept_invalid_hostnames(true);
|
||||
builder
|
||||
}
|
||||
|
||||
SslMode::VerifyCa => {
|
||||
let mut builder = get_tls_builder_for_verify(root_certificate_pem)?;
|
||||
builder.danger_accept_invalid_hostnames(true);
|
||||
builder
|
||||
}
|
||||
|
||||
SslMode::VerifyFull => {
|
||||
let builder = get_tls_builder_for_verify(root_certificate_pem)?;
|
||||
builder
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Ok(Some(MakeTlsConnector::new(
|
||||
connector.build().map_err(to_anyhow)?,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn get_raw_postgres_connection(
|
||||
database: &Postgres,
|
||||
logical_mode: bool,
|
||||
) -> Result<Client> {
|
||||
let ssl_mode = match database.sslmode.as_ref() {
|
||||
"disable" => SslMode::Disable,
|
||||
"" | "prefer" | "allow" => SslMode::Prefer,
|
||||
"require" => SslMode::Require,
|
||||
"verify-ca" => SslMode::VerifyCa,
|
||||
"verify-full" => SslMode::VerifyFull,
|
||||
ssl_mode => {
|
||||
return Err(Error::BadRequest(
|
||||
format!("Invalid ssl mode for postgres: {}, please put a valid ssl_mode among the following available ssl mode: ['disable', 'allow', 'prefer', 'verify-ca', 'verify-full']", ssl_mode),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let mut config = Config::new();
|
||||
config
|
||||
.dbname(&database.dbname)
|
||||
.host(&database.host)
|
||||
.user(&database.user)
|
||||
.ssl_mode(ssl_mode);
|
||||
|
||||
if logical_mode {
|
||||
config.replication_mode(rust_postgres::config::ReplicationMode::Logical);
|
||||
}
|
||||
|
||||
if let Some(port) = database.port {
|
||||
config.port(port);
|
||||
};
|
||||
|
||||
if !database.password.is_empty() {
|
||||
config.password(&database.password);
|
||||
}
|
||||
|
||||
let connector = build_tls_connector(ssl_mode, database.root_certificate_pem.as_ref())?;
|
||||
|
||||
let client = if let Some(connector) = connector {
|
||||
let (client, connection) = config.connect(connector).await.map_err(to_anyhow)?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
tracing::debug!("{:#?}", e);
|
||||
};
|
||||
tracing::info!("Successfully Connected into database");
|
||||
});
|
||||
client
|
||||
} else {
|
||||
let (client, connection) = config.connect(NoTls).await.map_err(to_anyhow)?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
tracing::debug!("{:#?}", e);
|
||||
};
|
||||
tracing::info!("Successfully Connected into database");
|
||||
});
|
||||
client
|
||||
};
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn get_pg_connection(
|
||||
authed: ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
db: &DB,
|
||||
postgres_resource_path: &str,
|
||||
w_id: &str,
|
||||
logical_mode: bool,
|
||||
) -> Result<Client> {
|
||||
) -> Result<PgConnection> {
|
||||
let database =
|
||||
try_get_resource_from_db_as::<Postgres>(&authed, user_db, db, postgres_resource_path, w_id)
|
||||
try_get_resource_from_db_as::<Postgres>(authed, user_db, db, postgres_resource_path, w_id)
|
||||
.await?;
|
||||
|
||||
Ok(get_raw_postgres_connection(&database, logical_mode).await?)
|
||||
Ok(get_raw_postgres_connection(&database).await?)
|
||||
}
|
||||
|
||||
pub async fn get_default_pg_connection(
|
||||
authed: ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
db: &DB,
|
||||
postgres_resource_path: &str,
|
||||
w_id: &str,
|
||||
) -> Result<Client> {
|
||||
get_pg_connection(authed, user_db, db, postgres_resource_path, w_id, false).await
|
||||
pub async fn get_raw_postgres_connection(db: &Postgres) -> Result<PgConnection> {
|
||||
let options = {
|
||||
let sslmode = if !db.sslmode.is_empty() {
|
||||
PgSslMode::from_str(&db.sslmode)?
|
||||
} else {
|
||||
PgSslMode::Prefer
|
||||
};
|
||||
let options = {
|
||||
let inner_options = PgConnectOptions::new()
|
||||
.host(&db.host)
|
||||
.database(&db.dbname)
|
||||
.ssl_mode(sslmode)
|
||||
.username(&db.user);
|
||||
|
||||
if let Some(port) = db.port {
|
||||
inner_options.port(port)
|
||||
} else {
|
||||
inner_options
|
||||
}
|
||||
};
|
||||
|
||||
let options = if let Some(root_certificate_pem) = &db.root_certificate_pem {
|
||||
options.ssl_root_cert_from_pem(root_certificate_pem.as_bytes().to_vec())
|
||||
} else {
|
||||
options
|
||||
};
|
||||
|
||||
if !db.password.is_empty() {
|
||||
options.password(&db.password)
|
||||
} else {
|
||||
options
|
||||
}
|
||||
};
|
||||
Ok(PgConnection::connect_with(&options).await?)
|
||||
}
|
||||
|
||||
pub async fn create_logical_replication_slot(tx: &Client, slot_name: &str) -> Result<()> {
|
||||
tx.execute(
|
||||
&format!("SELECT * FROM pg_create_logical_replication_slot($1, 'pgoutput')"),
|
||||
&[&slot_name],
|
||||
)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
pub async fn create_logical_replication_slot(
|
||||
pg_connection: &mut PgConnection,
|
||||
name: &str,
|
||||
) -> Result<()> {
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
pg_create_logical_replication_slot({}, 'pgoutput');"#,
|
||||
quote_literal(&name)
|
||||
);
|
||||
|
||||
sqlx::query(&query).execute(pg_connection).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_if_valid_publication_for_postgres_version(
|
||||
pg_connection: &Client,
|
||||
pg_connection: &mut PgConnection,
|
||||
table_to_track: Option<&[Relations]>,
|
||||
) -> Result<bool> {
|
||||
let postgres_version = get_postgres_version_internal(pg_connection).await?;
|
||||
@@ -221,7 +155,7 @@ async fn check_if_valid_publication_for_postgres_version(
|
||||
}
|
||||
|
||||
pub async fn create_pg_publication(
|
||||
pg_connection: &Client,
|
||||
pg_connection: &mut PgConnection,
|
||||
publication_name: &str,
|
||||
table_to_track: Option<&[Relations]>,
|
||||
transaction_to_track: &[String],
|
||||
@@ -243,7 +177,7 @@ pub async fn create_pg_publication(
|
||||
} else {
|
||||
if pg_14 && first {
|
||||
query.push_str(" TABLE ONLY ");
|
||||
first = false
|
||||
first = false;
|
||||
} else if !pg_14 {
|
||||
query.push_str(" TABLE ONLY ");
|
||||
}
|
||||
@@ -290,22 +224,20 @@ pub async fn create_pg_publication(
|
||||
query.push_str("');");
|
||||
}
|
||||
|
||||
pg_connection
|
||||
.execute(&query, &[])
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
sqlx::query(&query).execute(pg_connection).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn drop_publication(pg_connection: &Client, publication_name: &str) -> Result<()> {
|
||||
pub async fn drop_publication(
|
||||
pg_connection: &mut PgConnection,
|
||||
publication_name: &str,
|
||||
) -> Result<()> {
|
||||
let mut query = String::from("DROP PUBLICATION IF EXISTS ");
|
||||
let quoted_publication_name = quote_identifier(publication_name);
|
||||
query.push_str("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),
|
||||
))
|
||||
}
|
||||
@@ -417,7 +512,7 @@ impl PostgresConfig {
|
||||
};
|
||||
|
||||
let database = try_get_resource_from_db_as::<Postgres>(
|
||||
&authed,
|
||||
authed,
|
||||
Some(UserDB::new(db.clone())),
|
||||
&db,
|
||||
postgres_resource_path,
|
||||
@@ -432,13 +527,12 @@ impl PostgresConfig {
|
||||
"SELECT pubname FROM pg_publication WHERE pubname = {}",
|
||||
quote_literal(&publication_name)
|
||||
))
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
.await?;
|
||||
|
||||
if !publication.row_exist() {
|
||||
return Err(Error::BadConfig(
|
||||
return Err(Error::Common(error::Error::BadConfig(
|
||||
ERROR_PUBLICATION_NAME_NOT_EXISTS.to_string(),
|
||||
));
|
||||
)));
|
||||
}
|
||||
|
||||
let replication_slot = client
|
||||
@@ -446,19 +540,17 @@ impl PostgresConfig {
|
||||
"SELECT slot_name FROM pg_replication_slots WHERE slot_name = {}",
|
||||
quote_literal(&replication_slot_name)
|
||||
))
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
.await?;
|
||||
|
||||
if !replication_slot.row_exist() {
|
||||
return Err(Error::BadConfig(
|
||||
return Err(Error::Common(error::Error::BadConfig(
|
||||
ERROR_REPLICATION_SLOT_NOT_EXISTS.to_string(),
|
||||
));
|
||||
)));
|
||||
}
|
||||
|
||||
let (logical_replication_stream, logical_replication_settings) = client
|
||||
.get_logical_replication_stream(&publication_name, &replication_slot_name)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
.await?;
|
||||
|
||||
Ok((logical_replication_stream, logical_replication_settings))
|
||||
}
|
||||
@@ -493,7 +585,7 @@ impl PostgresConfig {
|
||||
|
||||
let user_db = UserDB::new(db.clone());
|
||||
|
||||
let mut pg_connection = get_default_pg_connection(
|
||||
let mut pg_connection = get_pg_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
&db,
|
||||
|
||||
@@ -1217,7 +1217,7 @@ async fn update_resource_type(
|
||||
)
|
||||
))]
|
||||
pub async fn try_get_resource_from_db_as<T>(
|
||||
authed: &ApiAuthed,
|
||||
authed: ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
db: &DB,
|
||||
resource_path: &str,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use axum::{body::Body, response::Response};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
@@ -29,6 +31,23 @@ pub struct WithStarredInfoQuery {
|
||||
pub with_starred_info: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RunnableKind {
|
||||
Script,
|
||||
Flow,
|
||||
}
|
||||
|
||||
impl Display for RunnableKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let runnable_kind = match self {
|
||||
RunnableKind::Script => "script",
|
||||
RunnableKind::Flow => "flow"
|
||||
};
|
||||
write!(f, "{}", runnable_kind)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
|
||||
let is_admin = is_super_admin_email(db, email).await?;
|
||||
|
||||
|
||||
@@ -548,8 +548,6 @@ pub(crate) async fn tarball_workspace(
|
||||
authentication_resource_path,
|
||||
script_path,
|
||||
is_flow,
|
||||
summary,
|
||||
description,
|
||||
edited_by,
|
||||
edited_at,
|
||||
email,
|
||||
|
||||
@@ -80,7 +80,6 @@ backon.workspace = true
|
||||
openidconnect = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
strum_macros.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
semver.workspace = true
|
||||
croner = "2.0.6"
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -22,14 +22,10 @@ use croner::Cron;
|
||||
use rand::{distr::Alphanumeric, rng, Rng};
|
||||
use reqwest::Client;
|
||||
use semver::Version;
|
||||
use serde::{de::Error as SerdeDeserializerError, Deserialize, Deserializer, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Display;
|
||||
use std::{fs::DirBuilder as SyncDirBuilder, str::FromStr};
|
||||
use tokio::fs::DirBuilder as AsyncDirBuilder;
|
||||
use url::Url;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub const MAX_PER_PAGE: usize = 10000;
|
||||
pub const DEFAULT_PER_PAGE: usize = 1000;
|
||||
@@ -37,10 +33,6 @@ pub const DEFAULT_PER_PAGE: usize = 1000;
|
||||
pub const GIT_VERSION: &str =
|
||||
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
|
||||
pub const AGENT_JWT_PREFIX: &str = "jwt_agent_";
|
||||
pub const WORKER_NAME_PREFIX: &str = "wk";
|
||||
pub const AGENT_WORKER_NAME_PREFIX: &str = "ag";
|
||||
|
||||
use crate::CRITICAL_ALERT_MUTE_UI_ENABLED;
|
||||
use std::panic::{self, AssertUnwindSafe, Location};
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -61,12 +53,6 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
).unwrap_or(Version::new(0, 1, 0));
|
||||
|
||||
pub static ref HOSTNAME :String = std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| {
|
||||
gethostname()
|
||||
.to_str()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| rd_string(5))
|
||||
});
|
||||
|
||||
pub static ref MODE_AND_ADDONS: ModeAndAddons = {
|
||||
let mut search_addon = false;
|
||||
@@ -84,7 +70,7 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
Mode::Worker
|
||||
} else if &x == "agent" {
|
||||
println!("Binary is in 'agent' mode with BASE_INTERNAL_URL={}", std::env::var("BASE_INTERNAL_URL").unwrap_or_default());
|
||||
println!("Binary is in 'agent' mode");
|
||||
if std::env::var("BASE_INTERNAL_URL").is_err() {
|
||||
panic!("BASE_INTERNAL_URL is required in agent mode")
|
||||
}
|
||||
@@ -134,10 +120,6 @@ lazy_static::lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref AGENT_TOKEN: String = std::env::var("AGENT_TOKEN").unwrap_or_default();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ModeAndAddons {
|
||||
pub indexer: bool,
|
||||
@@ -185,6 +167,15 @@ pub async fn require_admin_or_devops(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hostname() -> String {
|
||||
std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| {
|
||||
gethostname()
|
||||
.to_str()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| rd_string(5))
|
||||
})
|
||||
}
|
||||
|
||||
fn instance_name(hostname: &str) -> String {
|
||||
hostname
|
||||
.replace(" ", "")
|
||||
@@ -195,30 +186,15 @@ fn instance_name(hostname: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
const DEFAULT_WORKER_SUFFIX_LEN: usize = 5;
|
||||
pub const SSH_AGENT_WORKER_SUFFIX: &'static str = "/ssh";
|
||||
|
||||
pub fn create_worker_suffix(hostname: &str, rd_string_len: usize, ssh_ag_worker: bool) -> String {
|
||||
let mut wk_suffix = format!("{}-{}", instance_name(hostname), rd_string(rd_string_len));
|
||||
if ssh_ag_worker {
|
||||
wk_suffix.push_str(SSH_AGENT_WORKER_SUFFIX);
|
||||
}
|
||||
wk_suffix
|
||||
}
|
||||
|
||||
pub fn create_ssh_agent_worker_suffix(hostname: &str) -> String {
|
||||
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, true)
|
||||
}
|
||||
|
||||
pub fn create_default_worker_suffix(hostname: &str) -> String {
|
||||
create_worker_suffix(hostname, DEFAULT_WORKER_SUFFIX_LEN, false)
|
||||
pub fn worker_suffix(hostname: &str, rd_string: &str) -> String {
|
||||
format!("{}-{}", instance_name(hostname), rd_string)
|
||||
}
|
||||
|
||||
pub fn worker_name_with_suffix(is_agent: bool, worker_group: &str, suffix: &str) -> String {
|
||||
if is_agent {
|
||||
format!("{}-{}-{}", AGENT_WORKER_NAME_PREFIX, worker_group, suffix)
|
||||
format!("ag-{}-{}", worker_group, suffix)
|
||||
} else {
|
||||
format!("{}-{}-{}", WORKER_NAME_PREFIX, worker_group, suffix)
|
||||
format!("wk-{}-{}", worker_group, suffix)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,21 +223,6 @@ pub async fn now_from_db<'c, E: sqlx::PgExecutor<'c>>(
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
pub async fn create_directory_async(directory_path: &str) {
|
||||
AsyncDirBuilder::new()
|
||||
.recursive(true)
|
||||
.create(directory_path)
|
||||
.await
|
||||
.expect("could not create dir");
|
||||
}
|
||||
|
||||
pub fn create_directory_sync(directory_path: &str) {
|
||||
SyncDirBuilder::new()
|
||||
.recursive(true)
|
||||
.create(directory_path)
|
||||
.expect("could not create dir");
|
||||
}
|
||||
|
||||
pub fn not_found_if_none<T, U: AsRef<str>>(opt: Option<T>, kind: &str, name: U) -> Result<T> {
|
||||
if let Some(o) = opt {
|
||||
Ok(o)
|
||||
@@ -526,18 +487,6 @@ impl<T> IsEmpty for Vec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IsEmpty for Option<T>
|
||||
where
|
||||
T: IsEmpty,
|
||||
{
|
||||
fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Some(v) => v.is_empty(),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_as_none<'de, D, T>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
@@ -547,26 +496,6 @@ where
|
||||
Ok(option.filter(|s| !s.is_empty()))
|
||||
}
|
||||
|
||||
pub fn is_empty<T>(value: &T) -> bool
|
||||
where
|
||||
T: IsEmpty,
|
||||
{
|
||||
value.is_empty()
|
||||
}
|
||||
|
||||
pub fn deserialize_url<'de, D: Deserializer<'de>>(
|
||||
de: D,
|
||||
) -> std::result::Result<Option<Url>, D::Error> {
|
||||
let intermediate = <Option<Cow<'de, str>>>::deserialize(de)?;
|
||||
|
||||
match intermediate.as_deref() {
|
||||
None | Some("") => Ok(None),
|
||||
Some(non_empty_string) => Url::parse(non_empty_string)
|
||||
.map(Some)
|
||||
.map_err(D::Error::custom),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_mute_workspace(_db: &DB, workspace_id: &str) -> Result<bool> {
|
||||
match sqlx::query!(
|
||||
"SELECT mute_critical_alerts FROM workspace_settings WHERE workspace_id = $1",
|
||||
@@ -818,20 +747,3 @@ impl<F: Future> Future for WarnAfterFuture<F> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RunnableKind {
|
||||
Script,
|
||||
Flow,
|
||||
}
|
||||
|
||||
impl Display for RunnableKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let runnable_kind = match self {
|
||||
RunnableKind::Script => "script",
|
||||
RunnableKind::Flow => "flow",
|
||||
};
|
||||
write!(f, "{}", runnable_kind)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user