Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca72c97a32 | ||
|
|
9a5baa8e53 | ||
|
|
0e3d835609 | ||
|
|
2a1bff3160 | ||
|
|
7a8a0bb163 | ||
|
|
deff3b0045 | ||
|
|
50ff183bae | ||
|
|
1b885ce2c2 | ||
|
|
cec82a9cd2 | ||
|
|
44ed4045f7 | ||
|
|
4fdca87de9 | ||
|
|
8a792c9c9d | ||
|
|
10b6b1dc04 | ||
|
|
9163060c90 | ||
|
|
2d572695ef | ||
|
|
ca8020cf82 | ||
|
|
84bd06e906 | ||
|
|
4b8f3580a4 | ||
|
|
0ea405415e | ||
|
|
d598731913 | ||
|
|
9c71503d74 | ||
|
|
5a96d64183 | ||
|
|
a017459e12 | ||
|
|
ce01fa1677 | ||
|
|
4a39e45476 | ||
|
|
60186b8c9f | ||
|
|
9f7aa01cac | ||
|
|
de00944f09 | ||
|
|
c1b220be26 | ||
|
|
50861fccc0 | ||
|
|
5e9870a8a9 | ||
|
|
1671005100 | ||
|
|
3d9ca62ab6 | ||
|
|
c3d49a352e | ||
|
|
7784c14726 | ||
|
|
e5e174ae95 | ||
|
|
1f09311a08 | ||
|
|
d832cd8b5f | ||
|
|
d9d63a4e31 | ||
|
|
d44976f35e | ||
|
|
02170032af | ||
|
|
de9a839af4 | ||
|
|
d9148eaa78 | ||
|
|
274eb78152 | ||
|
|
2774d394ad | ||
|
|
58194521b1 | ||
|
|
0d96cfb5a8 | ||
|
|
4c98410b83 | ||
|
|
8c6f5a320b | ||
|
|
8bc9a021a8 | ||
|
|
1c7d295c52 | ||
|
|
237307ba18 | ||
|
|
3ea12f1821 | ||
|
|
ae70c37363 | ||
|
|
2eb9cfd0a9 | ||
|
|
f9eb64aae2 | ||
|
|
fbdeb6be09 | ||
|
|
162070dcca | ||
|
|
a3feca7197 | ||
|
|
dc8bd6d2b5 | ||
|
|
b1b760bbeb | ||
|
|
7da4434106 | ||
|
|
24057811d1 | ||
|
|
2456ec6768 | ||
|
|
dbcf030e7b | ||
|
|
d35d2ccfd0 | ||
|
|
0501ad8a99 | ||
|
|
adeb30ca51 | ||
|
|
9022d4e1c8 | ||
|
|
22b102eda7 | ||
|
|
ceaf56c21e | ||
|
|
f767d92ec7 | ||
|
|
e9b7dca203 | ||
|
|
43b8a5ade3 | ||
|
|
9a8dcc9a25 | ||
|
|
0f29f0f190 | ||
|
|
771d740701 | ||
|
|
ad50d2fd69 | ||
|
|
7c0d9901e0 | ||
|
|
7578cebaf9 | ||
|
|
36d56e6f38 | ||
|
|
65f67ab160 | ||
|
|
789b4f6442 | ||
|
|
0c91646572 | ||
|
|
323912c73c | ||
|
|
8d8156bd07 | ||
|
|
4dda0fb8cd | ||
|
|
77735d859c | ||
|
|
8e27392afa | ||
|
|
eff3e1c43a | ||
|
|
844acbf00b | ||
|
|
e0040fe3b3 | ||
|
|
d0bad936f0 | ||
|
|
63340e6ce2 | ||
|
|
c839bae160 | ||
|
|
253452013c | ||
|
|
af70339d07 | ||
|
|
a01c26f0e2 | ||
|
|
b1bcdae01d | ||
|
|
b1a4f2ab50 | ||
|
|
1ad54e887e | ||
|
|
a446660345 | ||
|
|
c21182f502 | ||
|
|
54698b3eef | ||
|
|
59e8004f99 | ||
|
|
1271a65b5a | ||
|
|
414b9c338b | ||
|
|
53909750ef | ||
|
|
60e7fa764c | ||
|
|
a0c86ef4c7 | ||
|
|
5805554ac8 | ||
|
|
bd7607d162 | ||
|
|
7985a7bafa | ||
|
|
13896a5741 | ||
|
|
e881ed5e00 | ||
|
|
d6a654aaff |
58
.github/workflows/build_windows_worker_.yml
vendored
Normal file
58
.github/workflows/build_windows_worker_.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Build windows executable for this branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
SQLX_OFFLINE: true
|
||||
DISABLE_EMBEDDING: true
|
||||
RUST_LOG: info
|
||||
|
||||
jobs:
|
||||
cargo_build_windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Read EE repo commit hash
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ee_repo_ref = Get-Content .\backend\ee-repo-ref.txt
|
||||
echo "ee_repo_ref=$ee_repo_ref" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
- name: Checkout windmill-ee-private repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: windmill-labs/windmill-ee-private
|
||||
path: ./windmill-ee-private
|
||||
ref: ${{ env.ee_repo_ref }}
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Substitute EE code
|
||||
shell: bash
|
||||
run: |
|
||||
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
|
||||
|
||||
- name: Cargo build windows
|
||||
timeout-minutes: 90
|
||||
run: |
|
||||
vcpkg.exe install openssl-windows:x64-windows
|
||||
vcpkg.exe install openssl:x64-windows-static
|
||||
vcpkg.exe integrate install
|
||||
$env:VCPKGRS_DYNAMIC=1
|
||||
$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,deno_core
|
||||
|
||||
- name: Rename binary with corresponding architecture
|
||||
run: |
|
||||
Rename-Item -Path ".\backend\target\release\windmill.exe" -NewName "windmill-ee.exe"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windmill-ee-binary
|
||||
path: ./backend/target/release/windmill-ee.exe
|
||||
101
CHANGELOG.md
101
CHANGELOG.md
@@ -1,5 +1,106 @@
|
||||
# Changelog
|
||||
|
||||
## [1.423.2](https://github.com/windmill-labs/windmill/compare/v1.423.1...v1.423.2) (2024-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix intempestive expr type change in flow transform ([84bd06e](https://github.com/windmill-labs/windmill/commit/84bd06e90650e949c11e2b09447a7bad1ab60a95))
|
||||
|
||||
## [1.423.1](https://github.com/windmill-labs/windmill/compare/v1.423.0...v1.423.1) (2024-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **autoscaling:** autoscaling thresholds to be >= and not > ([de00944](https://github.com/windmill-labs/windmill/commit/de00944f09699ff33a382f5a14f515ccf90b2454))
|
||||
|
||||
## [1.423.0](https://github.com/windmill-labs/windmill/compare/v1.422.1...v1.423.0) (2024-11-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* s3 input available for public apps ([#4685](https://github.com/windmill-labs/windmill/issues/4685)) ([1671005](https://github.com/windmill-labs/windmill/commit/167100510032ab53cd609fb2c7629e67faceb093))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent underflow for autoscaling scalein ([5e9870a](https://github.com/windmill-labs/windmill/commit/5e9870a8a993032ec7d45c62e0023008da42c74a))
|
||||
* support multiple pip-extra-index-url with commas ([50861fc](https://github.com/windmill-labs/windmill/commit/50861fccc0cd86d4c76120c3306adf94f375330e))
|
||||
|
||||
## [1.422.1](https://github.com/windmill-labs/windmill/compare/v1.422.0...v1.422.1) (2024-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix password inputs ([e5e174a](https://github.com/windmill-labs/windmill/commit/e5e174ae9516f4c6b94ceb6e258b467f5c9a1f1a))
|
||||
|
||||
## [1.422.0](https://github.com/windmill-labs/windmill/compare/v1.421.2...v1.422.0) (2024-11-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* expandable subflows in flows ([#4683](https://github.com/windmill-labs/windmill/issues/4683)) ([d44976f](https://github.com/windmill-labs/windmill/commit/d44976f35e45ade510d1ec220b5a1503e11f3db9))
|
||||
* **frontend:** critical alerts UI ([#4653](https://github.com/windmill-labs/windmill/issues/4653)) ([d9148ea](https://github.com/windmill-labs/windmill/commit/d9148eaa78680a93d81d71847a7df67e01f3c110))
|
||||
|
||||
## [1.421.2](https://github.com/windmill-labs/windmill/compare/v1.421.1...v1.421.2) (2024-11-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bash:** correctly propagate exit errors ([8bc9a02](https://github.com/windmill-labs/windmill/commit/8bc9a021a88391147c9170f56b5a0edddd55bc7d))
|
||||
|
||||
## [1.421.1](https://github.com/windmill-labs/windmill/compare/v1.421.0...v1.421.1) (2024-11-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **python-client:** fix small break params of write_s3_file ([3ea12f1](https://github.com/windmill-labs/windmill/commit/3ea12f1821500e8b84549b892e3e1bb56a6ace4b))
|
||||
|
||||
## [1.421.0](https://github.com/windmill-labs/windmill/compare/v1.420.1...v1.421.0) (2024-11-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* http custom routes for static assets ([#4666](https://github.com/windmill-labs/windmill/issues/4666)) ([dc8bd6d](https://github.com/windmill-labs/windmill/commit/dc8bd6d2b5d6f5cd8521f9034853175ec78d5639))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve nested schema editor field change ([a3feca7](https://github.com/windmill-labs/windmill/commit/a3feca719799ea2bf08f2b49350e6b732a24abf4))
|
||||
|
||||
## [1.420.1](https://github.com/windmill-labs/windmill/compare/v1.420.0...v1.420.1) (2024-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve prop filtering on flow prop picker ([0501ad8](https://github.com/windmill-labs/windmill/commit/0501ad8a99f6f7d9fd26c996b754e8afe8f958b1))
|
||||
|
||||
## [1.420.0](https://github.com/windmill-labs/windmill/compare/v1.419.0...v1.420.0) (2024-11-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** detect expr in flow input transform + filter right panel based on expr ([#4651](https://github.com/windmill-labs/windmill/issues/4651)) ([e9b7dca](https://github.com/windmill-labs/windmill/commit/e9b7dca20387e775fa50aaecd832890251582cf9))
|
||||
* **frontend:** nodes from flow can be connected directly in expr input through a plug icon ([#4652](https://github.com/windmill-labs/windmill/issues/4652)) ([ceaf56c](https://github.com/windmill-labs/windmill/commit/ceaf56c21ee2e548bc93859f9e0303e53b25b241))
|
||||
|
||||
## [1.419.0](https://github.com/windmill-labs/windmill/compare/v1.418.0...v1.419.0) (2024-11-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add full-text search on windmill service logs ([#4576](https://github.com/windmill-labs/windmill/issues/4576)) ([77735d8](https://github.com/windmill-labs/windmill/commit/77735d859cfee9204f67a8e4f9885228d657a41d))
|
||||
* show path in flow script picker ([#4574](https://github.com/windmill-labs/windmill/issues/4574)) ([8e27392](https://github.com/windmill-labs/windmill/commit/8e27392afacbb725aaf9f9f892ab8a6171b59ce5))
|
||||
* websocket authentication ([#4635](https://github.com/windmill-labs/windmill/issues/4635)) ([4dda0fb](https://github.com/windmill-labs/windmill/commit/4dda0fb8cd8262ad3a2ab2b9d27e7043ac3bb891))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clarify error messages when job timeout or cancelled with more details ([771d740](https://github.com/windmill-labs/windmill/commit/771d740701902166f8b4e3f77aa9c5579237cb15))
|
||||
* **cli:** improve handling of deleted items on windows ([9a8dcc9](https://github.com/windmill-labs/windmill/commit/9a8dcc9a250caefa0b7c9523e1321599b7471c8b))
|
||||
* display logs in native mode when script fails ([#4655](https://github.com/windmill-labs/windmill/issues/4655)) ([7578ceb](https://github.com/windmill-labs/windmill/commit/7578cebaf92e729a26fd665e4b6f8357d34f59eb))
|
||||
* **frontend:** arg input json handling when the value is not of the same type as schema ([#4479](https://github.com/windmill-labs/windmill/issues/4479)) ([8d8156b](https://github.com/windmill-labs/windmill/commit/8d8156bd0773da3ddec81c46ad5fda114ecd3dda))
|
||||
* **frontend:** improve flow prop picker design ([323912c](https://github.com/windmill-labs/windmill/commit/323912c73c18d3bb6d136f2e6458389270364a0e))
|
||||
|
||||
## [1.418.0](https://github.com/windmill-labs/windmill/compare/v1.417.3...v1.418.0) (2024-11-04)
|
||||
|
||||
|
||||
|
||||
@@ -175,9 +175,9 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
|
||||
COPY --from=builder /frontend/build /static_frontend
|
||||
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
|
||||
|
||||
COPY --from=denoland/deno:2.0.2 --chmod=755 /usr/bin/deno /usr/bin/deno
|
||||
COPY --from=denoland/deno:2.0.4 --chmod=755 /usr/bin/deno /usr/bin/deno
|
||||
|
||||
COPY --from=oven/bun:1.1.32 /usr/local/bin/bun /usr/bin/bun
|
||||
COPY --from=oven/bun:1.1.34 /usr/local/bin/bun /usr/bin/bun
|
||||
|
||||
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
|
||||
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
@@ -355,6 +355,8 @@ you to have it being synced automatically everyday.
|
||||
See the [./frontend/README_DEV.md](./frontend/README_DEV.md) file for all
|
||||
running options.
|
||||
|
||||
Using [Nix](./frontend/README_DEV.md#nix).
|
||||
|
||||
### only Frontend
|
||||
This will use the backend of <https://app.windmill.dev> but your own frontend
|
||||
with hot-code reloading. Note that you will need to use a username / password login due to CSRF checks using a different auth provider.
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message) VALUES ('recovered_critical_error', $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "00ce4ed3ca0eac7cb6283b047353a64b9e78c4beb423f04baef9a53fbf87e9f9"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, edited_by = $6, email = $7, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL\n WHERE workspace_id = $8 AND path = $9",
|
||||
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL\n WHERE workspace_id = $10 AND path = $11",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,6 +10,8 @@
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"JsonbArray",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
@@ -18,5 +20,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "acbf74cf3302bfcf7615285070d3f8958932bb8a2dda715f1b9152ab44442780"
|
||||
"hash": "0b94bd4c98a11ca1b7e5e34dd1ee6fcb0b7a54ed4218fa3cf23cc929d009d50f"
|
||||
}
|
||||
48
backend/.sqlx/query-0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995.json
generated
Normal file
48
backend/.sqlx/query-0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995.json
generated
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, alert_type, message, created_at, acknowledged \n FROM alerts \n WHERE acknowledged = $1\n ORDER BY created_at DESC \n LIMIT $2 OFFSET $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "alert_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "message",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "acknowledged",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\" FROM http_trigger",
|
||||
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\" FROM http_trigger",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -65,6 +65,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -80,8 +85,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "fe4f91ca7e179e58041a5c0b1a36015175e9301c01a99e8f2b82cb853349a183"
|
||||
"hash": "11b698f82a54aac68b3617047dfe2b18dd6da7d962118fee276af354218baac2"
|
||||
}
|
||||
29
backend/.sqlx/query-1e7ce0c140410ae799f9c0c5772e4be4506bfd238c88f1b1c3ddf39b29071446.json
generated
Normal file
29
backend/.sqlx/query-1e7ce0c140410ae799f9c0c5772e4be4506bfd238c88f1b1c3ddf39b29071446.json
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n flow_status AS \"flow_status!: Json<Box<RawValue>>\",\n raw_flow->'modules'->(flow_status->'step')::int AS \"module: Json<Box<RawValue>>\"\n FROM queue WHERE id = $1 AND workspace_id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "flow_status!: Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "module: Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1e7ce0c140410ae799f9c0c5772e4be4506bfd238c88f1b1c3ddf39b29071446"
|
||||
}
|
||||
22
backend/.sqlx/query-31dd23f6768052e486668172e15eba1a3f9b6a8e5678a7ee8e5456ff4405d6f9.json
generated
Normal file
22
backend/.sqlx/query-31dd23f6768052e486668172e15eba1a3f9b6a8e5678a7ee8e5456ff4405d6f9.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb\n FROM completed_job\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "31dd23f6768052e486668172e15eba1a3f9b6a8e5678a7ee8e5456ff4405d6f9"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now())",
|
||||
"query": "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now())",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -27,11 +27,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c229744534f17f7622c3dee21bb1e7292ff17e6dffe58e80e53bff8baade07c8"
|
||||
"hash": "333b484ffa030dee08e7b1161fcbc48af411377d2d9f58f92fc9d5eacdf0fba1"
|
||||
}
|
||||
15
backend/.sqlx/query-349396e8fdd96d45875110bc06767e6eb876792ec8f83e9b03c2fb46bb12e0b9.json
generated
Normal file
15
backend/.sqlx/query-349396e8fdd96d45875110bc06767e6eb876792ec8f83e9b03c2fb46bb12e0b9.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE password SET login_type = $1 WHERE email = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "349396e8fdd96d45875110bc06767e6eb876792ec8f83e9b03c2fb46bb12e0b9"
|
||||
}
|
||||
15
backend/.sqlx/query-3a94ad52c6b7cde844fa868167248cd9ff63e5fdfa1d93d8fbec32a257b6b05e.json
generated
Normal file
15
backend/.sqlx/query-3a94ad52c6b7cde844fa868167248cd9ff63e5fdfa1d93d8fbec32a257b6b05e.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('recovered_critical_error', $1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3a94ad52c6b7cde844fa868167248cd9ff63e5fdfa1d93d8fbec32a257b6b05e"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE http_trigger \n SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, edited_by = $7, email = $8, is_async = $9, requires_auth = $10, edited_at = now() \n WHERE workspace_id = $11 AND path = $12",
|
||||
"query": "UPDATE http_trigger \n SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now() \n WHERE workspace_id = $12 AND path = $13",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -24,6 +24,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
@@ -34,5 +35,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4d8640e84fccf1a0b799d8396a51e69345137e68d5096c70ba0a4332075d97ea"
|
||||
"hash": "487d377e2df67fc3ea39d183ba9f99d45828d7c8e0ff10c5d74c454472e0493c"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message) VALUES ('critical_error', $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4c0067c2135a259aea5cc2db60f7375a9a33671be8ef406427d90f67a98c9c9f"
|
||||
}
|
||||
15
backend/.sqlx/query-4d22084a5d9860832f30e8f08cbfa1848ed3c1336fa4790f45b8189c4ac97d91.json
generated
Normal file
15
backend/.sqlx/query-4d22084a5d9860832f30e8f08cbfa1848ed3c1336fa4790f45b8189c4ac97d91.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('critical_error', $1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4d22084a5d9860832f30e8f08cbfa1848ed3c1336fa4790f45b8189c4ac97d91"
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now()) RETURNING *",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "server_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "last_server_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "filters",
|
||||
"type_info": "JsonbArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4e9668a46bad9e82baa51422946d373b18b6577198df7545c94bd19be3446775"
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT *\n FROM websocket_trigger\n WHERE enabled IS TRUE AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "server_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "last_server_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "filters",
|
||||
"type_info": "JsonbArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5303cb9dd5903aa4791ef8e5e5881a50a832e65c8c9632e2e12cd9c2747f2fc7"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, edited_by = $5, email = $6, is_async = $7, requires_auth = $8, edited_at = now() \n WHERE workspace_id = $9 AND path = $10",
|
||||
"query": "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now() \n WHERE workspace_id = $10 AND path = $11",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -22,6 +22,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
@@ -32,5 +33,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "55d44f569f8ebfccddf975e1a330ef0dc286f4138efe923832371cdbac7157b0"
|
||||
"hash": "7113d7cc72e44e4b7e01b69cc18cbe7b0399cf8ec0e9e6d2b05ceef589c432df"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH uuid_table as (\n select gen_random_uuid() as uuid from generate_series(1, $11)\n )\n INSERT INTO queue \n (id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout)\n (SELECT uuid, $1, $2, $3, $4, ('{ \"uuid\": \"' || uuid || '\" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14 FROM uuid_table) \n RETURNING id",
|
||||
"query": "WITH uuid_table as (\n select gen_random_uuid() as uuid from generate_series(1, $11)\n )\n INSERT INTO queue \n (id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout, raw_code, raw_lock, raw_flow, flow_status)\n (SELECT uuid, $1, $2, $3, $4, ('{ \"uuid\": \"' || uuid || '\" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14, $15, $16, $17, $18 FROM uuid_table) \n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -72,12 +72,16 @@
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Int4"
|
||||
"Int4",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0a686ca61444d7ad7484071727aa039a6ea6697e5a49a633b767c052aa3e0a18"
|
||||
"hash": "a69ba7471d1a5faa145bebbd17d43e6dbe02e9e92ebbc31a50c99c3a04719284"
|
||||
}
|
||||
12
backend/.sqlx/query-a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d.json
generated
Normal file
12
backend/.sqlx/query-a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts SET acknowledged = true WHERE acknowledged = false",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d"
|
||||
}
|
||||
22
backend/.sqlx/query-aa6907b7266ee437dfbd77a9bf6c047fda88f3e72a0d10323a5e4020cf857c42.json
generated
Normal file
22
backend/.sqlx/query-aa6907b7266ee437dfbd77a9bf6c047fda88f3e72a0d10323a5e4020cf857c42.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb\n FROM queue\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "aa6907b7266ee437dfbd77a9bf6c047fda88f3e72a0d10323a5e4020cf857c42"
|
||||
}
|
||||
22
backend/.sqlx/query-ad03e5acf10ef94abc37cb9f56b1775c67f075c8bc83458e8be2e242347218d6.json
generated
Normal file
22
backend/.sqlx/query-ad03e5acf10ef94abc37cb9f56b1775c67f075c8bc83458e8be2e242347218d6.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT wm_version FROM worker_ping WHERE wm_version != $1 AND ping_at > now() - interval '5 minutes'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "wm_version",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ad03e5acf10ef94abc37cb9f56b1775c67f075c8bc83458e8be2e242347218d6"
|
||||
}
|
||||
23
backend/.sqlx/query-ae543dfa106fa6ad4e9bf45cda1110d4702a80f455c63af6fcbd7cf45bbc4f7a.json
generated
Normal file
23
backend/.sqlx/query-ae543dfa106fa6ad4e9bf45cda1110d4702a80f455c63af6fcbd7cf45bbc4f7a.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT flow_version.value AS \"value: sqlx::types::Json<Box<RawValue>>\" FROM flow \n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.path = $1 AND flow.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ae543dfa106fa6ad4e9bf45cda1110d4702a80f455c63af6fcbd7cf45bbc4f7a"
|
||||
}
|
||||
65
backend/.sqlx/query-b5c839baab25c4dcdd503d380cf7a886242277cd50555f20b2e22e13942d2a3a.json
generated
Normal file
65
backend/.sqlx/query-b5c839baab25c4dcdd503d380cf7a886242277cd50555f20b2e22e13942d2a3a.json
generated
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n hostname,\n mode::text,\n worker_group,\n log_ts,\n file_path,\n ok_lines,\n err_lines,\n json_fmt\n FROM log_file\n WHERE log_ts > $1\n ORDER BY log_ts ASC LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hostname",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "mode",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "worker_group",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "log_ts",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "file_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "ok_lines",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "err_lines",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "json_fmt",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamp",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b5c839baab25c4dcdd503d380cf7a886242277cd50555f20b2e22e13942d2a3a"
|
||||
}
|
||||
14
backend/.sqlx/query-be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580.json
generated
Normal file
14
backend/.sqlx/query-be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts SET acknowledged = true WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\" FROM http_trigger WHERE workspace_id = $1",
|
||||
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\" FROM http_trigger WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -65,6 +65,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -82,8 +87,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "02f1a6eeb27067dc438459238e7b016f5ccf9e3fe0ffbe88471f15aad8f74441"
|
||||
"hash": "c9930fcfe79541af570eace58ba7e15a0816a6b4fd036cf7b991a210654b2633"
|
||||
}
|
||||
47
backend/.sqlx/query-cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b.json
generated
Normal file
47
backend/.sqlx/query-cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b.json
generated
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, alert_type, message, created_at, acknowledged \n FROM alerts \n ORDER BY created_at DESC \n LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "alert_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "message",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "acknowledged",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b"
|
||||
}
|
||||
16
backend/.sqlx/query-cd33a9d63f4706a7e3b1e23cd0a4b2e3ecb30aae6510d1fcd08493b07c8b0952.json
generated
Normal file
16
backend/.sqlx/query-cd33a9d63f4706a7e3b1e23cd0a4b2e3ecb30aae6510d1fcd08493b07c8b0952.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE websocket_trigger SET enabled = FALSE, error = $1, server_id = NULL, last_server_ping = NULL WHERE workspace_id = $2 AND path = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cd33a9d63f4706a7e3b1e23cd0a4b2e3ecb30aae6510d1fcd08493b07c8b0952"
|
||||
}
|
||||
64
backend/.sqlx/query-dd967c5983fa0ff05e2b320ad0e0b5a152784826cb8fb4381c1ffe228cb7feb6.json
generated
Normal file
64
backend/.sqlx/query-dd967c5983fa0ff05e2b320ad0e0b5a152784826cb8fb4381c1ffe228cb7feb6.json
generated
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n hostname,\n mode::text,\n worker_group,\n log_ts,\n file_path,\n ok_lines,\n err_lines,\n json_fmt\n FROM log_file\n ORDER BY log_ts ASC LIMIT $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hostname",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "mode",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "worker_group",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "log_ts",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "file_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "ok_lines",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "err_lines",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "json_fmt",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "dd967c5983fa0ff05e2b320ad0e0b5a152784826cb8fb4381c1ffe228cb7feb6"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow->'modules'->($1)->'value'->>'type' = 'flow' FROM queue WHERE id = $2",
|
||||
"query": "SELECT raw_flow->'modules'->($1)::text->'value'->>'type' = 'flow' FROM queue WHERE id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -19,5 +19,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3e539fef054ad31bc1736e27276087775a721a6ee7ae35b03fd4ce3563ea3838"
|
||||
"hash": "de1abe57b6aa61155f747a3bcb98359f70eade6654b5a884915f07f3ef3fe15e"
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT *\n FROM websocket_trigger\n WHERE workspace_id = $1 AND path = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "server_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "last_server_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "filters",
|
||||
"type_info": "JsonbArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f2baee15e6d1fecd6d2d7b39fda1b50a15ec8bd349f1081af54da5dc2f5e3021"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as \"http_method: _\", edited_by, email, edited_at, extra_perms, is_async, requires_auth\n FROM http_trigger\n WHERE workspace_id = $1 AND path = $2",
|
||||
"query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as \"http_method: _\", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as \"static_asset_config: _\"\n FROM http_trigger\n WHERE workspace_id = $1 AND path = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -80,6 +80,11 @@
|
||||
"ordinal": 12,
|
||||
"name": "requires_auth",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -101,8 +106,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4fb95eae1c871241efe2ef79615ce03cba0e4a12aad3274e4829d98e38ca1491"
|
||||
"hash": "f904702536c106b0e5da8facae119c6af887c49a29ae44b3a95350ff27fb1ccf"
|
||||
}
|
||||
438
backend/Cargo.lock
generated
438
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.418.0"
|
||||
version = "1.423.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -29,7 +29,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.418.0"
|
||||
version = "1.423.2"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -154,7 +154,7 @@ tracing-appender = "^0"
|
||||
prometheus = { version = "^0", default-features = false }
|
||||
cookie = { version = "0.17.0" }
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
rust-embed = "^6"
|
||||
rust-embed = { version = "^6", features = ["interpolate-folder-path"] }
|
||||
mime_guess = "^2"
|
||||
hex = "^0"
|
||||
sql-builder = "^3"
|
||||
@@ -180,6 +180,7 @@ tokio-util = { version = "^0", features = ["io"] }
|
||||
json-pointer = "^0"
|
||||
itertools = "^0"
|
||||
regex = "^1"
|
||||
semver = "^1"
|
||||
|
||||
deno_fetch = "0.195.0"
|
||||
deno_tls = "0.158.0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
f136a2f499e0fe7c10c54c79488851980d796eb2
|
||||
0d9c8813acd28848515c736e7b684220b5a785a3
|
||||
2
backend/migrations/20241030150214_ws_auth.down.sql
Normal file
2
backend/migrations/20241030150214_ws_auth.down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE websocket_trigger DROP COLUMN initial_messages, DROP COLUMN url_runnable_args;
|
||||
2
backend/migrations/20241030150214_ws_auth.up.sql
Normal file
2
backend/migrations/20241030150214_ws_auth.up.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE websocket_trigger ADD COLUMN initial_messages JSONB[] DEFAULT '{}', ADD COLUMN url_runnable_args JSONB DEFAULT '{}';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE alerts DROP COLUMN acknowledged;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Step 1: Add the new column 'acknowledged' to the 'alerts' table
|
||||
ALTER TABLE alerts
|
||||
ADD COLUMN acknowledged BOOLEAN DEFAULT NULL;
|
||||
|
||||
-- Step 2: Update all existing rows to set 'acknowledged' to true
|
||||
-- we don't want to pop up notifications to all users after migrations
|
||||
-- but only show new alerts from the point of the upgrade
|
||||
UPDATE alerts
|
||||
SET acknowledged = TRUE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE http_trigger DROP COLUMN static_asset_config;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE http_trigger ADD COLUMN static_asset_config JSONB;
|
||||
@@ -62,6 +62,7 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
|
||||
"lokalise" => "python-lokalise-api",
|
||||
"msgraph" => "msgraph-sdk",
|
||||
"pythonjsonlogger" => "python-json-logger",
|
||||
"socks" => "PySocks",
|
||||
};
|
||||
|
||||
fn replace_import(x: String) -> String {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
use anyhow::Context;
|
||||
use git_version::git_version;
|
||||
use monitor::{
|
||||
reload_timeout_wait_result_setting, send_current_log_file_to_object_store,
|
||||
send_logs_to_object_store,
|
||||
@@ -31,18 +30,19 @@ use windmill_common::ee::{maybe_renew_license_key_on_start, LICENSE_KEY_ID, LICE
|
||||
|
||||
use windmill_common::{
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
|
||||
CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING,
|
||||
ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
|
||||
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING,
|
||||
OAUTH_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING,
|
||||
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING,
|
||||
TIMEOUT_WAIT_RESULT_SETTING,
|
||||
},
|
||||
scripts::ScriptLang,
|
||||
stats_ee::schedule_stats,
|
||||
utils::{hostname, rd_string, Mode},
|
||||
utils::{hostname, rd_string, Mode, GIT_VERSION},
|
||||
worker::{reload_custom_tags_setting, HUB_CACHE_DIR, TMP_DIR, WORKER_GROUP},
|
||||
DB, METRICS_ENABLED,
|
||||
};
|
||||
@@ -67,16 +67,17 @@ use windmill_worker::{
|
||||
get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR,
|
||||
BUN_DEPSTAR_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM,
|
||||
GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR, POWERSHELL_CACHE_DIR,
|
||||
RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TMP_LOGS_DIR, UV_CACHE_DIR,
|
||||
PY311_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TMP_LOGS_DIR, UV_CACHE_DIR,
|
||||
};
|
||||
|
||||
use crate::monitor::{
|
||||
initial_load, load_keep_job_dir, load_metrics_debug_enabled, load_require_preexisting_user,
|
||||
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db, monitor_pool,
|
||||
reload_base_url_setting, reload_bunfig_install_scopes_setting,
|
||||
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
|
||||
reload_hub_base_url_setting, reload_job_default_timeout_setting, reload_jwt_secret_setting,
|
||||
reload_license_key, reload_npm_config_registry_setting, reload_pip_index_url_setting,
|
||||
reload_critical_alert_mute_ui_setting, reload_critical_error_channels_setting,
|
||||
reload_extra_pip_index_url_setting, reload_hub_base_url_setting,
|
||||
reload_job_default_timeout_setting, reload_jwt_secret_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_pip_index_url_setting,
|
||||
reload_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
|
||||
reload_worker_config,
|
||||
};
|
||||
@@ -84,7 +85,6 @@ use crate::monitor::{
|
||||
#[cfg(feature = "parquet")]
|
||||
use crate::monitor::reload_s3_cache_setting;
|
||||
|
||||
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
const DEFAULT_NUM_WORKERS: usize = 1;
|
||||
const DEFAULT_PORT: u16 = 8000;
|
||||
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
|
||||
@@ -531,7 +531,7 @@ Windmill Community Edition {GIT_VERSION}
|
||||
|
||||
#[cfg(feature = "tantivy")]
|
||||
let (index_reader, index_writer) = if should_index_jobs {
|
||||
let (r, w) = windmill_indexer::indexer_ee::init_index(&db).await?;
|
||||
let (r, w) = windmill_indexer::completed_runs_ee::init_index(&db).await?;
|
||||
(Some(r), Some(w))
|
||||
} else {
|
||||
(None, None)
|
||||
@@ -543,26 +543,61 @@ Windmill Community Edition {GIT_VERSION}
|
||||
let index_writer2 = index_writer.clone();
|
||||
async {
|
||||
if let Some(index_writer) = index_writer2 {
|
||||
windmill_indexer::indexer_ee::run_indexer(db.clone(), index_writer, indexer_rx)
|
||||
.await;
|
||||
windmill_indexer::completed_runs_ee::run_indexer(
|
||||
db.clone(),
|
||||
index_writer,
|
||||
indexer_rx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "tantivy", feature = "parquet"))]
|
||||
let (log_index_reader, log_index_writer) = if should_index_jobs {
|
||||
let (r, w) = windmill_indexer::service_logs_ee::init_index(&db).await?;
|
||||
(Some(r), Some(w))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "tantivy", feature = "parquet"))]
|
||||
let log_indexer_f = {
|
||||
let log_indexer_rx = killpill_rx.resubscribe();
|
||||
let log_index_writer2 = log_index_writer.clone();
|
||||
async {
|
||||
if let Some(log_index_writer) = log_index_writer2 {
|
||||
windmill_indexer::service_logs_ee::run_indexer(
|
||||
db.clone(),
|
||||
log_index_writer,
|
||||
log_indexer_rx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
let (index_reader, index_writer) = (None, None);
|
||||
let index_reader = None;
|
||||
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
let indexer_f = async { Ok(()) as anyhow::Result<()> };
|
||||
|
||||
#[cfg(not(all(feature = "tantivy", feature = "parquet")))]
|
||||
let log_index_reader = None;
|
||||
|
||||
#[cfg(not(all(feature = "tantivy", feature = "parquet")))]
|
||||
let log_indexer_f = async { Ok(()) as anyhow::Result<()> };
|
||||
|
||||
let server_f = async {
|
||||
if !is_agent {
|
||||
windmill_api::run_server(
|
||||
db.clone(),
|
||||
rsmq2,
|
||||
index_reader,
|
||||
index_writer,
|
||||
log_index_reader,
|
||||
addr,
|
||||
server_killpill_rx,
|
||||
base_internal_tx,
|
||||
@@ -782,6 +817,12 @@ Windmill Community Edition {GIT_VERSION}
|
||||
tracing::error!(error = %e, "Could not reload jwt secret setting");
|
||||
}
|
||||
},
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING => {
|
||||
tracing::info!("Critical alert UI setting changed");
|
||||
if let Err(e) = reload_critical_alert_mute_ui_setting(&db).await {
|
||||
tracing::error!(error = %e, "Could not reload critical alert UI setting");
|
||||
}
|
||||
},
|
||||
a @_ => {
|
||||
tracing::info!("Unrecognized Global Setting Change Payload: {:?}", a);
|
||||
}
|
||||
@@ -842,7 +883,8 @@ Windmill Community Edition {GIT_VERSION}
|
||||
monitor_f,
|
||||
server_f,
|
||||
metrics_f,
|
||||
indexer_f
|
||||
indexer_f,
|
||||
log_indexer_f
|
||||
)?;
|
||||
} else {
|
||||
tracing::info!("Nothing to do, exiting.");
|
||||
@@ -955,8 +997,9 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
|
||||
for x in [
|
||||
LOCK_CACHE_DIR,
|
||||
TMP_LOGS_DIR,
|
||||
PIP_CACHE_DIR,
|
||||
UV_CACHE_DIR,
|
||||
PY311_CACHE_DIR,
|
||||
PIP_CACHE_DIR,
|
||||
TAR_PIP_CACHE_DIR,
|
||||
DENO_CACHE_DIR,
|
||||
DENO_CACHE_DIR_DEPS,
|
||||
|
||||
@@ -39,7 +39,7 @@ use windmill_common::{
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
|
||||
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
|
||||
},
|
||||
@@ -51,11 +51,11 @@ use windmill_common::{
|
||||
utils::{now_from_db, rd_string, report_critical_error, Mode},
|
||||
worker::{
|
||||
load_worker_config, make_pull_query, make_suspended_pull_query, reload_custom_tags_setting,
|
||||
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SMTP_CONFIG, WORKER_CONFIG,
|
||||
WORKER_GROUP,
|
||||
update_min_version, DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SMTP_CONFIG,
|
||||
WORKER_CONFIG, WORKER_GROUP,
|
||||
},
|
||||
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS,
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED, CRITICAL_ALERT_MUTE_UI_ENABLED
|
||||
};
|
||||
use windmill_queue::cancel_job;
|
||||
use windmill_worker::{
|
||||
@@ -131,6 +131,10 @@ pub async fn initial_load(
|
||||
tracing::error!("Error loading expose debug metrics: {e:#}");
|
||||
}
|
||||
|
||||
if let Err(e) = reload_critical_alert_mute_ui_setting(db).await {
|
||||
tracing::error!("Error loading critical alert mute ui setting: {e:#}");
|
||||
}
|
||||
|
||||
if let Err(e) = load_tag_per_workspace_enabled(db).await {
|
||||
tracing::error!("Error loading default tag per workpsace: {e:#}");
|
||||
}
|
||||
@@ -226,6 +230,17 @@ pub async fn load_tag_per_workspace_workspaces(db: &DB) -> error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_critical_alert_mute_ui_setting(db: &DB) -> error::Result<()> {
|
||||
let mute = load_value_from_global_settings(db, CRITICAL_ALERT_MUTE_UI_SETTING).await;
|
||||
match mute {
|
||||
Ok(Some(serde_json::Value::Bool(t))) => {
|
||||
CRITICAL_ALERT_MUTE_UI_ENABLED.store(t, Ordering::Relaxed);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_metrics_debug_enabled(db: &DB) -> error::Result<()> {
|
||||
let metrics_enabled = load_value_from_global_settings(db, EXPOSE_DEBUG_METRICS_SETTING).await;
|
||||
match metrics_enabled {
|
||||
@@ -1065,6 +1080,10 @@ pub async fn monitor_db(
|
||||
}
|
||||
};
|
||||
|
||||
let update_min_worker_version_f = async {
|
||||
update_min_version(db).await;
|
||||
};
|
||||
|
||||
join!(
|
||||
expired_items_f,
|
||||
zombie_jobs_f,
|
||||
@@ -1073,6 +1092,7 @@ pub async fn monitor_db(
|
||||
worker_groups_alerts_f,
|
||||
jobs_waiting_alerts_f,
|
||||
apply_autoscaling_f,
|
||||
update_min_worker_version_f,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.418.0
|
||||
version: 1.423.2
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -190,7 +190,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/users/{username}:
|
||||
/w/{workspace}/users/get/{username}:
|
||||
get:
|
||||
summary: get user (require admin privilege)
|
||||
operationId: getUser
|
||||
@@ -283,6 +283,71 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/users/set_password_of/{user}:
|
||||
post:
|
||||
summary: set password for a specific user (require super admin)
|
||||
operationId: setPasswordForUser
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- name: user
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: set password
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
required:
|
||||
- password
|
||||
responses:
|
||||
"200":
|
||||
description: password set
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/users/set_login_type/{user}:
|
||||
post:
|
||||
summary: set login type for a specific user (require super admin)
|
||||
operationId: setLoginTypeForUser
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- name: user
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: set login type
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
login_type:
|
||||
type: string
|
||||
required:
|
||||
- login_type
|
||||
responses:
|
||||
"200":
|
||||
description: login type set
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/users/create:
|
||||
post:
|
||||
@@ -724,6 +789,8 @@ paths:
|
||||
type: string
|
||||
tls_implicit:
|
||||
type: boolean
|
||||
disable_tls:
|
||||
type: boolean
|
||||
required:
|
||||
- host
|
||||
- username
|
||||
@@ -731,6 +798,7 @@ paths:
|
||||
- port
|
||||
- from
|
||||
- tls_implicit
|
||||
- disable_tls
|
||||
required:
|
||||
- to
|
||||
- smtp
|
||||
@@ -770,6 +838,78 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/settings/critical_alerts:
|
||||
get:
|
||||
summary: Get all critical alerts
|
||||
operationId: getCriticalAlerts
|
||||
tags:
|
||||
- setting
|
||||
parameters:
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
description: The page number to retrieve (minimum value is 1)
|
||||
- in: query
|
||||
name: page_size
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
description: Number of alerts per page (maximum is 100)
|
||||
- in: query
|
||||
name: acknowledged
|
||||
schema:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Filter by acknowledgment status; true for acknowledged, false for unacknowledged, and omit for all alerts
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully retrieved all critical alerts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CriticalAlert'
|
||||
|
||||
/settings/critical_alerts/{id}/acknowledge:
|
||||
post:
|
||||
summary: Acknowledge a critical alert
|
||||
operationId: acknowledgeCriticalAlert
|
||||
tags:
|
||||
- setting
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: The ID of the critical alert to acknowledge
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully acknowledged the critical alert
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
example: "Critical alert acknowledged"
|
||||
|
||||
/settings/critical_alerts/acknowledge_all:
|
||||
post:
|
||||
summary: Acknowledge all unacknowledged critical alerts
|
||||
operationId: acknowledgeAllCriticalAlerts
|
||||
tags:
|
||||
- setting
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully acknowledged all unacknowledged critical alerts.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
example: "All unacknowledged critical alerts acknowledged"
|
||||
|
||||
/settings/test_license_key:
|
||||
post:
|
||||
@@ -8113,6 +8253,23 @@ paths:
|
||||
- id
|
||||
- values
|
||||
|
||||
/workers/queue_counts:
|
||||
get:
|
||||
summary: get counts of jobs waiting for an executor per tag
|
||||
operationId: getCountsOfJobsWaitingPerTag
|
||||
tags:
|
||||
- worker
|
||||
responses:
|
||||
"200":
|
||||
description: queue counts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
|
||||
|
||||
/configs/list_worker_groups:
|
||||
get:
|
||||
summary: list worker groups
|
||||
@@ -9143,6 +9300,14 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: content_type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: content_disposition
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: File content
|
||||
required: true
|
||||
@@ -9546,6 +9711,109 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/JobSearchHit"
|
||||
|
||||
/srch/index/search/service_logs:
|
||||
get:
|
||||
summary: Search through service logs with a string query
|
||||
operationId: searchLogsIndex
|
||||
tags:
|
||||
- indexSearch
|
||||
parameters:
|
||||
- name: search_query
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: mode
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: worker_group
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: hostname
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: min_ts
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- name: max_ts
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
responses:
|
||||
"200":
|
||||
description: search results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
query_parse_errors:
|
||||
description: a list of the terms that couldn't be parsed (and thus ignored)
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
hits:
|
||||
description: log files that matched the query
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/LogSearchHit"
|
||||
|
||||
/srch/index/search/count_service_logs:
|
||||
get:
|
||||
summary: Search and count the log line hits on every provided host
|
||||
operationId: countSearchLogsIndex
|
||||
tags:
|
||||
- indexSearch
|
||||
parameters:
|
||||
- name: search_query
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: hosts
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: min_ts
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- name: max_ts
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
responses:
|
||||
"200":
|
||||
description: search results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
query_parse_errors:
|
||||
description: a list of the terms that couldn't be parsed (and thus ignored)
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
count_per_host:
|
||||
description: count of log lines that matched the query per hostname
|
||||
type: object
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
@@ -11357,6 +11625,17 @@ components:
|
||||
type: string
|
||||
route_path:
|
||||
type: string
|
||||
static_asset_config:
|
||||
type: object
|
||||
properties:
|
||||
s3:
|
||||
type: string
|
||||
storage:
|
||||
type: string
|
||||
filename:
|
||||
type: string
|
||||
required:
|
||||
- s3
|
||||
is_flow:
|
||||
type: boolean
|
||||
extra_perms:
|
||||
@@ -11403,6 +11682,17 @@ components:
|
||||
type: string
|
||||
route_path:
|
||||
type: string
|
||||
static_asset_config:
|
||||
type: object
|
||||
properties:
|
||||
s3:
|
||||
type: string
|
||||
storage:
|
||||
type: string
|
||||
filename:
|
||||
type: string
|
||||
required:
|
||||
- s3
|
||||
is_flow:
|
||||
type: boolean
|
||||
http_method:
|
||||
@@ -11436,6 +11726,17 @@ components:
|
||||
type: string
|
||||
route_path:
|
||||
type: string
|
||||
static_asset_config:
|
||||
type: object
|
||||
properties:
|
||||
s3:
|
||||
type: string
|
||||
storage:
|
||||
type: string
|
||||
filename:
|
||||
type: string
|
||||
required:
|
||||
- s3
|
||||
is_flow:
|
||||
type: boolean
|
||||
http_method:
|
||||
@@ -11522,6 +11823,12 @@ components:
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
initial_messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
|
||||
required:
|
||||
- path
|
||||
@@ -11535,6 +11842,8 @@ components:
|
||||
- workspace_id
|
||||
- enabled
|
||||
- filters
|
||||
- initial_messages
|
||||
- url_runnable_args
|
||||
|
||||
NewWebsocketTrigger:
|
||||
type: object
|
||||
@@ -11560,6 +11869,12 @@ components:
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
initial_messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
|
||||
required:
|
||||
- path
|
||||
@@ -11567,6 +11882,8 @@ components:
|
||||
- url
|
||||
- is_flow
|
||||
- filters
|
||||
- initial_messages
|
||||
- url_runnable_args
|
||||
|
||||
EditWebsocketTrigger:
|
||||
type: object
|
||||
@@ -11590,6 +11907,12 @@ components:
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
initial_messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
|
||||
required:
|
||||
- path
|
||||
@@ -11597,6 +11920,34 @@ components:
|
||||
- url
|
||||
- is_flow
|
||||
- filters
|
||||
- initial_messages
|
||||
- url_runnable_args
|
||||
|
||||
WebsocketTriggerInitialMessage:
|
||||
anyOf:
|
||||
- type: object
|
||||
properties:
|
||||
raw_message:
|
||||
type: string
|
||||
required:
|
||||
- raw_message
|
||||
- type: object
|
||||
properties:
|
||||
runnable_result:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
is_flow:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- args
|
||||
- is_flow
|
||||
required:
|
||||
- runnable_result
|
||||
|
||||
Group:
|
||||
type: object
|
||||
@@ -11917,6 +12268,10 @@ components:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
s3_inputs:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
execution_mode:
|
||||
type: string
|
||||
enum: [viewer, publisher, anonymous]
|
||||
@@ -12500,6 +12855,12 @@ components:
|
||||
dancer:
|
||||
type: string
|
||||
|
||||
LogSearchHit:
|
||||
type: object
|
||||
properties:
|
||||
dancer:
|
||||
type: string
|
||||
|
||||
AutoscalingEvent:
|
||||
type: object
|
||||
properties:
|
||||
@@ -12517,3 +12878,24 @@ components:
|
||||
applied_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
CriticalAlert:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: Unique identifier for the alert
|
||||
alert_type:
|
||||
type: string
|
||||
description: Type of alert (e.g., critical_error)
|
||||
message:
|
||||
type: string
|
||||
description: The message content of the alert
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Time when the alert was created
|
||||
acknowledged:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Acknowledgment status of the alert, can be true, false, or null if not set
|
||||
|
||||
@@ -7,6 +7,12 @@ use std::collections::HashMap;
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
use crate::{job_helpers_ee::{
|
||||
get_random_file_name, get_s3_resource, get_workspace_s3_resource, upload_file_internal,
|
||||
UploadFileResponse,
|
||||
}, users::fetch_api_authed_from_permissioned_as};
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
resources::get_resource_value_interpolated_internal,
|
||||
@@ -23,7 +29,13 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
#[cfg(feature = "parquet")]
|
||||
use itertools::Itertools;
|
||||
use magic_crypt::MagicCryptTrait;
|
||||
#[cfg(feature = "parquet")]
|
||||
use object_store::{Attribute, Attributes};
|
||||
#[cfg(feature = "parquet")]
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, value::RawValue};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -32,6 +44,8 @@ use sqlx::{types::Uuid, FromRow};
|
||||
use std::str;
|
||||
use windmill_audit::audit_ee::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::s3_helpers::build_object_store_client;
|
||||
use windmill_common::{
|
||||
apps::ListAppQuery,
|
||||
db::UserDB,
|
||||
@@ -69,6 +83,7 @@ pub fn workspaced_service() -> Router {
|
||||
pub fn unauthed_service() -> Router {
|
||||
Router::new()
|
||||
.route("/execute_component/*path", post(execute_component))
|
||||
.route("/upload_s3_file/*path", post(upload_s3_file_from_app))
|
||||
.route("/public_app/:secret", get(get_public_app_by_secret))
|
||||
.route("/public_resource/*path", get(get_public_resource))
|
||||
}
|
||||
@@ -179,6 +194,14 @@ pub struct PolicyTriggerableInputs {
|
||||
allow_user_resources: AllowUserResources,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct S3Input {
|
||||
allowed_resources: Vec<String>,
|
||||
allow_user_resources: bool,
|
||||
allow_workspace_resource: bool,
|
||||
file_key_regex: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Policy {
|
||||
pub on_behalf_of: Option<String>,
|
||||
@@ -192,6 +215,7 @@ pub struct Policy {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub triggerables_v2: Option<HashMap<String, PolicyTriggerableInputs>>,
|
||||
pub execution_mode: ExecutionMode,
|
||||
pub s3_inputs: Option<Vec<S3Input>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -432,9 +456,7 @@ async fn get_latest_version(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
|
||||
) -> JsonResult<Option<AppHistory>> {
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let row = sqlx::query!(
|
||||
"SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg
|
||||
@@ -457,7 +479,6 @@ async fn get_latest_version(
|
||||
} else {
|
||||
return Ok(Json(None));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async fn update_app_history(
|
||||
@@ -1067,6 +1088,49 @@ fn digest(code: &str) -> String {
|
||||
format!("rawscript/{:x}", result)
|
||||
}
|
||||
|
||||
async fn get_on_behalf_details_from_policy_and_authed(
|
||||
policy: &Policy,
|
||||
opt_authed: &Option<ApiAuthed>,
|
||||
) -> Result<(String, String, String)> {
|
||||
let (username, permissioned_as, email) = match policy.execution_mode {
|
||||
ExecutionMode::Anonymous => {
|
||||
let username = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| a.username.clone())
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
|
||||
(username, permissioned_as, email)
|
||||
}
|
||||
ExecutionMode::Publisher => {
|
||||
let username = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| a.username.clone())
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(
|
||||
"publisher execution mode requires authentication".to_string(),
|
||||
)
|
||||
})?;
|
||||
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
|
||||
(username, permissioned_as, email)
|
||||
}
|
||||
ExecutionMode::Viewer => {
|
||||
let (username, email) = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| (a.username.clone(), a.email.clone()))
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest("Required to be authed in viewer mode".to_string())
|
||||
})?;
|
||||
(
|
||||
username.clone(),
|
||||
username_to_permissioned_as(&username),
|
||||
email,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok((username, permissioned_as, email))
|
||||
}
|
||||
|
||||
async fn execute_component(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -1129,6 +1193,7 @@ async fn execute_component(
|
||||
triggerables_v2: Some(hm),
|
||||
on_behalf_of: None,
|
||||
on_behalf_of_email: None,
|
||||
s3_inputs: None,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -1146,41 +1211,8 @@ async fn execute_component(
|
||||
}
|
||||
};
|
||||
|
||||
let (username, permissioned_as, email) = match policy.execution_mode {
|
||||
ExecutionMode::Anonymous => {
|
||||
let username = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| a.username.clone())
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
|
||||
(username, permissioned_as, email)
|
||||
}
|
||||
ExecutionMode::Publisher => {
|
||||
let username = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| a.username.clone())
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(
|
||||
"publisher execution mode requires authentication".to_string(),
|
||||
)
|
||||
})?;
|
||||
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
|
||||
(username, permissioned_as, email)
|
||||
}
|
||||
ExecutionMode::Viewer => {
|
||||
let (username, email) = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| (a.username.clone(), a.email.clone()))
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest("Required to be authed in viewer mode".to_string())
|
||||
})?;
|
||||
(
|
||||
username.clone(),
|
||||
username_to_permissioned_as(&username),
|
||||
email,
|
||||
)
|
||||
}
|
||||
};
|
||||
let (username, permissioned_as, email) =
|
||||
get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?;
|
||||
|
||||
let (job_payload, (args, job_id), tag) = match payload {
|
||||
ExecuteApp { args, component, raw_code: Some(raw_code), path: None, .. } => {
|
||||
@@ -1249,6 +1281,248 @@ async fn execute_component(
|
||||
Ok(uuid.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "parquet"))]
|
||||
async fn upload_s3_file_from_app() -> Result<()> {
|
||||
return Err(Error::BadRequest(
|
||||
"This endpoint requires the parquet feature to be enabled".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct UploadFileToS3Query {
|
||||
file_key: Option<String>,
|
||||
file_extension: Option<String>,
|
||||
s3_resource_path: Option<String>,
|
||||
content_type: Option<String>,
|
||||
content_disposition: Option<String>,
|
||||
force_viewer_file_key_regex: Option<String>,
|
||||
force_viewer_allow_user_resources: Option<bool>,
|
||||
force_viewer_allow_workspace_resource: Option<bool>,
|
||||
force_viewer_allowed_resources: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
async fn upload_s3_file_from_app(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(query): Query<UploadFileToS3Query>,
|
||||
request: axum::extract::Request,
|
||||
) -> JsonResult<UploadFileResponse> {
|
||||
let policy = if let Some(file_key_regex) = query.force_viewer_file_key_regex {
|
||||
Some(Policy {
|
||||
execution_mode: ExecutionMode::Viewer,
|
||||
triggerables: None,
|
||||
triggerables_v2: None,
|
||||
on_behalf_of: None,
|
||||
on_behalf_of_email: None,
|
||||
s3_inputs: Some(vec![S3Input {
|
||||
file_key_regex: file_key_regex,
|
||||
allow_user_resources: query.force_viewer_allow_user_resources.unwrap_or(false),
|
||||
allow_workspace_resource: query
|
||||
.force_viewer_allow_workspace_resource
|
||||
.unwrap_or(false),
|
||||
allowed_resources: query
|
||||
.force_viewer_allowed_resources
|
||||
.map(|s| s.split(',').map(|s| s.to_string()).collect())
|
||||
.unwrap_or_default(),
|
||||
}]),
|
||||
})
|
||||
} else {
|
||||
let policy_o = sqlx::query_scalar!(
|
||||
"SELECT policy from app WHERE path = $1 AND workspace_id = $2",
|
||||
&path.0,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
policy_o
|
||||
.map(|p| serde_json::from_value::<Policy>(p).map_err(to_anyhow))
|
||||
.transpose()?
|
||||
};
|
||||
|
||||
let user_db = UserDB::new(db.clone());
|
||||
|
||||
let (s3_resource_opt, file_key) = if policy.as_ref().is_some_and(|p| p.s3_inputs.is_some()) {
|
||||
let policy = policy.unwrap();
|
||||
let s3_inputs = policy.s3_inputs.as_ref().unwrap();
|
||||
|
||||
let (username, permissioned_as, email) =
|
||||
get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?;
|
||||
|
||||
let on_behalf_authed =
|
||||
fetch_api_authed_from_permissioned_as(permissioned_as, email, &w_id, &db, username)
|
||||
.await?;
|
||||
|
||||
if let Some(file_key) = query.file_key {
|
||||
// file key is provided => requires workspace, user or list policy and must match the regex
|
||||
let matching_s3_inputs = if let Some(ref s3_resource_path) = query.s3_resource_path {
|
||||
s3_inputs
|
||||
.iter()
|
||||
.filter(|s3_input| {
|
||||
s3_input.allowed_resources.contains(s3_resource_path)
|
||||
|| s3_input.allow_user_resources
|
||||
})
|
||||
.sorted_by_key(|i| i.allow_user_resources) // consider user resources last
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
s3_inputs
|
||||
.iter()
|
||||
.filter(|s3_input| s3_input.allow_workspace_resource)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let matched_input = matching_s3_inputs.iter().find(|s3_input| {
|
||||
match Regex::new(&s3_input.file_key_regex) {
|
||||
Ok(re) => re.is_match(&file_key),
|
||||
Err(e) => {
|
||||
tracing::error!("Error compiling regex: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(matched_input) = matched_input {
|
||||
if let Some(ref s3_resource_path) = query.s3_resource_path {
|
||||
if matched_input.allow_user_resources {
|
||||
if let Some(authed) = opt_authed {
|
||||
(
|
||||
Some(
|
||||
get_s3_resource(
|
||||
&authed,
|
||||
&db,
|
||||
Some(user_db),
|
||||
"",
|
||||
&w_id,
|
||||
s3_resource_path,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
)
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
"User resources are not allowed without being logged in"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
(
|
||||
Some(
|
||||
get_s3_resource(
|
||||
&on_behalf_authed,
|
||||
&db,
|
||||
Some(user_db),
|
||||
"",
|
||||
&w_id,
|
||||
s3_resource_path,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let (_, s3_resource_opt) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None)
|
||||
.await?;
|
||||
(s3_resource_opt, file_key)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
"No matching s3 resource found for the given file key".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// no file key => requires unnamed upload policy => allow workspace resource and file_key_regex is empty
|
||||
let has_unnamed_policy = s3_inputs.iter().any(|s3_input| {
|
||||
s3_input.allow_workspace_resource && s3_input.file_key_regex.is_empty()
|
||||
});
|
||||
|
||||
if !has_unnamed_policy {
|
||||
return Err(Error::BadRequest(
|
||||
"no policy found for unnamed s3 file uplooad".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// for now, we place all files into `windmill_uploads` folder with a random name
|
||||
// TODO: make the folder configurable via the workspace settings
|
||||
let file_key = get_random_file_name(query.file_extension);
|
||||
|
||||
let (_, s3_resource_opt) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
(s3_resource_opt, file_key)
|
||||
}
|
||||
} else {
|
||||
// backward compatibility (no policy)
|
||||
// if no policy but logged in, use the user's auth to get the s3 resource
|
||||
if let Some(authed) = opt_authed {
|
||||
let file_key = query
|
||||
.file_key
|
||||
.unwrap_or_else(|| get_random_file_name(query.file_extension));
|
||||
|
||||
if let Some(ref s3_resource_path) = query.s3_resource_path {
|
||||
(
|
||||
Some(
|
||||
get_s3_resource(
|
||||
&authed,
|
||||
&db,
|
||||
Some(user_db),
|
||||
"",
|
||||
&w_id,
|
||||
s3_resource_path,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
)
|
||||
} else {
|
||||
let (_, s3_resource) =
|
||||
get_workspace_s3_resource(&authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
(s3_resource, file_key)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest("Missing s3 policy".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let s3_resource = s3_resource_opt.ok_or(Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
))?;
|
||||
let s3_client = build_object_store_client(&s3_resource).await?;
|
||||
|
||||
let options = Attributes::from_iter(vec![
|
||||
(
|
||||
Attribute::ContentType,
|
||||
query.content_type.unwrap_or_else(|| {
|
||||
mime_guess::from_path(&file_key)
|
||||
.first_or_octet_stream()
|
||||
.to_string()
|
||||
}),
|
||||
),
|
||||
(
|
||||
Attribute::ContentDisposition,
|
||||
query.content_disposition.unwrap_or("inline".to_string()),
|
||||
),
|
||||
])
|
||||
.into();
|
||||
|
||||
upload_file_internal(s3_client, &file_key, request, options).await?;
|
||||
|
||||
return Ok(Json(UploadFileResponse { file_key }));
|
||||
}
|
||||
|
||||
fn get_on_behalf_of(policy: &Policy) -> Result<(String, String)> {
|
||||
let permissioned_as = policy
|
||||
.on_behalf_of
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sql_builder::{bind::Bind, SqlBuilder};
|
||||
use sqlx::prelude::FromRow;
|
||||
use std::collections::HashMap;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use windmill_audit::{audit_ee::audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{self, JsonResult},
|
||||
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
};
|
||||
use windmill_queue::PushArgsOwned;
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
use crate::job_helpers_ee::get_workspace_s3_resource;
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
jobs::{
|
||||
@@ -27,6 +8,31 @@ use crate::{
|
||||
},
|
||||
users::{fetch_api_authed, OptAuthed},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
#[cfg(feature = "parquet")]
|
||||
use http::header::IF_NONE_MATCH;
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sql_builder::{bind::Bind, SqlBuilder};
|
||||
use sqlx::prelude::FromRow;
|
||||
use std::collections::HashMap;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use windmill_audit::{audit_ee::audit_log, ActionKind};
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::s3_helpers::build_object_store_client;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{self, JsonResult},
|
||||
s3_helpers::S3Object,
|
||||
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
};
|
||||
use windmill_queue::PushArgsOwned;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/:\w+").unwrap();
|
||||
@@ -99,6 +105,7 @@ struct NewTrigger {
|
||||
is_async: bool,
|
||||
requires_auth: bool,
|
||||
http_method: HttpMethod,
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
@@ -116,6 +123,7 @@ struct Trigger {
|
||||
is_async: bool,
|
||||
requires_auth: bool,
|
||||
http_method: HttpMethod,
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -127,6 +135,7 @@ struct EditTrigger {
|
||||
is_async: bool,
|
||||
requires_auth: bool,
|
||||
http_method: HttpMethod,
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -182,7 +191,7 @@ async fn get_trigger(
|
||||
let path = path.to_path();
|
||||
let trigger = sqlx::query_as!(
|
||||
Trigger,
|
||||
r#"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as "http_method: _", edited_by, email, edited_at, extra_perms, is_async, requires_auth
|
||||
r#"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as "http_method: _", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as "static_asset_config: _"
|
||||
FROM http_trigger
|
||||
WHERE workspace_id = $1 AND path = $2"#,
|
||||
w_id,
|
||||
@@ -209,7 +218,7 @@ async fn create_trigger(
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now())",
|
||||
"INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now())",
|
||||
w_id,
|
||||
ct.path,
|
||||
ct.route_path,
|
||||
@@ -218,7 +227,8 @@ async fn create_trigger(
|
||||
ct.is_flow,
|
||||
ct.is_async,
|
||||
ct.requires_auth,
|
||||
ct.http_method as HttpMethod,
|
||||
ct.http_method as _,
|
||||
ct.static_asset_config as _,
|
||||
&authed.username,
|
||||
&authed.email
|
||||
)
|
||||
@@ -261,14 +271,15 @@ async fn update_trigger(
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE http_trigger
|
||||
SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, edited_by = $7, email = $8, is_async = $9, requires_auth = $10, edited_at = now()
|
||||
WHERE workspace_id = $11 AND path = $12",
|
||||
SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now()
|
||||
WHERE workspace_id = $12 AND path = $13",
|
||||
ct.route_path,
|
||||
&route_path_key,
|
||||
ct.script_path,
|
||||
ct.path,
|
||||
ct.is_flow,
|
||||
ct.http_method as HttpMethod,
|
||||
ct.http_method as _,
|
||||
ct.static_asset_config as _,
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
ct.is_async,
|
||||
@@ -279,12 +290,13 @@ async fn update_trigger(
|
||||
.execute(&mut *tx).await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, edited_by = $5, email = $6, is_async = $7, requires_auth = $8, edited_at = now()
|
||||
WHERE workspace_id = $9 AND path = $10",
|
||||
"UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now()
|
||||
WHERE workspace_id = $10 AND path = $11",
|
||||
ct.script_path,
|
||||
ct.path,
|
||||
ct.is_flow,
|
||||
ct.http_method as HttpMethod,
|
||||
ct.http_method as _,
|
||||
ct.static_asset_config as _,
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
ct.is_async,
|
||||
@@ -405,6 +417,7 @@ struct TriggerRoute {
|
||||
edited_by: String,
|
||||
email: String,
|
||||
http_method: HttpMethod,
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
}
|
||||
|
||||
async fn get_http_route_trigger(
|
||||
@@ -421,7 +434,7 @@ async fn get_http_route_trigger(
|
||||
let route_path = StripPath(splitted.collect::<Vec<_>>().join("/"));
|
||||
let triggers = sqlx::query_as!(
|
||||
TriggerRoute,
|
||||
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _" FROM http_trigger WHERE workspace_id = $1"#,
|
||||
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _", static_asset_config as "static_asset_config: _" FROM http_trigger WHERE workspace_id = $1"#,
|
||||
w_id
|
||||
)
|
||||
.fetch_all(db)
|
||||
@@ -430,7 +443,7 @@ async fn get_http_route_trigger(
|
||||
} else {
|
||||
let triggers = sqlx::query_as!(
|
||||
TriggerRoute,
|
||||
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _" FROM http_trigger"#,
|
||||
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _", static_asset_config as "static_asset_config: _" FROM http_trigger"#,
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
@@ -518,6 +531,90 @@ async fn route_job(
|
||||
Ok(trigger) => trigger,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "parquet"))]
|
||||
if trigger.static_asset_config.is_some() {
|
||||
return error::Error::InternalErr(
|
||||
"Static asset configuration is not supported in this build".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
if let Some(sqlx::types::Json(config)) = trigger.static_asset_config {
|
||||
let build_static_response_f = async {
|
||||
let (_, s3_resource_opt) = get_workspace_s3_resource(
|
||||
&authed,
|
||||
&db,
|
||||
None,
|
||||
&"NO_TOKEN".to_string(), // no token is provided in this case
|
||||
&trigger.workspace_id,
|
||||
config.storage,
|
||||
)
|
||||
.await?;
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
))?;
|
||||
let s3_client = build_object_store_client(&s3_resource).await?;
|
||||
let path = object_store::path::Path::from(config.s3);
|
||||
let s3_object = s3_client.get(&path).await.map_err(|err| {
|
||||
tracing::warn!("Error retrieving file from S3: {:?}", err);
|
||||
error::Error::InternalErr(format!("Error retrieving file: {}", err.to_string()))
|
||||
})?;
|
||||
let mut response_headers = http::HeaderMap::new();
|
||||
if let Some(ref e_tag) = s3_object.meta.e_tag {
|
||||
if let Some(if_none_match) = headers.get(IF_NONE_MATCH) {
|
||||
if if_none_match == e_tag {
|
||||
return Ok::<_, error::Error>((
|
||||
StatusCode::NOT_MODIFIED,
|
||||
response_headers,
|
||||
axum::body::Body::empty(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Ok(e_tag) = e_tag.parse() {
|
||||
response_headers.insert("etag", e_tag);
|
||||
}
|
||||
}
|
||||
response_headers.insert(
|
||||
"content-type",
|
||||
s3_object
|
||||
.attributes
|
||||
.get(&object_store::Attribute::ContentType)
|
||||
.map(|s| s.parse().ok())
|
||||
.flatten()
|
||||
.unwrap_or("application/octet-stream".parse().unwrap()),
|
||||
);
|
||||
response_headers.insert(
|
||||
"content-disposition",
|
||||
config.filename.as_ref().map_or_else(
|
||||
|| {
|
||||
s3_object
|
||||
.attributes
|
||||
.get(&object_store::Attribute::ContentDisposition)
|
||||
.map(|s| s.parse().ok())
|
||||
.flatten()
|
||||
.unwrap_or("inline".parse().unwrap())
|
||||
},
|
||||
|filename| {
|
||||
format!("inline; filename=\"{}\"", filename)
|
||||
.parse()
|
||||
.unwrap_or("inline".parse().unwrap())
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
let body_stream = axum::body::Body::from_stream(s3_object.into_stream());
|
||||
Ok::<_, error::Error>((StatusCode::OK, response_headers, body_stream))
|
||||
};
|
||||
match build_static_response_f.await {
|
||||
Ok((status, headers, body_stream)) => {
|
||||
return (status, headers, body_stream).into_response()
|
||||
}
|
||||
Err(e) => return e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
let headers = headers
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
|
||||
|
||||
@@ -3,3 +3,7 @@ use axum::Router;
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
use axum::Router;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::s3_helpers::StorageResourceType;
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
use crate::db::{ApiAuthed, DB};
|
||||
#[cfg(feature = "parquet")]
|
||||
use object_store::{ObjectStore, PutMultipartOpts};
|
||||
#[cfg(feature = "parquet")]
|
||||
use std::sync::Arc;
|
||||
use windmill_common::error;
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::{db::UserDB, s3_helpers::ObjectStoreResource};
|
||||
#[derive(Serialize)]
|
||||
pub struct UploadFileResponse {
|
||||
pub file_key: String,
|
||||
}
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
pub async fn get_workspace_s3_resource<'c>(
|
||||
_authed: &ApiAuthed,
|
||||
_db: &DB,
|
||||
_user_db: Option<UserDB>,
|
||||
_token: &str,
|
||||
_w_id: &str,
|
||||
_storage: Option<String>,
|
||||
) -> windmill_common::error::Result<(Option<bool>, Option<ObjectStoreResource>)> {
|
||||
// implementation is not open source
|
||||
Ok((None, None))
|
||||
}
|
||||
|
||||
pub fn get_random_file_name(_file_extension: Option<String>) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get_s3_resource<'c>(
|
||||
_authed: &ApiAuthed,
|
||||
_db: &DB,
|
||||
_user_db: Option<UserDB>,
|
||||
_token: &str,
|
||||
_w_id: &str,
|
||||
_resource_path: &str,
|
||||
_resource_type: Option<StorageResourceType>,
|
||||
_job_id: Option<Uuid>,
|
||||
) -> error::Result<ObjectStoreResource> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn upload_file_internal(
|
||||
_s3_client: Arc<dyn ObjectStore>,
|
||||
_file_key: &str,
|
||||
_request: axum::extract::Request,
|
||||
_options: PutMultipartOpts,
|
||||
) -> error::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use quick_cache::sync::Cache;
|
||||
use serde_json::value::RawValue;
|
||||
use sqlx::Pool;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
#[cfg(feature = "prometheus")]
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::io::AsyncReadExt;
|
||||
@@ -65,7 +66,7 @@ use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{self, to_anyhow, Error},
|
||||
flow_status::{Approval, FlowStatus, FlowStatusModule},
|
||||
flows::FlowValue,
|
||||
flows::{add_virtual_items_if_necessary, FlowValue},
|
||||
jobs::{script_path_to_payload, CompletedJob, JobKind, JobPayload, QueuedJob, RawCode},
|
||||
oauth2::HmacSha256,
|
||||
scripts::{ScriptHash, ScriptLang},
|
||||
@@ -600,7 +601,7 @@ async fn get_flow_job_debug_info(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::Result<Response> {
|
||||
let job = get_queued_job(&id, w_id.as_str(), &db).await?;
|
||||
let job = get_queued_job_ex(&db, &w_id, id, false, None).await?;
|
||||
if let Some(job) = job {
|
||||
let is_flow = job.is_flow();
|
||||
if job.is_flow_step || !is_flow {
|
||||
@@ -734,6 +735,67 @@ fn generate_get_job_query(no_logs: bool, table: &str) -> String {
|
||||
{join}
|
||||
WHERE id = $1 AND {table}.workspace_id = $2");
|
||||
}
|
||||
pub async fn get_queued_job_ex(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
job_id: Uuid,
|
||||
no_logs: bool,
|
||||
// first optional is if authed need to be checked, second is the opt_authed itself
|
||||
opt_authed: Option<&Option<ApiAuthed>>,
|
||||
) -> error::Result<Option<JobExtended<QueuedJob>>> {
|
||||
let query = if no_logs { &*GET_QUEUED_JOB_QUERY_NO_LOGS } else { &*GET_QUEUED_JOB_QUERY };
|
||||
let job = sqlx::query_as::<_, JobExtended<QueuedJob>>(query)
|
||||
.bind(job_id)
|
||||
.bind(workspace_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
if let Some(job) = job.as_ref() {
|
||||
if opt_authed.is_some_and(|x| x.is_none()) && job.created_by != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(job)
|
||||
}
|
||||
pub async fn get_completed_job_ex(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
job_id: Uuid,
|
||||
no_logs: bool,
|
||||
// first optional is if authed need to be checked, second is the opt_authed itself
|
||||
opt_authed: Option<&Option<ApiAuthed>>,
|
||||
) -> error::Result<Option<JobExtended<CompletedJob>>> {
|
||||
let query = if no_logs { &*GET_COMPLETED_JOB_QUERY_NO_LOGS } else { &*GET_COMPLETED_JOB_QUERY };
|
||||
let cjob = sqlx::query_as::<_, JobExtended<CompletedJob>>(query)
|
||||
.bind(job_id)
|
||||
.bind(workspace_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
if let Some(job) = cjob.as_ref() {
|
||||
if opt_authed.is_some_and(|x| x.is_none()) && job.created_by != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut cjob) = cjob {
|
||||
let CompletedJobWithFormattedResult { mut cj, result } = format_completed_job_result(cjob.inner);
|
||||
cj.result = match result {
|
||||
Some(FormattedResult::RawValue(rv)) => rv,
|
||||
Some(FormattedResult::Vec(v)) => Some(to_raw_value(&v)),
|
||||
None => None,
|
||||
}.map(sqlx::types::Json);
|
||||
cjob.inner = cj;
|
||||
return Ok(Some(cjob));
|
||||
}
|
||||
|
||||
Ok(cjob)
|
||||
}
|
||||
pub async fn get_job_internal(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
@@ -742,48 +804,18 @@ pub async fn get_job_internal(
|
||||
// first optional is if authed need to be checked, second is the opt_authed itself
|
||||
opt_authed: Option<&Option<ApiAuthed>>,
|
||||
) -> error::Result<Job> {
|
||||
let cjob_maybe = sqlx::query_as::<_, CompletedJob>(if no_logs {
|
||||
&*GET_COMPLETED_JOB_QUERY_NO_LOGS
|
||||
} else {
|
||||
&*GET_COMPLETED_JOB_QUERY
|
||||
})
|
||||
.bind(job_id)
|
||||
.bind(workspace_id)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.map(Job::CompletedJob);
|
||||
|
||||
if let Some(cjob) = cjob_maybe {
|
||||
Ok(match cjob {
|
||||
Job::CompletedJob(cjob) => {
|
||||
if opt_authed.is_some_and(|x| x.is_none()) && cjob.created_by != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Job::CompletedJobWithFormattedResult(format_completed_job_result(cjob))
|
||||
}
|
||||
cjob => cjob,
|
||||
})
|
||||
} else {
|
||||
let job_o = sqlx::query_as::<_, QueuedJob>(if no_logs {
|
||||
&*GET_QUEUED_JOB_QUERY_NO_LOGS
|
||||
} else {
|
||||
&*GET_QUEUED_JOB_QUERY
|
||||
})
|
||||
.bind(job_id)
|
||||
.bind(workspace_id)
|
||||
.fetch_optional(db)
|
||||
let cjob = get_completed_job_ex(db, workspace_id, job_id, no_logs, opt_authed.clone())
|
||||
.await?
|
||||
.map(Job::QueuedJob);
|
||||
let job: Job = not_found_if_none(job_o, "Job", job_id.to_string())?;
|
||||
if opt_authed.is_some_and(|x| x.is_none()) && job.created_by() != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
|
||||
));
|
||||
.map(Job::CompletedJob);
|
||||
|
||||
match cjob {
|
||||
Some(cjob) => Ok(cjob),
|
||||
None => {
|
||||
let job_maybe = get_queued_job_ex(db, workspace_id, job_id, no_logs, opt_authed)
|
||||
.await?
|
||||
.map(Job::QueuedJob);
|
||||
not_found_if_none(job_maybe, "Job", job_id.to_string())
|
||||
}
|
||||
Ok(job)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1735,7 +1767,6 @@ async fn resume_suspended_job_internal(
|
||||
let trigger_email = match &parent_flow {
|
||||
Job::CompletedJob(job) => &job.email,
|
||||
Job::QueuedJob(job) => &job.email,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.email,
|
||||
};
|
||||
conditionally_require_authed_user(authed.clone(), flow_status, trigger_email)?;
|
||||
|
||||
@@ -2010,7 +2041,6 @@ pub async fn get_suspended_job_flow(
|
||||
let trigger_email = match &flow {
|
||||
Job::CompletedJob(job) => &job.email,
|
||||
Job::QueuedJob(job) => &job.email,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.email,
|
||||
};
|
||||
conditionally_require_authed_user(authed.clone(), flow_status.clone(), trigger_email)?;
|
||||
|
||||
@@ -2225,13 +2255,45 @@ pub async fn get_resume_urls(
|
||||
Ok(Json(res))
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Serialize)]
|
||||
pub struct JobExtended<T> {
|
||||
#[sqlx(flatten)]
|
||||
#[serde(flatten)]
|
||||
inner: T,
|
||||
|
||||
#[sqlx(skip)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub self_wait_time_ms: Option<i64>,
|
||||
#[sqlx(skip)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aggregate_wait_time_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl<T> JobExtended<T> {
|
||||
pub fn new(self_wait_time_ms: Option<i64>, aggregate_wait_time_ms: Option<i64>, inner: T) -> Self {
|
||||
Self { inner, self_wait_time_ms, aggregate_wait_time_ms }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for JobExtended<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for JobExtended<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Job {
|
||||
QueuedJob(QueuedJob),
|
||||
CompletedJob(CompletedJob),
|
||||
#[serde(rename = "CompletedJob")]
|
||||
CompletedJobWithFormattedResult(CompletedJobWithFormattedResult),
|
||||
QueuedJob(JobExtended<QueuedJob>),
|
||||
CompletedJob(JobExtended<CompletedJob>),
|
||||
}
|
||||
|
||||
impl Job {
|
||||
@@ -2239,27 +2301,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => &job.created_by,
|
||||
Job::CompletedJob(job) => &job.created_by,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.created_by,
|
||||
}
|
||||
}
|
||||
pub fn raw_flow(&self) -> Option<FlowValue> {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job
|
||||
.raw_flow
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
Job::CompletedJob(job) => job
|
||||
.raw_flow
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job
|
||||
.cj
|
||||
.raw_flow
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2279,13 +2320,6 @@ impl Job {
|
||||
job.logs = Some(logs.to_string());
|
||||
}
|
||||
}
|
||||
Job::CompletedJobWithFormattedResult(job) => {
|
||||
if let Some(ref mut l) = job.cj.logs {
|
||||
l.push_str(logs);
|
||||
} else {
|
||||
job.cj.logs = Some(logs.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2293,7 +2327,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.logs.as_ref().map(|l| l.len()),
|
||||
Job::CompletedJob(job) => job.logs.as_ref().map(|l| l.len()),
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.logs.as_ref().map(|l| l.len()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2301,7 +2334,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.logs.clone(),
|
||||
Job::CompletedJob(job) => job.logs.clone(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.logs.clone(),
|
||||
}
|
||||
}
|
||||
pub fn flow_status(&self) -> Option<FlowStatus> {
|
||||
@@ -2316,19 +2348,12 @@ impl Job {
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job
|
||||
.cj
|
||||
.flow_status
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
}
|
||||
}
|
||||
pub fn is_flow_step(&self) -> bool {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.is_flow_step,
|
||||
Job::CompletedJob(job) => job.is_flow_step,
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.is_flow_step,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2343,7 +2368,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => &job.job_kind,
|
||||
Job::CompletedJob(job) => &job.job_kind,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.job_kind,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2351,7 +2375,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.id,
|
||||
Job::CompletedJob(job) => job.id,
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2359,7 +2382,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => &job.workspace_id,
|
||||
Job::CompletedJob(job) => &job.workspace_id,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.workspace_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2367,7 +2389,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.script_path.as_ref(),
|
||||
Job::CompletedJob(job) => job.script_path.as_ref(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.script_path.as_ref(),
|
||||
}
|
||||
.map(String::as_str)
|
||||
.unwrap_or("tmp/main")
|
||||
@@ -2377,7 +2398,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.args.as_ref(),
|
||||
Job::CompletedJob(job) => job.args.as_ref(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.args.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2426,10 +2446,6 @@ impl Job {
|
||||
job.self_wait_time_ms = self_wait_time;
|
||||
job.aggregate_wait_time_ms = aggregate_wait_time;
|
||||
}
|
||||
Job::CompletedJobWithFormattedResult(job) => {
|
||||
job.cj.self_wait_time_ms = self_wait_time;
|
||||
job.cj.aggregate_wait_time_ms = aggregate_wait_time;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2557,86 +2573,82 @@ impl UnifiedJob {
|
||||
impl<'a> From<UnifiedJob> for Job {
|
||||
fn from(uj: UnifiedJob) -> Self {
|
||||
match uj.typ.as_ref() {
|
||||
"CompletedJob" => Job::CompletedJob(CompletedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
id: uj.id,
|
||||
parent_job: uj.parent_job,
|
||||
created_by: uj.created_by,
|
||||
created_at: uj.created_at,
|
||||
started_at: uj.started_at.unwrap_or(uj.created_at),
|
||||
duration_ms: uj.duration_ms.unwrap(),
|
||||
success: uj.success.unwrap(),
|
||||
script_hash: uj.script_hash,
|
||||
script_path: uj.script_path,
|
||||
args: None,
|
||||
result: None,
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
deleted: uj.deleted,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
raw_code: None,
|
||||
canceled_reason: None,
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
is_skipped: uj.is_skipped,
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
mem_peak: uj.mem_peak,
|
||||
tag: uj.tag,
|
||||
priority: uj.priority,
|
||||
labels: uj.labels,
|
||||
self_wait_time_ms: uj.self_wait_time_ms,
|
||||
aggregate_wait_time_ms: uj.aggregate_wait_time_ms,
|
||||
}),
|
||||
"QueuedJob" => Job::QueuedJob(QueuedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
id: uj.id,
|
||||
parent_job: uj.parent_job,
|
||||
created_by: uj.created_by,
|
||||
created_at: uj.created_at,
|
||||
started_at: uj.started_at,
|
||||
script_hash: uj.script_hash,
|
||||
script_path: uj.script_path,
|
||||
args: None,
|
||||
running: uj.running.unwrap(),
|
||||
scheduled_for: uj.scheduled_for.unwrap(),
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
raw_code: None,
|
||||
raw_lock: None,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
canceled_reason: None,
|
||||
last_ping: None,
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
same_worker: false,
|
||||
pre_run_error: None,
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
suspend: uj.suspend,
|
||||
mem_peak: uj.mem_peak,
|
||||
root_job: None,
|
||||
leaf_jobs: None,
|
||||
tag: uj.tag,
|
||||
concurrent_limit: uj.concurrent_limit,
|
||||
concurrency_time_window_s: uj.concurrency_time_window_s,
|
||||
timeout: None,
|
||||
flow_step_id: None,
|
||||
cache_ttl: None,
|
||||
priority: uj.priority,
|
||||
self_wait_time_ms: uj.self_wait_time_ms,
|
||||
aggregate_wait_time_ms: uj.aggregate_wait_time_ms,
|
||||
}),
|
||||
"CompletedJob" => Job::CompletedJob(JobExtended::new(uj.self_wait_time_ms, uj.aggregate_wait_time_ms, CompletedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
id: uj.id,
|
||||
parent_job: uj.parent_job,
|
||||
created_by: uj.created_by,
|
||||
created_at: uj.created_at,
|
||||
started_at: uj.started_at.unwrap_or(uj.created_at),
|
||||
duration_ms: uj.duration_ms.unwrap(),
|
||||
success: uj.success.unwrap(),
|
||||
script_hash: uj.script_hash,
|
||||
script_path: uj.script_path,
|
||||
args: None,
|
||||
result: None,
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
deleted: uj.deleted,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
raw_code: None,
|
||||
canceled_reason: None,
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
is_skipped: uj.is_skipped,
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
mem_peak: uj.mem_peak,
|
||||
tag: uj.tag,
|
||||
priority: uj.priority,
|
||||
labels: uj.labels,
|
||||
})),
|
||||
"QueuedJob" => Job::QueuedJob(JobExtended::new(uj.self_wait_time_ms, uj.aggregate_wait_time_ms, QueuedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
id: uj.id,
|
||||
parent_job: uj.parent_job,
|
||||
created_by: uj.created_by,
|
||||
created_at: uj.created_at,
|
||||
started_at: uj.started_at,
|
||||
script_hash: uj.script_hash,
|
||||
script_path: uj.script_path,
|
||||
args: None,
|
||||
running: uj.running.unwrap(),
|
||||
scheduled_for: uj.scheduled_for.unwrap(),
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
raw_code: None,
|
||||
raw_lock: None,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
canceled_reason: None,
|
||||
last_ping: None,
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
same_worker: false,
|
||||
pre_run_error: None,
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
suspend: uj.suspend,
|
||||
mem_peak: uj.mem_peak,
|
||||
root_job: None,
|
||||
leaf_jobs: None,
|
||||
tag: uj.tag,
|
||||
concurrent_limit: uj.concurrent_limit,
|
||||
concurrency_time_window_s: uj.concurrency_time_window_s,
|
||||
timeout: None,
|
||||
flow_step_id: None,
|
||||
cache_ttl: None,
|
||||
priority: uj.priority,
|
||||
})),
|
||||
t => panic!("job type {} not valid", t),
|
||||
}
|
||||
}
|
||||
@@ -3079,7 +3091,7 @@ pub async fn run_workflow_as_code(
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let job = get_queued_job(&job_id, &w_id, &db).await?;
|
||||
let job = get_queued_job_ex(&db, &w_id, job_id, true, None).await?;
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
@@ -3087,6 +3099,7 @@ pub async fn run_workflow_as_code(
|
||||
}
|
||||
|
||||
let job = not_found_if_none(job, "Queued Job", &job_id.to_string())?;
|
||||
let JobExtended { inner: job, .. } = job;
|
||||
let (job_payload, tag, _delete_after_use, timeout) = match job.job_kind {
|
||||
JobKind::Preview => (
|
||||
JobPayload::Code(RawCode {
|
||||
@@ -4275,18 +4288,26 @@ async fn run_flow_dependencies_job(
|
||||
wait_result
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BatchRawScript {
|
||||
content: String,
|
||||
language: Option<ScriptLang>,
|
||||
lock: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BatchInfo {
|
||||
kind: String,
|
||||
flow_value: Option<FlowValue>,
|
||||
path: Option<String>,
|
||||
rawscript: Option<BatchRawScript>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn add_batch_jobs(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Extension(_rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Path((w_id, n)): Path<(String, i32)>,
|
||||
Json(batch_info): Json<BatchInfo>,
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
@@ -4302,6 +4323,10 @@ async fn add_batch_jobs(
|
||||
concurrent_limit,
|
||||
concurrent_time_window_s,
|
||||
timeout,
|
||||
raw_code,
|
||||
raw_lock,
|
||||
raw_flow,
|
||||
flow_status
|
||||
) = match batch_info.kind.as_str() {
|
||||
"script" => {
|
||||
if let Some(path) = batch_info.path {
|
||||
@@ -4329,6 +4354,10 @@ async fn add_batch_jobs(
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
timeout,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
@@ -4336,61 +4365,72 @@ async fn add_batch_jobs(
|
||||
))?
|
||||
}
|
||||
}
|
||||
"flow" => {
|
||||
let mut uuids: Vec<Uuid> = Vec::new();
|
||||
let payload = if let Some(ref fv) = batch_info.flow_value {
|
||||
JobPayload::RawFlow { value: fv.clone(), path: None, restarted_from: None }
|
||||
} else {
|
||||
if let Some(path) = batch_info.path.as_ref() {
|
||||
JobPayload::Flow {
|
||||
path: path.to_string(),
|
||||
dedicated_worker: None,
|
||||
apply_preprocessor: false,
|
||||
}
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Path is required if no value is not provided"
|
||||
))?
|
||||
}
|
||||
};
|
||||
let mut tx = PushIsolationLevel::IsolatedRoot(db.clone(), rsmq);
|
||||
for _ in 0..n {
|
||||
let ehm = HashMap::new();
|
||||
let (uuid, ntx) = push(
|
||||
&db,
|
||||
tx,
|
||||
&w_id,
|
||||
payload.clone(),
|
||||
PushArgs::from(&ehm),
|
||||
authed.display_username(),
|
||||
&authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
"rawscript" => {
|
||||
if let Some(rawscript) = batch_info.rawscript {
|
||||
(
|
||||
None,
|
||||
None,
|
||||
JobKind::Preview,
|
||||
rawscript.language,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
true,
|
||||
Some(rawscript.content),
|
||||
rawscript.lock,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
)
|
||||
.await?;
|
||||
tx = PushIsolationLevel::Transaction(ntx);
|
||||
uuids.push(uuid);
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"rawscript is required for `rawscript` kind"
|
||||
))?
|
||||
}
|
||||
match tx {
|
||||
PushIsolationLevel::Transaction(tx) => {
|
||||
tx.commit().await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
return Ok(Json(uuids));
|
||||
}
|
||||
"flow" => {
|
||||
let (mut value, job_kind, path) = if let Some(value) = batch_info.flow_value {
|
||||
(value, JobKind::FlowPreview, None)
|
||||
} else if let Some(path) = batch_info.path {
|
||||
let value_json = sqlx::query!(
|
||||
"SELECT flow_version.value AS \"value: sqlx::types::Json<Box<RawValue>>\" FROM flow
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2",
|
||||
&path, &w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::InternalErr(format!("not found flow at path {:?}", path)))?;
|
||||
let value =
|
||||
serde_json::from_str::<FlowValue>(value_json.value.get()).map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"could not convert json to flow for {path}: {err:?}"
|
||||
))
|
||||
})?;
|
||||
(value, JobKind::Flow, Some(path))
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Path is required if no value is not provided"
|
||||
))?
|
||||
};
|
||||
add_virtual_items_if_necessary(&mut value.modules);
|
||||
let flow_status = FlowStatus::new(&value);
|
||||
(
|
||||
None, // script_hash
|
||||
path, // script_path
|
||||
job_kind, // job_kind
|
||||
None, // language
|
||||
None, // dedicated_worker
|
||||
value.concurrency_key.clone(), // custom_concurrency_key
|
||||
value.concurrent_limit.clone(), // concurrent_limit
|
||||
value.concurrency_time_window_s, // concurrency_time_window_s
|
||||
None, // timeout
|
||||
None, // raw_code
|
||||
None, // raw_lock
|
||||
Some(value), // raw_flow
|
||||
Some(flow_status), // flow_status
|
||||
)
|
||||
}
|
||||
"noop" => (
|
||||
None,
|
||||
@@ -4402,6 +4442,10 @@ async fn add_batch_jobs(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
_ => {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
@@ -4428,8 +4472,8 @@ async fn add_batch_jobs(
|
||||
select gen_random_uuid() as uuid from generate_series(1, $11)
|
||||
)
|
||||
INSERT INTO queue
|
||||
(id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout)
|
||||
(SELECT uuid, $1, $2, $3, $4, ('{ "uuid": "' || uuid || '" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14 FROM uuid_table)
|
||||
(id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout, raw_code, raw_lock, raw_flow, flow_status)
|
||||
(SELECT uuid, $1, $2, $3, $4, ('{ "uuid": "' || uuid || '" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14, $15, $16, $17, $18 FROM uuid_table)
|
||||
RETURNING id"#,
|
||||
hash.map(|h| h.0),
|
||||
path,
|
||||
@@ -4444,7 +4488,11 @@ async fn add_batch_jobs(
|
||||
n,
|
||||
concurrent_limit,
|
||||
concurrent_time_window_s,
|
||||
timeout
|
||||
timeout,
|
||||
raw_code,
|
||||
raw_lock,
|
||||
raw_flow.map(sqlx::types::Json) as Option<sqlx::types::Json<FlowValue>>,
|
||||
flow_status.map(sqlx::types::Json) as Option<sqlx::types::Json<FlowStatus>>
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
@@ -5023,24 +5071,10 @@ async fn get_completed_job<'a>(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::Result<Response> {
|
||||
let job_o = sqlx::query_as::<_, CompletedJob>("SELECT id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
|
||||
CASE WHEN args is null or pg_column_size(args) < 90000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 90000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind,
|
||||
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
|
||||
raw_lock, email, visible_to_owner, mem_peak, tag, priority, result->'wm_labels' as labels FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
.bind(id)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&db)
|
||||
let job_o = get_completed_job_ex(&db, &w_id, id, false, Some(&opt_authed))
|
||||
.await?;
|
||||
|
||||
let cj = not_found_if_none(job_o, "Completed Job", id.to_string())?;
|
||||
|
||||
if opt_authed.is_none() && cj.created_by != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
|
||||
));
|
||||
}
|
||||
let cj = format_completed_job_result(cj);
|
||||
|
||||
let response = Json(cj).into_response();
|
||||
// let extra_log = query_scalar!(
|
||||
// "SELECT substr(logs, $1) as logs FROM large_logs WHERE workspace_id = $2 AND job_id = $3",
|
||||
|
||||
@@ -25,7 +25,6 @@ use argon2::Argon2;
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use axum::{middleware::from_extractor, routing::get, Extension, Router};
|
||||
use db::DB;
|
||||
use git_version::git_version;
|
||||
use http::HeaderValue;
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
@@ -40,7 +39,7 @@ use tower_http::{
|
||||
};
|
||||
use windmill_common::db::UserDB;
|
||||
use windmill_common::worker::{ALL_TAGS, CLOUD_HOSTED};
|
||||
use windmill_common::{BASE_URL, INSTANCE_NAME};
|
||||
use windmill_common::{BASE_URL, INSTANCE_NAME, utils::GIT_VERSION};
|
||||
|
||||
use crate::scim_ee::has_scim_token;
|
||||
use windmill_common::error::AppError;
|
||||
@@ -93,9 +92,6 @@ mod workers;
|
||||
mod workspaces;
|
||||
mod workspaces_ee;
|
||||
|
||||
pub const GIT_VERSION: &str =
|
||||
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
|
||||
pub const DEFAULT_BODY_LIMIT: usize = 2097152 * 100; // 200MB
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -154,18 +150,18 @@ pub async fn add_webhook_allowed_origin(
|
||||
type IndexReader = ();
|
||||
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
type IndexWriter = ();
|
||||
type ServiceLogIndexReader = ();
|
||||
|
||||
#[cfg(feature = "tantivy")]
|
||||
type IndexReader = windmill_indexer::indexer_ee::IndexReader;
|
||||
type IndexReader = windmill_indexer::completed_runs_ee::IndexReader;
|
||||
#[cfg(feature = "tantivy")]
|
||||
type IndexWriter = windmill_indexer::indexer_ee::IndexWriter;
|
||||
type ServiceLogIndexReader = windmill_indexer::service_logs_ee::ServiceLogIndexReader;
|
||||
|
||||
pub async fn run_server(
|
||||
db: DB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
index_reader: Option<IndexReader>,
|
||||
index_writer: Option<IndexWriter>,
|
||||
job_index_reader: Option<IndexReader>,
|
||||
log_index_reader: Option<ServiceLogIndexReader>,
|
||||
addr: SocketAddr,
|
||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||
port_tx: tokio::sync::oneshot::Sender<String>,
|
||||
@@ -205,8 +201,9 @@ pub async fn run_server(
|
||||
.layer(Extension(rsmq.clone()))
|
||||
.layer(Extension(user_db.clone()))
|
||||
.layer(Extension(auth_cache.clone()))
|
||||
.layer(Extension(index_reader))
|
||||
.layer(Extension(index_writer))
|
||||
.layer(Extension(job_index_reader))
|
||||
.layer(Extension(log_index_reader))
|
||||
// .layer(Extension(index_writer))
|
||||
.layer(CookieManagerLayer::new())
|
||||
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())))
|
||||
.layer(DefaultBodyLimit::max(
|
||||
@@ -322,6 +319,10 @@ pub async fn run_server(
|
||||
"/srch/w/:workspace_id/index",
|
||||
indexer_ee::workspaced_service(),
|
||||
)
|
||||
.nest(
|
||||
"/srch/index",
|
||||
indexer_ee::global_service(),
|
||||
)
|
||||
.nest("/oidc", oidc_ee::global_service())
|
||||
.nest(
|
||||
"/saml",
|
||||
|
||||
@@ -58,7 +58,10 @@ pub fn global_service() -> Router {
|
||||
)
|
||||
.route("/renew_license_key", post(renew_license_key))
|
||||
.route("/customer_portal", post(create_customer_portal_session))
|
||||
.route("/test_critical_channels", post(test_critical_channels));
|
||||
.route("/test_critical_channels", post(test_critical_channels))
|
||||
.route("/critical_alerts", get(get_critical_alerts))
|
||||
.route("/critical_alerts/:id/acknowledge", post(acknowledge_critical_alert))
|
||||
.route("/critical_alerts/acknowledge_all", post(acknowledge_all_critical_alerts));
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
{
|
||||
@@ -430,3 +433,117 @@ pub async fn test_critical_channels(
|
||||
pub async fn test_critical_channels() -> Result<String> {
|
||||
Ok("Critical channels require EE".to_string())
|
||||
}
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CriticalAlert {
|
||||
id: i32,
|
||||
alert_type: String,
|
||||
message: String,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
acknowledged: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize)]
|
||||
pub struct AlertQueryParams {
|
||||
pub page: Option<i32>,
|
||||
pub page_size: Option<i32>,
|
||||
pub acknowledged: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn get_critical_alerts(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Query(params): Query<AlertQueryParams>,
|
||||
) -> JsonResult<Vec<CriticalAlert>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
// Default pagination values if not provided
|
||||
let page = params.page.unwrap_or(1).max(1);
|
||||
let page_size = params.page_size.unwrap_or(10).min(100) as i64;
|
||||
let offset = ((page - 1) * page_size as i32) as i64;
|
||||
|
||||
let alerts = if let Some(acknowledged) = params.acknowledged {
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, acknowledged
|
||||
FROM alerts
|
||||
WHERE acknowledged = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3",
|
||||
acknowledged,
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, acknowledged
|
||||
FROM alerts
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2",
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(Json(alerts))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn get_critical_alerts() -> error::Error {
|
||||
error::Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn acknowledge_critical_alert(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Path(id): Path<i32>,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE alerts SET acknowledged = true WHERE id = $1",
|
||||
id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Acknowledged critical alert with id: {}", id);
|
||||
Ok("Critical alert acknowledged".to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn acknowledge_critical_alert() -> error::Error {
|
||||
error::Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn acknowledge_all_critical_alerts(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE alerts SET acknowledged = true WHERE acknowledged = false"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Acknowledged all unacknowledged critical alerts");
|
||||
Ok("All unacknowledged critical alerts acknowledged".to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn acknowledge_all_critical_alerts() -> error::Error {
|
||||
error::Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub async fn static_handler(OriginalUri(original_uri): OriginalUri) -> StaticFil
|
||||
}
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../../frontend/build/"]
|
||||
#[folder = "${FRONTEND_BUILD_DIR:-../../frontend/build/}"]
|
||||
struct Asset;
|
||||
pub struct StaticFile(Uri);
|
||||
|
||||
|
||||
@@ -90,6 +90,9 @@ pub fn global_service() -> Router {
|
||||
.route("/accept_invite", post(accept_invite))
|
||||
.route("/list_as_super_admin", get(list_users_as_super_admin))
|
||||
.route("/setpassword", post(set_password))
|
||||
.route("/set_password_of/:user", post(set_password_of_user))
|
||||
.route("/set_login_type/:user", post(set_login_type))
|
||||
|
||||
.route("/create", post(create_user))
|
||||
.route("/update/:user", post(update_user))
|
||||
.route("/delete/:user", delete(delete_user))
|
||||
@@ -129,7 +132,8 @@ fn username_override_from_label(label: Option<String>) -> Option<String> {
|
||||
Some(label)
|
||||
if label.starts_with("webhook-")
|
||||
|| label.starts_with("http-")
|
||||
|| label.starts_with("email-") =>
|
||||
|| label.starts_with("email-")
|
||||
|| label.starts_with("ws-") =>
|
||||
{
|
||||
Some(label)
|
||||
}
|
||||
@@ -745,10 +749,20 @@ pub async fn fetch_api_authed(
|
||||
username_override: String,
|
||||
) -> error::Result<ApiAuthed> {
|
||||
let permissioned_as = username_to_permissioned_as(username.as_str());
|
||||
fetch_api_authed_from_permissioned_as(permissioned_as, email, w_id, db, username_override).await
|
||||
}
|
||||
|
||||
pub async fn fetch_api_authed_from_permissioned_as(
|
||||
permissioned_as: String,
|
||||
email: String,
|
||||
w_id: &str,
|
||||
db: &DB,
|
||||
username_override: String,
|
||||
) -> error::Result<ApiAuthed> {
|
||||
let authed =
|
||||
fetch_authed_from_permissioned_as(permissioned_as, email.clone(), w_id, db).await?;
|
||||
Ok(ApiAuthed {
|
||||
username: username,
|
||||
username: authed.username,
|
||||
email: email,
|
||||
is_admin: authed.is_admin,
|
||||
is_operator: authed.is_operator,
|
||||
@@ -855,6 +869,12 @@ pub struct EditPassword {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditLoginType {
|
||||
pub login_type: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
pub struct TruncatedToken {
|
||||
pub label: Option<String>,
|
||||
@@ -2017,7 +2037,52 @@ async fn set_password(
|
||||
authed: ApiAuthed,
|
||||
Json(ep): Json<EditPassword>,
|
||||
) -> Result<String> {
|
||||
crate::users_ee::set_password(db, argon2, authed, ep).await
|
||||
let email = authed.email.clone();
|
||||
crate::users_ee::set_password(db, argon2, authed, &email, ep).await
|
||||
}
|
||||
|
||||
async fn set_password_of_user(
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(argon2): Extension<Arc<Argon2<'_>>>,
|
||||
Path(email): Path<String>,
|
||||
authed: ApiAuthed,
|
||||
Json(ep): Json<EditPassword>,
|
||||
) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
crate::users_ee::set_password(db, argon2, authed, &email, ep).await
|
||||
}
|
||||
|
||||
async fn set_login_type(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(email): Path<String>,
|
||||
authed: ApiAuthed,
|
||||
Json(et): Json<EditLoginType>,
|
||||
) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE password SET login_type = $1 WHERE email = $2",
|
||||
et.login_type,
|
||||
email
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"users.set_login_type",
|
||||
ActionKind::Update,
|
||||
"global",
|
||||
Some(&email),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(format!("login type of {} updated to {}", email, et.login_type))
|
||||
|
||||
}
|
||||
|
||||
async fn login(
|
||||
|
||||
@@ -27,6 +27,7 @@ pub async fn set_password(
|
||||
_db: DB,
|
||||
_argon2: Arc<Argon2<'_>>,
|
||||
_authed: ApiAuthed,
|
||||
_user_email: &str,
|
||||
_ep: EditPassword,
|
||||
) -> Result<String> {
|
||||
Err(Error::InternalErr(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
routing::{delete, get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use futures::{stream::SplitSink, SinkExt, StreamExt};
|
||||
use http::StatusCode;
|
||||
use itertools::Itertools;
|
||||
use rand::seq::SliceRandom;
|
||||
@@ -11,16 +12,20 @@ use serde::{
|
||||
de::{self, MapAccess, Visitor},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use serde_json::{value::RawValue, Value};
|
||||
use sql_builder::{bind::Bind, SqlBuilder};
|
||||
use sqlx::prelude::FromRow;
|
||||
use std::{collections::HashMap, fmt};
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
|
||||
use uuid::Uuid;
|
||||
use windmill_audit::{audit_ee::audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{self, JsonResult},
|
||||
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
|
||||
error::{self, to_anyhow, JsonResult},
|
||||
utils::{
|
||||
not_found_if_none, paginate, report_critical_error, require_admin, Pagination, StripPath,
|
||||
},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
INSTANCE_NAME,
|
||||
};
|
||||
@@ -28,9 +33,7 @@ use windmill_queue::PushArgsOwned;
|
||||
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
jobs::{
|
||||
run_wait_result_flow_by_path_internal, run_wait_result_script_by_path_internal, RunJobQuery,
|
||||
},
|
||||
jobs::{run_flow_by_path_inner, run_script_by_path_inner, RunJobQuery},
|
||||
users::fetch_api_authed,
|
||||
};
|
||||
|
||||
@@ -52,7 +55,29 @@ struct NewWebsocketTrigger {
|
||||
script_path: String,
|
||||
is_flow: bool,
|
||||
enabled: Option<bool>,
|
||||
filters: Vec<serde_json::Value>,
|
||||
filters: Vec<Box<RawValue>>,
|
||||
initial_messages: Vec<Box<RawValue>>,
|
||||
url_runnable_args: Box<RawValue>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsonFilter {
|
||||
key: String,
|
||||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Filter {
|
||||
JsonFilter(JsonFilter),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
enum InitialMessage {
|
||||
#[serde(rename = "raw_message")]
|
||||
RawMessage(String),
|
||||
#[serde(rename = "runnable_result")]
|
||||
RunnableResult { path: String, args: Box<RawValue>, is_flow: bool },
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
@@ -70,7 +95,9 @@ pub struct WebsocketTrigger {
|
||||
extra_perms: serde_json::Value,
|
||||
error: Option<String>,
|
||||
enabled: bool,
|
||||
filters: Vec<serde_json::Value>,
|
||||
filters: Vec<sqlx::types::Json<Box<RawValue>>>,
|
||||
initial_messages: Vec<sqlx::types::Json<Box<RawValue>>>,
|
||||
url_runnable_args: sqlx::types::Json<Box<RawValue>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -79,7 +106,9 @@ struct EditWebsocketTrigger {
|
||||
url: String,
|
||||
script_path: String,
|
||||
is_flow: bool,
|
||||
filters: Vec<serde_json::Value>,
|
||||
filters: Vec<Box<RawValue>>,
|
||||
initial_messages: Vec<Box<RawValue>>,
|
||||
url_runnable_args: Box<RawValue>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -133,14 +162,13 @@ async fn get_websocket_trigger(
|
||||
) -> error::JsonResult<WebsocketTrigger> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let path = path.to_path();
|
||||
let trigger = sqlx::query_as!(
|
||||
WebsocketTrigger,
|
||||
let trigger = sqlx::query_as::<_, WebsocketTrigger>(
|
||||
r#"SELECT *
|
||||
FROM websocket_trigger
|
||||
WHERE workspace_id = $1 AND path = $2"#,
|
||||
w_id,
|
||||
path,
|
||||
)
|
||||
.bind(w_id)
|
||||
.bind(path)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -164,19 +192,27 @@ async fn create_websocket_trigger(
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
sqlx::query_as!(
|
||||
WebsocketTrigger,
|
||||
"INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now()) RETURNING *",
|
||||
w_id,
|
||||
ct.path,
|
||||
ct.url,
|
||||
ct.script_path,
|
||||
ct.is_flow,
|
||||
ct.enabled.unwrap_or(true),
|
||||
&ct.filters,
|
||||
&authed.username,
|
||||
&authed.email
|
||||
|
||||
let filters = ct.filters.into_iter().map(sqlx::types::Json).collect_vec();
|
||||
let initial_messages = ct
|
||||
.initial_messages
|
||||
.into_iter()
|
||||
.map(sqlx::types::Json)
|
||||
.collect_vec();
|
||||
sqlx::query_as::<_, WebsocketTrigger>(
|
||||
"INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, initial_messages, url_runnable_args, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now()) RETURNING *",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.bind(&ct.path)
|
||||
.bind(ct.url)
|
||||
.bind(ct.script_path)
|
||||
.bind(ct.is_flow)
|
||||
.bind(ct.enabled.unwrap_or(true))
|
||||
.bind(filters.as_slice())
|
||||
.bind(initial_messages.as_slice())
|
||||
.bind(sqlx::types::Json(ct.url_runnable_args))
|
||||
.bind(&authed.username)
|
||||
.bind(&authed.email)
|
||||
.fetch_one(&mut *tx).await?;
|
||||
|
||||
audit_log(
|
||||
@@ -204,15 +240,24 @@ async fn update_websocket_trigger(
|
||||
let path = path.to_path();
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let filters = ct.filters.into_iter().map(sqlx::types::Json).collect_vec();
|
||||
let initial_messages = ct
|
||||
.initial_messages
|
||||
.into_iter()
|
||||
.map(sqlx::types::Json)
|
||||
.collect_vec();
|
||||
|
||||
// important to update server_id, last_server_ping and error to NULL to stop current websocket listener
|
||||
sqlx::query!(
|
||||
"UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, edited_by = $6, email = $7, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL
|
||||
WHERE workspace_id = $8 AND path = $9",
|
||||
"UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL
|
||||
WHERE workspace_id = $10 AND path = $11",
|
||||
ct.url,
|
||||
ct.script_path,
|
||||
ct.path,
|
||||
ct.is_flow,
|
||||
&ct.filters,
|
||||
filters.as_slice() as &[sqlx::types::Json<Box<RawValue>>],
|
||||
initial_messages.as_slice() as &[sqlx::types::Json<Box<RawValue>>],
|
||||
sqlx::types::Json(ct.url_runnable_args) as sqlx::types::Json<Box<RawValue>>,
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
w_id,
|
||||
@@ -335,8 +380,7 @@ async fn listen_to_unlistened_websockets(
|
||||
rsmq: &Option<rsmq_async::MultiplexedRsmq>,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
) -> () {
|
||||
match sqlx::query_as!(
|
||||
WebsocketTrigger,
|
||||
match sqlx::query_as::<_, WebsocketTrigger>(
|
||||
r#"SELECT *
|
||||
FROM websocket_trigger
|
||||
WHERE enabled IS TRUE AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')"#
|
||||
@@ -422,8 +466,6 @@ impl<'de, 'a> Visitor<'de> for SupersetVisitor<'a> {
|
||||
if key == self.key {
|
||||
// Deserialize the value for the key and check if it's a superset
|
||||
let json_value: Value = map.next_value()?;
|
||||
tracing::info!("json_value: {:?}", json_value);
|
||||
tracing::info!("value_to_check: {:?}", self.value_to_check);
|
||||
return Ok(is_superset(&json_value, self.value_to_check));
|
||||
} else {
|
||||
// Skip the value if it's not the one we're interested in
|
||||
@@ -470,149 +512,429 @@ where
|
||||
deserializer.deserialize_map(SupersetVisitor { key, value_to_check })
|
||||
}
|
||||
|
||||
async fn wait_runnable_result(
|
||||
path: String,
|
||||
is_flow: bool,
|
||||
args: &Box<RawValue>,
|
||||
ws_trigger: &WebsocketTrigger,
|
||||
username_override: String,
|
||||
db: &DB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
) -> error::Result<String> {
|
||||
let user_db = UserDB::new(db.clone());
|
||||
let authed = fetch_api_authed(
|
||||
ws_trigger.edited_by.clone(),
|
||||
ws_trigger.email.clone(),
|
||||
&ws_trigger.workspace_id,
|
||||
&db,
|
||||
username_override,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let args = serde_json::from_str::<Option<HashMap<String, Box<RawValue>>>>(args.get())
|
||||
.map_err(|e| error::Error::BadRequest(format!("invalid json: {}", e)))?
|
||||
.unwrap_or_else(HashMap::new);
|
||||
|
||||
let label_prefix = Some(format!("ws-{}-", ws_trigger.path));
|
||||
let (_, job_id) = if is_flow {
|
||||
run_flow_by_path_inner(
|
||||
authed,
|
||||
db.clone(),
|
||||
user_db,
|
||||
rsmq.clone(),
|
||||
ws_trigger.workspace_id.clone(),
|
||||
StripPath(path.clone()),
|
||||
RunJobQuery::default(),
|
||||
PushArgsOwned { args, extra: None },
|
||||
label_prefix,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
run_script_by_path_inner(
|
||||
authed,
|
||||
db.clone(),
|
||||
user_db,
|
||||
rsmq.clone(),
|
||||
ws_trigger.workspace_id.clone(),
|
||||
StripPath(path.clone()),
|
||||
RunJobQuery::default(),
|
||||
PushArgsOwned { args, extra: None },
|
||||
label_prefix,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
let start_time = tokio::time::Instant::now();
|
||||
|
||||
loop {
|
||||
if start_time.elapsed() > tokio::time::Duration::from_secs(300) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Timed out after 5m waiting for runnable {path} (is_flow: {is_flow}) to complete",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct RawResult {
|
||||
result: Option<sqlx::types::Json<Box<RawValue>>>,
|
||||
success: bool,
|
||||
}
|
||||
|
||||
let result = sqlx::query_as::<_, RawResult>(
|
||||
"SELECT result, success FROM completed_job WHERE id = $1 AND workspace_id = $2",
|
||||
)
|
||||
.bind(Uuid::parse_str(&job_id).unwrap())
|
||||
.bind(&ws_trigger.workspace_id)
|
||||
.fetch_optional(db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Some(r)) => {
|
||||
if !r.success {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Runnable {path} (is_flow: {is_flow}) failed: {:?}",
|
||||
r.result
|
||||
)
|
||||
.into());
|
||||
} else {
|
||||
return Ok(r.result.map(|r| r.get().to_owned()).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// not yet done, wait for 5s and check again
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Error fetching job result for runnable {path} (is_flow: {is_flow}): {err}",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_initial_messages(
|
||||
ws_trigger: &WebsocketTrigger,
|
||||
mut writer: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
|
||||
db: &DB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
) -> error::Result<()> {
|
||||
let initial_messages: Vec<InitialMessage> = ws_trigger
|
||||
.initial_messages
|
||||
.iter()
|
||||
.filter_map(|m| serde_json::from_str(m.get()).ok())
|
||||
.collect_vec();
|
||||
|
||||
for start_message in initial_messages {
|
||||
match start_message {
|
||||
InitialMessage::RawMessage(msg) => {
|
||||
let msg = if msg.starts_with("\"") && msg.ends_with("\"") {
|
||||
msg[1..msg.len() - 1].to_string()
|
||||
} else {
|
||||
msg
|
||||
};
|
||||
tracing::info!(
|
||||
"Sending raw message initial message to websocket {}: {}",
|
||||
ws_trigger.url,
|
||||
msg
|
||||
);
|
||||
writer
|
||||
.send(tokio_tungstenite::tungstenite::Message::Text(msg))
|
||||
.await
|
||||
.map_err(to_anyhow)
|
||||
.with_context(|| "failed to send raw message")?;
|
||||
}
|
||||
InitialMessage::RunnableResult { path, is_flow, args } => {
|
||||
tracing::info!(
|
||||
"Running runnable {path} (is_flow: {is_flow}) for initial message to websocket {}",
|
||||
ws_trigger.url,
|
||||
);
|
||||
|
||||
let result = wait_runnable_result(
|
||||
path.clone(),
|
||||
is_flow,
|
||||
&args,
|
||||
ws_trigger,
|
||||
"init".to_string(),
|
||||
db,
|
||||
rsmq.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!(
|
||||
"Sending runnable {path} (is_flow: {is_flow}) result to websocket {}",
|
||||
ws_trigger.url
|
||||
);
|
||||
|
||||
let result = if result.starts_with("\"") && result.ends_with("\"") {
|
||||
result[1..result.len() - 1].to_string()
|
||||
} else {
|
||||
result
|
||||
};
|
||||
|
||||
writer
|
||||
.send(tokio_tungstenite::tungstenite::Message::Text(result))
|
||||
.await
|
||||
.map_err(to_anyhow)
|
||||
.with_context(|| {
|
||||
format!("Failed to send runnable {path} (is_flow: {is_flow}) result")
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_url_from_runnable(
|
||||
path: &str,
|
||||
is_flow: bool,
|
||||
ws_trigger: &WebsocketTrigger,
|
||||
db: &DB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
) -> error::Result<String> {
|
||||
tracing::info!("Running runnable {path} (is_flow: {is_flow}) to get websocket URL",);
|
||||
|
||||
let result = wait_runnable_result(
|
||||
path.to_string(),
|
||||
is_flow,
|
||||
&ws_trigger.url_runnable_args.0,
|
||||
ws_trigger,
|
||||
"url".to_string(),
|
||||
db,
|
||||
rsmq,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if result.starts_with("\"") && result.ends_with("\"") {
|
||||
Ok(result[1..result.len() - 1].to_string())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Runnable {path} (is_flow: {is_flow}) did not return a string").into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_ping(db: &DB, ws_trigger: &WebsocketTrigger, error: Option<&str>) -> Option<()> {
|
||||
match sqlx::query_scalar!(
|
||||
"UPDATE websocket_trigger SET last_server_ping = now(), error = $1 WHERE workspace_id = $2 AND path = $3 AND server_id = $4 AND enabled IS TRUE RETURNING 1",
|
||||
error,
|
||||
ws_trigger.workspace_id,
|
||||
ws_trigger.path,
|
||||
*INSTANCE_NAME
|
||||
).fetch_optional(db).await {
|
||||
Ok(updated) => {
|
||||
if updated.flatten().is_none() {
|
||||
tracing::info!("Websocket {} changed, disabled, or deleted, stopping...", ws_trigger.url);
|
||||
return None;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Error updating ping of websocket {}: {:?}", ws_trigger.url, err);
|
||||
}
|
||||
};
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
async fn loop_ping(db: &DB, ws_trigger: &WebsocketTrigger, error: Option<&str>) -> () {
|
||||
loop {
|
||||
if let None = update_ping(db, ws_trigger, error).await {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn disable_with_error(db: &DB, ws_trigger: &WebsocketTrigger, error: String) {
|
||||
match sqlx::query!(
|
||||
"UPDATE websocket_trigger SET enabled = FALSE, error = $1, server_id = NULL, last_server_ping = NULL WHERE workspace_id = $2 AND path = $3",
|
||||
error,
|
||||
ws_trigger.workspace_id,
|
||||
ws_trigger.path,
|
||||
)
|
||||
.execute(db).await {
|
||||
Ok(_) => {
|
||||
report_critical_error(format!("Disabling websocket {} because of error: {}", ws_trigger.url, error), db.clone()).await;
|
||||
},
|
||||
Err(disable_err) => {
|
||||
report_critical_error(
|
||||
format!("Could not disable websocket {} with err {}, disabling because of error {}", ws_trigger.path, disable_err, error),
|
||||
db.clone()
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_to_websocket(
|
||||
ws_trigger: WebsocketTrigger,
|
||||
db: DB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> () {
|
||||
async fn update_ping(db: DB, ws_trigger: &WebsocketTrigger, error: Option<&str>) -> Option<()> {
|
||||
match sqlx::query_scalar!(
|
||||
"UPDATE websocket_trigger SET last_server_ping = now(), error = $1 WHERE workspace_id = $2 AND path = $3 AND server_id = $4 AND enabled IS TRUE RETURNING 1",
|
||||
error,
|
||||
ws_trigger.workspace_id,
|
||||
ws_trigger.path,
|
||||
*INSTANCE_NAME
|
||||
).fetch_optional(&db).await {
|
||||
Ok(updated) => {
|
||||
if updated.flatten().is_none() {
|
||||
tracing::info!("Websocket {} changed, disabled, or deleted, stopping...", ws_trigger.url);
|
||||
return None;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Error updating ping of websocket {}: {:?}", ws_trigger.url, err);
|
||||
}
|
||||
};
|
||||
|
||||
Some(())
|
||||
}
|
||||
update_ping(&db, &ws_trigger, Some("Connecting...")).await;
|
||||
|
||||
let url = ws_trigger.url.as_str();
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsonFilter {
|
||||
key: String,
|
||||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Filter {
|
||||
JsonFilter(JsonFilter),
|
||||
}
|
||||
let filters: Vec<Filter> = ws_trigger
|
||||
.filters
|
||||
.iter()
|
||||
.filter_map(|m| serde_json::from_value(m.clone()).ok())
|
||||
.filter_map(|m| serde_json::from_str(m.get()).ok())
|
||||
.collect_vec();
|
||||
|
||||
loop {
|
||||
let connect_url = if url.starts_with("$") {
|
||||
if url.starts_with("$flow:") || url.starts_with("$script:") {
|
||||
let path = url.splitn(2, ':').nth(1).unwrap();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws_trigger, Some(
|
||||
"Waiting on runnable to return websocket URL..."
|
||||
)) => {
|
||||
return;
|
||||
},
|
||||
url_result = get_url_from_runnable(path, url.starts_with("$flow:"), &ws_trigger, &db, rsmq.clone()) => match url_result {
|
||||
Ok(url) => url,
|
||||
Err(err) => {
|
||||
disable_with_error(
|
||||
&db,
|
||||
&ws_trigger,
|
||||
format!(
|
||||
"Error getting websocket URL from runnable after 5 tries: {:?}",
|
||||
err
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
disable_with_error(
|
||||
&db,
|
||||
&ws_trigger,
|
||||
format!("Invalid websocket runnable path: {}", url),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
url.to_string()
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
connection = connect_async(url) => {
|
||||
_ = loop_ping(&db, &ws_trigger, Some("Connecting...")) => {
|
||||
return;
|
||||
},
|
||||
connection = connect_async(connect_url) => {
|
||||
match connection {
|
||||
Ok((ws_stream, _)) => {
|
||||
tracing::info!("Listening to websocket {}", url);
|
||||
if let None = update_ping(db.clone(), &ws_trigger, None).await {
|
||||
if let None = update_ping(&db, &ws_trigger, None).await {
|
||||
return;
|
||||
}
|
||||
let (writer, mut reader) = ws_stream.split();
|
||||
let mut last_ping = tokio::time::Instant::now();
|
||||
let (_, mut read) = ws_stream.split();
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
}
|
||||
_ = async {
|
||||
if let Err(err) = send_initial_messages(&ws_trigger, writer, &db, rsmq.clone()).await {
|
||||
disable_with_error(&db, &ws_trigger, format!("Error sending initial messages: {:?}", err)).await;
|
||||
} else {
|
||||
// if initial messages sent successfully, wait forever
|
||||
futures::future::pending::<()>().await;
|
||||
}
|
||||
msg = read.next() => {
|
||||
if let Some(msg) = msg {
|
||||
if last_ping.elapsed() > tokio::time::Duration::from_secs(5) {
|
||||
if let None = update_ping(db.clone(), &ws_trigger, None).await {
|
||||
} => {
|
||||
// was disabled => exit
|
||||
return;
|
||||
},
|
||||
_ = async {
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
msg = reader.next() => {
|
||||
if let Some(msg) = msg {
|
||||
if last_ping.elapsed() > tokio::time::Duration::from_secs(5) {
|
||||
if let None = update_ping(&db, &ws_trigger, None).await {
|
||||
return;
|
||||
}
|
||||
last_ping = tokio::time::Instant::now();
|
||||
}
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
match msg {
|
||||
tokio_tungstenite::tungstenite::Message::Text(text) => {
|
||||
let mut should_handle = true;
|
||||
for filter in &filters {
|
||||
match filter {
|
||||
Filter::JsonFilter(JsonFilter { key, value }) => {
|
||||
let mut deserializer = serde_json::Deserializer::from_str(text.as_str());
|
||||
should_handle = match is_value_superset(&mut deserializer, key, &value) {
|
||||
Ok(filter_match) => {
|
||||
filter_match
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Error deserializing filter for websocket {}: {:?}", url, err);
|
||||
false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if !should_handle {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if should_handle {
|
||||
if let Err(err) = run_job(&db, rsmq.clone(), &ws_trigger, text).await {
|
||||
report_critical_error(format!("Failed to trigger job from websocket {}: {:?}", ws_trigger.url, err), db.clone()).await;
|
||||
};
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("Error reading from websocket {}: {:?}", url, err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Websocket {} closed", url);
|
||||
if let None =
|
||||
update_ping(&db, &ws_trigger, Some("Websocket closed")).await
|
||||
{
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {
|
||||
if let None = update_ping(&db, &ws_trigger, None).await {
|
||||
return;
|
||||
}
|
||||
last_ping = tokio::time::Instant::now();
|
||||
}
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
match msg {
|
||||
tokio_tungstenite::tungstenite::Message::Text(text) => {
|
||||
let mut should_handle = true;
|
||||
for filter in &filters {
|
||||
match filter {
|
||||
Filter::JsonFilter(JsonFilter { key, value }) => {
|
||||
let mut deserializer = serde_json::Deserializer::from_str(text.as_str());
|
||||
should_handle = match is_value_superset(&mut deserializer, key, &value) {
|
||||
Ok(filter_match) => {
|
||||
filter_match
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Error deserializing filter for websocket {}: {:?}", url, err);
|
||||
false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if !should_handle {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if should_handle {
|
||||
let db_ = db.clone();
|
||||
let rsmq_ = rsmq.clone();
|
||||
let ws_trigger_ = ws_trigger.clone();
|
||||
tokio::spawn(async move {
|
||||
let url = ws_trigger_.url.clone();
|
||||
if let Err(err) = run_job(db_, rsmq_, ws_trigger_, text).await {
|
||||
tracing::error!("Error running job on websocket {}: {:?}", url, err);
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("Error reading from websocket {}: {:?}", url, err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Websocket {} closed", url);
|
||||
if let None =
|
||||
update_ping(db.clone(), &ws_trigger, Some("Websocket closed")).await
|
||||
{
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
break;
|
||||
},
|
||||
}
|
||||
},
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {
|
||||
if let None = update_ping(db.clone(), &ws_trigger, None).await {
|
||||
return;
|
||||
}
|
||||
last_ping = tokio::time::Instant::now();
|
||||
},
|
||||
}
|
||||
} => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error connecting to websocket {}: {:?}", url, err);
|
||||
if let None =
|
||||
update_ping(db.clone(), &ws_trigger, Some(err.to_string().as_str())).await
|
||||
update_ping(&db, &ws_trigger, Some(err.to_string().as_str())).await
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -625,16 +947,18 @@ async fn listen_to_websocket(
|
||||
}
|
||||
|
||||
async fn run_job(
|
||||
db: DB,
|
||||
db: &DB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
trigger: WebsocketTrigger,
|
||||
trigger: &WebsocketTrigger,
|
||||
msg: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let args = PushArgsOwned {
|
||||
args: HashMap::from([("msg".to_string(), to_raw_value(&msg))]),
|
||||
extra: Some(HashMap::from([(
|
||||
"wm_trigger".to_string(),
|
||||
to_raw_value(&serde_json::json!({"kind": "websocket"})),
|
||||
to_raw_value(
|
||||
&serde_json::json!({"kind": "websocket", "websocket": { "url": trigger.url }}),
|
||||
),
|
||||
)])),
|
||||
};
|
||||
let label_prefix = Some(format!("ws-{}-", trigger.path));
|
||||
@@ -643,7 +967,7 @@ async fn run_job(
|
||||
trigger.edited_by.clone(),
|
||||
trigger.email.clone(),
|
||||
&trigger.workspace_id,
|
||||
&db,
|
||||
db,
|
||||
"anonymous".to_string(),
|
||||
)
|
||||
.await?;
|
||||
@@ -653,27 +977,27 @@ async fn run_job(
|
||||
let run_query = RunJobQuery::default();
|
||||
|
||||
if trigger.is_flow {
|
||||
run_wait_result_flow_by_path_internal(
|
||||
db,
|
||||
run_query,
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_flow_by_path_inner(
|
||||
authed,
|
||||
rsmq,
|
||||
db.clone(),
|
||||
user_db,
|
||||
args,
|
||||
rsmq,
|
||||
trigger.workspace_id.clone(),
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
label_prefix,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
run_wait_result_script_by_path_internal(
|
||||
db,
|
||||
run_query,
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_script_by_path_inner(
|
||||
authed,
|
||||
rsmq,
|
||||
db.clone(),
|
||||
user_db,
|
||||
rsmq,
|
||||
trigger.workspace_id.clone(),
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
label_prefix,
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ pub fn global_service() -> Router {
|
||||
)
|
||||
.route("/get_default_tags", get(get_default_tags))
|
||||
.route("/queue_metrics", get(get_queue_metrics))
|
||||
.route("/queue_counts", get(get_queue_counts))
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
@@ -176,3 +177,12 @@ async fn get_queue_metrics(
|
||||
|
||||
Ok(Json(queue_metrics))
|
||||
}
|
||||
|
||||
async fn get_queue_counts(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
) -> JsonResult<std::collections::HashMap<String, u32>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let queue_counts = windmill_common::queue::get_queue_counts(&db).await;
|
||||
Ok(Json(queue_counts))
|
||||
}
|
||||
@@ -58,6 +58,7 @@ async-stream.workspace = true
|
||||
const_format.workspace = true
|
||||
crc.workspace = true
|
||||
windmill-macros.workspace = true
|
||||
semver.workspace = true
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemalloc-ctl = { optional = true, workspace = true }
|
||||
|
||||
@@ -16,6 +16,7 @@ use rand::Rng;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
more_serde::{default_empty_string, default_id, default_null, default_true, is_default},
|
||||
scripts::{Schema, ScriptHash, ScriptLang},
|
||||
};
|
||||
@@ -651,3 +652,29 @@ pub fn add_virtual_items_if_necessary(modules: &mut Vec<FlowModule>) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn has_failure_module<'c>(flow: sqlx::types::Uuid, db: &sqlx::Pool<sqlx::Postgres>, completed: bool) -> Result<bool, Error> {
|
||||
if completed {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT raw_flow->'failure_module' != 'null'::jsonb
|
||||
FROM completed_job
|
||||
WHERE id = $1",
|
||||
flow
|
||||
)
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT raw_flow->'failure_module' != 'null'::jsonb
|
||||
FROM queue
|
||||
WHERE id = $1",
|
||||
flow
|
||||
)
|
||||
}
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error during retrieval of has_failure_module: {e:#}"
|
||||
))
|
||||
})
|
||||
.map(|v| v.unwrap_or(false))
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation
|
||||
pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";
|
||||
pub const HUB_ACCESSIBLE_URL_SETTING: &str = "hub_accessible_url";
|
||||
pub const CRITICAL_ERROR_CHANNELS_SETTING: &str = "critical_error_channels";
|
||||
pub const CRITICAL_ALERT_MUTE_UI_SETTING: &str = "critical_alert_mute_ui";
|
||||
pub const DEV_INSTANCE_SETTING: &str = "dev_instance";
|
||||
pub const JWT_SECRET_SETTING: &str = "jwt_secret";
|
||||
pub const EMAIL_DOMAIN_SETTING: &str = "email_domain";
|
||||
|
||||
@@ -108,13 +108,6 @@ pub struct QueuedJob {
|
||||
pub cache_ttl: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub priority: Option<i16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(skip)]
|
||||
pub self_wait_time_ms: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(skip)]
|
||||
pub aggregate_wait_time_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl QueuedJob {
|
||||
@@ -198,8 +191,6 @@ impl Default for QueuedJob {
|
||||
flow_step_id: None,
|
||||
cache_ttl: None,
|
||||
priority: None,
|
||||
self_wait_time_ms: None,
|
||||
aggregate_wait_time_ms: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,13 +244,6 @@ pub struct CompletedJob {
|
||||
pub priority: Option<i16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub labels: Option<serde_json::Value>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(skip)]
|
||||
pub self_wait_time_ms: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(skip)]
|
||||
pub aggregate_wait_time_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl CompletedJob {
|
||||
|
||||
@@ -86,6 +86,8 @@ lazy_static::lazy_static! {
|
||||
pub static ref METRICS_ENABLED: AtomicBool = AtomicBool::new(std::env::var("METRICS_PORT").is_ok() || std::env::var("METRICS_ADDR").is_ok());
|
||||
pub static ref METRICS_DEBUG_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static ref CRITICAL_ALERT_MUTE_UI_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static ref BASE_URL: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
|
||||
pub static ref IS_READY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
|
||||
@@ -109,10 +109,13 @@ pub struct S3AwsOidcResource {
|
||||
pub audience: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct S3Object {
|
||||
pub s3: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub storage: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub filename: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct Smtp {
|
||||
pub port: u16,
|
||||
pub from: String,
|
||||
pub tls_implicit: Option<bool>,
|
||||
pub disable_tls: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq)]
|
||||
@@ -20,6 +21,7 @@ pub struct SmtpConfigOpt {
|
||||
pub smtp_port: Option<u16>,
|
||||
pub smtp_from: Option<String>,
|
||||
pub smtp_tls_implicit: Option<bool>,
|
||||
pub smtp_disable_tls: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
|
||||
@@ -39,6 +41,7 @@ pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
|
||||
username,
|
||||
password,
|
||||
tls_implicit: config.smtp_tls_implicit,
|
||||
disable_tls: config.smtp_disable_tls,
|
||||
port: config.smtp_port.unwrap_or(587),
|
||||
from: config
|
||||
.smtp_from
|
||||
@@ -60,6 +63,9 @@ pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
|
||||
tls_implicit: std::env::var("SMTP_TLS_IMPLICIT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok()),
|
||||
disable_tls: std::env::var("SMTP_DISABLE_TLS")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok()),
|
||||
port: std::env::var("SMTP_PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok())
|
||||
@@ -87,6 +93,7 @@ impl Default for SmtpConfigOpt {
|
||||
smtp_port: None,
|
||||
smtp_tls_implicit: None,
|
||||
smtp_username: None,
|
||||
smtp_disable_tls: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use semver::Version;
|
||||
|
||||
pub const MAX_PER_PAGE: usize = 10000;
|
||||
pub const DEFAULT_PER_PAGE: usize = 1000;
|
||||
@@ -28,12 +29,19 @@ pub const DEFAULT_PER_PAGE: usize = 1000;
|
||||
pub const GIT_VERSION: &str =
|
||||
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use crate::CRITICAL_ALERT_MUTE_UI_ENABLED;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||
.user_agent("windmill/beta")
|
||||
.timeout(std::time::Duration::from_secs(20))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build().unwrap();
|
||||
pub static ref GIT_SEM_VERSION: Version = Version::parse(
|
||||
// skip first `v` character.
|
||||
GIT_VERSION.split_at(1).1
|
||||
).unwrap_or(Version::new(0, 1, 0));
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -244,9 +252,12 @@ pub fn generate_lock_id(database_name: &str) -> i64 {
|
||||
pub async fn report_critical_error(error_message: String, _db: DB) -> () {
|
||||
tracing::error!("CRITICAL ERROR: {error_message}");
|
||||
|
||||
let mute = CRITICAL_ALERT_MUTE_UI_ENABLED.load(Ordering::Relaxed);
|
||||
|
||||
if let Err(err) = sqlx::query!(
|
||||
"INSERT INTO alerts (alert_type, message) VALUES ('critical_error', $1)",
|
||||
error_message
|
||||
"INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('critical_error', $1, $2)",
|
||||
error_message,
|
||||
mute
|
||||
)
|
||||
.execute(&_db)
|
||||
.await
|
||||
@@ -261,9 +272,12 @@ pub async fn report_critical_error(error_message: String, _db: DB) -> () {
|
||||
pub async fn report_recovered_critical_error(message: String, _db: DB) -> () {
|
||||
tracing::info!("RECOVERED CRITICAL ERROR: {message}");
|
||||
|
||||
let mute = CRITICAL_ALERT_MUTE_UI_ENABLED.load(Ordering::Relaxed);
|
||||
|
||||
if let Err(err) = sqlx::query!(
|
||||
"INSERT INTO alerts (alert_type, message) VALUES ('recovered_critical_error', $1)",
|
||||
message
|
||||
"INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('recovered_critical_error', $1, $2)",
|
||||
message,
|
||||
mute
|
||||
)
|
||||
.execute(&_db)
|
||||
.await
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use const_format::concatcp;
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
@@ -88,6 +89,7 @@ lazy_static::lazy_static! {
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
pub static ref MIN_VERSION: Arc<RwLock<Version>> = Arc::new(RwLock::new(Version::new(0, 0, 0)));
|
||||
}
|
||||
|
||||
pub async fn make_suspended_pull_query(wc: &WorkerConfig) {
|
||||
@@ -308,6 +310,8 @@ fn parse_file<T: FromStr>(path: &str) -> Option<T> {
|
||||
pub struct PythonAnnotations {
|
||||
pub no_cache: bool,
|
||||
pub no_uv: bool,
|
||||
pub no_uv_install: bool,
|
||||
pub no_uv_compile: bool,
|
||||
}
|
||||
|
||||
#[annotations("//")]
|
||||
@@ -548,6 +552,31 @@ pub fn get_windmill_memory_usage() -> Option<i64> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_min_version<'c, E: sqlx::Executor<'c, Database = sqlx::Postgres>>(executor: E) -> bool {
|
||||
use crate::utils::{GIT_VERSION, GIT_SEM_VERSION};
|
||||
|
||||
// fetch all pings with a different version than self from the last 5 minutes.
|
||||
let pings = sqlx::query_scalar!(
|
||||
"SELECT wm_version FROM worker_ping WHERE wm_version != $1 AND ping_at > now() - interval '5 minutes'",
|
||||
GIT_VERSION
|
||||
).fetch_all(executor).await.unwrap_or_default();
|
||||
|
||||
let cur_version = GIT_SEM_VERSION.clone();
|
||||
let min_version = pings
|
||||
.iter()
|
||||
.filter(|x| !x.is_empty())
|
||||
.filter_map(|x| semver::Version::parse(x.split_at(1).1).ok())
|
||||
.min()
|
||||
.unwrap_or_else(|| cur_version.clone());
|
||||
|
||||
if min_version != cur_version {
|
||||
tracing::info!("Minimal worker version: {min_version}");
|
||||
}
|
||||
|
||||
*MIN_VERSION.write().await = min_version.clone();
|
||||
min_version >= cur_version
|
||||
}
|
||||
|
||||
pub async fn update_ping(worker_instance: &str, worker_name: &str, ip: &str, db: &DB) {
|
||||
let (tags, dw) = {
|
||||
let wc = WORKER_CONFIG.read().await.clone();
|
||||
|
||||
21
backend/windmill-indexer/src/completed_runs_ee.rs
Normal file
21
backend/windmill-indexer/src/completed_runs_ee.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::error::Error;
|
||||
use anyhow::anyhow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexReader;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexWriter;
|
||||
|
||||
pub async fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
|
||||
Err(anyhow!("Cannot initialize index: not in EE").into())
|
||||
}
|
||||
|
||||
pub async fn run_indexer(
|
||||
_db: Pool<Postgres>,
|
||||
mut _index_writer: IndexWriter,
|
||||
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
tracing::error!("Cannot run indexer: not in EE");
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::error::Error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexReader;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexWriter;
|
||||
|
||||
pub async fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
|
||||
Err(anyhow!("Cannot initialize index: not in EE").into())
|
||||
}
|
||||
|
||||
pub async fn run_indexer(
|
||||
_db: Pool<Postgres>,
|
||||
mut _index_writer: IndexWriter,
|
||||
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
tracing::error!("Cannot run indexer: not in EE");
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub mod completed_runs_ee;
|
||||
pub mod service_logs_ee;
|
||||
pub mod indexer_ee;
|
||||
|
||||
21
backend/windmill-indexer/src/service_logs_ee.rs
Normal file
21
backend/windmill-indexer/src/service_logs_ee.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::error::Error;
|
||||
use anyhow::anyhow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexReader;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexWriter;
|
||||
|
||||
pub async fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
|
||||
Err(anyhow!("Cannot initialize index: not in EE").into())
|
||||
}
|
||||
|
||||
pub async fn run_indexer(
|
||||
_db: Pool<Postgres>,
|
||||
mut _index_writer: IndexWriter,
|
||||
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
tracing::error!("Cannot run indexer: not in EE");
|
||||
}
|
||||
@@ -71,6 +71,9 @@ use windmill_common::BASE_URL;
|
||||
#[cfg(feature = "cloud")]
|
||||
use windmill_common::users::SUPERADMIN_SYNC_EMAIL;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::flows::has_failure_module;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::worker::CLOUD_HOSTED;
|
||||
|
||||
@@ -482,13 +485,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize)]
|
||||
struct RawFlowFailureModule {
|
||||
#[cfg(feature = "enterprise")]
|
||||
failure_module: Option<Box<RawValue>>,
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn add_completed_job_error<R: rsmq_async::RsmqConnection + Clone + Send>(
|
||||
db: &Pool<Postgres>,
|
||||
@@ -952,15 +948,7 @@ pub async fn add_completed_job<
|
||||
} else if !skip_downstream_error_handlers
|
||||
&& (matches!(queued_job.job_kind, JobKind::Script)
|
||||
|| matches!(queued_job.job_kind, JobKind::Flow)
|
||||
&& queued_job
|
||||
.raw_flow
|
||||
.as_ref()
|
||||
.and_then(|v| {
|
||||
serde_json::from_str::<RawFlowFailureModule>((**v).get())
|
||||
.ok()
|
||||
.and_then(|v| v.failure_module)
|
||||
})
|
||||
.is_none())
|
||||
&& !has_failure_module(job_id, db, true).await.unwrap_or(false))
|
||||
&& queued_job.parent_job.is_none()
|
||||
{
|
||||
let result = serde_json::from_str(
|
||||
|
||||
93
backend/windmill-worker/nsjail/download.py.pip.config.proto
Normal file
93
backend/windmill-worker/nsjail/download.py.pip.config.proto
Normal file
@@ -0,0 +1,93 @@
|
||||
name: "python download pip"
|
||||
|
||||
mode: ONCE
|
||||
hostname: "python"
|
||||
log_level: ERROR
|
||||
time_limit: 900
|
||||
|
||||
rlimit_as: 2048
|
||||
rlimit_cpu: 1000
|
||||
rlimit_fsize: 1024
|
||||
rlimit_nofile: 64
|
||||
|
||||
envar: "HOME=/user"
|
||||
envar: "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH"
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
clone_newnet: false
|
||||
clone_newuser: {CLONE_NEWUSER}
|
||||
|
||||
keep_caps: true
|
||||
keep_env: true
|
||||
|
||||
mount {
|
||||
src: "/bin"
|
||||
dst: "/bin"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/lib"
|
||||
dst: "/lib"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/lib64"
|
||||
dst: "/lib64"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/usr"
|
||||
dst: "/usr"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/etc"
|
||||
dst: "/etc"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/null"
|
||||
dst: "/dev/null"
|
||||
is_bind: true
|
||||
rw: true
|
||||
}
|
||||
|
||||
mount {
|
||||
dst: "/tmp"
|
||||
fstype: "tmpfs"
|
||||
rw: true
|
||||
options: "size=500000000"
|
||||
}
|
||||
|
||||
|
||||
mount {
|
||||
src: "{WORKER_DIR}/download_deps.py.pip.sh"
|
||||
dst: "/download_deps.sh"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{CACHE_DIR}"
|
||||
dst: "{CACHE_DIR}"
|
||||
is_bind: true
|
||||
rw: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/urandom"
|
||||
dst: "/dev/urandom"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
exec_bin {
|
||||
path: "/bin/sh"
|
||||
arg: "/download_deps.sh"
|
||||
}
|
||||
|
||||
24
backend/windmill-worker/nsjail/download_deps.py.pip.sh
Executable file
24
backend/windmill-worker/nsjail/download_deps.py.pip.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#/bin/sh
|
||||
|
||||
INDEX_URL_ARG=$([ -z "$INDEX_URL" ] && echo ""|| echo "--index-url $INDEX_URL" )
|
||||
EXTRA_INDEX_URL_ARG=$([ -z "$EXTRA_INDEX_URL" ] && echo ""|| echo "--extra-index-url $EXTRA_INDEX_URL" )
|
||||
TRUSTED_HOST_ARG=$([ -z "$TRUSTED_HOST" ] && echo "" || echo "--trusted-host $TRUSTED_HOST")
|
||||
|
||||
if [ ! -z "$INDEX_URL" ]
|
||||
then
|
||||
echo "\$INDEX_URL is set to $INDEX_URL"
|
||||
fi
|
||||
|
||||
if [ ! -z "$EXTRA_INDEX_URL" ]
|
||||
then
|
||||
echo "\$EXTRA_INDEX_URL is set to $EXTRA_INDEX_URL"
|
||||
fi
|
||||
|
||||
if [ ! -z "$TRUSTED_HOST" ]
|
||||
then
|
||||
echo "\$TRUSTED_HOST is set to $TRUSTED_HOST"
|
||||
fi
|
||||
|
||||
CMD="/usr/local/bin/python3 -m pip install -v \"$REQ\" -I -t \"$TARGET\" --no-cache --no-color --no-deps --isolated --no-warn-conflicts --disable-pip-version-check $INDEX_URL_ARG $EXTRA_INDEX_URL_ARG $TRUSTED_HOST_ARG"
|
||||
echo $CMD
|
||||
eval $CMD
|
||||
@@ -19,6 +19,18 @@ then
|
||||
echo "\$TRUSTED_HOST is set to $TRUSTED_HOST"
|
||||
fi
|
||||
|
||||
CMD="/usr/local/bin/python3 -m pip install -v \"$REQ\" -I -t \"$TARGET\" --no-cache --no-color --no-deps --isolated --no-warn-conflicts --disable-pip-version-check $INDEX_URL_ARG $EXTRA_INDEX_URL_ARG $TRUSTED_HOST_ARG"
|
||||
CMD="/usr/local/bin/uv pip install
|
||||
\"$REQ\"
|
||||
--target \"$TARGET\"
|
||||
--no-cache
|
||||
--no-config
|
||||
--no-color
|
||||
--no-deps
|
||||
--link-mode=copy
|
||||
$INDEX_URL_ARG $EXTRA_INDEX_URL_ARG $TRUSTED_HOST_ARG
|
||||
--index-strategy unsafe-best-match
|
||||
--system
|
||||
"
|
||||
|
||||
echo $CMD
|
||||
eval $CMD
|
||||
|
||||
@@ -116,6 +116,8 @@ async fn handle_ansible_python_deps(
|
||||
job_dir,
|
||||
worker_dir,
|
||||
&mut Some(occupancy_metrics),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
additional_python_paths.append(&mut venv_path);
|
||||
|
||||
@@ -89,8 +89,18 @@ cat bp | tail -1 >> ./result2.out &
|
||||
# Run main.sh in the same process group
|
||||
{bash} ./main.sh "$@" 2>&1 | tee bp &
|
||||
|
||||
# Wait for all background processes to finish
|
||||
wait
|
||||
pid=$!
|
||||
|
||||
# Wait for main.sh to finish and capture its exit status
|
||||
wait $pid
|
||||
exit_status=$?
|
||||
|
||||
# Clean up the named pipe and background processes
|
||||
rm -f bp
|
||||
|
||||
|
||||
# Exit with the captured status
|
||||
exit $exit_status
|
||||
"#,
|
||||
bash = BIN_BASH.as_str(),
|
||||
);
|
||||
|
||||
@@ -1304,6 +1304,8 @@ try {{
|
||||
job.args.as_ref()
|
||||
};
|
||||
|
||||
append_logs(&job.id, &job.workspace_id, format!("{init_logs}\n"), db).await;
|
||||
|
||||
let result = crate::js_eval::eval_fetch_timeout(
|
||||
env_code,
|
||||
inner_content.clone(),
|
||||
@@ -1324,14 +1326,7 @@ try {{
|
||||
"Executed native code in {}ms",
|
||||
started_at.elapsed().as_millis()
|
||||
);
|
||||
append_logs(
|
||||
&job.id,
|
||||
&job.workspace_id,
|
||||
format!("{}\n{}", init_logs, result.1),
|
||||
db,
|
||||
)
|
||||
.await;
|
||||
return Ok(result.0);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
append_logs(&job.id, &job.workspace_id, init_logs, db).await;
|
||||
|
||||
@@ -562,7 +562,7 @@ pub async fn resolve_job_timeout(
|
||||
_w_id: &str,
|
||||
_job_id: Uuid,
|
||||
custom_timeout_secs: Option<i32>,
|
||||
) -> (Duration, Option<String>) {
|
||||
) -> (Duration, Option<String>, bool) {
|
||||
let mut warn_msg: Option<String> = None;
|
||||
#[cfg(feature = "cloud")]
|
||||
let cloud_premium_workspace = *CLOUD_HOSTED
|
||||
@@ -587,12 +587,12 @@ pub async fn resolve_job_timeout(
|
||||
Some(timeout_secs)
|
||||
if Duration::from_secs(timeout_secs as u64) < global_max_timeout_duration =>
|
||||
{
|
||||
(Duration::from_secs(timeout_secs as u64), warn_msg)
|
||||
(Duration::from_secs(timeout_secs as u64), warn_msg, true)
|
||||
}
|
||||
Some(timeout_secs) => {
|
||||
warn_msg = Some(format!("WARNING: Custom job timeout of {timeout_secs} seconds was greater than the maximum timeout. It will be ignored and the max timeout will be used instead"));
|
||||
tracing::warn!(warn_msg);
|
||||
(global_max_timeout_duration, warn_msg)
|
||||
(global_max_timeout_duration, warn_msg, false)
|
||||
}
|
||||
None => {
|
||||
// fallback to default timeout or max if not set
|
||||
@@ -610,7 +610,7 @@ pub async fn resolve_job_timeout(
|
||||
global_max_timeout_duration
|
||||
}
|
||||
};
|
||||
(default_timeout, warn_msg)
|
||||
(default_timeout, warn_msg, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -799,6 +799,7 @@ pub async fn get_cached_resource_value_if_valid(
|
||||
S3Object {
|
||||
s3: s3_file_key.clone(),
|
||||
storage: cached_resource.storage.clone(),
|
||||
filename: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -20,13 +20,21 @@ use std::sync::Arc;
|
||||
pub async fn build_tar_and_push(
|
||||
s3_client: Arc<dyn ObjectStore>,
|
||||
folder: String,
|
||||
no_uv: bool,
|
||||
) -> error::Result<()> {
|
||||
use object_store::path::Path;
|
||||
|
||||
use crate::PY311_CACHE_DIR;
|
||||
|
||||
tracing::info!("Started building and pushing piptar {folder}");
|
||||
let start = Instant::now();
|
||||
let folder_name = folder.split("/").last().unwrap();
|
||||
let tar_path = format!("{PIP_CACHE_DIR}/{folder_name}_tar.tar",);
|
||||
let prefix = if no_uv {
|
||||
PIP_CACHE_DIR
|
||||
} else {
|
||||
PY311_CACHE_DIR
|
||||
};
|
||||
let tar_path = format!("{prefix}/{folder_name}_tar.tar",);
|
||||
|
||||
let tar_file = std::fs::File::create(&tar_path)?;
|
||||
let mut tar = tar::Builder::new(tar_file);
|
||||
@@ -46,7 +54,10 @@ pub async fn build_tar_and_push(
|
||||
// })?;
|
||||
if let Err(e) = s3_client
|
||||
.put(
|
||||
&Path::from(format!("/tar/pip/{folder_name}.tar")),
|
||||
&Path::from(format!(
|
||||
"/tar/{}/{folder_name}.tar",
|
||||
if no_uv { "pip" } else { "python_311" }
|
||||
)),
|
||||
std::fs::read(&tar_path)?.into(),
|
||||
)
|
||||
.await
|
||||
@@ -71,7 +82,11 @@ pub async fn build_tar_and_push(
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub async fn pull_from_tar(client: Arc<dyn ObjectStore>, folder: String) -> error::Result<()> {
|
||||
pub async fn pull_from_tar(
|
||||
client: Arc<dyn ObjectStore>,
|
||||
folder: String,
|
||||
no_uv: bool,
|
||||
) -> error::Result<()> {
|
||||
use windmill_common::s3_helpers::attempt_fetch_bytes;
|
||||
|
||||
let folder_name = folder.split("/").last().unwrap();
|
||||
@@ -79,7 +94,10 @@ pub async fn pull_from_tar(client: Arc<dyn ObjectStore>, folder: String) -> erro
|
||||
tracing::info!("Attempting to pull piptar {folder_name} from bucket");
|
||||
|
||||
let start = Instant::now();
|
||||
let tar_path = format!("tar/pip/{folder_name}.tar");
|
||||
let tar_path = format!(
|
||||
"tar/{}/{folder_name}.tar",
|
||||
if no_uv { "pip" } else { "python_311" }
|
||||
);
|
||||
let bytes = attempt_fetch_bytes(client, &tar_path).await?;
|
||||
|
||||
// tracing::info!("B: {target} {folder}");
|
||||
|
||||
@@ -48,6 +48,7 @@ use futures::{
|
||||
|
||||
use crate::common::{resolve_job_timeout, OccupancyMetrics};
|
||||
use crate::job_logger::{append_job_logs, append_with_limit, LARGE_LOG_THRESHOLD_SIZE};
|
||||
use crate::job_logger_ee::process_streaming_log_lines;
|
||||
use crate::{MAX_RESULT_SIZE, MAX_WAIT_FOR_SIGINT, MAX_WAIT_FOR_SIGTERM};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -143,15 +144,40 @@ pub async fn handle_child(
|
||||
occupancy_metrics,
|
||||
);
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum KillReason {
|
||||
TooManyLogs,
|
||||
Timeout,
|
||||
Cancelled,
|
||||
Timeout { is_job_specific: bool },
|
||||
Cancelled(Option<CanceledBy>),
|
||||
AlreadyCompleted,
|
||||
}
|
||||
|
||||
let (timeout_duration, timeout_warn_msg) =
|
||||
impl std::fmt::Debug for KillReason {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
KillReason::TooManyLogs => f.write_str("too many logs (max size: 2MB)"),
|
||||
KillReason::Timeout { is_job_specific } => f.write_str(if *is_job_specific {
|
||||
"timeout after exceeding job-specific duration limit"
|
||||
} else {
|
||||
"timeout after exceeding instance-wide job duration limit"
|
||||
}),
|
||||
KillReason::Cancelled(canceled_by) => {
|
||||
let mut reason = "cancelled".to_string();
|
||||
if let Some(canceled_by) = canceled_by {
|
||||
if let Some(by) = canceled_by.username.as_ref() {
|
||||
reason.push_str(&format!(" by {}", by));
|
||||
}
|
||||
if let Some(rsn) = canceled_by.reason.as_ref() {
|
||||
reason.push_str(&format!(" (reason: {})", rsn));
|
||||
}
|
||||
}
|
||||
f.write_str(&reason)
|
||||
}
|
||||
KillReason::AlreadyCompleted => f.write_str("already completed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (timeout_duration, timeout_warn_msg, is_job_specific) =
|
||||
resolve_job_timeout(&db, w_id, job_id, custom_timeout).await;
|
||||
if let Some(msg) = timeout_warn_msg {
|
||||
append_logs(&job_id, w_id, msg.as_str(), db).await;
|
||||
@@ -165,9 +191,9 @@ pub async fn handle_child(
|
||||
biased;
|
||||
result = child.wait() => return result.map(Ok),
|
||||
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
|
||||
_ = sleep(timeout_duration) => KillReason::Timeout,
|
||||
_ = sleep(timeout_duration) => KillReason::Timeout { is_job_specific },
|
||||
ex = update_job, if job_id != Uuid::nil() => match ex {
|
||||
UpdateJobPollingExit::Done => KillReason::Cancelled,
|
||||
UpdateJobPollingExit::Done(canceled_by) => KillReason::Cancelled(canceled_by),
|
||||
UpdateJobPollingExit::AlreadyCompleted => KillReason::AlreadyCompleted,
|
||||
},
|
||||
};
|
||||
@@ -175,7 +201,7 @@ pub async fn handle_child(
|
||||
drop(tx);
|
||||
|
||||
let set_reason = async {
|
||||
if kill_reason == KillReason::Timeout {
|
||||
if matches!(kill_reason, KillReason::Timeout { .. }) {
|
||||
if let Err(err) = sqlx::query(
|
||||
r#"
|
||||
UPDATE queue
|
||||
@@ -402,13 +428,15 @@ pub async fn handle_child(
|
||||
Err(Error::AlreadyCompleted("Job already completed".to_string()))
|
||||
}
|
||||
_ => Err(Error::ExecutionErr(format!(
|
||||
"job process killed because {kill_reason:#?}"
|
||||
"job process terminated due to {kill_reason:#?}"
|
||||
))),
|
||||
},
|
||||
Err(err) => Err(Error::ExecutionErr(format!("job process io error: {err}"))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async fn get_mem_peak(pid: Option<u32>, nsjail: bool) -> i32 {
|
||||
if pid.is_none() {
|
||||
return -1;
|
||||
@@ -505,7 +533,10 @@ where
|
||||
})?,
|
||||
ex = update_job, if job_id != Uuid::nil() => {
|
||||
match ex {
|
||||
UpdateJobPollingExit::Done => Err(Error::ExecutionErr("Job cancelled".to_string())).map_err(to_anyhow)?,
|
||||
UpdateJobPollingExit::Done(canceled_by) => {
|
||||
let (by, reason) = canceled_by.as_ref().map_or(("unknown".to_string(), "unknown".to_string()), |x| (x.username.clone().unwrap_or("".to_string()), x.reason.clone().unwrap_or("".to_string())));
|
||||
Err(Error::ExecutionErr(format!("Job cancelled by {by} (reason: {reason})",))).map_err(to_anyhow)?
|
||||
},
|
||||
UpdateJobPollingExit::AlreadyCompleted => Err(Error::AlreadyCompleted("Job already completed".to_string())).map_err(to_anyhow)?,
|
||||
}
|
||||
}
|
||||
@@ -515,7 +546,7 @@ where
|
||||
}
|
||||
|
||||
pub enum UpdateJobPollingExit {
|
||||
Done,
|
||||
Done(Option<CanceledBy>),
|
||||
AlreadyCompleted,
|
||||
}
|
||||
|
||||
@@ -640,7 +671,7 @@ where
|
||||
}
|
||||
tracing::info!("job {job_id} finished");
|
||||
|
||||
UpdateJobPollingExit::Done
|
||||
UpdateJobPollingExit::Done(canceled_by_ref.clone())
|
||||
}
|
||||
|
||||
/// takes stdout and stderr from Child, panics if either are not present
|
||||
@@ -661,19 +692,24 @@ fn child_joined_output_stream(
|
||||
|
||||
let stdout = BufReader::new(stdout).lines();
|
||||
let stderr = BufReader::new(stderr).lines();
|
||||
stream::select(lines_to_stream(stderr), lines_to_stream(stdout))
|
||||
stream::select(lines_to_stream(stderr, true), lines_to_stream(stdout, false))
|
||||
}
|
||||
|
||||
pub fn lines_to_stream<R: tokio::io::AsyncBufRead + Unpin>(
|
||||
mut lines: tokio::io::Lines<R>,
|
||||
stderr: bool,
|
||||
) -> impl futures::Stream<Item = io::Result<String>> {
|
||||
stream::poll_fn(move |cx| {
|
||||
std::pin::Pin::new(&mut lines)
|
||||
.poll_next_line(cx)
|
||||
.map(|result| result.transpose())
|
||||
.map(|result| {
|
||||
process_streaming_log_lines(result, stderr)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn process_status(status: ExitStatus) -> error::Result<()> {
|
||||
if status.success() {
|
||||
Ok(())
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
use itertools::Itertools;
|
||||
use swc_ecma_parser::lexer::util::CharExt;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use object_store::path::Path;
|
||||
use regex::Regex;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS;
|
||||
|
||||
use windmill_common::error::{self};
|
||||
use windmill_common::worker::{CLOUD_HOSTED, TMP_DIR};
|
||||
use windmill_common::worker::CLOUD_HOSTED;
|
||||
|
||||
use windmill_queue::append_logs;
|
||||
|
||||
@@ -19,6 +10,12 @@ use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::DB;
|
||||
|
||||
use crate::job_logger_ee::default_disk_log_storage;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use crate::job_logger_ee::s3_storage;
|
||||
|
||||
|
||||
pub enum CompactLogs {
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
NotEE,
|
||||
@@ -28,150 +25,7 @@ pub enum CompactLogs {
|
||||
S3,
|
||||
}
|
||||
|
||||
async fn compact_logs(
|
||||
job_id: Uuid,
|
||||
w_id: &str,
|
||||
db: &DB,
|
||||
nlogs: String,
|
||||
total_size: Arc<AtomicU32>,
|
||||
compact_kind: CompactLogs,
|
||||
_worker_name: &str,
|
||||
) -> error::Result<(String, String)> {
|
||||
let mut prev_logs = sqlx::query_scalar!(
|
||||
"SELECT logs FROM job_logs WHERE job_id = $1 AND workspace_id = $2",
|
||||
job_id,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
let size = prev_logs.char_indices().count() as i32;
|
||||
let nlogs_len = nlogs.char_indices().count();
|
||||
let to_keep_in_db = usize::max(
|
||||
usize::min(nlogs_len, 3000),
|
||||
nlogs_len % LARGE_LOG_THRESHOLD_SIZE,
|
||||
);
|
||||
let extra_split = to_keep_in_db < nlogs_len;
|
||||
let stored_in_storage_len = if extra_split {
|
||||
nlogs_len - to_keep_in_db
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let extra_to_newline = nlogs
|
||||
.chars()
|
||||
.skip(stored_in_storage_len)
|
||||
.find_position(|x| x.is_line_break())
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(to_keep_in_db);
|
||||
let stored_in_storage_to_newline = stored_in_storage_len + extra_to_newline;
|
||||
|
||||
let (append_to_storage, stored_in_db) = if extra_split {
|
||||
if stored_in_storage_to_newline == nlogs.len() {
|
||||
(nlogs.as_ref(), "".to_string())
|
||||
} else {
|
||||
let split_idx = nlogs
|
||||
.char_indices()
|
||||
.nth(stored_in_storage_to_newline)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0);
|
||||
let (append_to_storage, stored_in_db) = nlogs.split_at(split_idx);
|
||||
// tracing::error!("{append_to_storage} ||||| {stored_in_db}");
|
||||
// tracing::error!(
|
||||
// "{:?} {:?} {} {}",
|
||||
// excess_prev_logs.lines().last(),
|
||||
// current_logs.lines().next(),
|
||||
// split_idx,
|
||||
// excess_size_modulo
|
||||
// );
|
||||
(append_to_storage, stored_in_db.to_string())
|
||||
}
|
||||
} else {
|
||||
// tracing::error!("{:?}", nlogs.lines().last());
|
||||
("", nlogs.to_string())
|
||||
};
|
||||
|
||||
let new_size_with_excess = size + stored_in_storage_to_newline as i32;
|
||||
|
||||
let new_size = total_size.fetch_add(
|
||||
new_size_with_excess as u32,
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
) + new_size_with_excess as u32;
|
||||
|
||||
let path = format!(
|
||||
"logs/{job_id}/{}_{new_size}.txt",
|
||||
chrono::Utc::now().timestamp_millis()
|
||||
);
|
||||
|
||||
let mut new_current_logs = match compact_kind {
|
||||
CompactLogs::NoS3 => format!("\n[windmill] No object storage set in instance settings. Previous logs have been saved to disk at {path}"),
|
||||
CompactLogs::S3 => format!("\n[windmill] Previous logs have been saved to object storage at {path}"),
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
CompactLogs::NotEE => format!("\n[windmill] Previous logs have been saved to disk at {path}"),
|
||||
};
|
||||
new_current_logs.push_str(&stored_in_db);
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE job_logs SET logs = $1, log_offset = $2,
|
||||
log_file_index = array_append(coalesce(log_file_index, array[]::text[]), $3)
|
||||
WHERE workspace_id = $4 AND job_id = $5",
|
||||
new_current_logs,
|
||||
new_size as i32,
|
||||
path,
|
||||
w_id,
|
||||
job_id
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
prev_logs.push_str(&append_to_storage);
|
||||
|
||||
return Ok((prev_logs, path));
|
||||
}
|
||||
|
||||
async fn default_disk_log_storage(
|
||||
job_id: Uuid,
|
||||
w_id: &str,
|
||||
db: &DB,
|
||||
nlogs: String,
|
||||
total_size: Arc<AtomicU32>,
|
||||
compact_kind: CompactLogs,
|
||||
worker_name: &str,
|
||||
) {
|
||||
match compact_logs(
|
||||
job_id,
|
||||
&w_id,
|
||||
&db,
|
||||
nlogs,
|
||||
total_size,
|
||||
compact_kind,
|
||||
worker_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => tracing::error!("Could not compact logs for job {job_id}: {e:?}",),
|
||||
Ok((prev_logs, path)) => {
|
||||
let path = format!("{}/{}", TMP_DIR, path);
|
||||
let splitted = &path.split("/").collect_vec();
|
||||
tokio::fs::create_dir_all(splitted.into_iter().take(splitted.len() - 1).join("/"))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Could not create logs directory: {e:?}",);
|
||||
e
|
||||
})
|
||||
.ok();
|
||||
let created = tokio::fs::File::create(&path).await;
|
||||
if let Err(e) = created {
|
||||
tracing::error!("Could not create logs file {path}: {e:?}",);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = tokio::fs::write(&path, prev_logs).await {
|
||||
tracing::error!("Could not write to logs file {path}: {e:?}");
|
||||
} else {
|
||||
tracing::info!("Logs length of {job_id} has exceeded a threshold. Previous logs have been saved to disk at {path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn append_job_logs(
|
||||
job_id: Uuid,
|
||||
@@ -184,43 +38,7 @@ pub(crate) async fn append_job_logs(
|
||||
) -> () {
|
||||
if must_compact_logs {
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if let Some(os) = OBJECT_STORE_CACHE_SETTINGS.read().await.clone() {
|
||||
match compact_logs(
|
||||
job_id,
|
||||
&w_id,
|
||||
&db,
|
||||
logs,
|
||||
total_size,
|
||||
CompactLogs::S3,
|
||||
&worker_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => tracing::error!("Could not compact logs for job {job_id}: {e:?}",),
|
||||
Ok((prev_logs, path)) => {
|
||||
tracing::info!("Logs length of {job_id} has exceeded a threshold. Previous logs have been saved to object storage at {path}");
|
||||
let path2 = path.clone();
|
||||
if let Err(e) = os
|
||||
.put(&Path::from(path), prev_logs.to_string().into_bytes().into())
|
||||
.await
|
||||
{
|
||||
tracing::error!("Could not save logs to s3: {e:?}");
|
||||
}
|
||||
tracing::info!("Logs of {job_id} saved to object storage at {path2}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
default_disk_log_storage(
|
||||
job_id,
|
||||
&w_id,
|
||||
&db,
|
||||
logs,
|
||||
total_size,
|
||||
CompactLogs::NoS3,
|
||||
&worker_name,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
s3_storage(job_id, &w_id, &db, logs, total_size, &worker_name).await;
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
{
|
||||
@@ -252,6 +70,7 @@ pub fn append_with_limit(dst: &mut String, src: &str, limit: &mut usize) {
|
||||
if *NO_LOGS_AT_ALL {
|
||||
return;
|
||||
}
|
||||
|
||||
let src_str;
|
||||
let src = {
|
||||
src_str = RE_00.replace_all(src, "");
|
||||
|
||||
30
backend/windmill-worker/src/job_logger_ee.rs
Normal file
30
backend/windmill-worker/src/job_logger_ee.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use std::io;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
use windmill_common::DB;
|
||||
|
||||
use crate::job_logger::CompactLogs;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub(crate) async fn s3_storage(_job_id: Uuid, _w_id: &String, _db: &sqlx::Pool<sqlx::Postgres>, _logs: &String, _total_size: &Arc<AtomicU32>, _worker_name: &String) {
|
||||
tracing::info!("Logs length of {job_id} has exceeded a threshold. Implementation to store excess on s3 in not OSS");
|
||||
}
|
||||
|
||||
pub(crate) async fn default_disk_log_storage(
|
||||
job_id: Uuid,
|
||||
_w_id: &str,
|
||||
_db: &DB,
|
||||
_nlogs: String,
|
||||
_total_size: Arc<AtomicU32>,
|
||||
_compact_kind: CompactLogs,
|
||||
_worker_name: &str,
|
||||
) {
|
||||
tracing::info!("Logs length of {job_id} has exceeded a threshold. Implementation to store excess on disk in not OSS");
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn process_streaming_log_lines(r: Result<Option<String>, io::Error>, _stderr: bool) -> Option<Result<String, io::Error>> {
|
||||
r.transpose()
|
||||
}
|
||||
@@ -754,9 +754,9 @@ pub async fn eval_fetch_timeout(
|
||||
_w_id: &str,
|
||||
_load_client: bool,
|
||||
_occupation_metrics: &mut OccupancyMetrics,
|
||||
) -> anyhow::Result<(Box<RawValue>, String)> {
|
||||
) -> anyhow::Result<Box<RawValue>> {
|
||||
use serde_json::value::to_raw_value;
|
||||
Ok((to_raw_value("require deno_core").unwrap(), "".to_string()))
|
||||
Ok(to_raw_value("require deno_core").unwrap())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
@@ -774,7 +774,9 @@ pub async fn eval_fetch_timeout(
|
||||
w_id: &str,
|
||||
load_client: bool,
|
||||
occupation_metrics: &mut OccupancyMetrics,
|
||||
) -> anyhow::Result<(Box<RawValue>, String)> {
|
||||
) -> anyhow::Result<Box<RawValue>> {
|
||||
use windmill_queue::append_logs;
|
||||
|
||||
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
|
||||
|
||||
let parsed_args = windmill_parser_ts::parse_deno_signature(&ts_expr, true, None)?.args;
|
||||
@@ -805,6 +807,8 @@ pub async fn eval_fetch_timeout(
|
||||
));
|
||||
}
|
||||
|
||||
let db_ = db.clone();
|
||||
let w_id_ = w_id.to_string();
|
||||
let result_f = tokio::task::spawn_blocking(move || {
|
||||
let ops = vec![op_get_static_args(), op_log()];
|
||||
let ext = Extension { name: "windmill", ops: ops.into(), ..Default::default() };
|
||||
@@ -892,28 +896,31 @@ pub async fn eval_fetch_timeout(
|
||||
.build()?;
|
||||
|
||||
let future = async {
|
||||
tokio::select! {
|
||||
let r = tokio::select! {
|
||||
r = eval_fetch(&mut js_runtime, &js_expr, Some(env_code), load_client) => Ok(r),
|
||||
_ = memory_limit_rx.recv() => Err(Error::ExecutionErr("Memory limit reached, killing isolate".to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
append_logs(
|
||||
&job_id,
|
||||
w_id_.as_str(),
|
||||
format!(
|
||||
"{extra_logs}{}",
|
||||
js_runtime.op_state().borrow().borrow::<LogString>().s
|
||||
),
|
||||
db_,
|
||||
)
|
||||
.await;
|
||||
|
||||
r
|
||||
};
|
||||
let r = runtime.block_on(future)?;
|
||||
// tracing::info!("total: {:?}", instant.elapsed());
|
||||
|
||||
(r as anyhow::Result<Box<RawValue>>).map(|x| {
|
||||
(
|
||||
x,
|
||||
js_runtime
|
||||
.op_state()
|
||||
.borrow()
|
||||
.borrow::<LogString>()
|
||||
.s
|
||||
.clone(),
|
||||
)
|
||||
})
|
||||
r
|
||||
});
|
||||
|
||||
let (res, logs) = run_future_with_polling_update_job_poller(
|
||||
let res = run_future_with_polling_update_job_poller(
|
||||
job_id,
|
||||
job_timeout,
|
||||
db,
|
||||
@@ -932,7 +939,7 @@ pub async fn eval_fetch_timeout(
|
||||
e
|
||||
})?;
|
||||
*mem_peak = (res.get().len() / 1000) as i32;
|
||||
Ok((res, format!("{extra_logs}{logs}")))
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
|
||||
@@ -29,7 +29,7 @@ mod rust_executor;
|
||||
mod worker;
|
||||
mod worker_flow;
|
||||
mod worker_lockfiles;
|
||||
|
||||
mod job_logger_ee;
|
||||
pub use worker::*;
|
||||
|
||||
pub use result_processor::handle_job_error;
|
||||
|
||||
@@ -42,6 +42,10 @@ lazy_static::lazy_static! {
|
||||
static ref USE_PIP_COMPILE: bool = std::env::var("USE_PIP_COMPILE")
|
||||
.ok().map(|flag| flag == "true").unwrap_or(false);
|
||||
|
||||
/// Use pip install
|
||||
static ref USE_PIP_INSTALL: bool = std::env::var("USE_PIP_INSTALL")
|
||||
.ok().map(|flag| flag == "true").unwrap_or(false);
|
||||
|
||||
|
||||
static ref RELATIVE_IMPORT_REGEX: Regex = Regex::new(r#"(import|from)\s(((u|f)\.)|\.)"#).unwrap();
|
||||
|
||||
@@ -50,6 +54,8 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
|
||||
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT_FALLBACK: &str =
|
||||
include_str!("../nsjail/download.py.pip.config.proto");
|
||||
const NSJAIL_CONFIG_RUN_PYTHON3_CONTENT: &str = include_str!("../nsjail/run.python3.config.proto");
|
||||
const RELATIVE_PYTHON_LOADER: &str = include_str!("../loader.py");
|
||||
|
||||
@@ -66,8 +72,8 @@ use crate::{
|
||||
},
|
||||
handle_child::handle_child,
|
||||
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, LOCK_CACHE_DIR,
|
||||
NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, PROXY_ENVS, TZ_ENV,
|
||||
UV_CACHE_DIR,
|
||||
NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, PROXY_ENVS,
|
||||
PY311_CACHE_DIR, TZ_ENV, UV_CACHE_DIR,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -199,8 +205,11 @@ pub async fn uv_pip_compile(
|
||||
.clone()
|
||||
.map(handle_ephemeral_token);
|
||||
if let Some(url) = pip_extra_index_url.as_ref() {
|
||||
args.extend(["--extra-index-url", url, "--no-emit-index-url"]);
|
||||
pip_args.push(format!("--extra-index-url {}", url));
|
||||
url.split(",").for_each(|url| {
|
||||
args.extend(["--extra-index-url", url]);
|
||||
pip_args.push(format!("--extra-index-url {}", url));
|
||||
});
|
||||
args.push("--no-emit-index-url");
|
||||
}
|
||||
let pip_index_url = PIP_INDEX_URL
|
||||
.read()
|
||||
@@ -279,7 +288,9 @@ pub async fn uv_pip_compile(
|
||||
.clone()
|
||||
.map(handle_ephemeral_token);
|
||||
if let Some(url) = pip_extra_index_url.as_ref() {
|
||||
args.extend(["--extra-index-url", url]);
|
||||
url.split(",").for_each(|url| {
|
||||
args.extend(["--extra-index-url", url]);
|
||||
});
|
||||
}
|
||||
let pip_index_url = PIP_INDEX_URL
|
||||
.read()
|
||||
@@ -896,10 +907,10 @@ async fn handle_python_deps(
|
||||
.unwrap_or_else(|| vec![])
|
||||
.clone();
|
||||
|
||||
let annotations = windmill_common::worker::PythonAnnotations::parse(inner_content);
|
||||
let requirements = match requirements_o {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
let annotation = windmill_common::worker::PythonAnnotations::parse(inner_content);
|
||||
let mut already_visited = vec![];
|
||||
|
||||
let requirements = windmill_parser_py_imports::parse_python_imports(
|
||||
@@ -924,8 +935,8 @@ async fn handle_python_deps(
|
||||
worker_name,
|
||||
w_id,
|
||||
occupancy_metrics,
|
||||
annotation.no_uv,
|
||||
annotation.no_cache,
|
||||
annotations.no_uv || annotations.no_uv_compile,
|
||||
annotations.no_cache,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -950,6 +961,8 @@ async fn handle_python_deps(
|
||||
job_dir,
|
||||
worker_dir,
|
||||
occupancy_metrics,
|
||||
annotations.no_uv || annotations.no_uv_install,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
additional_python_paths.append(&mut venv_path);
|
||||
@@ -961,6 +974,7 @@ lazy_static::lazy_static! {
|
||||
static ref PIP_SECRET_VARIABLE: Regex = Regex::new(r"\$\{PIP_SECRET:([^\s\}]+)\}").unwrap();
|
||||
}
|
||||
|
||||
/// pip install, include cached or pull from S3
|
||||
pub async fn handle_python_reqs(
|
||||
requirements: Vec<&str>,
|
||||
job_id: &Uuid,
|
||||
@@ -972,12 +986,22 @@ pub async fn handle_python_reqs(
|
||||
job_dir: &str,
|
||||
worker_dir: &str,
|
||||
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
|
||||
// TODO: Remove (Deprecated)
|
||||
mut no_uv_install: bool,
|
||||
is_ansible: bool,
|
||||
) -> error::Result<Vec<String>> {
|
||||
let mut req_paths: Vec<String> = vec![];
|
||||
let mut vars = vec![("PATH", PATH_ENV.as_str())];
|
||||
let pip_extra_index_url;
|
||||
let pip_index_url;
|
||||
|
||||
no_uv_install |= *USE_PIP_INSTALL;
|
||||
|
||||
if no_uv_install && !is_ansible {
|
||||
append_logs(&job_id, w_id, "\nFallback to pip (Deprecated!)\n", db).await;
|
||||
tracing::warn!("Fallback to pip");
|
||||
}
|
||||
|
||||
if !*DISABLE_NSJAIL {
|
||||
pip_extra_index_url = PIP_EXTRA_INDEX_URL
|
||||
.read()
|
||||
@@ -1008,10 +1032,21 @@ pub async fn handle_python_reqs(
|
||||
let _ = write_file(
|
||||
job_dir,
|
||||
"download.config.proto",
|
||||
&NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT
|
||||
.replace("{WORKER_DIR}", &worker_dir)
|
||||
.replace("{CACHE_DIR}", PIP_CACHE_DIR)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
|
||||
&(if no_uv_install {
|
||||
NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT_FALLBACK
|
||||
} else {
|
||||
NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT
|
||||
})
|
||||
.replace("{WORKER_DIR}", &worker_dir)
|
||||
.replace(
|
||||
"{CACHE_DIR}",
|
||||
if no_uv_install {
|
||||
PIP_CACHE_DIR
|
||||
} else {
|
||||
PY311_CACHE_DIR
|
||||
},
|
||||
)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
|
||||
)?;
|
||||
};
|
||||
|
||||
@@ -1021,8 +1056,14 @@ pub async fn handle_python_reqs(
|
||||
if req.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let py_prefix = if no_uv_install {
|
||||
PIP_CACHE_DIR
|
||||
} else {
|
||||
PY311_CACHE_DIR
|
||||
};
|
||||
|
||||
let venv_p = format!(
|
||||
"{PIP_CACHE_DIR}/{}",
|
||||
"{py_prefix}/{}",
|
||||
req.replace(' ', "").replace('/', "").replace(':', "")
|
||||
);
|
||||
if metadata(&venv_p).await.is_ok() {
|
||||
@@ -1068,7 +1109,10 @@ pub async fn handle_python_reqs(
|
||||
.map(|(req, venv_p)| {
|
||||
let os = os.clone();
|
||||
async move {
|
||||
if pull_from_tar(os, venv_p.clone()).await.is_ok() {
|
||||
if pull_from_tar(os, venv_p.clone(), no_uv_install)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
PullFromTar::Pulled(venv_p.to_string())
|
||||
} else {
|
||||
PullFromTar::NotPulled(req.to_string(), venv_p.to_string())
|
||||
@@ -1109,7 +1153,7 @@ pub async fn handle_python_reqs(
|
||||
|
||||
for (req, venv_p) in req_with_penv {
|
||||
let mut logs1 = String::new();
|
||||
logs1.push_str("\n\n--- PIP INSTALL ---\n");
|
||||
logs1.push_str("\n\n--- UV PIP INSTALL ---\n");
|
||||
logs1.push_str(&format!("\n{req} is being installed for the first time.\n It will be cached for all ulterior uses."));
|
||||
append_logs(&job_id, w_id, logs1, db).await;
|
||||
|
||||
@@ -1145,21 +1189,47 @@ pub async fn handle_python_reqs(
|
||||
#[cfg(windows)]
|
||||
let req = format!("{}", req);
|
||||
|
||||
let mut command_args = vec![
|
||||
PYTHON_PATH.as_str(),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
&req,
|
||||
"-I",
|
||||
"--no-deps",
|
||||
"--no-color",
|
||||
"--isolated",
|
||||
"--no-warn-conflicts",
|
||||
"--disable-pip-version-check",
|
||||
"-t",
|
||||
venv_p.as_str(),
|
||||
];
|
||||
let mut command_args = if no_uv_install {
|
||||
vec![
|
||||
PYTHON_PATH.as_str(),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
&req,
|
||||
"-I",
|
||||
"--no-deps",
|
||||
"--no-color",
|
||||
"--isolated",
|
||||
"--no-warn-conflicts",
|
||||
"--disable-pip-version-check",
|
||||
"-t",
|
||||
venv_p.as_str(),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
UV_PATH.as_str(),
|
||||
"pip",
|
||||
"install",
|
||||
&req,
|
||||
"--no-deps",
|
||||
"--no-color",
|
||||
// "-p",
|
||||
// "3.11",
|
||||
// Prevent uv from discovering configuration files.
|
||||
"--no-config",
|
||||
"--link-mode=copy",
|
||||
// TODO: Doublecheck it
|
||||
"--system",
|
||||
// Prefer main index over extra
|
||||
// https://docs.astral.sh/uv/pip/compatibility/#packages-that-exist-on-multiple-indexes
|
||||
// TODO: Use env variable that can be toggled from UI
|
||||
"--index-strategy",
|
||||
"unsafe-best-match",
|
||||
"--target",
|
||||
venv_p.as_str(),
|
||||
"--no-cache",
|
||||
]
|
||||
};
|
||||
let pip_extra_index_url = PIP_EXTRA_INDEX_URL
|
||||
.read()
|
||||
.await
|
||||
@@ -1167,7 +1237,9 @@ pub async fn handle_python_reqs(
|
||||
.map(handle_ephemeral_token);
|
||||
|
||||
if let Some(url) = pip_extra_index_url.as_ref() {
|
||||
command_args.extend(["--extra-index-url", url]);
|
||||
url.split(",").for_each(|url| {
|
||||
command_args.extend(["--extra-index-url", url]);
|
||||
});
|
||||
}
|
||||
let pip_index_url = PIP_INDEX_URL
|
||||
.read()
|
||||
@@ -1189,7 +1261,7 @@ pub async fn handle_python_reqs(
|
||||
|
||||
envs.push(("HOME", HOME_ENV.as_str()));
|
||||
|
||||
tracing::debug!("pip install command: {:?}", command_args);
|
||||
tracing::debug!("uv pip install command: {:?}", command_args);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -1200,7 +1272,12 @@ pub async fn handle_python_reqs(
|
||||
.envs(envs)
|
||||
.args([
|
||||
"-x",
|
||||
&format!("{}/pip-{}.lock", LOCK_CACHE_DIR, fssafe_req),
|
||||
&format!(
|
||||
"{}/{}-{}.lock",
|
||||
LOCK_CACHE_DIR,
|
||||
if no_uv_install { "pip" } else { "py311" },
|
||||
fssafe_req
|
||||
),
|
||||
"--command",
|
||||
&command_args.join(" "),
|
||||
])
|
||||
@@ -1211,16 +1288,20 @@ pub async fn handle_python_reqs(
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut pip_cmd = Command::new(PYTHON_PATH.as_str());
|
||||
pip_cmd
|
||||
.env_clear()
|
||||
let installer_path = if no_uv_install { command_args[0] } else { "uv" };
|
||||
let mut cmd: Command = Command::new(&installer_path);
|
||||
cmd.env_clear()
|
||||
.envs(envs)
|
||||
.envs(PROXY_ENVS.clone())
|
||||
.env("SystemRoot", SYSTEM_ROOT.as_str())
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
)
|
||||
.args(&command_args[1..])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
start_child_process(pip_cmd, PYTHON_PATH.as_str()).await?
|
||||
start_child_process(cmd, installer_path).await?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1233,7 +1314,7 @@ pub async fn handle_python_reqs(
|
||||
false,
|
||||
worker_name,
|
||||
&w_id,
|
||||
&format!("pip install {req}"),
|
||||
&format!("uv pip install {req}"),
|
||||
None,
|
||||
false,
|
||||
occupancy_metrics,
|
||||
@@ -1253,7 +1334,7 @@ pub async fn handle_python_reqs(
|
||||
tracing::warn!("S3 cache not available in the pro plan");
|
||||
} else {
|
||||
let venv_p = venv_p.clone();
|
||||
tokio::spawn(build_tar_and_push(os, venv_p));
|
||||
tokio::spawn(build_tar_and_push(os, venv_p, no_uv_install));
|
||||
}
|
||||
}
|
||||
req_paths.push(venv_p);
|
||||
|
||||
@@ -295,6 +295,7 @@ pub async fn process_result(
|
||||
message: format!("error during execution of the script:\n{}", err),
|
||||
name: "ExecutionErr".to_string(),
|
||||
step_id: job.flow_step_id.clone(),
|
||||
exit_code: None,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -608,6 +609,8 @@ pub struct SerializedError {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub step_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exit_code: Option<i32>,
|
||||
}
|
||||
pub fn extract_error_value(log_lines: &str, i: i32, step_id: Option<String>) -> Box<RawValue> {
|
||||
return to_raw_value(&SerializedError {
|
||||
@@ -617,5 +620,6 @@ pub fn extract_error_value(log_lines: &str, i: i32, step_id: Option<String>) ->
|
||||
),
|
||||
name: "ExecutionErr".to_string(),
|
||||
step_id,
|
||||
exit_code: Some(i),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,7 +236,14 @@ pub const TMP_LOGS_DIR: &str = concatcp!(TMP_DIR, "/logs");
|
||||
pub const ROOT_CACHE_NOMOUNT_DIR: &str = concatcp!(TMP_DIR, "/cache_nomount/");
|
||||
|
||||
pub const LOCK_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "lock");
|
||||
// Used as fallback now
|
||||
pub const PIP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "pip");
|
||||
|
||||
// pub const PY310_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_310");
|
||||
pub const PY311_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_311");
|
||||
// pub const PY312_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_312");
|
||||
// pub const PY313_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_313");
|
||||
|
||||
pub const UV_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "uv");
|
||||
pub const TAR_PIP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/pip");
|
||||
pub const DENO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "deno");
|
||||
@@ -257,6 +264,7 @@ const NUM_SECS_PING: u64 = 5;
|
||||
const NUM_SECS_READINGS: u64 = 60;
|
||||
|
||||
const INCLUDE_DEPS_PY_SH_CONTENT: &str = include_str!("../nsjail/download_deps.py.sh");
|
||||
const INCLUDE_DEPS_PY_SH_CONTENT_FALLBACK: &str = include_str!("../nsjail/download_deps.py.pip.sh");
|
||||
|
||||
pub const DEFAULT_CLOUD_TIMEOUT: u64 = 900;
|
||||
pub const DEFAULT_SELFHOSTED_TIMEOUT: u64 = 604800; // 7 days
|
||||
@@ -311,6 +319,7 @@ lazy_static::lazy_static! {
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
// pub static ref DISABLE_NSJAIL: bool = false;
|
||||
pub static ref DISABLE_NSJAIL: bool = std::env::var("DISABLE_NSJAIL")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
@@ -740,6 +749,13 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
"download_deps.py.sh",
|
||||
INCLUDE_DEPS_PY_SH_CONTENT,
|
||||
);
|
||||
|
||||
// TODO: Remove (Deprecated)
|
||||
let _ = write_file(
|
||||
&worker_dir,
|
||||
"download_deps.py.pip.sh",
|
||||
INCLUDE_DEPS_PY_SH_CONTENT_FALLBACK,
|
||||
);
|
||||
}
|
||||
|
||||
let mut last_ping = Instant::now() - Duration::from_secs(NUM_SECS_PING + 1);
|
||||
@@ -1703,7 +1719,7 @@ async fn do_nativets(
|
||||
canceled_by: &mut Option<CanceledBy>,
|
||||
worker_name: &str,
|
||||
occupancy_metrics: &mut OccupancyMetrics,
|
||||
) -> windmill_common::error::Result<(Box<RawValue>, String)> {
|
||||
) -> windmill_common::error::Result<Box<RawValue>> {
|
||||
let args = build_args_map(job, client, db).await?.map(Json);
|
||||
let job_args = if args.is_some() {
|
||||
args.as_ref()
|
||||
@@ -1711,7 +1727,7 @@ async fn do_nativets(
|
||||
job.args.as_ref()
|
||||
};
|
||||
|
||||
let result = eval_fetch_timeout(
|
||||
Ok(eval_fetch_timeout(
|
||||
env_code,
|
||||
code.clone(),
|
||||
transpile_ts(code)?,
|
||||
@@ -1726,8 +1742,7 @@ async fn do_nativets(
|
||||
true,
|
||||
occupancy_metrics,
|
||||
)
|
||||
.await?;
|
||||
Ok((result.0, result.1))
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Default)]
|
||||
@@ -1904,8 +1919,10 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
}
|
||||
};
|
||||
if job.is_flow() {
|
||||
let flow = job.parse_raw_flow();
|
||||
handle_flow(
|
||||
job,
|
||||
flow,
|
||||
db,
|
||||
&client.get_authed().await,
|
||||
None,
|
||||
@@ -2356,7 +2373,8 @@ async fn handle_code_execution_job(
|
||||
.map(|(k, v)| format!("const {} = '{}';\nprocess.env['{}'] = '{}';\n", k, v, k, v))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"));
|
||||
let (result, ts_logs) = do_nativets(
|
||||
|
||||
let result = do_nativets(
|
||||
job,
|
||||
&client,
|
||||
env_code,
|
||||
@@ -2368,7 +2386,6 @@ async fn handle_code_execution_job(
|
||||
occupancy_metrics,
|
||||
)
|
||||
.await?;
|
||||
append_logs(&job.id, &job.workspace_id, ts_logs, db).await;
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -49,7 +48,7 @@ use windmill_common::{
|
||||
Approval, BranchAllStatus, BranchChosen, FlowStatus, FlowStatusModule, RetryStatus,
|
||||
MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL,
|
||||
},
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform, Retry, Suspend},
|
||||
flows::{has_failure_module, FlowModule, FlowModuleValue, FlowValue, InputTransform, Retry, Suspend},
|
||||
};
|
||||
use windmill_queue::schedule::get_schedule_opt;
|
||||
use windmill_queue::{
|
||||
@@ -177,11 +176,6 @@ struct RecoveryObject {
|
||||
recover: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize)]
|
||||
pub struct RowFlowStatus {
|
||||
pub flow_status: sqlx::types::Json<Box<serde_json::value::RawValue>>,
|
||||
pub current_module: Option<sqlx::types::Json<Box<serde_json::value::RawValue>>>,
|
||||
}
|
||||
// #[instrument(level = "trace", skip_all)]
|
||||
pub async fn update_flow_status_after_job_completion_internal<
|
||||
R: rsmq_async::RsmqConnection + Send + Sync + Clone,
|
||||
@@ -207,6 +201,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
let (
|
||||
should_continue_flow,
|
||||
flow_job,
|
||||
flow_value,
|
||||
stop_early,
|
||||
skip_if_stop_early,
|
||||
nresult,
|
||||
@@ -215,35 +210,28 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
) = {
|
||||
// tracing::debug!("UPDATE FLOW STATUS: {flow:?} {success} {result:?} {w_id} {depth}");
|
||||
|
||||
let old_status_json = sqlx::query_as::<_, RowFlowStatus>(
|
||||
"SELECT flow_status, raw_flow->'modules'->(flow_status->'step')::int as current_module FROM queue WHERE id = $1 AND workspace_id = $2",
|
||||
let (old_status, current_module) = sqlx::query!(
|
||||
"SELECT
|
||||
flow_status AS \"flow_status!: Json<Box<RawValue>>\",
|
||||
raw_flow->'modules'->(flow_status->'step')::int AS \"module: Json<Box<RawValue>>\"
|
||||
FROM queue WHERE id = $1 AND workspace_id = $2 LIMIT 1",
|
||||
flow, w_id
|
||||
)
|
||||
.bind(flow)
|
||||
.bind(w_id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"fetching flow status {flow} while reporting {success} {result:?}: {e:#}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let old_status = serde_json::from_str::<FlowStatus>(old_status_json.flow_status.get())
|
||||
.or_else(|e| {
|
||||
Err(Error::InternalErr(format!(
|
||||
"requiring status to be parsable as FlowStatus: {e:?}"
|
||||
)))
|
||||
})?;
|
||||
|
||||
let current_module = if let Some(x) = old_status_json.current_module {
|
||||
Some(serde_json::from_str::<FlowModule>(x.0.get()).or_else(|e| {
|
||||
Err(Error::InternalErr(format!(
|
||||
.map_err(|e| Error::InternalErr(
|
||||
format!("fetching flow status {flow} while reporting {success} {result:?}: {e:#}")
|
||||
))
|
||||
.and_then(|record| Ok((
|
||||
serde_json::from_str::<FlowStatus>(record.flow_status.0.get()).map_err(|e| Error::InternalErr(
|
||||
format!("requiring current module to be parsable as FlowStatus: {e:?}")
|
||||
))?,
|
||||
record.module.map(|json| {
|
||||
serde_json::from_str::<FlowModule>(json.0.get()).map_err(|e| Error::InternalErr(format!(
|
||||
"requiring current module to be parsable as FlowModule: {e:?}"
|
||||
)))
|
||||
})?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}).transpose()?,
|
||||
)))?;
|
||||
|
||||
let module_step = Step::from_i32_and_len(old_status.step, old_status.modules.len());
|
||||
|
||||
@@ -303,16 +291,15 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
|
||||
let is_flow = if let Some(step) = step {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT raw_flow->'modules'->($1)->'value'->>'type' = 'flow' FROM queue WHERE id = $2",
|
||||
step as i32,
|
||||
&flow
|
||||
"SELECT raw_flow->'modules'->($1)::text->'value'->>'type' = 'flow' FROM queue WHERE id = $2 LIMIT 1",
|
||||
step as i32, flow
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error during retrieval of step's type: {e:#}"))
|
||||
})?
|
||||
.unwrap_or(false)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error during retrieval of step's type: {e:#}"))
|
||||
})?
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -938,10 +925,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
tracing::info!(id = %flow_job.id, root_id = %job_root, "update flow status");
|
||||
|
||||
let module = get_module(&flow_job, &module_step);
|
||||
// tracing::error!(
|
||||
// "UPDATE FLOW STATUS 3: {module:#?} {unrecoverable} {} {is_last_step} {success} {skip_error_handler} is_failure_step {is_failure_step}", flow_job.canceled
|
||||
// );
|
||||
let flow_value = flow_job.parse_raw_flow();
|
||||
|
||||
let should_continue_flow = match success {
|
||||
_ if stop_early => false,
|
||||
@@ -953,7 +937,14 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
}
|
||||
false
|
||||
if next_retry(
|
||||
&module.and_then(|m| m.retry.clone()).unwrap_or_default(),
|
||||
flow_value
|
||||
.as_ref()
|
||||
.and_then(|value| match module_step {
|
||||
Step::PreprocessorStep => value.preprocessor_module.as_ref().and_then(|m| m.retry.as_ref()),
|
||||
Step::Step(i) => value.modules.get(i).as_ref().and_then(|m| m.retry.as_ref()),
|
||||
Step::FailureStep => value.failure_module.as_ref().and_then(|m| m.retry.as_ref()),
|
||||
})
|
||||
.unwrap_or(&Retry::default()),
|
||||
&old_status.retry,
|
||||
)
|
||||
.is_some() =>
|
||||
@@ -963,7 +954,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
false
|
||||
if !is_failure_step
|
||||
&& !skip_error_handler
|
||||
&& has_failure_module(flow, db).await? =>
|
||||
&& has_failure_module(flow, db, false).await? =>
|
||||
{
|
||||
true
|
||||
}
|
||||
@@ -975,6 +966,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
(
|
||||
should_continue_flow,
|
||||
flow_job,
|
||||
flow_value,
|
||||
stop_early,
|
||||
skip_if_stop_early,
|
||||
nresult,
|
||||
@@ -1042,13 +1034,11 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
let args_hash =
|
||||
hash_args(db, client, w_id, job_id_for_status, &flow_job.args).await;
|
||||
let flow_path = flow_job.script_path();
|
||||
let version_hash = if let Some(rc) = flow_job.raw_flow.as_ref() {
|
||||
use std::hash::Hasher;
|
||||
let mut s = DefaultHasher::new();
|
||||
serde_json::to_string(&rc.0)
|
||||
.unwrap_or_default()
|
||||
.hash(&mut s);
|
||||
format!("flow_{}", hex::encode(s.finish().to_be_bytes()))
|
||||
let version_hash = if let Some(sqlx::types::Json(s)) = flow_job.raw_flow.as_ref() {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = DefaultHasher::new();
|
||||
s.get().hash(&mut h);
|
||||
format!("flow_{}", hex::encode(h.finish().to_be_bytes()))
|
||||
} else {
|
||||
"flow_unknown".to_string()
|
||||
};
|
||||
@@ -1107,6 +1097,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
tracing::debug!(id = %flow_job.id, "start handle flow");
|
||||
match handle_flow(
|
||||
flow_job.clone(),
|
||||
flow_value,
|
||||
db,
|
||||
client,
|
||||
Some(nresult.clone()),
|
||||
@@ -1239,19 +1230,6 @@ async fn retrieve_flow_jobs_results(
|
||||
Ok(to_raw_value(&results))
|
||||
}
|
||||
|
||||
fn get_module(flow_job: &QueuedJob, module_step: &Step) -> Option<FlowModule> {
|
||||
let raw_flow = flow_job.parse_raw_flow();
|
||||
if let Some(raw_flow) = raw_flow {
|
||||
match module_step {
|
||||
Step::PreprocessorStep => raw_flow.preprocessor_module.map(|x| *x.clone()),
|
||||
Step::Step(i) => raw_flow.modules.get(*i).map(|x| x.clone()),
|
||||
Step::FailureStep => raw_flow.failure_module.map(|x| *x.clone()),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_skip_branchall_failure<'c>(
|
||||
job: &Uuid,
|
||||
branch: usize,
|
||||
@@ -1290,23 +1268,6 @@ async fn compute_skip_branchall_failure<'c>(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn has_failure_module<'c>(flow: Uuid, db: &DB) -> Result<bool, Error> {
|
||||
sqlx::query_scalar::<_, Option<bool>>(
|
||||
"SELECT raw_flow->'failure_module' != 'null'::jsonb
|
||||
FROM queue
|
||||
WHERE id = $1",
|
||||
)
|
||||
.bind(flow)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error during retrieval of has_failure_module: {e:#}"
|
||||
))
|
||||
})
|
||||
.map(|v| v.unwrap_or(false))
|
||||
}
|
||||
|
||||
// async fn retrieve_cleanup_module<'c>(flow_uuid: Uuid, db: &DB) -> Result<FlowCleanupModule, Error> {
|
||||
// tracing::warn!("Retrieving cleanup module of flow {}", flow_uuid);
|
||||
// let raw_value = sqlx::query_scalar!(
|
||||
@@ -1523,6 +1484,7 @@ async fn transform_input(
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn handle_flow<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
flow_job: Arc<QueuedJob>,
|
||||
flow_value: Option<FlowValue>,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
client: &AuthedClient,
|
||||
last_result: Option<Arc<Box<RawValue>>>,
|
||||
@@ -1531,8 +1493,7 @@ pub async fn handle_flow<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
rsmq: Option<R>,
|
||||
job_completed_tx: Sender<SendResult>,
|
||||
) -> anyhow::Result<()> {
|
||||
let flow = flow_job
|
||||
.parse_raw_flow()
|
||||
let flow = flow_value
|
||||
.with_context(|| "Unable to parse flow definition")?;
|
||||
let status = flow_job
|
||||
.parse_flow_status()
|
||||
|
||||
@@ -1312,6 +1312,8 @@ async fn python_dep(
|
||||
job_dir,
|
||||
worker_dir,
|
||||
occupancy_metrics,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DenoLandProvider } from "https://deno.land/x/cliffy@v0.25.7/command/upg
|
||||
import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
import { VERSION, createBenchScript, getFlowPayload, login } from "./lib.ts";
|
||||
|
||||
@@ -172,8 +173,8 @@ export async function main({
|
||||
kind: "script",
|
||||
path: "f/benchmarks/" + kind,
|
||||
});
|
||||
} else if (["2steps"].includes(kind)) {
|
||||
nStepsFlow = 2;
|
||||
} else if (["2steps", "bigscriptinflow"].includes(kind)) {
|
||||
nStepsFlow = kind == "2steps" ? 2 : 1;
|
||||
const payload = getFlowPayload(kind);
|
||||
body = JSON.stringify({
|
||||
kind: "flow",
|
||||
@@ -194,6 +195,15 @@ export async function main({
|
||||
kind: "script",
|
||||
path: kind.substr(7),
|
||||
});
|
||||
} else if (kind == "bigrawscript") {
|
||||
noVerify = true;
|
||||
body = JSON.stringify({
|
||||
kind: "rawscript",
|
||||
rawscript: {
|
||||
language: api.RawScript.language.BASH,
|
||||
content: "# let's bloat that bash script, 3.. 2.. 1.. BOOM\n".repeat(25000) + "echo \"$WM_FLOW_JOB_ID\"\n",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unknown script pattern " + kind);
|
||||
}
|
||||
@@ -246,7 +256,7 @@ export async function main({
|
||||
} else {
|
||||
const elapsed = start ? Date.now() - start : 0;
|
||||
completedJobs = await getCompletedJobsCount();
|
||||
if (kind === "2steps" || kind.startsWith("flow:")) {
|
||||
if (nStepsFlow > 0) {
|
||||
completedJobs = Math.floor(completedJobs / (nStepsFlow + 1));
|
||||
}
|
||||
const avgThr = ((completedJobs / elapsed) * 1000).toFixed(2);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.418.0";
|
||||
export const VERSION = "v1.423.2";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
@@ -235,6 +235,23 @@ export const getFlowPayload = (flowPattern: string): api.FlowPreview => {
|
||||
],
|
||||
},
|
||||
};
|
||||
} else if (flowPattern == "bigscriptinflow") {
|
||||
return {
|
||||
path: "bigscriptinflow",
|
||||
args: {},
|
||||
value: {
|
||||
modules: [
|
||||
{
|
||||
value: {
|
||||
input_transforms: {},
|
||||
language: api.RawScript.language.BASH,
|
||||
type: "rawscript",
|
||||
content: "# let's bloat that bash script, 3.. 2.. 1.. BOOM\n".repeat(25000) + "echo \"$WM_FLOW_JOB_ID\"\n",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
path: "2steps",
|
||||
|
||||
@@ -30,5 +30,13 @@
|
||||
{
|
||||
"kind": "nativets",
|
||||
"jobs": 2000
|
||||
},
|
||||
{
|
||||
"kind": "bigrawscript",
|
||||
"jobs": 10000
|
||||
},
|
||||
{
|
||||
"kind": "bigscriptinflow",
|
||||
"jobs": 10000
|
||||
}
|
||||
]
|
||||
@@ -60,7 +60,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.418.0";
|
||||
export const VERSION = "1.423.2";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
15
cli/sync.ts
15
cli/sync.ts
@@ -1398,11 +1398,12 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
log.info(`Deleting ${typ} ${change.path}`);
|
||||
}
|
||||
const workspaceId = workspace.workspaceId;
|
||||
const target = change.path.replaceAll(SEP, "/");
|
||||
switch (typ) {
|
||||
case "script": {
|
||||
const script = await wmill.getScriptByPath({
|
||||
workspace: workspaceId,
|
||||
path: removeExtensionToPath(change.path),
|
||||
path: removeExtensionToPath(target),
|
||||
});
|
||||
await wmill.archiveScriptByHash({
|
||||
workspace: workspaceId,
|
||||
@@ -1419,37 +1420,37 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
case "resource":
|
||||
await wmill.deleteResource({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".resource.json"),
|
||||
path: removeSuffix(target, ".resource.json"),
|
||||
});
|
||||
break;
|
||||
case "resource-type":
|
||||
await wmill.deleteResourceType({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".resource-type.json"),
|
||||
path: removeSuffix(target, ".resource-type.json"),
|
||||
});
|
||||
break;
|
||||
case "flow":
|
||||
await wmill.deleteFlowByPath({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".flow/flow.json"),
|
||||
path: removeSuffix(target, ".flow/flow.json"),
|
||||
});
|
||||
break;
|
||||
case "app":
|
||||
await wmill.deleteApp({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".app/app.json"),
|
||||
path: removeSuffix(target, ".app/app.json"),
|
||||
});
|
||||
break;
|
||||
case "schedule":
|
||||
await wmill.deleteSchedule({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".schedule.json"),
|
||||
path: removeSuffix(target, ".schedule.json"),
|
||||
});
|
||||
break;
|
||||
case "variable":
|
||||
await wmill.deleteVariable({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".variable.json"),
|
||||
path: removeSuffix(target, ".variable.json"),
|
||||
});
|
||||
break;
|
||||
case "user": {
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
|
||||
# Install UV
|
||||
RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.4.18/uv-installer.sh | sh && mv /root/.cargo/bin/uv /usr/local/bin/uv
|
||||
|
||||
COPY --from=oven/bun:1.1.32 /usr/local/bin/bun /usr/bin/bun
|
||||
COPY --from=oven/bun:1.1.34 /usr/local/bin/bun /usr/bin/bun
|
||||
|
||||
# add the docker client to call docker from a worker if enabled
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
@@ -19,7 +19,7 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
|
||||
# Install UV
|
||||
RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.4.18/uv-installer.sh | sh && mv /root/.cargo/bin/uv /usr/local/bin/uv
|
||||
|
||||
COPY --from=oven/bun:1.1.32 /usr/local/bin/bun /usr/bin/bun
|
||||
COPY --from=oven/bun:1.1.34 /usr/local/bin/bun /usr/bin/bun
|
||||
|
||||
# add the docker client to call docker from a worker if enabled
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user