Compare commits

...
119 Commits
Author SHA1 Message Date
Ruben Fiszelandrubenfiszel e6d5689bf9 chore(main): release 1.372.0 (#4165)
* chore(main): release 1.372.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-08-01 16:28:17 +02:00
Ruben Fiszel efcf0e40f6 fix: fix raw_deps handling 2024-08-01 16:24:30 +02:00
Ruben Fiszel 42e3ae92e3 fix: improve cancel_selection job for running jobs 2024-08-01 16:08:17 +02:00
Ruben Fiszel b5464e2906 feat: variables created by password fields expire after 7 days 2024-08-01 16:05:03 +02:00
Ruben Fiszel 2c27083e2a avoir mistaken editor edit/deploy interaction 2024-08-01 10:07:15 +02:00
Ruben Fiszel 2b6f84e5d8 more clearly display loading when jobs filter change 2024-08-01 09:25:18 +02:00
Ruben Fiszel c6b07561da nit fix occupancy rate 2024-08-01 09:15:55 +02:00
Ruben Fiszel 1a47325055 improve queue indices 2024-07-31 23:44:40 +02:00
Ruben Fiszelandrubenfiszel dd1a7a1b3d chore(main): release 1.371.4 (#4158)
* chore(main): release 1.371.4

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-31 18:08:01 +02:00
Henri Courdent 79851101ae doc_link_fixes (#4157) 2024-07-31 18:03:49 +02:00
Henri Courdent 6808ee5a70 Order components + names alignment (#4160) 2024-07-31 18:03:23 +02:00
Faton Ramadani efeb65be4c fix(frontend): If multiple recompute all present, interval is now in sync (#4162) 2024-07-31 17:45:52 +02:00
Faton Ramadani 10c699759e fix(frontend): fix recompute all (#4161)
* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): fix recompute all

* fix(frontend): fix recompute all

* fix(frontend): clean up

* fix(frontend): fix how the onClick callback is called
2024-07-31 17:27:38 +02:00
Ruben Fiszel d69aa8d484 fix: improve index usage and runs page performance 2024-07-31 13:08:28 +02:00
Ruben Fiszel b95337fcb9 improve job result deser 2024-07-31 02:32:33 +02:00
Ruben Fiszel 9921731fdf add limits to variable list 2024-07-31 01:37:51 +02:00
Ruben Fiszelandrubenfiszel e7cccd099e chore(main): release 1.371.3 (#4156)
* chore(main): release 1.371.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-30 21:54:23 +02:00
Ruben Fiszel be8cedfe7f fix: for codebase bundle, use cjs exports instead 2024-07-30 21:48:40 +02:00
Ruben Fiszelandrubenfiszel 1a548f32a1 chore(main): release 1.371.2 (#4155)
* chore(main): release 1.371.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-30 21:28:46 +02:00
Ruben Fiszel 72ff465300 improve banner for esbuild bundle 2024-07-30 21:23:31 +02:00
Ruben Fiszel 716bb7118b fix: improve codebase handling by ignoring creating lock and bundle 2024-07-30 20:22:34 +02:00
Ruben Fiszel 4fe797eeab chore: update backend tests to 1.79 2024-07-30 18:34:59 +02:00
Ruben Fiszelandrubenfiszel 459a51c111 chore(main): release 1.371.1 (#4153)
* chore(main): release 1.371.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-30 18:21:29 +02:00
Ruben Fiszel 8c81e47564 fix: make SLEEP_QUEUE adaptative to number of subworkers 2024-07-30 17:57:14 +02:00
Ruben Fiszel f59046a924 fix: database connections now scale linearly with number of subworkers 2024-07-30 17:45:03 +02:00
Ruben Fiszelandrubenfiszel e5c73d66d3 chore(main): release 1.371.0 (#4151)
* chore(main): release 1.371.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-30 16:28:16 +02:00
Ruben Fiszel cf3512ee03 update rust image to 1.79 2024-07-30 15:46:43 +02:00
Ruben Fiszel 28277da35b fix: submit and form persistence on app on render change 2024-07-30 15:43:37 +02:00
Ruben Fiszel f63b176328 add proxy to helper 2024-07-30 12:37:24 +02:00
Ruben Fiszel ccb892feab add proxy and useragent support for native scripts 2024-07-30 12:31:47 +02:00
Faton Ramadani 5992b82180 feat(frontend): support array of objects in schema (#4106)
* feat(frontend): Support array of objets

* feat(frontend): add support for LightweightArgInput

* feat(frontend): support objects and arrays of objects in LightweightArgInput
2024-07-30 11:42:49 +02:00
Ruben Fiszel 6a33624416 fix: fix bunnative lock creation 2024-07-30 10:01:29 +02:00
Ruben Fiszel 06433a6ec6 fix: always consider electron as external for bundles 2024-07-30 09:38:19 +02:00
Ruben Fiszel e02e644d4c fix: support bunnative in the CLI 2024-07-30 08:58:32 +02:00
Ruben Fiszel 9942e86765 bump deno and deno_core to 1.45.4 2024-07-30 08:43:52 +02:00
Ruben Fiszelandrubenfiszel 9f8862680c chore(main): release 1.370.0 (#4150)
* chore(main): release 1.370.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-29 21:36:30 +02:00
Ruben Fiszel 6e91005daa fix: improve native runtime with axios support 2024-07-29 21:13:02 +02:00
HugoCasa 5031a8cb01 feat: get completed flow node result by api/download + eval list result json path optim (#4108)
* feat: get completed flow node result by api

* fix: missing file

* feat: fallback to completed job + fix dfs

* feat: download all results + eval improvement

* fix: nit regex

* fix: nit
2024-07-29 20:30:55 +02:00
Ruben Fiszelandrubenfiszel 28810910b1 chore(main): release 1.369.1 (#4149)
* chore(main): release 1.369.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-29 20:22:26 +02:00
Faton Ramadaniandellipsis-dev[bot] 71badb959d fix(frontend): Fix how new items are processed in App select (#4109)
* fix(frontend): Fix how new items are procesed

* fix(frontend): Fix regex

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix(frontend): avoid duplicates

* fix(frontend): simplify code

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2024-07-29 20:21:39 +02:00
Ruben Fiszel bc94acb378 fix: fix lang picker artefact for script editor 2024-07-29 20:18:56 +02:00
Ruben Fiszelandrubenfiszel ca345d4755 chore(main): release 1.369.0 (#4147)
* chore(main): release 1.369.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-29 19:55:34 +02:00
Lewis J Ellis b250890b1f Add platform: 'node' to codebase esbuild (#4148) 2024-07-29 19:49:59 +02:00
Ruben Fiszel 18f22be2ba feat: remove nativets in favor of bun with native pragma 2024-07-29 19:26:26 +02:00
Ruben Fiszel b02baa2cfb feat: remove nativets in favor of bun with native pragma 2024-07-29 19:20:00 +02:00
Faton Ramadani fb89eed8fa feat(frontend): App bar as components (#4103)
* feat(frontend): wip

* feat(frontend): add migration to the new top bar

* feat(frontend): fix topbar styling

* feat(frontend): fix migration code

* feat(frontend): fix migration code

* feat(frontend): fix migration code

* feat(frontend): wip

* feat(frontend): done

* feat(frontend): done

* feat(frontend): remove migration

* feat(frontend): fix top bar styling

* feat(frontend): fix sync issues

* feat(frontend): improve style

* feat(frontend): Redesign Recompute all

* feat(frontend): change icon + make the default title font bigger

* feat(frontend): Remove unecesary clearInterval

* feat(frontend): Fix dropdown menu button style
2024-07-29 17:26:18 +02:00
HugoCasa eb6557a6be feat: add support for text/plain webhook (#4146) 2024-07-29 15:56:25 +02:00
Ruben Fiszel 4f114cbcab nit less cloning 2024-07-28 23:31:50 +02:00
Ruben Fiszel 2cc24b675e simplify noop code handling 2024-07-28 20:10:59 +02:00
Ruben Fiszelandrubenfiszel 40caa795fb chore(main): release 1.368.3 (#4143)
* chore(main): release 1.368.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-28 19:34:13 +02:00
Ruben Fiszel e45a8a4591 fix: update bun to 1.1.21 2024-07-28 14:55:47 +02:00
Ruben Fiszel 3f1ed35f3e clarify //nobundling 2024-07-28 14:31:23 +02:00
Ruben Fiszelandrubenfiszel 9deba3e715 chore(main): release 1.368.2 (#4141)
* chore(main): release 1.368.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-28 11:09:31 +02:00
Ruben Fiszel 1e2e907982 fix: disable prebundling for nodejs mode scripts 2024-07-28 11:06:43 +02:00
Ruben Fiszel c3848e2e30 fix: add the nobundling option for bun 2024-07-28 10:56:04 +02:00
Ruben Fiszel 35d567e2e0 small nit BASE_URL 2024-07-28 10:25:33 +02:00
Ruben Fiszelandrubenfiszel 7099ec48e6 chore(main): release 1.368.1 (#4140)
* chore(main): release 1.368.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-27 12:46:47 +02:00
Ruben Fiszel df8cf402c6 improve slightly concurrency run min started_at query 2024-07-27 12:43:24 +02:00
Ruben Fiszel 2b67fbcd98 improve slightly concurrency run min started_at query 2024-07-27 12:41:01 +02:00
Ruben Fiszel ec9aad4dae improve slightly concurrency run min started_at query 2024-07-27 12:28:55 +02:00
Ruben Fiszel 99623f31af fix: improve runs page performance through pg indices 2024-07-27 12:16:13 +02:00
Ruben Fiszel 0e908ec71f reload servers on metrics setting change 2024-07-27 07:57:49 +02:00
Ruben Fiszelandrubenfiszel c52ca177ea chore(main): release 1.368.0 (#4129)
* chore(main): release 1.368.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-26 23:27:34 +02:00
Ruben Fiszel 545a57fd2c fix: allow colors for bun and log error directly 2024-07-26 22:42:24 +02:00
Ruben Fiszel f1d5be8a2a feat: add FORCE_<env> to allow to override db settings 2024-07-26 21:47:06 +02:00
Ruben Fiszel 6167fbb9a5 add loading timing 2024-07-26 21:05:48 +02:00
Ruben Fiszel 07c764c167 fix tests 2024-07-26 19:26:00 +02:00
Ruben Fiszel 78fd99cb68 feat: pre-bundle bun scripts (#4132) 2024-07-26 19:00:30 +02:00
Henri Courdent 54e4397a0b Upated thumbnail repo and content of readme (#4136) 2024-07-26 18:24:53 +02:00
wendrul c270ab1171 fix: search modal improvements (#4128)
* Message when runs filter is empty

* Improve height and width of modal
2024-07-26 11:08:16 +02:00
HugoCasaandRuben Fiszel 12f9e56154 feat: job view audit logging based on env variable (#4131)
* feat: job view audit logging based on env variable

* Update jobs.rs

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2024-07-26 11:07:15 +02:00
HugoCasa 6df9eca2cf feat: multi sql statement with pg fix (#4134)
* Revert "Revert "feat: multi statement sql (#4104)" (#4133)"

This reverts commit 38f0949ce3.

* fix: sort pg args
2024-07-26 09:23:39 +02:00
Ruben Fiszel 38f0949ce3 Revert "feat: multi statement sql (#4104)" (#4133)
This reverts commit 23108b4688.
2024-07-26 01:24:03 +02:00
HugoCasa 23108b4688 feat: multi statement sql (#4104)
* feat: multi statement pg

* fix: add other flavors

* feat: make pg params start at 1 and sequential

* fix: improve sql statement parsing

* add tests

* fix: allow no semi in last statement

* fix: merge conflict

* fix: minor improvement

* fix: parser version
2024-07-25 20:46:04 +02:00
Ruben Fiszel 8daccfc876 improve s3 bucket tests 2024-07-25 13:02:59 +02:00
Ruben Fiszel d87ae08362 fix npm_mode check from raw_deps 2024-07-25 12:58:04 +02:00
Henri Courdent 7102dd81c0 Changelog update frontend 25 07 (#4130) 2024-07-25 11:21:17 +02:00
Ruben Fiszel 8e615c900d fix: support npmjs mode for raw deps of package.json from CLI 2024-07-25 09:53:18 +02:00
Ruben Fiszel 9300ee6f47 fix docker-compose 2024-07-24 19:05:31 +02:00
Ruben Fiszelandrubenfiszel 01f2eeb3fe chore(main): release 1.367.2 (#4127)
* chore(main): release 1.367.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-24 18:28:56 +02:00
Ruben Fiszel 92dac027f2 fix: move bun cache to non mounted volume to benefit from cache optimization 2024-07-24 18:25:21 +02:00
Ruben Fiszel 9c63623436 avoid loops in symlinks 2024-07-24 16:45:04 +02:00
Ruben Fiszel 289766281c only use symlinks from buntar to job 2024-07-24 16:04:52 +02:00
Ruben Fiszel 9fd3ec5b03 only use symlinks from buntar to job 2024-07-24 15:54:46 +02:00
Ruben Fiszel 80d4fb0352 fix: use symlink and straight copy as fallback methods for buntar 2024-07-24 15:45:08 +02:00
Ruben Fiszelandrubenfiszel aaf067eedb chore(main): release 1.367.1 (#4126)
* chore(main): release 1.367.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-24 15:27:14 +02:00
Ruben Fiszel 204e2fafda fix: delete buntar if any issue while creating it 2024-07-24 15:23:06 +02:00
Ruben Fiszel d1012ea930 remove shared from hardlinks 2024-07-24 15:14:27 +02:00
Ruben Fiszel 7fbd0028c1 fix: generate lockfile with npm when npm mode is used 2024-07-24 14:48:25 +02:00
Ruben Fiszelandrubenfiszel eead10bb80 chore(main): release 1.367.0 (#4118)
* chore(main): release 1.367.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-24 13:55:40 +02:00
Ruben Fiszel d49b2d0f06 fix: fix webhooks urls after BASE_URL change 2024-07-24 13:31:06 +02:00
Ruben Fiszel 83d3f491b3 chore: update sqlx to 0.8.0 2024-07-24 11:51:54 +02:00
Ruben Fiszel 6d1fd24d37 chore: update sqlx to 0.8.0 2024-07-24 11:48:33 +02:00
wendrul 8fcda68af0 improve search backup on s3 2024-07-24 11:31:37 +02:00
Ruben Fiszel 20390c53e8 fix: respect sorting when downloading csv f rom auto table 2024-07-24 11:12:51 +02:00
Ruben Fiszel c0f3a40c07 better split tantivy 2024-07-24 09:03:54 +02:00
Ruben Fiszel 7047e2975d better split tantivy 2024-07-24 08:15:14 +02:00
wendrul 5355af427d Prepare sqlx (#4122) 2024-07-23 20:03:30 +02:00
wendrul eceb472d16 Prepare sqlx (#4121) 2024-07-23 19:54:31 +02:00
wendrul cac39a1051 feat: job search index backed up and loaded from s3 (#4100)
* Add fn to get logs from disk and store as string

* Update dependencies

* Add async

* Update dependecies

* Add missing async

* Move JOB_RETENTION_SECS to windmill_common

* Update ee repo ref
2024-07-23 19:35:29 +02:00
HugoCasa 14a4f12826 feat: apply workspace specific tags only to some workspaces (#4107) 2024-07-23 19:12:11 +02:00
Ruben Fiszel 97f6b2e7ad fix: make result_json path stable for python executors 2024-07-23 17:39:40 +02:00
HugoCasa c6dc06bf70 fix: sqlx build (#4120) 2024-07-23 17:14:36 +02:00
Ruben Fiszel 9ab5b2e32d fix: preserve force json across code preview 2024-07-23 16:42:29 +02:00
HugoCasaandRuben Fiszel 13e9e8de9e fix: cgroupv1 mem limit + granular memory reporting (#4119)
* fix: cgroupv1 mem limit + granular memory reporting

* update ee ref

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2024-07-23 16:24:22 +02:00
HugoCasaandRuben Fiszel 73decb24b2 fix: filter audit logs end user by username + complete resource filter (#4105)
* fix: filter audit logs end user by username + complete resource filter

* update ee ref

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2024-07-23 16:23:33 +02:00
HugoCasa d096704b27 feat: use jwks for external jwt auth (#4089)
* feat: use jwks for external jwts

* fix: build

* update ee ref

* fix: nit error msg
2024-07-23 16:22:36 +02:00
Ruben Fiszel 48e9f089be feat: use hardlinks instead of tar to improve bun cache performances 2024-07-23 15:30:09 +02:00
Ruben Fiszelandrubenfiszel 99509ac683 chore(main): release 1.366.6 (#4117)
* chore(main): release 1.366.6

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-23 12:50:06 +02:00
Ruben Fiszel ae4cbb0401 fix: fix copilot completion after base_url change 2024-07-23 12:21:12 +02:00
Ruben Fiszel 05388053e8 improve scrollbar styling for chrome 2024-07-23 08:57:04 +02:00
Ruben Fiszelandrubenfiszel a117a4d017 chore(main): release 1.366.5 (#4116)
* chore(main): release 1.366.5

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-22 22:54:50 +02:00
Ruben Fiszel bb861cfede fix: fix BASE_URL build conf 2024-07-22 22:51:08 +02:00
Ruben Fiszelandrubenfiszel 0f91539977 chore(main): release 1.366.4 (#4115)
* chore(main): release 1.366.4

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-07-22 22:39:39 +02:00
Ruben Fiszel ab686ea69e fix sveltekit conf 2024-07-22 22:36:56 +02:00
Ruben Fiszel e7cdc2b934 fix sveltekit conf 2024-07-22 22:31:53 +02:00
Ruben Fiszel 7d4dc8df26 fix sveltekit conf 2024-07-22 22:25:53 +02:00
Ruben Fiszel 6e33e4e0b9 fix: fix BASE_URL build conf 2024-07-22 21:53:08 +02:00
Ruben Fiszel 8a89652255 nit fix 2024-07-22 21:24:53 +02:00
191 changed files with 5304 additions and 2209 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.78-slim-bookworm
ARG RUST_IMAGE=rust:1.79-slim-bookworm
ARG PYTHON_IMAGE=python:3.11.4-slim-bookworm
FROM ${DEBIAN_IMAGE} as downloader
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.78.0
toolchain: 1.79.0
# - uses: Swatinem/rust-cache@v2
# with:
# workspaces: |
+185
View File
@@ -1,5 +1,190 @@
# Changelog
## [1.372.0](https://github.com/windmill-labs/windmill/compare/v1.371.4...v1.372.0) (2024-08-01)
### Features
* variables created by password fields expire after 7 days ([b5464e2](https://github.com/windmill-labs/windmill/commit/b5464e2906bdaedb4752e198e40b7e8c4dbb1338))
### Bug Fixes
* fix raw_deps handling ([efcf0e4](https://github.com/windmill-labs/windmill/commit/efcf0e40f68342f8330cf5d610979eb7fe92ce09))
* improve cancel_selection job for running jobs ([42e3ae9](https://github.com/windmill-labs/windmill/commit/42e3ae92e3841579f8ac0821b58ebb72c4204c98))
## [1.371.4](https://github.com/windmill-labs/windmill/compare/v1.371.3...v1.371.4) (2024-07-31)
### Bug Fixes
* **frontend:** fix recompute all ([#4161](https://github.com/windmill-labs/windmill/issues/4161)) ([10c6997](https://github.com/windmill-labs/windmill/commit/10c699759e1d06d009783b9f5cba2cbf3ba6a7da))
* **frontend:** If multiple recompute all present, interval is now in sync ([#4162](https://github.com/windmill-labs/windmill/issues/4162)) ([efeb65b](https://github.com/windmill-labs/windmill/commit/efeb65be4c91491bb4fbedfbc160da7517ebe1d6))
* improve index usage and runs page performance ([d69aa8d](https://github.com/windmill-labs/windmill/commit/d69aa8d484de016ae7e7bbd190ce6c254818c861))
## [1.371.3](https://github.com/windmill-labs/windmill/compare/v1.371.2...v1.371.3) (2024-07-30)
### Bug Fixes
* for codebase bundle, use cjs exports instead ([be8cedf](https://github.com/windmill-labs/windmill/commit/be8cedfe7fe42105a375c92536ec7f5e5a1343bf))
## [1.371.2](https://github.com/windmill-labs/windmill/compare/v1.371.1...v1.371.2) (2024-07-30)
### Bug Fixes
* improve codebase handling by ignoring creating lock and bundle ([716bb71](https://github.com/windmill-labs/windmill/commit/716bb7118b0d5b8dd86799d5514f37b068613256))
## [1.371.1](https://github.com/windmill-labs/windmill/compare/v1.371.0...v1.371.1) (2024-07-30)
### Bug Fixes
* database connections now scale linearly with number of subworkers ([f59046a](https://github.com/windmill-labs/windmill/commit/f59046a9249962ba7e1739de579cd8d885ec63a8))
## [1.371.0](https://github.com/windmill-labs/windmill/compare/v1.370.0...v1.371.0) (2024-07-30)
### Features
* **frontend:** support array of objects in schema ([#4106](https://github.com/windmill-labs/windmill/issues/4106)) ([5992b82](https://github.com/windmill-labs/windmill/commit/5992b821803d9cdc45fa0dee100f159dd3ed557f))
### Bug Fixes
* always consider electron as external for bundles ([06433a6](https://github.com/windmill-labs/windmill/commit/06433a6ec65534e1a11085cefd6a67699e4eb0a0))
* fix bunnative lock creation ([6a33624](https://github.com/windmill-labs/windmill/commit/6a33624416cc48e2a1ab21afbd2ac4ab48be2559))
* submit and form persistence on app on render change ([28277da](https://github.com/windmill-labs/windmill/commit/28277da35bae1c18f16e85d093989e0d419b3601))
* support bunnative in the CLI ([e02e644](https://github.com/windmill-labs/windmill/commit/e02e644d4ca16773bae3cc2a6b52ac08417d3905))
## [1.370.0](https://github.com/windmill-labs/windmill/compare/v1.369.1...v1.370.0) (2024-07-29)
### Features
* get completed flow node result by api/download + eval list result json path optim ([#4108](https://github.com/windmill-labs/windmill/issues/4108)) ([5031a8c](https://github.com/windmill-labs/windmill/commit/5031a8cb015e3ba8d668f5de769b8d67da82c116))
### Bug Fixes
* improve native runtime with axios support ([6e91005](https://github.com/windmill-labs/windmill/commit/6e91005daa4bbe2f0bd1f8b1c2cce1f68485f9ef))
## [1.369.1](https://github.com/windmill-labs/windmill/compare/v1.369.0...v1.369.1) (2024-07-29)
### Bug Fixes
* fix lang picker artefact for script editor ([bc94acb](https://github.com/windmill-labs/windmill/commit/bc94acb378b1310271dd3dda80614e0e38b5285f))
## [1.369.0](https://github.com/windmill-labs/windmill/compare/v1.368.3...v1.369.0) (2024-07-29)
### Features
* add support for text/plain webhook ([#4146](https://github.com/windmill-labs/windmill/issues/4146)) ([eb6557a](https://github.com/windmill-labs/windmill/commit/eb6557a6beb5d3b2324b30f2abc117b1a122c3f7))
* **frontend:** App bar as components ([#4103](https://github.com/windmill-labs/windmill/issues/4103)) ([fb89eed](https://github.com/windmill-labs/windmill/commit/fb89eed8fa43568ffe3a22c5a6dd0b9f38890a37))
* remove nativets in favor of bun with native pragma ([18f22be](https://github.com/windmill-labs/windmill/commit/18f22be2bae3136f1d9ff06c74f3751504bef60b))
* remove nativets in favor of bun with native pragma ([b02baa2](https://github.com/windmill-labs/windmill/commit/b02baa2cfb242f7f6ed64e9861a8562272fb2974))
## [1.368.3](https://github.com/windmill-labs/windmill/compare/v1.368.2...v1.368.3) (2024-07-28)
### Bug Fixes
* update bun to 1.1.21 ([e45a8a4](https://github.com/windmill-labs/windmill/commit/e45a8a4591649de240a4bff0a25301a2586618bf))
## [1.368.2](https://github.com/windmill-labs/windmill/compare/v1.368.1...v1.368.2) (2024-07-28)
### Bug Fixes
* add the nobundling option for bun ([c3848e2](https://github.com/windmill-labs/windmill/commit/c3848e2e3011c7e74d5ae6eb85605476ef2998f4))
* disable prebundling for nodejs mode scripts ([1e2e907](https://github.com/windmill-labs/windmill/commit/1e2e907982eab2ea7e713d9cfbec046527616fa2))
## [1.368.1](https://github.com/windmill-labs/windmill/compare/v1.368.0...v1.368.1) (2024-07-27)
### Bug Fixes
* improve runs page performance through pg indices ([99623f3](https://github.com/windmill-labs/windmill/commit/99623f31af109463b065688058d0cab8ea9910de))
## [1.368.0](https://github.com/windmill-labs/windmill/compare/v1.367.2...v1.368.0) (2024-07-26)
### Features
* add FORCE_&lt;env&gt; to allow to override db settings ([f1d5be8](https://github.com/windmill-labs/windmill/commit/f1d5be8a2a76d5c0d37c55ba762f2cd7a6f6982f))
* job view audit logging based on env variable ([#4131](https://github.com/windmill-labs/windmill/issues/4131)) ([12f9e56](https://github.com/windmill-labs/windmill/commit/12f9e56154b32d839f49dd967ada4952ca4c395d))
* multi sql statement with pg fix ([#4134](https://github.com/windmill-labs/windmill/issues/4134)) ([6df9eca](https://github.com/windmill-labs/windmill/commit/6df9eca2cffef754ee27b3219f18d0ad69615c83))
* multi statement sql ([#4104](https://github.com/windmill-labs/windmill/issues/4104)) ([23108b4](https://github.com/windmill-labs/windmill/commit/23108b4688ec9c9683371ee70a50658f6baae42f))
* pre-bundle bun scripts ([#4132](https://github.com/windmill-labs/windmill/issues/4132)) ([78fd99c](https://github.com/windmill-labs/windmill/commit/78fd99cb6843c7f8045acd6a6a8df851ec6b35d1))
### Bug Fixes
* allow colors for bun and log error directly ([545a57f](https://github.com/windmill-labs/windmill/commit/545a57fd2c425f02d867f4183aac37131e17210f))
* search modal improvements ([#4128](https://github.com/windmill-labs/windmill/issues/4128)) ([c270ab1](https://github.com/windmill-labs/windmill/commit/c270ab11718f8d41fe764d5d2116bb3b9aa20cb2))
* support npmjs mode for raw deps of package.json from CLI ([8e615c9](https://github.com/windmill-labs/windmill/commit/8e615c900de46632c52d78a1ec4b6ccc2cec79ff))
## [1.367.2](https://github.com/windmill-labs/windmill/compare/v1.367.1...v1.367.2) (2024-07-24)
### Bug Fixes
* move bun cache to non mounted volume to benefit from cache optimization ([92dac02](https://github.com/windmill-labs/windmill/commit/92dac027f26dfa880e22b1896b48605ae2ef2794))
* use symlink and straight copy as fallback methods for buntar ([80d4fb0](https://github.com/windmill-labs/windmill/commit/80d4fb0352606948b01f4918e11f80a0bef544f4))
## [1.367.1](https://github.com/windmill-labs/windmill/compare/v1.367.0...v1.367.1) (2024-07-24)
### Bug Fixes
* delete buntar if any issue while creating it ([204e2fa](https://github.com/windmill-labs/windmill/commit/204e2fafda681347813ab91f96cee4194db2e156))
* generate lockfile with npm when npm mode is used ([7fbd002](https://github.com/windmill-labs/windmill/commit/7fbd0028c1f2dfbd6a5cde12e6a0e314ed31552a))
## [1.367.0](https://github.com/windmill-labs/windmill/compare/v1.366.6...v1.367.0) (2024-07-24)
### Features
* apply workspace specific tags only to some workspaces ([#4107](https://github.com/windmill-labs/windmill/issues/4107)) ([14a4f12](https://github.com/windmill-labs/windmill/commit/14a4f1282671d1c6a2481123cd5a4297fbd89607))
* job search index backed up and loaded from s3 ([#4100](https://github.com/windmill-labs/windmill/issues/4100)) ([cac39a1](https://github.com/windmill-labs/windmill/commit/cac39a105187bd005dee442c4968ebbd9ba163d7))
* use hardlinks instead of tar to improve bun cache performances ([48e9f08](https://github.com/windmill-labs/windmill/commit/48e9f089beff10828eef7a35fb341894eb50d25d))
* use jwks for external jwt auth ([#4089](https://github.com/windmill-labs/windmill/issues/4089)) ([d096704](https://github.com/windmill-labs/windmill/commit/d096704b27f169672d8949f8570b2b5cba25e78c))
### Bug Fixes
* cgroupv1 mem limit + granular memory reporting ([#4119](https://github.com/windmill-labs/windmill/issues/4119)) ([13e9e8d](https://github.com/windmill-labs/windmill/commit/13e9e8de9e49f9513ad0cd8ac5a3b8b356de02a9))
* filter audit logs end user by username + complete resource filter ([#4105](https://github.com/windmill-labs/windmill/issues/4105)) ([73decb2](https://github.com/windmill-labs/windmill/commit/73decb24b23e1a998957455cf4f4148be5990cce))
* fix webhooks urls after BASE_URL change ([d49b2d0](https://github.com/windmill-labs/windmill/commit/d49b2d0f06842e70180f931ac7b4ac683c1c65b6))
* make result_json path stable for python executors ([97f6b2e](https://github.com/windmill-labs/windmill/commit/97f6b2e7ad9962c99e566238bde40cdba0ffc3a8))
* preserve force json across code preview ([9ab5b2e](https://github.com/windmill-labs/windmill/commit/9ab5b2e32d78056752f654769154b90872a9ab85))
* respect sorting when downloading csv f rom auto table ([20390c5](https://github.com/windmill-labs/windmill/commit/20390c53e82686ac6a8b7a9bc1bf9fb8e014e3c5))
* sqlx build ([#4120](https://github.com/windmill-labs/windmill/issues/4120)) ([c6dc06b](https://github.com/windmill-labs/windmill/commit/c6dc06bf702b501225a51813a2489ce9ea3c4636))
## [1.366.6](https://github.com/windmill-labs/windmill/compare/v1.366.5...v1.366.6) (2024-07-23)
### Bug Fixes
* fix copilot completion after base_url change ([ae4cbb0](https://github.com/windmill-labs/windmill/commit/ae4cbb0401b01e1e378b2a982d7a610648c96239))
## [1.366.5](https://github.com/windmill-labs/windmill/compare/v1.366.4...v1.366.5) (2024-07-22)
### Bug Fixes
* fix BASE_URL build conf ([bb861cf](https://github.com/windmill-labs/windmill/commit/bb861cfedef1e5531f31b62dd73907b89ddd473a))
## [1.366.4](https://github.com/windmill-labs/windmill/compare/v1.366.3...v1.366.4) (2024-07-22)
### Bug Fixes
* fix BASE_URL build conf ([6e33e4e](https://github.com/windmill-labs/windmill/commit/6e33e4e0b9fce86407cc7cfc9a4a57e03ec5199b))
## [1.366.3](https://github.com/windmill-labs/windmill/compare/v1.366.2...v1.366.3) (2024-07-22)
+4 -4
View File
@@ -1,5 +1,5 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.78-slim-bookworm
ARG RUST_IMAGE=rust:1.79-slim-bookworm
ARG PYTHON_IMAGE=python:3.11.8-slim-bookworm
FROM ${RUST_IMAGE} AS rust_base
@@ -41,7 +41,7 @@ COPY /typescript-client/docs/ /frontend/static/tsdocs/
RUN npm run generate-backend-client
ENV NODE_OPTIONS "--max-old-space-size=8192"
ARG BASE_URL ""
ARG VITE_BASE_URL ""
RUN npm run build
@@ -155,9 +155,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:1.44.4 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=denoland/deno:1.45.4 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.1.18 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.21 /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
+21 -24
View File
@@ -3,7 +3,7 @@
</p>
<p align=center>
Open-source developer infrastructure for internal tools (APIs, background jobs, workflows and UIs). Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs and custom UIs to trigger workflows and scripts as internal apps.
Open-source developer infrastructure for internal tools (APIs, background jobs, workflows and UIs). Self-hostable alternative to Retool, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs and custom UIs to trigger workflows and scripts as internal apps.
<p align=center>
Scripts are turned into sharable UIs automatically, and can be composed together into flows or used into richer apps built with low-code. Supported script languages supported are: Python, TypeScript, Go, Bash, SQL, and GraphQL.
@@ -110,8 +110,8 @@ You can build your entire infra on top of Windmill!
```typescript
//import any dependency from npm
import * as wmill from "https://deno.land/x/windmill@v1.136.0/mod.ts";
import cowsay from "npm:cowsay@1.5.0";
import * as wmill from "windmill-client"
import * as cowsay from 'cowsay@1.5.0';
// fill the type, or use the +Resource type to get a type-safe reference to a resource
type Postgresql = {
@@ -131,7 +131,7 @@ export async function main(
e = { nested: "object" }
//f: wmill.Base64
) {
const email = Deno.env.get("WM_EMAIL");
const email = process.env["WM_EMAIL"];
// variables are permissioned and by path
let variable = await wmill.getVariable("f/company-folder/my_secret");
const lastTimeRun = await wmill.getState();
@@ -146,10 +146,8 @@ export async function main(
## CLI
We have a powerful CLI to interact with the windmill platform and sync your
scripts from local files, github repos and to run scripts and flows on the
instance from local commands. See
[more details](https://github.com/windmill-labs/windmill/tree/main/cli).
We have a powerful CLI to interact with the windmill platform and sync your scripts from local files, GitHub repos and to run scripts and flows on the instance from local commands. See
[more details](https://www.windmill.dev/docs/advanced/cli).
![CLI Screencast](./cli/vhs/output/setup.gif)
@@ -165,22 +163,21 @@ Code extension: <https://www.windmill.dev/docs/cli_local_dev/vscode-extension>.
## Stack
- Postgres as the database
- backend in Rust with the following highly-available and horizontally scalable
architecture:
- stateless API backend
- workers that pull jobs from a queue in Postgres (and later, Kafka or Redis.
Upvote [#173](#https://github.com/windmill-labs/windmill/issues/173) if
interested )
- frontend in Svelte
- scripts executions are sandboxed using google's
[nsjail](https://github.com/google/nsjail)
- javascript runtime is the
- Postgres as the database.
- Backend in Rust with the following highly-available and horizontally scalable.
Architecture:
- Stateless API backend.
- Workers that pull jobs from a queue in Postgres (and later, Kafka or Redis.
Upvote [#173](#https://github.com/windmill-labs/windmill/issues/173) if interested).
- Frontend in Svelte.
- Scripts executions are sandboxed using Google's
[nsjail](https://github.com/google/nsjail).
- Javascript runtime is the
[deno_core rust library](https://denolib.gitbook.io/guide/) (which itself uses
the [rusty_v8](https://github.com/denoland/rusty_v8) and hence V8 underneath)
- typescript runtime is deno
- python runtime is python3
- golang runtime is 1.19.1
the [rusty_v8](https://github.com/denoland/rusty_v8) and hence V8 underneath).
- TypeScript runtime is Bun and deno.
- Python runtime is python3.
- Golang runtime is 1.19.1.
## Fastest Self-Hostable Workflow Engine
@@ -399,4 +396,4 @@ running options.
## Copyright
Windmill Labs, Inc 2023
Windmill Labs, Inc 2023
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow_status FROM completed_job WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "flow_status",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
true
]
},
"hash": "061ff848f258dc880bec81d923370c905e689f37c6d931ee4559c3cfd394e168"
}
@@ -55,7 +55,8 @@
"graphql",
"powershell",
"mssql",
"php"
"php",
"bunnative"
]
}
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO windmill_migrations (name) VALUES ($1) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "15105be6247457fc01b7d65767ccdde047d0f0c172c7a01eeabe3bd8206a3069"
}
@@ -49,7 +49,8 @@
"graphql",
"powershell",
"mssql",
"php"
"php",
"bunnative"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description, account, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6, $7)",
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description, account, is_oauth, expires_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
"describe": {
"columns": [],
"parameters": {
@@ -11,10 +11,11 @@
"Bool",
"Varchar",
"Int4",
"Bool"
"Bool",
"Timestamptz"
]
},
"nullable": []
},
"hash": "2e4115bb2e6c8c85ad1492ad135d6b0454b342126cb5fa17e58caf71b32ee755"
"hash": "2be66f23536223549db9b50025932b6b1bad90b8fa47d97acb7d75aa3c37ef86"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT worker, worker_instance, vcpus, ping_at, started_at FROM worker_ping WHERE ping_at > now() - interval '30 days'",
"query": "SELECT worker, worker_instance, vcpus, memory, ping_at, started_at FROM worker_ping WHERE ping_at > now() - interval '30 days'",
"describe": {
"columns": [
{
@@ -20,11 +20,16 @@
},
{
"ordinal": 3,
"name": "memory",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "ping_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "started_at",
"type_info": "Timestamptz"
}
@@ -36,9 +41,10 @@
false,
false,
true,
true,
false,
false
]
},
"hash": "8480792eefdd0f31a49afec0f91262a6973c068da5413b154eff9b9da90db95f"
"hash": "43cc4e7d9846dbce81b61a25e8ceca636a08451db3288969ed9105fa0cb4e5b8"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = 'fix_job_completed_index')",
"query": "SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = $1)",
"describe": {
"columns": [
{
@@ -10,11 +10,13 @@
}
],
"parameters": {
"Left": []
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "2ae96e8af2dddd533a0377983ec36e30091089d0a96a37d8c625ddaf85d16a83"
"hash": "51a7e36ba1cc1616a19485eb5930cf90df758e85a8cac6037d044d5bd7440681"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO windmill_migrations (name) VALUES ('fix_job_completed_index') ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "57d830e07e40317150b24cf78248480c3a8b33e3e240385cae5703ccb9b14bec"
}
@@ -69,7 +69,8 @@
"graphql",
"powershell",
"mssql",
"php"
"php",
"bunnative"
]
}
}
@@ -54,7 +54,8 @@
"graphql",
"powershell",
"mssql",
"php"
"php",
"bunnative"
]
}
}
@@ -35,7 +35,8 @@
"graphql",
"powershell",
"mssql",
"php"
"php",
"bunnative"
]
}
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_at FROM script WHERE workspace_id = $1 AND hash = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": [
false
]
},
"hash": "8fef781ecef8bb7e4f5172adbab1d1eefa5cf05ff6521ee605af7ac2b90fa60b"
}
@@ -54,7 +54,8 @@
"graphql",
"powershell",
"mssql",
"php"
"php",
"bunnative"
]
}
}
@@ -49,7 +49,8 @@
"graphql",
"powershell",
"mssql",
"php"
"php",
"bunnative"
]
}
}
@@ -18,8 +18,8 @@
"Left": []
},
"nullable": [
false,
true
true,
false
]
},
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM variable WHERE expires_at IS NOT NULL AND expires_at > now() RETURNING path",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "bac12eff366f6b76f97d1a44041e0c1b5fdcb2c4205c1c1c0026b71ce404f089"
}
@@ -1,30 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT leaf_jobs->$1::text as leaf_jobs, parent_job FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "leaf_jobs",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "parent_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text"
]
},
"nullable": [
null,
true
]
},
"hash": "c8ac658002423906f2a6e43e423a9e24561dfaa6b07ce89063d0910d2342a83b"
}
@@ -75,7 +75,8 @@
"graphql",
"powershell",
"mssql",
"php"
"php",
"bunnative"
]
}
}
@@ -54,7 +54,8 @@
"graphql",
"powershell",
"mssql",
"php"
"php",
"bunnative"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3",
"query": "UPDATE script SET lock = $1, created_at = now() WHERE hash = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
@@ -12,5 +12,5 @@
},
"nullable": []
},
"hash": "d697b7311430e7bd5375ec5494179f4071e3bfe123d9600249cfa80c1103edd8"
"hash": "ec47955683d811b12e82ff3e6aeafa0df46a320c436bad5eb1acc127df138a61"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT CONCAT(coalesce(completed_job.logs, ''), coalesce(job_logs.logs, '')) as logs FROM completed_job\n LEFT JOIN job_logs ON job_logs.job_id = completed_job.id\n WHERE completed_job.id = $1 AND completed_job.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "logs",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "f036e930beff03e8a3f1f91c32267910c7d050f514dfba76991b644b86b914b9"
}
@@ -0,0 +1,41 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_by, CONCAT(coalesce(completed_job.logs, ''), coalesce(job_logs.logs, '')) as logs, job_logs.log_offset, job_logs.log_file_index\n FROM completed_job\n LEFT JOIN job_logs ON job_logs.job_id = completed_job.id\n WHERE completed_job.id = $1 AND completed_job.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "logs",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "log_offset",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "log_file_index",
"type_info": "TextArray"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
null,
false,
true
]
},
"hash": "f04fa0262091c5a4abf7dddafeec1b34ed806dda70592b0030067aed46a104d9"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COALESCE((SELECT MIN(started_at) as min_started_at\n FROM queue\n WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $2 AND canceled = false\n GROUP BY script_path), $3) as min_started_at, now() AS now",
"query": "SELECT COALESCE((SELECT MIN(started_at) as min_started_at\n FROM queue\n WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $2 AND canceled = false AND concurrent_limit > 0), $3) as min_started_at, now() AS now",
"describe": {
"columns": [
{
@@ -26,5 +26,5 @@
null
]
},
"hash": "3901cce744c9b246b661c817e068bdb3b1ab504ff8070fcccf6c909ad75f1f6d"
"hash": "f3d20b0fa17836538ec93b84b08ffc6b555371d6eeeadfc54f5bf0bcbe93d8b4"
}
+468 -362
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.366.3"
version = "1.372.0"
authors.workspace = true
edition.workspace = true
@@ -25,7 +25,7 @@ members = [
]
[workspace.package]
version = "1.366.3"
version = "1.372.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -39,7 +39,7 @@ incremental = true
[features]
default = []
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise", "windmill-indexer/enterprise"]
enterprise_saml = ["windmill-api/enterprise_saml"]
stripe = ["windmill-api/stripe"]
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark"]
@@ -47,13 +47,13 @@ flamegraph = ["windmill-common/flamegraph", "windmill-worker/flamegraph"]
loki = ["windmill-common/loki"]
pg_embed = ["dep:pg-embed"]
embedding = ["windmill-api/embedding"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet", "windmill-indexer/parquet"]
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus"]
flow_testing = ["windmill-worker/flow_testing"]
openidconnect = ["windmill-api/openidconnect"]
cloud = ["windmill-queue/cloud", "windmill-worker/cloud"]
jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-ctl"]
tantivy = ["windmill-indexer/tantivy"]
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy"]
[dependencies]
anyhow.workspace = true
@@ -64,7 +64,7 @@ windmill-common = { workspace = true, default-features = false }
windmill-git-sync.workspace = true
windmill-api = { workspace = true, default-features = false }
windmill-worker.workspace = true
windmill-indexer.workspace = true
windmill-indexer = { workspace = true, optional = true }
futures.workspace = true
tracing.workspace = true
sqlx.workspace = true
@@ -164,15 +164,15 @@ tokio-util = { version = "^0", features = ["io"] }
json-pointer = "^0"
itertools = "^0"
regex = "^1"
deno_fetch = "0.182.0"
deno_tls = "0.145.0"
deno_console = "0.158.0"
deno_url = "0.158.0"
deno_webidl = "0.158.0"
deno_web = "0.189.0"
deno_net = "0.150.0"
deno_core = "0.290.0"
deno_ast = { version = "=0.39.2", features = ["transpiling"] }
deno_fetch = "0.187.0"
deno_tls = "0.150.0"
deno_console = "0.163.0"
deno_url = "0.163.0"
deno_webidl = "0.163.0"
deno_web = "0.194.0"
deno_net = "0.155.0"
deno_core = "0.299.0"
deno_ast = { version = "=0.40.0", features = ["transpiling"] }
async-recursion = "^1"
swc_common = "=0.33.26"
swc_ecma_parser = "=0.144.3"
@@ -181,7 +181,7 @@ swc_ecma_visit = "=0.99.1"
base64 = "0.21.0"
hmac = "0.12.1"
sha2 = "0.10.6"
sqlx = { version = "0.7.3", features = [
sqlx = { version = "0.8.0", features = [
"macros",
"migrate",
"uuid",
+1 -1
View File
@@ -1 +1 @@
75e2aa1b2a8cff3cdcd64bab33eadab036e0c3fe
a64d2990e733aba3f8cbc6691a6090bcd481c891
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1 @@
CREATE INDEX IF NOT EXISTS queue_concurrency_started_at ON queue (workspace_id, script_path, started_at) WHERE concurrent_limit > 0 AND running = true AND job_kind != 'dependencies' AND canceled = false;-- Add up migration script here
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'bunnative';
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,3 @@
-- Add up migration script here
alter table queue drop constraint if exists queue_workspace_id_fkey;
alter table completed_job drop constraint if exists completed_job_workspace_id_fkey;
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,4 @@
-- Add up migration script here
DROP INDEX IF EXISTS queue_sort;
CREATE INDEX IF NOT EXISTS queue_sort ON queue (running, tag, priority DESC NULLS LAST, scheduled_for, created_at) WHERE running = false;
CREATE INDEX IF NOT EXISTS queue_sort_2 ON queue (running, priority DESC NULLS LAST, scheduled_for, created_at) WHERE running = false;
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE variable DROP COLUMN IF EXISTS expires_at;
@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE variable ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP WITH TIME ZONE;
UPDATE variable SET expires_at = (now() + '7 days'::interval) WHERE path LIKE 'u/%/secret_arg/%'
@@ -58,6 +58,7 @@ fn parse_bash_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
default: default.clone().map(|x| json!(x)),
otyp: None,
has_default: default.is_some(),
oidx: None,
});
} else {
break;
@@ -94,6 +95,7 @@ fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
default: default.clone(),
otyp: None,
has_default: default.is_some(),
oidx: None,
});
}
}
@@ -130,35 +132,40 @@ non_required="${5:-}"
name: "token".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "image".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "digest".to_string(),
typ: Typ::Str(None),
default: Some(json!("latest with spaces")),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "text".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "non_required".to_string(),
typ: Typ::Str(None),
default: Some(json!("")),
has_default: true
has_default: true,
oidx: None
}
],
no_main_func: None
+22 -8
View File
@@ -27,7 +27,14 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result<MainArgSignature> {
.iter()
.map(|param| {
let (otyp, typ) = parse_go_typ(&param.typ);
Arg { name: get_name(param), otyp, typ, default: None, has_default: false }
Arg {
name: get_name(param),
otyp,
typ,
default: None,
has_default: false,
oidx: None,
}
})
.collect_vec();
Ok(MainArgSignature {
@@ -179,28 +186,32 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
name: "x".to_string(),
typ: Typ::Int,
has_default: false,
default: None
default: None,
oidx: None
},
Arg {
otyp: Some("string".to_string()),
name: "y".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: Some("bool".to_string()),
name: "z".to_string(),
typ: Typ::Bool,
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: Some("[]string".to_string()),
name: "l".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: Some("struct { Name string `json:\"name\"` }".to_string()),
@@ -210,21 +221,24 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
typ: Box::new(Typ::Str(None))
},]),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: Some("interface{}".to_string()),
name: "n".to_string(),
typ: Typ::Object(vec![]),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: Some("map[string]interface{}".to_string()),
name: "m".to_string(),
typ: Typ::Object(vec![]),
default: None,
has_default: false
has_default: false,
oidx: None
},
],
no_main_func: Some(false)
@@ -51,6 +51,7 @@ fn parse_graphql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
default: parsed_default,
otyp: Some(typ.unwrap()),
has_default,
oidx: None,
});
}
@@ -93,21 +94,24 @@ query($i: Int, $arr: [String]!, $wahoo: String = "wahoo") {
name: "i".to_string(),
typ: Typ::Int,
default: None,
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: Some("[String]".to_string()),
name: "arr".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: Some("String".to_string()),
name: "wahoo".to_string(),
typ: Typ::Str(None),
default: Some(json!("wahoo")),
has_default: true
has_default: true,
oidx: None
}
],
no_main_func: None
+11 -5
View File
@@ -85,6 +85,7 @@ pub fn parse_php_signature(
typ,
has_default: default.is_some(),
default,
oidx: None,
}
})
.collect();
@@ -135,35 +136,40 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f
name: "input1".to_string(),
typ: Typ::Str(None),
has_default: true,
default: Some(Value::String("hey".to_string()))
default: Some(Value::String("hey".to_string())),
oidx: None
},
Arg {
otyp: None,
name: "input2".to_string(),
typ: Typ::Bool,
has_default: true,
default: Some(Value::Bool(false))
default: Some(Value::Bool(false)),
oidx: None
},
Arg {
otyp: None,
name: "input3".to_string(),
typ: Typ::Int,
has_default: true,
default: Some(Value::Number(Number::from(3)))
default: Some(Value::Number(Number::from(3))),
oidx: None
},
Arg {
otyp: None,
name: "input4".to_string(),
typ: Typ::Float,
has_default: true,
default: Some(Value::Number(Number::from_f64(f64::from(4.5)).unwrap()))
default: Some(Value::Number(Number::from_f64(f64::from(4.5)).unwrap())),
oidx: None
},
Arg {
otyp: None,
name: "resource".to_string(),
typ: Typ::Resource("stripe".to_string()),
has_default: false,
default: None
default: None,
oidx: None
}
],
no_main_func: Some(false)
+37 -18
View File
@@ -114,6 +114,7 @@ pub fn parse_python_signature(
typ,
has_default: default.is_some(),
default,
oidx: None,
}
})
.collect(),
@@ -271,49 +272,56 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
name: "test1".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "name".to_string(),
typ: Typ::Unknown,
default: Some(json!("<function call>")),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "byte".to_string(),
typ: Typ::Bytes,
default: Some(json!("<function call>")),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "f".to_string(),
typ: Typ::Str(None),
default: Some(json!("wewe")),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "g".to_string(),
typ: Typ::Int,
default: Some(json!(21)),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "h".to_string(),
typ: Typ::List(Box::new(Typ::Int)),
default: Some(json!([1, 2])),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "i".to_string(),
typ: Typ::Bool,
default: Some(json!(true)),
has_default: true
has_default: true,
oidx: None
},
],
no_main_func: Some(false),
@@ -352,28 +360,32 @@ def main(test1: str,
name: "test1".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "name".to_string(),
typ: Typ::Unknown,
default: Some(json!("<function call>")),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "byte".to_string(),
typ: Typ::Bytes,
default: Some(json!("<function call>")),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "resource".to_string(),
typ: Typ::Resource("postgresql".to_string()),
default: Some(json!("$res:g/all/resource")),
has_default: true
has_default: true,
oidx: None
}
],
no_main_func: Some(false),
@@ -407,28 +419,32 @@ def main(test1: str,
name: "test1".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "s3o".to_string(),
typ: Typ::Resource("S3Object".to_string()),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "name".to_string(),
typ: Typ::Str(None),
default: Some(json!("test")),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "byte".to_string(),
typ: Typ::Bytes,
default: Some(json!("<function call>")),
has_default: true
has_default: true,
oidx: None
}
],
no_main_func: Some(false),
@@ -459,7 +475,8 @@ def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): retu
name: "test1".to_string(),
typ: Typ::Str(Some(vec!["foo".to_string(), "bar".to_string()])),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
@@ -469,7 +486,8 @@ def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): retu
"bar".to_string()
])))),
default: None,
has_default: false
has_default: false,
oidx: None
}
],
no_main_func: Some(false),
@@ -499,7 +517,8 @@ def main(test1: DynSelect_foo): return
name: "test1".to_string(),
typ: Typ::DynSelect("foo".to_string()),
default: None,
has_default: false
has_default: false,
oidx: None
}],
no_main_func: Some(false),
}
+519 -34
View File
@@ -4,7 +4,11 @@ use anyhow::anyhow;
use regex::Regex;
use serde_json::json;
use std::collections::HashMap;
use std::{
collections::{HashMap, HashSet},
iter::Peekable,
str::CharIndices,
};
pub use windmill_parser::{Arg, MainArgSignature, Typ};
pub fn parse_mysql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
@@ -62,9 +66,32 @@ pub fn parse_db_resource(code: &str) -> Option<String> {
cap.map(|x| x.get(1).map(|x| x.as_str().to_string()).unwrap())
}
pub fn parse_sql_blocks(code: &str) -> Vec<&str> {
let mut blocks = vec![];
let mut last_idx = 0;
run_on_sql_statement_matches(
code,
|char, _| char == ';',
|idx, _| {
blocks.push(&code[last_idx..=idx]);
last_idx = idx + 1;
},
);
if last_idx < code.len() {
let last_block = &code[last_idx..];
if RE_NONEMPTY_SQL_BLOCK.is_match(last_block) {
blocks.push(last_block);
}
}
blocks
}
lazy_static::lazy_static! {
static ref RE_CODE_PGSQL: Regex = Regex::new(r#"(?m)\$(\d+)(?:::(\w+(?:\[\])?))?"#).unwrap();
static ref RE_NONEMPTY_SQL_BLOCK: Regex = Regex::new(r#"(?m)^\s*[^\s](?:[^-]|$)"#).unwrap();
static ref RE_DB: Regex = Regex::new(r#"(?m)^-- database (\S+) *(?:\r|\n|$)"#).unwrap();
// -- $1 name (type) = default
@@ -117,6 +144,7 @@ fn parse_mysql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
default: parsed_default,
otyp: Some(typ),
has_default,
oidx: None,
});
}
@@ -140,6 +168,7 @@ fn parse_mysql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
default: parsed_default,
otyp: Some(typ),
has_default,
oidx: None,
});
}
}
@@ -147,7 +176,105 @@ fn parse_mysql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
Ok(Some(args))
}
enum ParserState {
Normal,
InSingleQuote,
InDoubleQuote,
InSingleLineComment,
InMultiLineComment,
}
fn run_on_sql_statement_matches<
F1: FnMut(char, &mut Peekable<CharIndices>) -> bool,
F2: FnMut(usize, &mut Peekable<CharIndices>) -> (),
>(
code: &str,
mut cond: F1,
mut case: F2,
) {
let mut chars = code.char_indices().peekable();
let mut state = ParserState::Normal;
while let Some((idx, char)) = chars.next() {
match (&state, char) {
(ParserState::Normal, '\'') => {
state = ParserState::InSingleQuote;
}
(ParserState::Normal, '"') => {
state = ParserState::InDoubleQuote;
}
(ParserState::Normal, '-')
if chars.peek().is_some_and(|&(_, next_char)| next_char == '-') =>
{
state = ParserState::InSingleLineComment;
}
(ParserState::Normal, '/')
if chars.peek().is_some_and(|&(_, next_char)| next_char == '*') =>
{
state = ParserState::InMultiLineComment;
}
(ParserState::Normal, _) if cond(char, &mut chars) => {
case(idx, &mut chars);
}
(ParserState::InSingleQuote, '\'') => {
if chars
.peek()
.is_some_and(|&(_, next_char)| next_char == '\'')
{
chars.next(); // skip the escaped single quote
} else {
state = ParserState::Normal;
}
}
(ParserState::InDoubleQuote, '"') => {
if chars.peek().is_some_and(|&(_, next_char)| next_char == '"') {
chars.next(); // skip the escaped single quote
} else {
state = ParserState::Normal;
}
}
(ParserState::InSingleLineComment, '\n') => {
state = ParserState::Normal;
}
(ParserState::InMultiLineComment, '*')
if chars.peek().is_some_and(|&(_, next_char)| next_char == '/') =>
{
state = ParserState::Normal;
}
_ => {}
}
}
}
pub fn parse_pg_statement_arg_indices(code: &str) -> HashSet<i32> {
let mut arg_indices = HashSet::new();
run_on_sql_statement_matches(
code,
|char, chars| {
char == '$'
&& chars
.peek()
.is_some_and(|&(_, next_char)| next_char.is_ascii_digit())
},
|_, chars| {
let mut arg_idx = String::new();
while let Some(&(_, char)) = chars.peek() {
if char.is_ascii_digit() {
arg_idx.push(char);
chars.next();
} else {
break;
}
}
if let Ok(arg_idx) = arg_idx.parse::<i32>() {
arg_indices.insert(arg_idx);
}
},
);
arg_indices
}
fn parse_pg_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut args = vec![];
let mut hm: HashMap<i32, String> = HashMap::new();
for cap in RE_CODE_PGSQL.captures_iter(code) {
hm.insert(
@@ -159,39 +286,74 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
.unwrap_or_else(|| "text".to_string()),
);
}
let mut args = vec![];
for i in 1..50 {
if hm.contains_key(&i) {
let typ = hm.get(&i).unwrap().to_lowercase();
args.push(Arg {
name: format!("${}", i),
typ: parse_pg_typ(typ.as_str()),
default: None,
otyp: Some(typ),
has_default: false,
});
} else {
break;
}
for (i, v) in hm.iter() {
let typ = v.to_lowercase();
args.push(Arg {
name: format!("${}", i),
typ: parse_pg_typ(typ.as_str()),
default: None,
otyp: Some(typ),
has_default: false,
oidx: Some(*i),
});
}
args.sort_by(|a, b| a.oidx.unwrap().cmp(&b.oidx.unwrap()));
for cap in RE_ARG_PGSQL.captures_iter(code) {
let i = cap.get(1).and_then(|x| x.as_str().parse::<i32>().ok());
if i.is_none() || i.unwrap() as usize > args.len() {
continue;
}
let name = cap.get(2).map(|x| x.as_str().to_string()).unwrap();
let default = cap.get(3).map(|x| x.as_str().to_string());
let has_default = default.is_some();
let oarg = args[(i.unwrap() - 1) as usize].clone();
let parsed_default = default.and_then(|x| parsed_default(&oarg.typ, x));
let i = cap
.get(1)
.and_then(|x| x.as_str().parse::<i32>().ok())
.map(|x| x);
args[(i.unwrap() - 1) as usize] =
Arg { name, typ: oarg.typ, default: parsed_default, otyp: oarg.otyp, has_default };
if let Some(arg_pos) = args
.iter()
.position(|x| i.is_some_and(|i| x.oidx.unwrap() == i))
{
let name = cap.get(2).map(|x| x.as_str().to_string()).unwrap();
let default = cap.get(3).map(|x| x.as_str().to_string());
let has_default = default.is_some();
let oarg = args[arg_pos].clone();
let parsed_default = default.and_then(|x| parsed_default(&oarg.typ, x));
args[arg_pos] = Arg {
name,
typ: oarg.typ,
default: parsed_default,
otyp: oarg.otyp,
has_default,
oidx: oarg.oidx,
};
}
}
Ok(Some(args))
}
pub fn parse_sql_statement_named_params(code: &str, prefix: char) -> HashSet<String> {
let mut arg_names = HashSet::new();
run_on_sql_statement_matches(
code,
|char, chars| {
char == prefix
&& chars
.peek()
.is_some_and(|&(_, next_char)| next_char.is_alphanumeric())
},
|_, chars| {
let mut arg_name = String::new();
while let Some(&(_, char)) = chars.peek() {
if char.is_alphanumeric() {
arg_name.push(char);
chars.next();
} else {
break;
}
}
arg_names.insert(arg_name);
},
);
arg_names
}
fn parse_bigquery_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut args: Vec<Arg> = vec![];
@@ -213,6 +375,7 @@ fn parse_bigquery_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
default: parsed_default,
otyp: Some(typ),
has_default,
oidx: None,
});
}
@@ -240,6 +403,7 @@ fn parse_snowflake_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
default: parsed_default,
otyp: Some(typ),
has_default,
oidx: None,
});
}
@@ -267,6 +431,7 @@ fn parse_mssql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
default: parsed_default,
otyp: Some(typ),
has_default,
oidx: None,
});
}
@@ -372,7 +537,7 @@ mod tests {
use super::*;
#[test]
fn test_parse_sql_sig() -> anyhow::Result<()> {
fn test_parse_pgsql_sig() -> anyhow::Result<()> {
let code = r#"
SELECT * FROM table WHERE token=$1::TEXT AND image=$2::BIGINT
"#;
@@ -388,14 +553,237 @@ SELECT * FROM table WHERE token=$1::TEXT AND image=$2::BIGINT
name: "$1".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: Some(1),
},
Arg {
otyp: Some("bigint".to_string()),
name: "$2".to_string(),
typ: Typ::Int,
default: None,
has_default: false
has_default: false,
oidx: Some(2),
},
],
no_main_func: None
}
);
Ok(())
}
#[test]
fn test_parse_pgsql_mutli_sig() -> anyhow::Result<()> {
let code = r#"
-- $1 param1
-- $2 param2
-- $3 param3
SELECT $3::TEXT, $1::BIGINT;
SELECT $2::TEXT;
"#;
//println!("{}", serde_json::to_string()?);
assert_eq!(
parse_pgsql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: Some("bigint".to_string()),
name: "param1".to_string(),
typ: Typ::Int,
default: None,
has_default: false,
oidx: Some(1),
},
Arg {
otyp: Some("text".to_string()),
name: "param2".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: Some(2),
},
Arg {
otyp: Some("text".to_string()),
name: "param3".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: Some(3),
},
],
no_main_func: None
}
);
Ok(())
}
#[test]
fn test_parse_sql_blocks_multi_2semi() -> anyhow::Result<()> {
let code = r#"
-- $1 param1
-- $2 param2
-- $3 param3
SELECT '--', ';' $3::TEXT, $1::BIGINT;
-- ;
SELECT $2::TEXT;
"#;
assert_eq!(parse_sql_blocks(code).len(), 2);
Ok(())
}
#[test]
fn test_parse_sql_blocks_multi_1semi() -> anyhow::Result<()> {
let code = r#"
-- $1 param1
-- $2 param2
-- $3 param3
SELECT '--', ';' $3::TEXT, $1::BIGINT;
-- ;
SELECT $2::TEXT
"#;
assert_eq!(
parse_sql_blocks(code),
vec![
r#"
-- $1 param1
-- $2 param2
-- $3 param3
SELECT '--', ';' $3::TEXT, $1::BIGINT;"#,
r#"
-- ;
SELECT $2::TEXT
"#
]
);
Ok(())
}
#[test]
fn test_parse_sql_blocks_single_1semi() -> anyhow::Result<()> {
let code = r#"
-- $1 param1
-- $2 param2
-- $3 param3
SELECT '--', ';' $3::TEXT, $1::BIGINT;
-- hey
"#;
assert_eq!(
parse_sql_blocks(code),
vec![
r#"
-- $1 param1
-- $2 param2
-- $3 param3
SELECT '--', ';' $3::TEXT, $1::BIGINT;"#,
]
);
Ok(())
}
#[test]
fn test_parse_sql_blocks_single_nosemi() -> anyhow::Result<()> {
let code = r#"
-- $1 param1
-- $2 param2
-- $3 param3
SELECT '--', ';' $3::TEXT, $1::BIGINT
"#;
assert_eq!(
parse_sql_blocks(code),
vec![
r#"
-- $1 param1
-- $2 param2
-- $3 param3
SELECT '--', ';' $3::TEXT, $1::BIGINT
"#
]
);
Ok(())
}
#[test]
fn test_parse_mysql_positional_sig() -> anyhow::Result<()> {
let code = r#"
-- ? param1 (int) = 3
-- ? param2 (text)
SELECT ?, ?;
"#;
assert_eq!(
parse_mysql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: Some("int".to_string()),
name: "param1".to_string(),
typ: Typ::Int,
default: Some(json!(3)),
has_default: true,
oidx: None,
},
Arg {
otyp: Some("text".to_string()),
name: "param2".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: None
}
);
Ok(())
}
#[test]
fn test_parse_mysql_sig() -> anyhow::Result<()> {
let code = r#"
-- :param1 (int) = 3
-- :param2 (text)
-- :param3 (text)
SELECT :param3, :param1;
SELECT :param2;
"#;
assert_eq!(
parse_mysql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: Some("int".to_string()),
name: "param1".to_string(),
typ: Typ::Int,
default: Some(json!(3)),
has_default: true,
oidx: None,
},
Arg {
otyp: Some("text".to_string()),
name: "param2".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
Arg {
otyp: Some("text".to_string()),
name: "param3".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: None
@@ -408,9 +796,10 @@ SELECT * FROM table WHERE token=$1::TEXT AND image=$2::BIGINT
#[test]
fn test_parse_bigquery_sig() -> anyhow::Result<()> {
let code = r#"
-- @token (string)
-- @token (string) = abc
-- @image (int64)
SELECT * FROM table WHERE token=@token AND image=@image
SELECT * FROM table WHERE token=@token AND image=@image;
SELECT @token;
"#;
//println!("{}", serde_json::to_string()?);
assert_eq!(
@@ -423,15 +812,111 @@ SELECT * FROM table WHERE token=@token AND image=@image
otyp: Some("string".to_string()),
name: "token".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
default: Some(json!("abc")),
has_default: true,
oidx: None,
},
Arg {
otyp: Some("int64".to_string()),
name: "image".to_string(),
typ: Typ::Int,
default: None,
has_default: false
has_default: false,
oidx: None,
},
],
no_main_func: None
}
);
Ok(())
}
#[test]
fn test_parse_snowflake_sig() -> anyhow::Result<()> {
let code = r#"
-- ? param1 (int) = 3
-- ? param2 (varchar)
SELECT ?, ?;
-- ? param3 (varchar)
SELECT ?;
"#;
assert_eq!(
parse_snowflake_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: Some("int".to_string()),
name: "param1".to_string(),
typ: Typ::Int,
default: Some(json!(3)),
has_default: true,
oidx: None,
},
Arg {
otyp: Some("varchar".to_string()),
name: "param2".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
Arg {
otyp: Some("varchar".to_string()),
name: "param3".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
}
],
no_main_func: None
}
);
Ok(())
}
#[test]
fn test_parse_mssql_sig() -> anyhow::Result<()> {
let code = r#"
-- @p1 param1 (int) = 3
-- @p2 param2 (varchar)
-- @p3 param3 (varchar)
SELECT @p3, @p1;
SELECT @p2;
"#;
assert_eq!(
parse_mssql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: Some("int".to_string()),
name: "param1".to_string(),
typ: Typ::Int,
default: Some(json!(3)),
has_default: true,
oidx: None,
},
Arg {
otyp: Some("varchar".to_string()),
name: "param2".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
Arg {
otyp: Some("varchar".to_string()),
name: "param3".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: None
@@ -204,6 +204,7 @@ fn parse_param(
typ,
default: None,
has_default: ident.id.optional || nullable,
oidx: None,
})
}
// Pat::Object(ObjectPat { ... }) = todo!()
@@ -250,13 +251,13 @@ fn parse_param(
if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap());
}
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true })
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
}
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(&type_ann);
*counter += 1;
let name = format!("anon{}", counter);
Ok(Arg { otyp: None, name, typ, default: None, has_default: nullable })
Ok(Arg { otyp: None, name, typ, default: None, has_default: nullable, oidx: None })
}
_ => Err(anyhow::anyhow!(
"parameter syntax unsupported: `{}`: {:#?}",
@@ -3,7 +3,7 @@
"collaborators": [
"Ruben Fiszel <ruben@windmill.dev>"
],
"version": "1.364.4",
"version": "1.367.2",
"files": [
"windmill_parser_wasm_bg.wasm",
"windmill_parser_wasm.js",
@@ -14,4 +14,4 @@
"sideEffects": [
"./snippets/*"
]
}
}
@@ -6,6 +6,20 @@ heap.push(undefined, null, true, false);
function getObject(idx) { return heap[idx]; }
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let WASM_VECTOR_LEN = 0;
let cachedUint8Memory0 = null;
@@ -84,20 +98,6 @@ function getInt32Memory0() {
return cachedInt32Memory0;
}
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let cachedFloat64Memory0 = null;
function getFloat64Memory0() {
@@ -585,6 +585,13 @@ async function __wbg_load(module, imports) {
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_eval_aa725d466edcea2c = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'string' ? obj : undefined;
@@ -593,13 +600,6 @@ function __wbg_get_imports() {
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbg_eval_2ea6d5f9a10f336a = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_boolean_get = function(arg0) {
const v = getObject(arg0);
const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2;
@@ -28,77 +28,88 @@ export function main(test1?: string, test2: string = \"burkina\",
name: "test1".to_string(),
typ: Typ::Str(None),
default: None,
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "test2".to_string(),
typ: Typ::Str(None),
default: Some(json!("burkina")),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "test3".to_string(),
typ: Typ::Resource("postgres".to_string()),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "b64".to_string(),
typ: Typ::Bytes,
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "ls".to_string(),
typ: Typ::List(Box::new(Typ::Bytes)),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "email".to_string(),
typ: Typ::Email,
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "literal".to_string(),
typ: Typ::Str(Some(vec!["test".to_string()])),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "literal_union".to_string(),
typ: Typ::Str(Some(vec!["test".to_string(), "test2".to_string()])),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "opt_type".to_string(),
typ: Typ::Str(None),
default: None,
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "opt_type_union".to_string(),
typ: Typ::Str(None),
default: None,
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "opt_type_union_union2".to_string(),
typ: Typ::Str(None),
default: None,
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
@@ -108,21 +119,24 @@ export function main(test1?: string, test2: string = \"burkina\",
ObjectProperty { key: "b".to_string(), typ: Box::new(Typ::Float) }
]),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "literals_with_undefined".to_string(),
typ: Typ::Str(Some(vec!["foo".to_string(), "bar".to_string()])),
default: None,
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "dyn_select".to_string(),
typ: Typ::DynSelect("foo".to_string()),
default: None,
has_default: false
has_default: false,
oidx: None
}
],
no_main_func: Some(false)
@@ -154,35 +168,40 @@ export function main(test2 = \"burkina\",
name: "test2".to_string(),
typ: Typ::Str(None),
default: Some(json!("burkina")),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "bool".to_string(),
typ: Typ::Bool,
default: Some(json!(true)),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "float".to_string(),
typ: Typ::Float,
default: Some(json!(4.2)),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "int".to_string(),
typ: Typ::Int,
default: Some(json!(42)),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "ls".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: Some(json!(["test"])),
has_default: true
has_default: true,
oidx: None
},
Arg {
otyp: None,
@@ -192,7 +211,8 @@ export function main(test2 = \"burkina\",
ObjectProperty { key: "b".to_string(), typ: Box::new(Typ::Int) }
]),
default: Some(json!({"a": "test", "b": 42})),
has_default: true
has_default: true,
oidx: None
}
],
no_main_func: Some(false)
@@ -224,21 +244,24 @@ export function main(foo: FooBar, {a, b}: FooBar, {c, d}: FooBar = {a: \"foo\",
otyp: None,
typ: Typ::Resource("foo_bar".to_string()),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
name: "anon1".to_string(),
otyp: None,
typ: Typ::Resource("foo_bar".to_string()),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
name: "anon2".to_string(),
otyp: None,
typ: Typ::Resource("foo_bar".to_string()),
default: Some(json!({"a": "foo", "b": 42})),
has_default: true
has_default: true,
oidx: None
}
],
no_main_func: Some(false)
@@ -268,7 +291,8 @@ export function main(foo: (\"foo\" | \"bar\")[]) {
"bar".to_string()
])))),
default: None,
has_default: false
has_default: false,
oidx: None
}],
no_main_func: Some(false)
}
@@ -349,56 +373,64 @@ Write-Output 'Testing...'
name: "test_none".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "test_string".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "test_int".to_string(),
typ: Typ::Int,
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "test_decimal".to_string(),
typ: Typ::Float,
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "test_double".to_string(),
typ: Typ::Float,
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "test_single".to_string(),
typ: Typ::Float,
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "test_datetime_lower".to_string(),
typ: Typ::Datetime,
default: None,
has_default: false
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "test_datetime_upper".to_string(),
typ: Typ::Datetime,
default: None,
has_default: false
has_default: false,
oidx: None
}
],
no_main_func: None,
+1 -1
View File
@@ -9,5 +9,5 @@ name = "windmill_parser"
path = "./src/lib.rs"
[dependencies]
serde.workspace = true
serde = { version = "1.0", features = ["derive"] }
serde_json.workspace = true
@@ -57,6 +57,7 @@ pub struct Arg {
pub typ: Typ,
pub default: Option<serde_json::Value>,
pub has_default: bool,
pub oidx: Option<i32>,
}
pub fn json_to_typ(js: &Value) -> Typ {
@@ -16,5 +16,4 @@ wasm-bindgen-test.workspace = true
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql.workspace = true
wasm-bindgen.workspace = true
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen.workspace = true
+53 -36
View File
@@ -23,11 +23,11 @@ use windmill_common::ee::schedule_key_renewal;
use windmill_common::{
global_settings::{
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_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,
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,
},
@@ -54,18 +54,18 @@ use windmill_common::METRICS_ADDR;
use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING;
use windmill_worker::{
BUN_CACHE_DIR, BUN_TAR_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM,
GO_BIN_CACHE_DIR, GO_CACHE_DIR, HUB_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR,
POWERSHELL_CACHE_DIR, TAR_PIP_CACHE_DIR, TMP_LOGS_DIR,
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, HUB_CACHE_DIR,
LOCK_CACHE_DIR, PIP_CACHE_DIR, POWERSHELL_CACHE_DIR, TAR_PIP_CACHE_DIR, TMP_LOGS_DIR,
};
use crate::monitor::{
initial_load, load_keep_job_dir, load_metrics_debug_enabled, load_require_preexisting_user,
load_tag_per_workspace_enabled, 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,
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_retention_period_setting, reload_scim_token_setting, reload_server_config,
reload_worker_config,
};
@@ -365,28 +365,35 @@ Windmill Community Edition {GIT_VERSION}
#[cfg(feature = "tantivy")]
let should_index_jobs =
mode == Mode::Indexer || (enable_standalone_indexer && mode == Mode::Standalone);
#[cfg(not(feature = "tantivy"))]
let should_index_jobs = false;
mode == Mode::Indexer || (enable_standalone_indexer && mode == Mode::Standalone);
#[cfg(feature = "tantivy")]
let (index_reader, index_writer) = if should_index_jobs {
let (r, w) = windmill_indexer::indexer_ee::init_index()?;
let (r, w) = windmill_indexer::indexer_ee::init_index().await?;
(Some(r), Some(w))
} else {
(None, None)
};
let indexer_rx = killpill_rx.resubscribe();
let index_writer2 = index_writer.clone();
let indexer_f = async {
if let Some(index_writer) = index_writer2 {
windmill_indexer::indexer_ee::run_indexer(db.clone(), index_writer, indexer_rx)
.await;
#[cfg(feature = "tantivy")]
let indexer_f = {
let indexer_rx = killpill_rx.resubscribe();
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;
}
Ok(())
}
Ok(())
};
#[cfg(not(feature = "tantivy"))]
let (index_reader, index_writer) = (None, None);
#[cfg(not(feature = "tantivy"))]
let indexer_f = async { Ok(()) as anyhow::Result<()> };
let server_f = async {
if !is_agent {
windmill_api::run_server(
@@ -504,9 +511,14 @@ Windmill Community Edition {GIT_VERSION}
},
DEFAULT_TAGS_PER_WORKSPACE_SETTING => {
if let Err(e) = load_tag_per_workspace_enabled(&db).await {
tracing::error!("Error loading default tag per workpsace: {e:#}");
tracing::error!("Error loading default tag per workspace: {e:#}");
}
},
DEFAULT_TAGS_WORKSPACES_SETTING => {
if let Err(e) = load_tag_per_workspace_workspaces(&db).await {
tracing::error!("Error loading default tag per workspace workspaces: {e:#}");
}
}
RETENTION_PERIOD_SECS_SETTING => {
reload_retention_period_setting(&db).await
},
@@ -539,14 +551,12 @@ Windmill Community Edition {GIT_VERSION}
load_require_preexisting_user(&db).await;
},
EXPOSE_METRICS_SETTING => {
if worker_mode {
tracing::info!("Metrics setting changed, restarting");
// we wait a bit randomly to avoid having all serverss and workers shutdown at same time
let rd_delay = rand::thread_rng().gen_range(0..4);
tokio::time::sleep(Duration::from_secs(rd_delay)).await;
if let Err(e) = tx.send(()) {
tracing::error!(error = %e, "Could not send killpill to server");
}
tracing::info!("Metrics setting changed, restarting");
// we wait a bit randomly to avoid having all servers and workers shutdown at same time
let rd_delay = rand::thread_rng().gen_range(0..40);
tokio::time::sleep(Duration::from_secs(rd_delay)).await;
if let Err(e) = tx.send(()) {
tracing::error!(error = %e, "Could not send killpill to server");
}
},
EXPOSE_DEBUG_METRICS_SETTING => {
@@ -766,7 +776,8 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
DENO_CACHE_DIR_DEPS,
DENO_CACHE_DIR_NPM,
BUN_CACHE_DIR,
BUN_TAR_CACHE_DIR,
BUN_DEPSTAR_CACHE_DIR,
BUN_BUNDLE_CACHE_DIR,
GO_CACHE_DIR,
GO_BIN_CACHE_DIR,
HUB_CACHE_DIR,
@@ -779,6 +790,10 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
.expect("could not create initial worker dir");
}
tracing::info!(
"Starting {num_workers} workers and SLEEP_QUEUE={}ms",
*windmill_worker::SLEEP_QUEUE
);
for i in 1..(num_workers + 1) {
let db1 = db.clone();
let instance_name = instance_name.clone();
@@ -791,7 +806,9 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
let hostname = hostname.clone();
handles.push(tokio::spawn(async move {
tracing::info!(worker = %worker_name, "starting worker");
if num_workers > 1 {
tracing::info!(worker = %worker_name, "starting worker {i}");
}
let f = windmill_worker::run_worker(
&db1,
+61 -9
View File
@@ -28,10 +28,11 @@ use windmill_common::{
flow_status::FlowStatusModule,
global_settings::{
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
DEFAULT_TAGS_PER_WORKSPACE_SETTING, 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,
DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING,
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,
},
@@ -41,10 +42,10 @@ use windmill_common::{
users::truncate_token,
utils::{now_from_db, rd_string},
worker::{
load_worker_config, reload_custom_tags_setting, DEFAULT_TAGS_PER_WORKSPACE, SERVER_CONFIG,
WORKER_CONFIG,
load_worker_config, make_pull_query, make_suspended_pull_query, reload_custom_tags_setting,
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SERVER_CONFIG, WORKER_CONFIG,
},
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL,
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS,
METRICS_DEBUG_ENABLED, METRICS_ENABLED,
};
use windmill_queue::cancel_job;
@@ -104,8 +105,6 @@ lazy_static::lazy_static! {
"Number of jobs in the queue",
&["tag"]
).unwrap();
static ref JOB_RETENTION_SECS: Arc<RwLock<i64>> = Arc::new(RwLock::new(0));
}
pub async fn initial_load(
@@ -127,6 +126,10 @@ pub async fn initial_load(
tracing::error!("Error loading default tag per workpsace: {e:#}");
}
if let Err(e) = load_tag_per_workspace_workspaces(db).await {
tracing::error!("Error loading default tag per workpsace workspaces: {e:#}");
}
if server_mode {
load_require_preexisting_user(db).await;
}
@@ -204,6 +207,28 @@ pub async fn load_tag_per_workspace_enabled(db: &DB) -> error::Result<()> {
Ok(())
}
pub async fn load_tag_per_workspace_workspaces(db: &DB) -> error::Result<()> {
let workspaces = load_value_from_global_settings(db, DEFAULT_TAGS_WORKSPACES_SETTING).await;
match workspaces {
Ok(Some(serde_json::Value::Array(t))) => {
let workspaces = t
.iter()
.filter_map(|x| x.as_str())
.map(|x| x.to_string())
.collect::<Vec<String>>();
let mut w = DEFAULT_TAGS_WORKSPACES.write().await;
*w = Some(workspaces);
}
Ok(None) => {
let mut w = DEFAULT_TAGS_WORKSPACES.write().await;
*w = None;
}
_ => (),
};
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 {
@@ -381,6 +406,21 @@ pub async fn delete_expired_items(db: &DB) -> () {
Err(e) => tracing::error!("Error deleting cache resource {}", e.to_string()),
}
let deleted_expired_variables = sqlx::query_scalar!(
"DELETE FROM variable WHERE expires_at IS NOT NULL AND expires_at > now() RETURNING path",
)
.fetch_all(db)
.await;
match deleted_expired_variables {
Ok(res) => {
if res.len() > 0 {
tracing::info!("deleted {} expired variables {:?}", res.len(), res)
}
}
Err(e) => tracing::error!("Error deleting cache resource {}", e.to_string()),
}
let job_retention_secs = *JOB_RETENTION_SECS.read().await;
if job_retention_secs > 0 {
match db.begin().await {
@@ -650,6 +690,16 @@ pub async fn reload_option_setting<T: FromStr + DeserializeOwned>(
std_env_var: &str,
lock: Arc<RwLock<Option<T>>>,
) -> error::Result<()> {
let force_value = std::env::var(format!("FORCE_{}", std_env_var))
.ok()
.and_then(|x| x.parse::<T>().ok());
if let Some(force_value) = force_value {
let mut l = lock.write().await;
*l = Some(force_value);
return Ok(());
}
let q = load_value_from_global_settings(db, setting_name).await?;
let mut value = std::env::var(std_env_var)
@@ -913,6 +963,8 @@ pub async fn reload_worker_config(
let mut wc = WORKER_CONFIG.write().await;
tracing::info!("Reloading worker config...");
make_suspended_pull_query(&config).await;
make_pull_query(&config).await;
*wc = config
}
}
+1 -1
View File
@@ -71,7 +71,7 @@ if [ "$REVERT" == "YES" ]; then
ce_file="${ee_file/${EE_CODE_DIR}/.}"
ce_file="${root_dirpath}/backend/${ce_file}"
if [ "$REVERT_PREVIOUS" == "YES" ]; then
git checkout HEAD@{9} ${ce_file} || true
git checkout HEAD@{3} ${ce_file} || true
else
git restore --staged ${ce_file} || true
git restore ${ce_file} || true
+7 -3
View File
@@ -22,7 +22,7 @@ use windmill_api_client::types::{EditSchedule, NewSchedule, ScriptArgs};
use serde::Serialize;
use windmill_common::auth::JWT_SECRET;
use windmill_common::worker::{PriorityTags, WORKER_CONFIG};
use windmill_common::worker::WORKER_CONFIG;
use windmill_common::{
flow_status::{FlowStatus, FlowStatusModule, RestartedFrom},
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
@@ -1007,8 +1007,12 @@ fn spawn_test_worker(
{
let mut wc = WORKER_CONFIG.write().await;
(*wc).worker_tags = windmill_common::worker::DEFAULT_TAGS.clone();
(*wc).priority_tags_sorted =
vec![PriorityTags { priority: 0, tags: (*wc).worker_tags.clone() }]
(*wc).priority_tags_sorted = vec![windmill_common::worker::PriorityTags {
priority: 0,
tags: (*wc).worker_tags.clone(),
}];
windmill_common::worker::make_suspended_pull_query(&wc).await;
windmill_common::worker::make_pull_query(&wc).await;
}
windmill_worker::run_worker::<rsmq_async::MultiplexedRsmq>(
&db,
+2 -1
View File
@@ -18,6 +18,7 @@ embedding = ["dep:tinyvector", "dep:hf-hub", "dep:tokenizers", "dep:candle-core"
parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/parquet"]
prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus"]
openidconnect = ["dep:openidconnect"]
tantivy = ["dep:windmill-indexer"]
[dependencies]
windmill-queue.workspace = true
@@ -27,7 +28,7 @@ windmill-parser.workspace = true
windmill-parser-py-imports.workspace = true
windmill-parser-ts.workspace = true
windmill-git-sync.workspace = true
windmill-indexer.workspace = true
windmill-indexer = { workspace = true, optional = true }
tokio.workspace = true
anyhow.workspace = true
argon2.workspace = true
+9 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.366.3
version: 1.372.0
title: Windmill API
contact:
@@ -2444,6 +2444,8 @@ paths:
in: query
schema:
type: string
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: variable list
@@ -9770,6 +9772,9 @@ components:
type: boolean
is_refreshed:
type: boolean
expires_at:
type: string
format: date-time
required:
- workspace_id
- path
@@ -9808,6 +9813,9 @@ components:
type: integer
is_oauth:
type: boolean
expires_at:
type: string
format: date-time
required:
- path
- value
+154 -8
View File
@@ -249,14 +249,57 @@ async fn fix_flow_versioning_migration(
}
async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
// let has_done_migration = sqlx::query_scalar!(
// "SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = 'fix_job_completed_index')"
// )
// .fetch_one(db)
// .await?
// .unwrap_or(false);
// if !has_done_migration {
// tracing::info!("Applying fix_job_completed_index migration");
// let mut tx = db.begin().await?;
// let mut r = false;
// while !r {
// r = sqlx::query_scalar!("SELECT pg_try_advisory_lock(4242)")
// .fetch_one(&mut *tx)
// .await
// .map_err(|e| {
// tracing::error!("Error acquiring fix_job_completed_index lock: {e:#}");
// sqlx::migrate::MigrateError::Execute(e)
// })?
// .unwrap_or(false);
// if !r {
// tracing::info!("PG fix_job_completed_index_migration lock already acquired by another server or worker, retrying in 5s. (look for the advisory lock in pg_lock with granted = true)");
// tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// }
// }
// // sqlx::query(
// // "CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_completed_job_workspace_id_created_at_new ON completed_job (workspace_id, job_kind, is_skipped, is_flow_step, created_at DESC, started_at DESC)"
// // ).execute(db).await?;
// sqlx::query("DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at")
// .execute(db)
// .await?;
// sqlx::query!("INSERT INTO windmill_migrations (name) VALUES ('fix_job_completed_index') ON CONFLICT DO NOTHING")
// .execute(&mut *tx)
// .await?;
// let _ = sqlx::query("SELECT pg_advisory_unlock(4242)")
// .execute(&mut *tx)
// .await?;
// tx.commit().await?;
// }
let migration_job_name = "fix_job_completed_index_2";
let has_done_migration = sqlx::query_scalar!(
"SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = 'fix_job_completed_index')"
"SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = $1)",
migration_job_name
)
.fetch_one(db)
.await?
.unwrap_or(false);
if !has_done_migration {
tracing::info!("Applying fix_job_completed_index migration");
tracing::info!("Applying {migration_job_name} migration");
let mut tx = db.begin().await?;
let mut r = false;
while !r {
@@ -264,30 +307,133 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
.fetch_one(&mut *tx)
.await
.map_err(|e| {
tracing::error!("Error acquiring fix_job_completed_index lock: {e:#}");
tracing::error!("Error acquiring {migration_job_name} lock: {e:#}");
sqlx::migrate::MigrateError::Execute(e)
})?
.unwrap_or(false);
if !r {
tracing::info!("PG fix_job_completed_index_migration lock already acquired by another server or worker, retrying in 5s. (look for the advisory lock in pg_lock with granted = true)");
tracing::info!("PG {migration_job_name} lock already acquired by another server or worker, retrying in 5s. (look for the advisory lock in pg_lock with granted = true)");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}
sqlx::query(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_completed_job_workspace_id_created_at_new ON completed_job (workspace_id, job_kind, is_skipped, is_flow_step, created_at DESC, started_at DESC)"
tracing::info!("acquired lock for {migration_job_name}");
let has_done_migration = sqlx::query_scalar!(
"SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = $1)",
migration_job_name
)
.fetch_one(db)
.await?
.unwrap_or(false);
if !has_done_migration {
sqlx::query(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_completed_job_workspace_id_created_at_new_2 ON completed_job (workspace_id, job_kind, success, is_skipped, is_flow_step, created_at DESC)"
).execute(db).await?;
sqlx::query("DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at")
sqlx::query(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_completed_job_workspace_id_started_at_new ON completed_job (workspace_id, job_kind, success, is_skipped, is_flow_step, started_at DESC)"
).execute(db).await?;
sqlx::query(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at",
)
.execute(db)
.await?;
sqlx::query!("INSERT INTO windmill_migrations (name) VALUES ('fix_job_completed_index') ON CONFLICT DO NOTHING")
sqlx::query(
"DROP INDEX CONCURRENTLY IF EXISTS ix_completed_job_workspace_id_created_at_new",
)
.execute(db)
.await?;
sqlx::query!(
"INSERT INTO windmill_migrations (name) VALUES ($1) ON CONFLICT DO NOTHING",
migration_job_name
)
.execute(&mut *tx)
.await?;
tracing::info!("Finished applying {migration_job_name} migration");
} else {
tracing::info!("migration {migration_job_name} already done");
}
let _ = sqlx::query("SELECT pg_advisory_unlock(4242)")
.execute(&mut *tx)
.await?;
tx.commit().await?;
tracing::info!("released lock for {migration_job_name}");
}
let migration_job_name = "fix_job_completed_index_3";
let has_done_migration = sqlx::query_scalar!(
"SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = $1)",
migration_job_name
)
.fetch_one(db)
.await?
.unwrap_or(false);
if !has_done_migration {
tracing::info!("Applying {migration_job_name} migration");
let mut tx = db.begin().await?;
let mut r = false;
while !r {
r = sqlx::query_scalar!("SELECT pg_try_advisory_lock(4242)")
.fetch_one(&mut *tx)
.await
.map_err(|e| {
tracing::error!("Error acquiring {migration_job_name} lock: {e:#}");
sqlx::migrate::MigrateError::Execute(e)
})?
.unwrap_or(false);
if !r {
tracing::info!("PG {migration_job_name} lock already acquired by another server or worker, retrying in 5s. (look for the advisory lock in pg_lock with granted = true)");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}
tracing::info!("acquired lock for {migration_job_name}");
let has_done_migration = sqlx::query_scalar!(
"SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = $1)",
migration_job_name
)
.fetch_one(db)
.await?
.unwrap_or(false);
if !has_done_migration {
sqlx::query("DROP INDEX CONCURRENTLY IF EXISTS index_completed_job_on_schedule_path")
.execute(db)
.await?;
sqlx::query("DROP INDEX CONCURRENTLY IF EXISTS concurrency_limit_stats_queue")
.execute(db)
.await?;
sqlx::query("DROP INDEX CONCURRENTLY IF EXISTS root_job_index")
.execute(db)
.await?;
sqlx::query("DROP INDEX CONCURRENTLY IF EXISTS index_completed_on_created")
.execute(db)
.await?;
sqlx::query!(
"INSERT INTO windmill_migrations (name) VALUES ($1) ON CONFLICT DO NOTHING",
migration_job_name
)
.execute(&mut *tx)
.await?;
tracing::info!("Finished applying {migration_job_name} migration");
} else {
tracing::info!("migration {migration_job_name} already done");
}
let _ = sqlx::query("SELECT pg_advisory_unlock(4242)")
.execute(&mut *tx)
.await?;
tx.commit().await?;
tracing::info!("released lock for {migration_job_name}");
}
Ok(())
+18 -2
View File
@@ -1,4 +1,8 @@
use anyhow::anyhow;
#[cfg(feature = "enterprise")]
use std::sync::Arc;
#[cfg(feature = "enterprise")]
use tokio::sync::RwLock;
pub async fn validate_license_key(_license_key: String) -> anyhow::Result<String> {
// Implementation is not open source
@@ -9,8 +13,20 @@ pub async fn validate_license_key(_license_key: String) -> anyhow::Result<String
pub async fn jwt_ext_auth(
_w_id: Option<&String>,
_token: &str,
) -> Option<(crate::db::ApiAuthed, usize)> {
_external_jwks: Option<Arc<RwLock<ExternalJwks>>>,
) -> anyhow::Result<(crate::db::ApiAuthed, usize)> {
// Implementation is not open source
None
Err(anyhow!("External JWT auth is not open source"))
}
#[cfg(feature = "enterprise")]
pub struct ExternalJwks;
#[cfg(feature = "enterprise")]
impl ExternalJwks {
pub async fn load() -> Option<Arc<RwLock<Self>>> {
// Implementation is not open source
None
}
}
+122 -10
View File
@@ -8,6 +8,7 @@
use axum::body::Body;
use axum::http::HeaderValue;
use quick_cache::sync::Cache;
use serde_json::value::RawValue;
use sqlx::Pool;
use std::collections::HashMap;
@@ -297,11 +298,17 @@ struct JsonPath {
pub approver: Option<String>,
}
async fn get_result_by_id(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>,
Query(JsonPath { json_path, .. }): Query<JsonPath>,
) -> windmill_common::error::JsonResult<Box<JsonRawValue>> {
let res = windmill_queue::get_result_by_id(db, w_id, flow_id, node_id, json_path).await?;
let res =
windmill_queue::get_result_by_id(db.clone(), w_id.clone(), flow_id, node_id, json_path)
.await?;
log_job_view(&db, Some(&authed), &w_id, &flow_id).await?;
Ok(Json(res))
}
@@ -622,6 +629,9 @@ async fn get_flow_job_debug_info(
jobs.insert(job.id().to_string(), job);
}
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(jobs).into_response())
} else {
Err(error::Error::NotFound(format!(
@@ -651,6 +661,9 @@ async fn get_job(
)
.await?;
job.fetch_outstanding_wait_time(&db).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(job).into_response())
}
@@ -871,6 +884,9 @@ async fn get_job_logs(
));
}
let logs = record.logs.unwrap_or_default();
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(r) = get_logs_from_store(record.log_offset, &logs, &record.log_file_index).await
{
@@ -900,6 +916,9 @@ async fn get_job_logs(
));
}
let logs = text.logs.unwrap_or_default();
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(r) = get_logs_from_store(text.log_offset, &logs, &text.log_file_index).await {
return r.map(content_plain);
@@ -938,6 +957,9 @@ async fn get_args(
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
));
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(record.args.map(|x| x.0).unwrap_or_default()))
} else {
let record = sqlx::query_as::<_, RawArgs>(
@@ -955,6 +977,9 @@ async fn get_args(
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
));
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(record.args.map(|x| x.0).unwrap_or_default()))
}
}
@@ -1368,7 +1393,7 @@ async fn cancel_jobs(
sqlx::query!(
"DELETE FROM queue WHERE id = any($1) AND workspace_id = $2",
&jobs,
&trivial_jobs,
w_id
)
.execute(&mut *tx)
@@ -1944,7 +1969,7 @@ pub async fn get_suspended_job_flow(
Job::QueuedJob(job) => &job.email,
Job::CompletedJobWithFormattedResult(job) => &job.cj.email,
};
conditionally_require_authed_user(authed, flow_status.clone(), trigger_email)?;
conditionally_require_authed_user(authed.clone(), flow_status.clone(), trigger_email)?;
let approvers_from_status = match flow_module_status {
FlowStatusModule::Success { approvers, .. } => approvers.to_owned(),
@@ -1971,6 +1996,8 @@ pub async fn get_suspended_job_flow(
approvers_from_status
};
log_job_view(&db, authed.as_ref(), &w_id, &job).await?;
Ok(Json(SuspendedJobFlow { job: flow, approvers }).into_response())
}
@@ -3256,6 +3283,76 @@ lazy_static::lazy_static! {
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(200);
static ref JOB_VIEW_AUDIT_LOGS: bool = std::env::var("JOB_VIEW_AUDIT_LOGS")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(false);
static ref JOB_VIEW_CACHE: JobViewCache = JobViewCache::new(50000);
}
struct JobViewCache {
cache: Cache<String, std::time::Instant>,
}
impl JobViewCache {
fn new(items_capacity: usize) -> Self {
Self { cache: Cache::new(items_capacity) }
}
fn get_or_insert(&self, key: &str) -> Option<std::time::Instant> {
match self.cache.get(key) {
Some(t) if t < std::time::Instant::now() => {
self.cache.insert(
key.to_string(),
std::time::Instant::now() + std::time::Duration::from_secs(60),
);
None
}
v => {
self.cache.insert(
key.to_string(),
std::time::Instant::now() + std::time::Duration::from_secs(60),
);
v
}
}
}
}
async fn log_job_view(
db: &DB,
opt_authed: Option<&ApiAuthed>,
w_id: &str,
job_id: &Uuid,
) -> error::Result<()> {
if *JOB_VIEW_AUDIT_LOGS {
let audit_author = match opt_authed {
Some(authed) => AuditAuthor::from(authed),
None => AuditAuthor {
username: "anonymous".to_string(),
username_override: None,
email: "anonymous".to_string(),
},
};
if JOB_VIEW_CACHE
.get_or_insert(&format!("{}_{}", job_id, audit_author.email))
.is_none()
{
audit_log(
db,
&audit_author,
"jobs.view",
ActionKind::Execute,
w_id,
Some(&job_id.to_string()),
None,
)
.await?;
};
}
Ok(())
}
pub async fn run_wait_result_job_by_path_get(
@@ -3875,22 +3972,27 @@ async fn run_dependencies_job(
let raw_script = req.raw_scripts[0].clone();
let script_path = raw_script.script_path;
let ehm = HashMap::new();
let raw_code = raw_script.raw_code.unwrap_or_else(|| "".to_string());
let language = raw_script.language;
let (args, raw_code) = if let Some(deps) = req.raw_deps {
let mut hm = HashMap::new();
hm.insert(
"raw_deps".to_string(),
JsonRawValue::from_string("true".to_string()).unwrap(),
);
if language == ScriptLang::Bun {
let annotation = windmill_common::worker::get_annotation(&raw_code);
hm.insert(
"npm_mode".to_string(),
JsonRawValue::from_string(annotation.npm_mode.to_string()).unwrap(),
);
}
(PushArgs { extra: Some(hm), args: &ehm }, deps)
} else {
(
PushArgs::from(&ehm),
raw_script.raw_code.unwrap_or_else(|| "".to_string()),
)
(PushArgs::from(&ehm), raw_code)
};
let language = raw_script.language;
let (uuid, tx) = push(
&db,
PushIsolationLevel::IsolatedRoot(db.clone(), rsmq),
@@ -4402,6 +4504,7 @@ async fn get_job_update(
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
));
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &job_id).await?;
Ok(Json(JobUpdate {
running: if !running && record.running {
Some(true)
@@ -4437,6 +4540,7 @@ async fn get_job_update(
.to_string(),
));
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &job_id).await?;
Ok(Json(JobUpdate {
running: Some(false),
completed: Some(true),
@@ -4688,7 +4792,7 @@ async fn get_completed_job<'a>(
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)
.bind(&w_id)
.fetch_optional(&db)
.await?;
@@ -4710,6 +4814,9 @@ async fn get_completed_job<'a>(
// )
// .fetch_optional(db)
// .await.ok().flatten().flatten();
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(response)
}
@@ -4804,6 +4911,8 @@ async fn get_completed_job_result(
raw_result.result.map(|x| x.0),
);
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(result).into_response())
}
@@ -4845,6 +4954,9 @@ async fn get_completed_job_result_maybe(
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
));
}
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(CompletedJobResult {
started: Some(true),
success: Some(res.success),
+19 -2
View File
@@ -7,6 +7,8 @@
*/
use crate::db::ApiAuthed;
#[cfg(feature = "enterprise")]
use crate::ee::ExternalJwks;
#[cfg(feature = "embedding")]
use crate::embeddings::load_embeddings_db;
use crate::oauth2_ee::AllClients;
@@ -141,11 +143,22 @@ pub async fn add_webhook_allowed_origin(
next.run(req).await
}
#[cfg(not(feature = "tantivy"))]
type IndexReader = ();
#[cfg(not(feature = "tantivy"))]
type IndexWriter = ();
#[cfg(feature = "tantivy")]
type IndexReader = windmill_indexer::indexer_ee::IndexReader;
#[cfg(feature = "tantivy")]
type IndexWriter = windmill_indexer::indexer_ee::IndexWriter;
pub async fn run_server(
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
index_reader: Option<windmill_indexer::indexer_ee::IndexReader>,
index_writer: Option<windmill_indexer::indexer_ee::IndexWriter>,
index_reader: Option<IndexReader>,
index_writer: Option<IndexWriter>,
addr: SocketAddr,
mut rx: tokio::sync::broadcast::Receiver<()>,
port_tx: tokio::sync::oneshot::Sender<String>,
@@ -164,9 +177,13 @@ pub async fn run_server(
}
let user_db = UserDB::new(db.clone());
#[cfg(feature = "enterprise")]
let ext_jwks = ExternalJwks::load().await;
let auth_cache = Arc::new(users::AuthCache::new(
db.clone(),
std::env::var("SUPERADMIN_SECRET").ok(),
#[cfg(feature = "enterprise")]
ext_jwks,
));
let argon2 = Arc::new(Argon2::default());
+16 -3
View File
@@ -52,7 +52,7 @@ use windmill_common::{
utils::{
not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath,
},
worker::to_raw_value,
worker::{get_annotation, to_raw_value},
HUB_BASE_URL,
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
@@ -570,21 +570,34 @@ async fn create_script_internal<'c>(
let lock = if !(ns.language == ScriptLang::Python3
|| ns.language == ScriptLang::Go
|| ns.language == ScriptLang::Bun
|| ns.language == ScriptLang::Bunnative
|| ns.language == ScriptLang::Deno
|| ns.language == ScriptLang::Php)
|| ns.codebase.is_some()
{
Some(String::new())
} else {
ns.lock
.and_then(|e| if e.is_empty() { None } else { Some(e) })
};
let needs_lock_gen = lock.is_none();
let needs_lock_gen = lock.is_none() && codebase.is_none();
let envs = ns.envs.as_ref().map(|x| x.as_slice());
let envs = if ns.envs.is_none() || ns.envs.as_ref().unwrap().is_empty() {
None
} else {
envs
};
let lang = if &ns.language == &ScriptLang::Bun || &ns.language == &ScriptLang::Bunnative {
let anns = get_annotation(&ns.content);
if anns.native_mode {
ScriptLang::Bunnative
} else {
ScriptLang::Bun
}
} else {
ns.language.clone()
};
sqlx::query!(
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, \
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
@@ -604,7 +617,7 @@ async fn create_script_internal<'c>(
ns.is_template.unwrap_or(false),
extra_perms,
lock,
ns.language.clone() as ScriptLang,
lang as ScriptLang,
ns.kind.unwrap_or(ScriptKind::Script) as ScriptKind,
ns.tag,
ns.draft_only,
+11 -8
View File
@@ -116,14 +116,17 @@ pub async fn test_s3_bucket(
let client = build_object_store_from_settings(test_s3_bucket).await?;
let mut list = client.list(Some(&object_store::path::Path::from("".to_string())));
let first_file = list
.next()
.await
.ok_or_else(|| {
error::Error::InternalErr("Failed to list files in blob storage".to_string())
})?
.map_err(|e| anyhow::anyhow!("error listing bucket: {e:#}"))?;
tracing::info!("Listed files: {:?}", first_file);
let first_file = list.next().await;
if first_file.is_some() {
if let Err(e) = first_file.as_ref().unwrap() {
tracing::error!("error listing bucket: {e:#}");
error::Error::InternalErr(format!("Failed to list files in blob storage: {e:#}"));
}
tracing::info!("Listed files: {:?}", first_file.unwrap());
} else {
tracing::info!("No files in blob storage");
}
let path = object_store::path::Path::from(format!(
"/test-s3-bucket-{uuid}",
uuid = uuid::Uuid::new_v4()
+31 -5
View File
@@ -12,6 +12,8 @@ use std::sync::Arc;
use crate::db::ApiAuthed;
#[cfg(feature = "enterprise")]
use crate::ee::ExternalJwks;
use crate::oauth2_ee::{check_nb_of_user, InstanceEvent};
use crate::utils::{
generate_instance_wide_unique_username, get_and_delete_pending_username_or_generate,
@@ -39,6 +41,8 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use time::OffsetDateTime;
#[cfg(feature = "enterprise")]
use tokio::sync::RwLock;
use tower_cookies::{Cookie, Cookies};
use tracing::{Instrument, Span};
use windmill_audit::audit_ee::{audit_log, AuditAuthor};
@@ -145,11 +149,23 @@ pub struct AuthCache {
cache: Cache<(String, String), ExpiringAuthCache>,
db: DB,
superadmin_secret: Option<String>,
#[cfg(feature = "enterprise")]
ext_jwks: Option<Arc<RwLock<ExternalJwks>>>,
}
impl AuthCache {
pub fn new(db: DB, superadmin_secret: Option<String>) -> Self {
AuthCache { cache: Cache::new(300), db, superadmin_secret }
pub fn new(
db: DB,
superadmin_secret: Option<String>,
#[cfg(feature = "enterprise")] ext_jwks: Option<Arc<RwLock<ExternalJwks>>>,
) -> Self {
AuthCache {
cache: Cache::new(300),
db,
superadmin_secret,
#[cfg(feature = "enterprise")]
ext_jwks,
}
}
pub async fn invalidate(&self, w_id: &str, token: String) {
@@ -168,9 +184,19 @@ impl AuthCache {
}
#[cfg(feature = "enterprise")]
_ if token.starts_with("jwt_ext_") => {
let authed_and_exp =
crate::ee::jwt_ext_auth(w_id.as_ref(), token.trim_start_matches("jwt_ext_"))
.await;
let authed_and_exp = match crate::ee::jwt_ext_auth(
w_id.as_ref(),
token.trim_start_matches("jwt_ext_"),
self.ext_jwks.clone(),
)
.await
{
Ok(r) => Some(r),
Err(e) => {
tracing::error!("JWT_EXT auth error: {:?}", e);
None
}
};
if let Some((authed, exp)) = authed_and_exp.clone() {
self.cache.insert(
+14 -5
View File
@@ -25,7 +25,7 @@ use windmill_audit::ActionKind;
use windmill_common::{
db::UserDB,
error::{Error, JsonResult, Result},
utils::{not_found_if_none, StripPath},
utils::{not_found_if_none, paginate, Pagination, StripPath},
variables::{
build_crypt, get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable,
},
@@ -92,7 +92,10 @@ async fn list_variables(
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(lq): Query<ListVariableQuery>,
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<ListableVariable>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, ListableVariable>(
@@ -100,17 +103,22 @@ async fn list_variables(
is_secret, variable.description, variable.extra_perms, account, is_oauth, (now() > account.expires_at) as is_expired,
account.refresh_error,
resource.path IS NOT NULL as is_linked,
account.refresh_token != '' as is_refreshed
account.refresh_token != '' as is_refreshed,
variable.expires_at
from variable
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = $1
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = $1
WHERE variable.workspace_id = $1 AND variable.path NOT LIKE 'u/' || $2 || '/secret_arg/%'
AND variable.path LIKE $3 || '%'
ORDER BY path",
ORDER BY path
LIMIT $4 OFFSET $5
",
)
.bind(&w_id)
.bind(&authed.username)
.bind(&lq.path_start.unwrap_or_default())
.bind(per_page as i32)
.bind(offset as i32)
.fetch_all(&mut *tx)
.await?;
@@ -310,8 +318,8 @@ async fn create_variable(
sqlx::query!(
"INSERT INTO variable
(workspace_id, path, value, is_secret, description, account, is_oauth)
VALUES ($1, $2, $3, $4, $5, $6, $7)",
(workspace_id, path, value, is_secret, description, account, is_oauth, expires_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
&w_id,
variable.path,
value,
@@ -319,6 +327,7 @@ async fn create_variable(
variable.description,
variable.account,
variable.is_oauth.unwrap_or(false),
variable.expires_at
)
.execute(&mut *tx)
.await?;
+3 -3
View File
@@ -2498,7 +2498,7 @@ async fn tarball_workspace(
ScriptLang::Mssql => "ms.sql",
ScriptLang::Graphql => "gql",
ScriptLang::Nativets => "fetch.ts",
ScriptLang::Bun => {
ScriptLang::Bun | ScriptLang::Bunnative => {
if default_ts.as_ref().is_some_and(|x| x == "bun") {
"ts"
} else {
@@ -2599,9 +2599,9 @@ async fn tarball_workspace(
if !skip_variables.unwrap_or(false) {
let variables =
sqlx::query_as::<_, ExportableListableVariable>(if !skip_secrets.unwrap_or(false) {
"SELECT * FROM variable WHERE workspace_id = $1 AND path NOT LIKE 'u/%/secret_arg/%'"
"SELECT * FROM variable WHERE workspace_id = $1 AND expires_at IS NULL"
} else {
"SELECT * FROM variable WHERE workspace_id = $1 AND is_secret = false AND path NOT LIKE 'u/%/secret_arg/%'"
"SELECT * FROM variable WHERE workspace_id = $1 AND is_secret = false AND expires_at IS NULL"
})
.bind(&w_id)
.fetch_all(&mut *tx)
+4 -2
View File
@@ -11,7 +11,7 @@ jemalloc = ["dep:tikv-jemalloc-ctl"]
prometheus = ["dep:prometheus"]
flamegraph = ["dep:tracing-flame"]
loki = ["dep:tracing-loki"]
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts", "dep:bytes"]
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts"]
[lib]
name = "windmill_common"
@@ -48,8 +48,10 @@ prometheus = { workspace = true, optional = true }
aws-config = { workspace = true, optional = true }
aws-sdk-sts = { workspace = true, optional = true }
indexmap.workspace = true
bytes = { workspace = true, optional = true }
bytes.workspace = true
mail-send.workspace = true
futures-core.workspace = true
async-stream.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemalloc-ctl = { optional = true, workspace = true }
+11 -3
View File
@@ -38,14 +38,18 @@ pub struct Flow {
pub dedicated_worker: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(skip_serializing_if = "is_none_or_false")]
pub ws_error_handler_muted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(skip_serializing_if = "is_none_or_false")]
pub visible_to_runner_only: Option<bool>,
}
fn is_none_or_false(b: &Option<bool>) -> bool {
b.is_none() || !b.unwrap()
}
#[derive(Serialize, sqlx::FromRow)]
pub struct ListableFlow {
pub workspace_id: String,
@@ -406,7 +410,7 @@ pub enum FlowModuleValue {
lock: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(skip_serializing_if = "is_none_or_empty")]
tag: Option<String>,
language: ScriptLang,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -419,6 +423,10 @@ pub enum FlowModuleValue {
Identity,
}
fn is_none_or_empty(expr: &Option<String>) -> bool {
expr.is_none() || expr.as_ref().unwrap().is_empty()
}
#[derive(Deserialize)]
struct UntaggedFlowModuleValue {
#[serde(rename = "type")]
@@ -1,5 +1,6 @@
pub const CUSTOM_TAGS_SETTING: &str = "custom_tags";
pub const DEFAULT_TAGS_PER_WORKSPACE_SETTING: &str = "default_tags_per_workspace";
pub const DEFAULT_TAGS_WORKSPACES_SETTING: &str = "default_tags_workspaces";
pub const BASE_URL_SETTING: &str = "base_url";
pub const OAUTH_SETTING: &str = "oauths";
pub const RETENTION_PERIOD_SECS_SETTING: &str = "retention_period_secs";
+77 -2
View File
@@ -1,20 +1,23 @@
use std::collections::HashMap;
use bytes::Bytes;
use futures_core::Stream;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use sqlx::{types::Json, Pool, Postgres, Transaction};
use tokio::io::AsyncReadExt;
use uuid::Uuid;
pub const ENTRYPOINT_OVERRIDE: &str = "_ENTRYPOINT_OVERRIDE";
use crate::{
error::{self, Error},
error::{self, to_anyhow, Error},
flow_status::{FlowStatus, RestartedFrom},
flows::{FlowValue, Retry},
get_latest_deployed_hash_for_path,
scripts::{ScriptHash, ScriptLang},
worker::to_raw_value,
worker::{to_raw_value, TMP_DIR},
};
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)]
@@ -580,3 +583,75 @@ pub fn format_completed_job_result(mut cj: CompletedJob) -> CompletedJobWithForm
cj.result = None; // very important to avoid sending the result twice
CompletedJobWithFormattedResult { cj, result: Some(sql_result) }
}
pub async fn get_logs_from_disk(
log_offset: i32,
logs: &str,
log_file_index: &Option<Vec<String>>,
) -> Option<impl Stream<Item = Result<Bytes, anyhow::Error>>> {
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
for file_p in &file_index {
if !tokio::fs::metadata(format!("{TMP_DIR}/{file_p}"))
.await
.is_ok()
{
return None;
}
}
let logs = logs.to_string();
let stream = async_stream::stream! {
for file_p in file_index.clone() {
let mut file = tokio::fs::File::open(format!("{TMP_DIR}/{file_p}")).await.map_err(to_anyhow)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await.map_err(to_anyhow)?;
yield Ok(bytes::Bytes::from(buffer)) as anyhow::Result<bytes::Bytes>;
}
yield Ok(bytes::Bytes::from(logs))
};
return Some(stream);
}
}
return None;
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
pub async fn get_logs_from_store(
log_offset: i32,
logs: &str,
log_file_index: &Option<Vec<String>>,
) -> Option<impl Stream<Item = Result<Bytes, object_store::Error>>> {
use crate::s3_helpers::OBJECT_STORE_CACHE_SETTINGS;
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
tracing::debug!("Getting logs from store: {file_index:?}");
if let Some(os) = OBJECT_STORE_CACHE_SETTINGS.read().await.clone() {
tracing::debug!("object store client present, streaming from there");
let logs = logs.to_string();
let stream = async_stream::stream! {
for file_p in file_index.clone() {
let file_p_2 = file_p.clone();
let file = os.get(&object_store::path::Path::from(file_p)).await;
if let Ok(file) = file {
if let Ok(bytes) = file.bytes().await {
yield Ok(bytes::Bytes::from(bytes)) as object_store::Result<bytes::Bytes>;
}
} else {
tracing::debug!("error getting file from store: {file_p_2}: {}", file.err().unwrap());
}
}
yield Ok(bytes::Bytes::from(logs))
};
return Some(stream);
} else {
tracing::debug!("object store client not present, cannot stream logs from store");
}
}
}
return None;
}
+5 -1
View File
@@ -79,6 +79,9 @@ lazy_static::lazy_static! {
pub static ref CRITICAL_ERROR_CHANNELS: Arc<RwLock<Vec<CriticalErrorChannel>>> = Arc::new(RwLock::new(vec![]));
pub static ref JOB_RETENTION_SECS: Arc<RwLock<i64>> = Arc::new(RwLock::new(0));
}
pub async fn shutdown_signal(
@@ -219,11 +222,12 @@ pub async fn connect_db(
DEFAULT_MAX_CONNECTIONS_INDEXER
} else {
DEFAULT_MAX_CONNECTIONS_WORKER
* std::env::var("NUM_WORKERS")
+ std::env::var("NUM_WORKERS")
.ok()
.map(|x| x.parse().ok())
.flatten()
.unwrap_or(1)
- 1
}
}
}
+2
View File
@@ -34,6 +34,7 @@ pub enum ScriptLang {
Powershell,
Postgresql,
Bun,
Bunnative,
Mysql,
Bigquery,
Snowflake,
@@ -46,6 +47,7 @@ impl ScriptLang {
pub fn as_str(&self) -> &'static str {
match self {
ScriptLang::Bun => "bun",
ScriptLang::Bunnative => "bunnative",
ScriptLang::Nativets => "nativets",
ScriptLang::Deno => "deno",
ScriptLang::Python3 => "python3",
+10 -1
View File
@@ -40,10 +40,10 @@ pub struct ListableVariable {
pub is_refreshed: Option<bool>,
pub refresh_error: Option<String>,
pub is_linked: Option<bool>,
pub expires_at: Option<chrono::DateTime<Utc>>,
}
#[derive(Serialize, Deserialize, sqlx::FromRow)]
pub struct ExportableListableVariable {
pub workspace_id: String,
pub path: String,
@@ -51,8 +51,16 @@ pub struct ExportableListableVariable {
pub is_secret: bool,
pub description: String,
pub extra_perms: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub account: Option<i32>,
#[serde(skip_serializing_if = "is_none_or_false")]
pub is_oauth: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<chrono::DateTime<Utc>>,
}
fn is_none_or_false(b: &Option<bool>) -> bool {
b.is_none() || !b.unwrap()
}
#[derive(Deserialize)]
@@ -63,6 +71,7 @@ pub struct CreateVariable {
pub description: String,
pub account: Option<i32>,
pub is_oauth: Option<bool>,
pub expires_at: Option<chrono::DateTime<Utc>>,
}
pub async fn build_crypt(db: &DB, w_id: &str) -> crate::error::Result<MagicCrypt256> {
+202 -2
View File
@@ -43,6 +43,7 @@ lazy_static::lazy_static! {
];
pub static ref DEFAULT_TAGS_PER_WORKSPACE: AtomicBool = AtomicBool::new(false);
pub static ref DEFAULT_TAGS_WORKSPACES: Arc<RwLock<Option<Vec<String>>>> = Arc::new(RwLock::new(None));
pub static ref WORKER_CONFIG: Arc<RwLock<WorkerConfig>> = Arc::new(RwLock::new(WorkerConfig {
@@ -56,6 +57,10 @@ lazy_static::lazy_static! {
env_vars: Default::default(),
}));
pub static ref WORKER_PULL_QUERIES: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
pub static ref WORKER_SUSPENDED_PULL_QUERY: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref SERVER_CONFIG: Arc<RwLock<ServerConfig>> = Arc::new(RwLock::new(ServerConfig { smtp: Default::default(), timeout_wait_result: 20 }));
@@ -75,6 +80,63 @@ lazy_static::lazy_static! {
}
pub async fn make_suspended_pull_query(wc: &WorkerConfig) {
let query = format!(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
, suspend_until = null
WHERE id = (
SELECT id
FROM queue
WHERE suspend_until IS NOT NULL AND (suspend <= 0 OR suspend_until <= now()) AND tag IN ({})
ORDER BY priority DESC NULLS LAST, created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
running, script_hash, script_path, args, null as logs, raw_code, canceled, canceled_by,
canceled_reason, last_ping, job_kind, schedule_path, permissioned_as,
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
timeout, flow_step_id, cache_ttl, priority", wc.worker_tags.iter().map(|x| format!("'{x}'")).join(", "));
let mut l = WORKER_SUSPENDED_PULL_QUERY.write().await;
*l = query;
}
pub async fn make_pull_query(wc: &WorkerConfig) {
let mut queries = vec![];
for tags in wc.priority_tags_sorted.iter() {
let query = format!("UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
, suspend_until = null
WHERE id = (
SELECT id
FROM queue
WHERE running = false AND tag IN ({}) AND scheduled_for <= now()
ORDER BY priority DESC NULLS LAST, scheduled_for
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
running, script_hash, script_path, args, null as logs, raw_code, canceled, canceled_by,
canceled_reason, last_ping, job_kind, schedule_path, permissioned_as,
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
timeout, flow_step_id, cache_ttl, priority", tags.tags.iter().map(|x| format!("'{x}'")).join(", "));
queries.push(query);
}
let mut l = WORKER_PULL_QUERIES.write().await;
*l = queries;
}
pub const TMP_DIR: &str = "/tmp/windmill";
pub async fn reload_custom_tags_setting(db: &DB) -> error::Result<()> {
@@ -152,6 +214,144 @@ fn parse_file<T: FromStr>(path: &str) -> Option<T> {
.flatten()
}
pub struct Annotations {
pub npm_mode: bool,
pub nodejs_mode: bool,
pub native_mode: bool,
pub nobundling: bool,
}
pub fn get_annotation(inner_content: &str) -> Annotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("//"))
.map(|x| x.to_string().replace("//", "").trim().to_string())
.collect_vec();
let nodejs_mode: bool = annotations.contains(&"nodejs".to_string());
let npm_mode: bool = annotations.contains(&"npm".to_string());
let native_mode: bool = annotations.contains(&"native".to_string());
//TODO: remove || npm_mode when bun build is more powerful
let nobundling: bool = annotations.contains(&"nobundling".to_string()) || nodejs_mode;
Annotations { npm_mode, nodejs_mode, native_mode, nobundling }
}
pub async fn load_cache(bin_path: &str, _remote_path: &str) -> (bool, String) {
if tokio::fs::metadata(&bin_path).await.is_ok() {
(true, format!("loaded from local cache: {}\n", bin_path))
} else {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = crate::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
let started = std::time::Instant::now();
use crate::s3_helpers::attempt_fetch_bytes;
if let Ok(mut x) = attempt_fetch_bytes(os, _remote_path).await {
if let Err(e) = write_binary_file(bin_path, &mut x) {
tracing::error!("could not write bundle/bin file locally: {e:?}");
return (
false,
"error writing bundle/bin file from object store".to_string(),
);
}
tracing::info!("loaded from object store {}", bin_path);
return (
true,
format!(
"loaded bin/bundle from object store {} in {}ms",
bin_path,
started.elapsed().as_millis()
),
);
}
}
(false, "".to_string())
}
}
pub async fn exists_in_cache(bin_path: &str, _remote_path: &str) -> bool {
if tokio::fs::metadata(&bin_path).await.is_ok() {
return true;
} else {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = crate::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
return os
.get(&object_store::path::Path::from(_remote_path))
.await
.is_ok();
}
return false;
}
}
pub async fn save_cache(
local_cache_path: &str,
_remote_cache_path: &str,
origin: &str,
) -> crate::error::Result<String> {
let mut _cached_to_s3 = false;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = crate::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
use object_store::path::Path;
if let Err(e) = os
.put(
&Path::from(_remote_cache_path),
std::fs::read(origin)?.into(),
)
.await
{
tracing::error!(
"Failed to put go bin to object store: {_remote_cache_path}. Error: {:?}",
e
);
} else {
_cached_to_s3 = true;
}
}
// if !*CLOUD_HOSTED {
if true {
std::fs::copy(origin, local_cache_path)?;
Ok(format!(
"\nwrote cached binary: {} (backed by EE distributed object store: {_cached_to_s3})\n",
local_cache_path
))
} else if _cached_to_s3 {
Ok(format!(
"wrote cached binary to object store {}\n",
local_cache_path
))
} else {
Ok("".to_string())
}
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
fn write_binary_file(main_path: &str, byts: &mut bytes::Bytes) -> error::Result<()> {
use std::fs::{File, Permissions};
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
let mut file = File::create(main_path)?;
file.write_all(byts)?;
file.set_permissions(Permissions::from_mode(0o755))?;
file.flush()?;
Ok(())
}
fn get_cgroupv2_path() -> Option<String> {
let cgroup_path: String = parse_file("/proc/self/cgroup")?;
@@ -186,9 +386,9 @@ pub fn get_vcpus() -> Option<i64> {
}
pub fn get_memory() -> Option<i64> {
if Path::new("/sys/fs/cgroup/memory/memory.max").exists() {
if Path::new("/sys/fs/cgroup/memory/memory.limit_in_bytes").exists() {
// cgroup v1
parse_file("/sys/fs/cgroup/memory/memory.max")
parse_file("/sys/fs/cgroup/memory/memory.limit_in_bytes")
} else {
// cgroup v2
let cgroup_path = get_cgroupv2_path()?;
+7 -2
View File
@@ -10,11 +10,12 @@ path = "src/lib.rs"
[features]
default = []
tantivy = ["dep:tantivy"]
parquet = ["dep:object_store"]
enterprise = []
[dependencies]
windmill-common.workspace = true
tantivy = {workspace = true, optional = true}
tantivy.workspace = true
tokio.workspace = true
sqlx.workspace = true
anyhow.workspace = true
@@ -24,3 +25,7 @@ tracing.workspace = true
chrono.workspace = true
uuid.workspace = true
futures.workspace = true
tempfile.workspace = true
bytes.workspace = true
object_store = { workspace = true, optional = true}
tokio-tar.workspace = true
+1 -1
View File
@@ -8,7 +8,7 @@ pub struct IndexReader;
#[derive(Clone)]
pub struct IndexWriter;
pub fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
pub async fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
Err(anyhow!("Cannot initialize index: not in EE").into())
}
+190 -242
View File
@@ -6,12 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
sync::Arc,
vec,
};
use std::{borrow::Borrow, collections::HashMap, sync::Arc, vec};
use anyhow::Context;
use async_recursion::async_recursion;
@@ -63,7 +58,10 @@ use windmill_common::{
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
utils::{not_found_if_none, report_critical_error, StripPath},
worker::{to_raw_value, DEFAULT_TAGS_PER_WORKSPACE, NO_LOGS, WORKER_CONFIG},
worker::{
to_raw_value, DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, NO_LOGS, WORKER_CONFIG,
WORKER_PULL_QUERIES, WORKER_SUSPENDED_PULL_QUERY,
},
BASE_URL, DB, METRICS_ENABLED,
};
@@ -1759,8 +1757,7 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
let min_started_at = sqlx::query!(
"SELECT COALESCE((SELECT MIN(started_at) as min_started_at
FROM queue
WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $2 AND canceled = false
GROUP BY script_path), $3) as min_started_at, now() AS now",
WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $2 AND canceled = false AND concurrent_limit > 0), $3) as min_started_at, now() AS now",
job_script_path,
&pulled_job.workspace_id,
completed_count.max_ended_at
@@ -1983,35 +1980,11 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
* suspend_until is non-null
* and suspend = 0 when the resume messages are received
* or suspend_until <= now() if it has timed out */
let config = WORKER_CONFIG.read().await.clone();
let tags = config.worker_tags.clone();
#[cfg(not(feature = "enterprise"))]
let priority_tags_sorted = vec![PriorityTags { priority: 0, tags: tags.clone() }];
#[cfg(feature = "enterprise")]
let priority_tags_sorted = config.priority_tags_sorted.clone();
drop(config);
let query = WORKER_SUSPENDED_PULL_QUERY.read().await;
let r = if suspend_first {
sqlx::query_as::<_, QueuedJob>("UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
, suspend_until = null
WHERE id = (
SELECT id
FROM queue
WHERE suspend_until IS NOT NULL AND (suspend <= 0 OR suspend_until <= now()) AND tag = ANY($1)
ORDER BY priority DESC NULLS LAST, created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
running, script_hash, script_path, args, null as logs, raw_code, canceled, canceled_by,
canceled_reason, last_ping, job_kind, schedule_path, permissioned_as,
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
timeout, flow_step_id, cache_ttl, priority")
.bind(tags)
// tracing::info!("Pulling job with query: {}", query);
sqlx::query_as::<_, QueuedJob>(&query)
.fetch_optional(db)
.await?
} else {
@@ -2022,41 +1995,16 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
// let instant = Instant::now();
let mut highest_priority_job: Option<QueuedJob> = None;
for priority_tags in priority_tags_sorted {
let r = sqlx::query_as::<_, QueuedJob>(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
, suspend_until = null
WHERE id = (
SELECT id
FROM queue
WHERE running = false AND scheduled_for <= now() AND tag = ANY($1)
ORDER BY priority DESC NULLS LAST, scheduled_for, created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
running, script_hash, script_path, args, null as logs, raw_code, canceled, canceled_by,
canceled_reason, last_ping, job_kind, schedule_path, permissioned_as,
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
timeout, flow_step_id, cache_ttl, priority",
)
.bind(priority_tags.tags.clone())
.fetch_optional(db)
.await?;
let queries = WORKER_PULL_QUERIES.read().await;
for query in queries.iter() {
// tracing::info!("Pulling job with query: {}", query);
let r = sqlx::query_as::<_, QueuedJob>(query)
.fetch_optional(db)
.await?;
if let Some(pulled_job) = r {
let id = pulled_job.id;
highest_priority_job = Some(pulled_job);
tracing::debug!(
"Pulling for job {id} with tags {:?} with priority {}",
priority_tags.tags,
priority_tags.priority
);
break;
}
// else continue pulling for lower priority tags
@@ -2197,35 +2145,51 @@ pub async fn get_result_by_id(
{
Ok(res) => Ok(res),
Err(_) => {
let running_flow_job = sqlx::query_as::<_, QueuedJob>(
let running_flow_job =sqlx::query_as::<_, QueuedJob>(
"SELECT * FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $1), $1) = id AND workspace_id = $2"
).bind(flow_id)
.bind(&w_id)
.fetch_optional(&db)
.await?;
.fetch_optional(&db).await?;
match running_flow_job {
Some(job) => {
let restarted_from = windmill_common::utils::not_found_if_none(
job.parse_flow_status()
.map(|status| status.restarted_from)
.flatten(),
"Flow result by id in leaf jobs",
format!("{}, {}", flow_id, node_id),
)?;
let restarted_from = windmill_common::utils::not_found_if_none(
running_flow_job
.map(|fj| fj.parse_flow_status())
.flatten()
.map(|status| status.restarted_from)
.flatten(),
"Flow result by id in leaf jobs",
format!("{}, {}", flow_id, node_id),
)?;
get_result_by_id_from_original_flow(
&db,
w_id.as_str(),
&restarted_from.flow_job_id,
node_id.as_str(),
json_path.clone(),
)
.await
get_result_by_id_from_original_flow(
&db,
w_id.as_str(),
&restarted_from.flow_job_id,
node_id.as_str(),
json_path.clone(),
)
.await
}
None => {
get_result_by_id_from_original_flow(
&db,
w_id.as_str(),
&flow_id,
node_id.as_str(),
json_path.clone(),
)
.await
}
}
}
}
}
#[derive(FromRow)]
struct FlowJobResult {
leaf_jobs: Option<Json<Box<RawValue>>>,
parent_job: Option<Uuid>,
}
#[async_recursion]
pub async fn get_result_by_id_from_running_flow(
db: &Pool<Postgres>,
@@ -2234,12 +2198,11 @@ pub async fn get_result_by_id_from_running_flow(
node_id: &str,
json_path: Option<String>,
) -> error::Result<Box<RawValue>> {
let flow_job_result = sqlx::query!(
"SELECT leaf_jobs->$1::text as leaf_jobs, parent_job FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3",
node_id,
flow_id,
w_id,
)
let flow_job_result = sqlx::query_as::<_, FlowJobResult>(
"SELECT leaf_jobs->$1::text as leaf_jobs, parent_job FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3")
.bind(node_id)
.bind(flow_id)
.bind(w_id)
.fetch_optional(db)
.await?;
@@ -2251,7 +2214,7 @@ pub async fn get_result_by_id_from_running_flow(
let job_result = flow_job_result
.leaf_jobs
.map(|x| serde_json::from_value(x).ok())
.map(|x| serde_json::from_str(x.get()).ok())
.flatten();
if job_result.is_none() && flow_job_result.parent_job.is_some() {
@@ -2274,6 +2237,61 @@ pub async fn get_result_by_id_from_running_flow(
}
#[async_recursion]
async fn get_completed_flow_node_result_rec(
db: &Pool<Postgres>,
w_id: &str,
subflows: Vec<CompletedJob>,
node_id: &str,
json_path: Option<String>,
) -> error::Result<Option<Box<RawValue>>> {
for subflow in subflows {
let flow_status = subflow.parse_flow_status().ok_or_else(|| {
error::Error::InternalErr(format!("Could not parse flow status of {}", subflow.id))
})?;
if let Some(node_status) = flow_status
.modules
.iter()
.find(|module| module.id() == node_id)
{
return match (node_status.job(), node_status.flow_jobs()) {
(Some(leaf_job_uuid), None) => extract_result_from_job_result(
db,
w_id,
JobResult::SingleJob(leaf_job_uuid),
json_path.clone(),
)
.await
.map(Some),
(Some(_), Some(jobs)) => extract_result_from_job_result(
db,
w_id,
JobResult::ListJob(jobs),
json_path.clone(),
)
.await
.map(Some),
_ => Err(error::Error::NotFound(format!(
"Flow result by id in leaf jobs not found at name {}",
node_id,
))),
};
} else {
let subflows = sqlx::query_as::<_, CompletedJob>(
"SELECT *, null as labels FROM completed_job WHERE parent_job = $1 AND workspace_id = $2 AND flow_status IS NOT NULL",
).bind(subflow.id).bind(w_id).fetch_all(db).await?;
match get_completed_flow_node_result_rec(db, w_id, subflows, node_id, json_path.clone())
.await?
{
Some(res) => return Ok(Some(res)),
None => continue,
};
}
}
Ok(None)
}
async fn get_result_by_id_from_original_flow(
db: &Pool<Postgres>,
w_id: &str,
@@ -2292,130 +2310,16 @@ async fn get_result_by_id_from_original_flow(
let flow_job = windmill_common::utils::not_found_if_none(
flow_job,
"Flow result by id in leaf jobs",
format!("{}", completed_flow_id),
format!("{}, {}", completed_flow_id, node_id),
)?;
let mut leaf_jobs_for_flow = HashMap::<String, JobResult>::new();
compute_leaf_jobs_for_completed_flow(
db,
w_id,
flow_job.id,
Some(flow_job.clone()),
&mut leaf_jobs_for_flow,
)
.await?;
if !leaf_jobs_for_flow.contains_key(&node_id.to_string()) {
// if the flow is itself a restart flow, the step job might be from the upstream flow
let restarted_from = windmill_common::utils::not_found_if_none(
flow_job
.parse_flow_status()
.map(|status| status.restarted_from)
.flatten(),
"Flow result by id in leaf jobs",
format!("{}", completed_flow_id),
)?;
return get_result_by_id_from_original_flow(
db,
w_id,
&restarted_from.flow_job_id,
node_id,
json_path,
)
.await;
match get_completed_flow_node_result_rec(db, w_id, vec![flow_job], node_id, json_path).await? {
Some(res) => Ok(res),
None => Err(error::Error::NotFound(format!(
"Flow result by id in leaf jobs not found at name {}, {}",
completed_flow_id, node_id
))),
}
// if the job is in the leaf_jobs map, then fetch its result and return
let leaf_job_uuid = leaf_jobs_for_flow.get(&node_id.to_string()).unwrap();
extract_result_from_job_result(db, w_id, leaf_job_uuid.to_owned(), json_path).await
}
#[async_recursion]
async fn compute_leaf_jobs_for_completed_flow(
db: &Pool<Postgres>,
w_id: &str,
completed_flow_id: Uuid,
completed_flow_row: Option<CompletedJob>, // if provided, will be used, otherwise completed_flow_id must be set and the job definition will be pulled from DB
recursive_result: &mut HashMap<String, JobResult>,
) -> error::Result<()> {
let flow_status = match completed_flow_row {
Some(job) => job.parse_flow_status(),
None => {
let job_status_raw = sqlx::query_scalar!(
"SELECT flow_status FROM completed_job WHERE id = $1 AND workspace_id = $2",
completed_flow_id,
w_id,
)
.fetch_one(db)
.await?;
job_status_raw
.map(|raw| serde_json::from_value::<FlowStatus>(raw).ok())
.flatten()
}
};
let flow_job_status_modules = windmill_common::utils::not_found_if_none(
flow_status.map(|fs| fs.modules),
"Flow result by id in leaf jobs",
format!("{}", completed_flow_id),
)?;
let children_jobs = sqlx::query_as::<_, (Uuid, JobKind)>(
"SELECT id, job_kind FROM completed_job WHERE parent_job = $1 AND workspace_id = $2",
)
.bind(completed_flow_id)
.bind(w_id)
.fetch_all(db)
.await?;
for child_job in children_jobs {
let child_job_id = child_job.0;
let child_job_kind = child_job.1;
match child_job_kind {
JobKind::Script | JobKind::Preview | JobKind::Script_Hub => {
// if is potentially a leaf job. Get its step_id from the initial flow definition and add it to the result map
for module in &flow_job_status_modules {
if module.job().map(|id| id == child_job_id).unwrap_or(false) {
recursive_result.insert(module.id(), JobResult::SingleJob(child_job_id));
}
}
}
JobKind::Flow | JobKind::FlowPreview | JobKind::SingleScriptFlow => {
// Extract the leaf job for this flow and add them to the result map
for module in &flow_job_status_modules {
// we add the module as an element of ListJob for this step ID and recursiively extract leaf job of the sub-flow
match module {
FlowStatusModule::Success { flow_jobs: Some(jobs), .. } => {
let jobs_set: HashSet<Uuid> = HashSet::from_iter(jobs.iter().cloned());
if jobs_set.contains(&child_job_id) {
let new_list_job = match recursive_result.get(&module.id()) {
Some(JobResult::ListJob(jobs_list)) => {
let mut jobs_list_c = jobs_list.clone();
jobs_list_c.push(child_job_id);
jobs_list_c
}
_ => vec![child_job_id],
};
recursive_result
.insert(module.id(), JobResult::ListJob(new_list_job));
}
}
_ => {}
}
}
compute_leaf_jobs_for_completed_flow(
db,
w_id,
child_job_id,
None,
recursive_result,
)
.await?;
}
_ => {} // do nothing
}
}
Ok(())
}
async fn extract_result_from_job_result(
@@ -2425,27 +2329,52 @@ async fn extract_result_from_job_result(
json_path: Option<String>,
) -> error::Result<Box<RawValue>> {
match job_result {
JobResult::ListJob(job_ids) => {
let rows = sqlx::query_as::<_, ResultWithId>(
"SELECT id, result FROM completed_job WHERE id = ANY($1) AND workspace_id = $2",
)
.bind(job_ids.as_slice())
.bind(w_id)
.fetch_all(db)
.await?
.into_iter()
.filter_map(|x| x.result.map(|y| (x.id, y)))
.collect::<HashMap<Uuid, Json<Box<RawValue>>>>();
let result = job_ids
JobResult::ListJob(job_ids) => match json_path {
Some(json_path) => {
let mut parts = json_path.split(".");
let Some(idx) = parts.next().map(|x| x.parse::<usize>().ok()).flatten() else {
return Ok(to_raw_value(&serde_json::Value::Null));
};
let Some(job_id) = job_ids.get(idx).cloned() else {
return Ok(to_raw_value(&serde_json::Value::Null));
};
Ok(sqlx::query_as::<_, ResultR>(
"SELECT result #> $3 as result FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
.bind(job_id)
.bind(w_id)
.bind(
parts.map(|x| x.to_string()).collect::<Vec<_>>()
)
.fetch_optional(db)
.await?
.map(|r| r.result.map(|x| x.0))
.flatten()
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null)))
}
None => {
let rows = sqlx::query_as::<_, ResultWithId>(
"SELECT id, result FROM completed_job WHERE id = ANY($1) AND workspace_id = $2",
)
.bind(job_ids.as_slice())
.bind(w_id)
.fetch_all(db)
.await?
.into_iter()
.map(|id| {
rows.get(&id)
.map(|x| x.0.clone())
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null))
})
.collect::<Vec<_>>();
Ok(to_raw_value(&result))
}
.filter_map(|x| x.result.map(|y| (x.id, y)))
.collect::<HashMap<Uuid, Json<Box<RawValue>>>>();
let result = job_ids
.into_iter()
.map(|id| {
rows.get(&id)
.map(|x| x.0.clone())
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null))
})
.collect::<Vec<_>>();
Ok(to_raw_value(&result))
}
},
JobResult::SingleJob(x) => Ok(sqlx::query_as::<_, ResultR>(
"SELECT result #> $3 as result FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
@@ -2861,6 +2790,14 @@ where
Error::BadRequest(format!("Cloud events batching is not supported yet"))
.into_response(),
)
} else if content_type.unwrap().starts_with("text/plain") {
let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
let str = String::from_utf8(bytes.to_vec())
.map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())?;
extra.insert("raw_string".to_string(), to_raw_value(&str));
Ok(PushArgsOwned { extra: Some(extra), args: HashMap::new() })
} else if content_type
.unwrap()
.starts_with("application/x-www-form-urlencoded")
@@ -3640,7 +3577,13 @@ pub async fn push<'c, 'd, R: rsmq_async::RsmqConnection + Send + 'c>(
.map(|e| (Some(e.0), e.1))
.unwrap_or_else(|| (None, None));
let per_workspace: bool = DEFAULT_TAGS_PER_WORKSPACE.load(std::sync::atomic::Ordering::Relaxed);
let per_workspace_workspaces = DEFAULT_TAGS_WORKSPACES.read().await;
let per_workspace = DEFAULT_TAGS_PER_WORKSPACE.load(std::sync::atomic::Ordering::Relaxed)
&& (per_workspace_workspaces.is_none()
|| per_workspace_workspaces
.as_ref()
.unwrap()
.contains(&workspace_id.to_string()));
let tag = if dedicated_worker.is_some_and(|x| x) {
format!(
@@ -3687,10 +3630,15 @@ pub async fn push<'c, 'd, R: rsmq_async::RsmqConnection + Send + 'c>(
language
.as_ref()
.map(|x| {
if per_workspace {
format!("{}-{}", x.as_str(), workspace_id)
let tag_lang = if x == &ScriptLang::Bunnative {
ScriptLang::Nativets.as_str()
} else {
x.as_str().to_string()
x.as_str()
};
if per_workspace {
format!("{}-{}", tag_lang, workspace_id)
} else {
tag_lang.to_string()
}
})
.unwrap_or_else(default)
@@ -1,8 +1,8 @@
use std::fmt::Debug;
use std::{fmt::Debug, future::Future, pin::Pin};
use futures_core::{future::BoxFuture, stream::BoxStream};
use rsmq_async::{RedisBytes, RsmqConnection};
use sqlx::{Postgres, Transaction};
use sqlx::{Database, Postgres, Transaction};
pub enum RedisOp {
SendMessage(RedisBytes, Option<chrono::DateTime<chrono::Utc>>, String),
@@ -138,15 +138,20 @@ impl<'c, 'b, R: RsmqConnection + Send> sqlx::Executor<'b> for &'b mut QueueTrans
self.transaction.fetch_optional(query)
}
fn prepare_with<'e, 'q: 'e>(
fn prepare_with<'e, 'q>(
self,
sql: &'q str,
parameters: &'e [<Self::Database as sqlx::Database>::TypeInfo],
) -> BoxFuture<
'e,
Result<<Self::Database as sqlx::database::HasStatement<'q>>::Statement, sqlx::Error>,
parameters: &'e [<Self::Database as Database>::TypeInfo],
) -> Pin<
Box<
dyn Future<Output = Result<<Self::Database as Database>::Statement<'q>, sqlx::Error>>
+ Send
+ 'e,
>,
>
where
'q: 'e,
'c: 'e,
'b: 'e,
{
self.transaction.prepare_with(sql, parameters)
@@ -151,7 +151,7 @@ iface_no_lo: true
mount {
src: "{CACHE_DIR}"
dst: "/tmp/windmill/cache/bun"
dst: "/tmp/windmill/cache_nomount/bun"
is_bind: true
mandatory: false
rw: true
+189 -118
View File
@@ -1,9 +1,14 @@
use futures::TryFutureExt;
use std::collections::HashMap;
use futures::future::BoxFuture;
use futures::{FutureExt, TryFutureExt};
use serde_json::{json, value::RawValue, Value};
use windmill_common::error::to_anyhow;
use windmill_common::jobs::QueuedJob;
use windmill_common::{error::Error, worker::to_raw_value};
use windmill_parser_sql::{parse_bigquery_sig, parse_db_resource};
use windmill_parser_sql::{
parse_bigquery_sig, parse_db_resource, parse_sql_blocks, parse_sql_statement_named_params,
};
use windmill_queue::{CanceledBy, HTTP_CLIENT};
use serde::Deserialize;
@@ -57,119 +62,35 @@ struct BigqueryError {
message: String,
}
pub async fn do_bigquery(
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
query: &str,
db: &sqlx::Pool<sqlx::Postgres>,
mem_peak: &mut i32,
canceled_by: &mut Option<CanceledBy>,
worker_name: &str,
column_order: &mut Option<Vec<String>>,
) -> windmill_common::error::Result<Box<RawValue>> {
let bigquery_args = build_args_values(job, client, db).await?;
fn do_bigquery_inner<'a>(
query: &'a str,
all_statement_values: &'a HashMap<String, Value>,
project_id: &'a str,
token: &'a str,
timeout_ms: i32,
column_order: Option<&'a mut Option<Vec<String>>>,
) -> windmill_common::error::Result<BoxFuture<'a, windmill_common::error::Result<Box<RawValue>>>> {
let param_names = parse_sql_statement_named_params(query, '@');
let inline_db_res_path = parse_db_resource(&query);
let statement_values = all_statement_values
.iter()
.filter_map(|(name, val)| {
if param_names.contains(name) {
Some(val)
} else {
None
}
})
.collect::<Vec<&Value>>();
let db_arg = if let Some(inline_db_res_path) = inline_db_res_path {
Some(
client
.get_authed()
.await
.get_resource_value_interpolated::<serde_json::Value>(
&inline_db_res_path,
Some(job.id.to_string()),
)
.await?,
)
} else {
bigquery_args.get("database").cloned()
};
let database = if let Some(db) = db_arg {
db.to_string()
} else {
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let service_account = CustomServiceAccount::from_json(&database)
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let authentication_manager = AuthenticationManager::from(service_account);
let scopes = &["https://www.googleapis.com/auth/bigquery"];
let token = authentication_manager
.get_token(scopes)
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let mut statement_values: Vec<Value> = vec![];
let sig = parse_bigquery_sig(&query)
.map_err(|x| Error::ExecutionErr(x.to_string()))?
.args;
for arg in &sig {
let arg_t = arg.otyp.clone().unwrap_or_else(|| "string".to_string());
let arg_n = arg.clone().name;
let arg_v = bigquery_args.get(&arg.name).cloned().unwrap_or(json!(""));
let bigquery_v = if arg_t.ends_with("[]") {
let base_type = arg_t.strip_suffix("[]").unwrap_or(&arg_t);
json!({
"name": arg.name,
"parameterType": {
"type": "ARRAY",
"arrayType": {
"type": base_type.to_uppercase()
}
},
"parameterValue": {
"arrayValues": bigquery_args
.get(&arg.name)
.unwrap_or(&json!([]))
.as_array()
.unwrap_or(&vec![])
.iter()
.map(|x| {
convert_val(base_type.to_string(), x.clone())
})
.collect::<Vec<Value>>()
}
})
} else {
json!({
"name": arg_n,
"parameterType": {
"type": arg_t.to_uppercase()
},
"parameterValue": {
"value": convert_val(arg_t, arg_v),
}
})
};
statement_values.push(bigquery_v);
}
let timeout_ms = i32::try_from(
resolve_job_timeout(&db, &job.workspace_id, job.id, job.timeout)
.await
.0
.as_millis(),
)
.unwrap_or(200000);
let result_f = async {
let result_f = async move {
let response = HTTP_CLIENT
.post(
"https://bigquery.googleapis.com/bigquery/v2/projects/".to_string()
+ authentication_manager
.project_id()
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?
.as_str()
+ project_id
+ "/queries",
)
.bearer_auth(token.as_str())
.bearer_auth(token)
.json(&json!({
"query": query,
"useLegacySql": false,
@@ -222,16 +143,18 @@ pub async fn do_bigquery(
));
}
*column_order = Some(
result
.schema
.as_ref()
.unwrap()
.fields
.iter()
.map(|x| x.name.clone())
.collect::<Vec<String>>(),
);
if let Some(column_order) = column_order {
*column_order = Some(
result
.schema
.as_ref()
.unwrap()
.fields
.iter()
.map(|x| x.name.clone())
.collect::<Vec<String>>(),
);
}
let rows = result
.rows
@@ -268,6 +191,154 @@ pub async fn do_bigquery(
},
}
};
Ok(result_f.boxed())
}
pub async fn do_bigquery(
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
query: &str,
db: &sqlx::Pool<sqlx::Postgres>,
mem_peak: &mut i32,
canceled_by: &mut Option<CanceledBy>,
worker_name: &str,
column_order: &mut Option<Vec<String>>,
) -> windmill_common::error::Result<Box<RawValue>> {
let bigquery_args = build_args_values(job, client, db).await?;
let inline_db_res_path = parse_db_resource(&query);
let db_arg = if let Some(inline_db_res_path) = inline_db_res_path {
Some(
client
.get_authed()
.await
.get_resource_value_interpolated::<serde_json::Value>(
&inline_db_res_path,
Some(job.id.to_string()),
)
.await?,
)
} else {
bigquery_args.get("database").cloned()
};
let database = if let Some(db) = db_arg {
db.to_string()
} else {
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let service_account = CustomServiceAccount::from_json(&database)
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let authentication_manager = AuthenticationManager::from(service_account);
let scopes = &["https://www.googleapis.com/auth/bigquery"];
let token = authentication_manager
.get_token(scopes)
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let timeout_ms = i32::try_from(
resolve_job_timeout(&db, &job.workspace_id, job.id, job.timeout)
.await
.0
.as_millis(),
)
.unwrap_or(200000);
let project_id = authentication_manager
.project_id()
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let queries = parse_sql_blocks(query);
let mut statement_values: HashMap<String, Value> = HashMap::new();
let sig = parse_bigquery_sig(&query)
.map_err(|x| Error::ExecutionErr(x.to_string()))?
.args;
for arg in &sig {
let arg_t = arg.otyp.clone().unwrap_or_else(|| "string".to_string());
let arg_n = arg.clone().name;
let arg_v = bigquery_args.get(&arg.name).cloned().unwrap_or(json!(""));
let bigquery_v = if arg_t.ends_with("[]") {
let base_type = arg_t.strip_suffix("[]").unwrap_or(&arg_t);
json!({
"name": arg.name,
"parameterType": {
"type": "ARRAY",
"arrayType": {
"type": base_type.to_uppercase()
}
},
"parameterValue": {
"arrayValues": bigquery_args
.get(&arg.name)
.unwrap_or(&json!([]))
.as_array()
.unwrap_or(&vec![])
.iter()
.map(|x| {
convert_val(base_type.to_string(), x.clone())
})
.collect::<Vec<Value>>()
}
})
} else {
json!({
"name": arg_n,
"parameterType": {
"type": arg_t.to_uppercase()
},
"parameterValue": {
"value": convert_val(arg_t, arg_v),
}
})
};
statement_values.insert(arg_n, bigquery_v);
}
let result_f = if queries.len() > 1 {
let futures = queries
.iter()
.map(|x| {
do_bigquery_inner(
x,
&statement_values,
&project_id,
token.as_str(),
timeout_ms,
None,
)
})
.collect::<windmill_common::error::Result<Vec<_>>>()?;
let f = async {
let mut res: Vec<Box<RawValue>> = vec![];
for fut in futures {
let r = fut.await?;
res.push(r);
}
Ok(to_raw_value(&res))
};
f.boxed()
} else {
do_bigquery_inner(
query,
&statement_values,
&project_id,
token.as_str(),
timeout_ms,
Some(column_order),
)?
};
let r = run_future_with_polling_update_job_poller(
job.id,
job.timeout,
+514 -119
View File
@@ -1,4 +1,4 @@
use std::{collections::HashMap, process::Stdio};
use std::{collections::HashMap, fs, io, path::Path, process::Stdio, time::Instant};
use base64::Engine;
use itertools::Itertools;
@@ -14,11 +14,12 @@ use crate::common::build_envs_map;
use crate::{
common::{
create_args_and_out_file, get_main_override, get_reserved_variables, handle_child,
parse_npm_config, read_result, start_child_process, write_file, write_file_binary,
parse_npm_config, read_file_content, read_result, start_child_process, write_file,
write_file_binary,
},
AuthedClientBackgroundTask, BUNFIG_INSTALL_SCOPES, BUN_CACHE_DIR, BUN_PATH, BUN_TAR_CACHE_DIR,
DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NODE_PATH, NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH,
PATH_ENV, TZ_ENV,
AuthedClientBackgroundTask, BUNFIG_INSTALL_SCOPES, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR,
BUN_DEPSTAR_CACHE_DIR, BUN_PATH, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NODE_PATH,
NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH, PATH_ENV, TZ_ENV,
};
use tokio::{fs::File, process::Command};
@@ -33,7 +34,11 @@ use windmill_common::variables;
use windmill_common::{
error::{self, Result},
get_latest_hash_for_path,
jobs::QueuedJob,
scripts::ScriptLang,
worker::{exists_in_cache, get_annotation, save_cache},
DB,
};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
@@ -68,6 +73,8 @@ pub async fn gen_lockfile(
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url).await;
let mut empty_deps = false;
if let Some(raw_deps) = raw_deps {
gen_bunfig(job_dir).await?;
write_file(job_dir, "package.json", raw_deps.as_str()).await?;
@@ -120,37 +127,49 @@ pub async fn gen_lockfile(
false,
)
.await?;
let new_package_json = read_file_content(&format!("{job_dir}/package.json")).await?;
empty_deps = new_package_json
== r#"{
"dependencies": {}
}"#;
}
install_lockfile(
mem_peak,
canceled_by,
job_id,
w_id,
db,
job_dir,
worker_name,
common_bun_proc_envs,
npm_mode,
)
.await?;
if !empty_deps {
install_lockfile(
mem_peak,
canceled_by,
job_id,
w_id,
db,
job_dir,
worker_name,
common_bun_proc_envs,
npm_mode,
)
.await?;
} else {
append_logs(job_id, w_id, "\nempty dependencies, skipping install", db).await;
}
if export_pkg && !npm_mode {
if export_pkg {
let mut content = "".to_string();
{
let mut file = File::open(format!("{job_dir}/package.json")).await?;
file.read_to_string(&mut content).await?;
}
content.push_str(BUN_LOCKB_SPLIT);
{
let file = format!("{job_dir}/bun.lockb");
if tokio::fs::metadata(&file).await.is_ok() {
let mut file = File::open(&file).await?;
let mut buf = vec![];
file.read_to_end(&mut buf).await?;
content.push_str(&base64::engine::general_purpose::STANDARD.encode(&buf));
} else {
content.push_str(&EMPTY_FILE);
if !npm_mode {
content.push_str(BUN_LOCKB_SPLIT);
{
let file = format!("{job_dir}/bun.lockb");
if !empty_deps && tokio::fs::metadata(&file).await.is_ok() {
let mut file = File::open(&file).await?;
let mut buf = vec![];
file.read_to_end(&mut buf).await?;
content.push_str(&base64::engine::general_purpose::STANDARD.encode(&buf));
} else {
content.push_str(&EMPTY_FILE);
}
}
}
Ok(Some(content))
@@ -279,30 +298,21 @@ pub async fn install_lockfile(
Ok(())
}
struct Annotations {
npm_mode: bool,
nodejs_mode: bool,
#[derive(PartialEq)]
enum LoaderMode {
Node,
Bun,
BunBundle,
NodeBundle,
BrowserBundle,
}
fn get_annotation(inner_content: &str) -> Annotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("//"))
.map(|x| x.to_string().replace("//", "").trim().to_string())
.collect_vec();
let nodejs_mode: bool = annotations.contains(&"nodejs".to_string());
let npm_mode: bool = annotations.contains(&"npm".to_string());
Annotations { npm_mode, nodejs_mode }
}
pub async fn build_loader(
async fn build_loader(
job_dir: &str,
base_internal_url: &str,
token: &str,
w_id: &str,
current_path: &str,
nodejs_mode: bool,
mode: LoaderMode,
) -> Result<()> {
let loader = RELATIVE_BUN_LOADER
.replace("W_ID", w_id)
@@ -313,7 +323,7 @@ pub async fn build_loader(
&crate::common::use_flow_root_path(current_path),
)
.replace("RAW_GET_ENDPOINT", "raw_unpinned");
if nodejs_mode {
if mode == LoaderMode::Node {
write_file(
&job_dir,
"node_builder.ts",
@@ -336,6 +346,7 @@ const bo = await Bun.build({{
target: "node",
plugins: [p],
external: fileNames,
minify: true,
}});
if (!bo.success) {{
@@ -347,7 +358,7 @@ if (!bo.success) {{
),
)
.await?;
} else {
} else if mode == LoaderMode::Bun {
write_file(
&job_dir,
"loader.bun.js",
@@ -363,7 +374,47 @@ plugin(p)
),
)
.await?;
};
} else if mode == LoaderMode::BunBundle
|| mode == LoaderMode::NodeBundle
|| mode == LoaderMode::BrowserBundle
{
write_file(
&job_dir,
"node_builder.ts",
&format!(
r#"
{}
const bo = await Bun.build({{
entrypoints: ["{job_dir}/main.ts"],
outdir: "./",
target: "{}",
plugins: [p],
external: ["electron"],
minify: {{
identifiers: false,
syntax: true,
whitespace: false
}},
}});
if (!bo.success) {{
bo.logs.forEach((l) => console.log(l));
process.exit(1);
}}
"#,
loader,
if mode == LoaderMode::BunBundle {
"bun"
} else if mode == LoaderMode::NodeBundle {
"node"
} else {
"browser"
}
),
)
.await?;
}
Ok(())
}
@@ -402,15 +453,52 @@ pub async fn generate_wrapper_mjs(
false,
)
.await?;
tokio::fs::rename(
fs::rename(
format!("{job_dir}/wrapper.js"),
format!("{job_dir}/wrapper.mjs"),
)
.await
.map_err(|e| error::Error::InternalErr(format!("Could not move wrapper to mjs: {e:#}")))?;
Ok(())
}
pub async fn generate_bun_bundle(
job_dir: &str,
w_id: &str,
job_id: &Uuid,
worker_name: &str,
db: &sqlx::Pool<sqlx::Postgres>,
timeout: Option<i32>,
mem_peak: &mut i32,
canceled_by: &mut Option<CanceledBy>,
common_bun_proc_envs: &HashMap<String, String>,
) -> Result<()> {
let mut child = Command::new(&*BUN_PATH);
child
.current_dir(job_dir)
.env_clear()
.envs(common_bun_proc_envs.clone())
.env("PATH", PATH_ENV.as_str())
.args(vec!["run", "node_builder.ts"])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child_process = start_child_process(child, &*BUN_PATH).await?;
handle_child(
job_id,
db,
mem_peak,
canceled_by,
child_process,
false,
worker_name,
w_id,
"bun build",
timeout,
false,
)
.await?;
Ok(())
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
pub async fn pull_codebase(w_id: &str, id: &str, job_dir: &str) -> Result<()> {
let path = windmill_common::s3_helpers::bundle(&w_id, &id);
@@ -447,18 +535,151 @@ pub async fn pull_codebase(_w_id: &str, _id: &str, _job_dir: &str) -> Result<()>
));
}
fn untar_file(file_path: &str, output_dir: &str) -> anyhow::Result<()> {
// Open the tar file
let file = std::fs::File::open(file_path)?;
let file = std::io::BufReader::new(file);
#[cfg(unix)]
pub fn copy_recursively(
source: impl AsRef<Path>,
destination: impl AsRef<Path>,
skip: Option<&Vec<String>>,
) -> io::Result<()> {
let mut stack = Vec::new();
stack.push((
source.as_ref().to_path_buf(),
destination.as_ref().to_path_buf(),
0,
));
while let Some((current_source, current_destination, level)) = stack.pop() {
for entry in fs::read_dir(&current_source)? {
let entry = entry?;
let filetype = entry.file_type()?;
let destination = current_destination.join(entry.file_name());
if level == 0 {
if let Some(skip) = skip {
if skip.contains(&entry.file_name().to_string_lossy().to_string()) {
continue;
}
}
}
let original = entry.path();
// For a plain tar file, use it directly
let mut archive = tar::Archive::new(file);
archive.unpack(output_dir)?;
if filetype.is_dir() {
fs::create_dir_all(&destination)?;
stack.push((entry.path(), destination, level + 1));
} else {
fs::hard_link(&original, &destination)?
}
}
}
Ok(())
}
pub async fn prebundle_script(
inner_content: &str,
lockfile: Option<String>,
script_path: &str,
job_id: &Uuid,
w_id: &str,
db: &DB,
job_dir: &str,
base_internal_url: &str,
worker_name: &str,
token: &str,
) -> Result<()> {
let (local_path, remote_path) =
compute_bundle_local_and_remote_path(inner_content, &lockfile, script_path, db, w_id).await;
if exists_in_cache(&local_path, &remote_path).await {
return Ok(());
}
let annotation = get_annotation(inner_content);
if annotation.nobundling {
return Ok(());
}
let origin = format!("{job_dir}/main.js");
write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?).await?;
build_loader(
job_dir,
base_internal_url,
&token,
w_id,
script_path,
if annotation.nodejs_mode {
LoaderMode::NodeBundle
} else if annotation.native_mode {
LoaderMode::BrowserBundle
} else {
LoaderMode::BunBundle
},
)
.await?;
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url).await;
generate_bun_bundle(
job_dir,
w_id,
job_id,
worker_name,
db,
None,
&mut 0,
&mut None,
&common_bun_proc_envs,
)
.await?;
save_cache(&local_path, &remote_path, &origin).await?;
Ok(())
}
pub const BUN_BUNDLE_OBJECT_STORE_PREFIX: &str = "bun_bundle/";
async fn get_script_import_updated_at(db: &DB, w_id: &str, script_path: &str) -> Result<String> {
let script_hash = get_latest_hash_for_path(&mut db.begin().await?, w_id, script_path).await?;
let last_updated_at = sqlx::query_scalar!(
"SELECT created_at FROM script WHERE workspace_id = $1 AND hash = $2",
w_id,
script_hash.0 .0
)
.fetch_one(db)
.await?;
Ok(last_updated_at.to_string())
}
async fn compute_bundle_local_and_remote_path(
inner_content: &str,
requirements_o: &Option<String>,
script_path: &str,
db: &DB,
w_id: &str,
) -> (String, String) {
let mut input_src = format!(
"{}{}",
inner_content,
requirements_o
.as_ref()
.map(|x| x.to_string())
.unwrap_or_default()
);
let relative_imports = crate::worker_lockfiles::extract_relative_imports(
&inner_content,
script_path,
&Some(ScriptLang::Bun),
);
for path in relative_imports.unwrap_or_default() {
if let Ok(updated_at) = get_script_import_updated_at(db, w_id, &path).await {
input_src.push_str(&path);
input_src.push_str(&updated_at.to_string());
}
}
let hash = windmill_common::utils::calculate_hash(&input_src);
let local_path = format!("{BUN_BUNDLE_CACHE_DIR}/{hash}");
let remote_path = format!("{BUN_BUNDLE_OBJECT_STORE_PREFIX}{hash}");
(local_path, remote_path)
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_bun_job(
requirements_o: Option<String>,
@@ -475,17 +696,35 @@ pub async fn handle_bun_job(
envs: HashMap<String, String>,
shared_mount: &str,
) -> error::Result<Box<RawValue>> {
if !codebase.is_some() {
let mut annotation = windmill_common::worker::get_annotation(inner_content);
let (mut has_bundle_cache, cache_logs, local_path, remote_path) =
if requirements_o.is_some() && !annotation.nobundling && codebase.is_none() {
let (local_path, remote_path) = compute_bundle_local_and_remote_path(
inner_content,
&requirements_o,
job.script_path(),
db,
&job.workspace_id,
)
.await;
let (cache, logs) =
windmill_common::worker::load_cache(&local_path, &remote_path).await;
(cache, logs, local_path, remote_path)
} else {
(false, "".to_string(), "".to_string(), "".to_string())
};
if !codebase.is_some() && !has_bundle_cache {
let _ = write_file(job_dir, "main.ts", inner_content).await?;
} else {
} else if !annotation.native_mode && codebase.is_none() {
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#).await?;
};
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url).await;
let mut annotation = get_annotation(inner_content);
if codebase.is_some() {
annotation.nodejs_mode = true
}
@@ -499,47 +738,62 @@ pub async fn handle_bun_job(
}
let mut gbuntar_name = None;
if let Some(codebase) = codebase.as_ref() {
if has_bundle_cache {
let target = format!("{job_dir}/main.js");
std::os::unix::fs::symlink(&local_path, &target).map_err(|e| {
error::Error::ExecutionErr(format!(
"could not copy cached binary from {local_path} to {job_dir}/main: {e:?}"
))
})?;
} else if let Some(codebase) = codebase.as_ref() {
pull_codebase(&job.workspace_id, codebase, job_dir).await?;
} else if let Some(reqs) = requirements_o {
} else if let Some(reqs) = requirements_o.as_ref() {
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
if splitted.len() != 2 {
if splitted.len() != 2 && !annotation.npm_mode {
return Err(error::Error::ExecutionErr(
format!("Invalid requirements, expected to find //bun.lockb split pattern in reqs. Found: |{reqs}|")
));
}
let _ = write_file(job_dir, "package.json", &splitted[0]).await?;
let lockb = splitted[1];
let lockb = if annotation.npm_mode { "" } else { splitted[1] };
if lockb != EMPTY_FILE {
let _ = write_file_binary(
job_dir,
"bun.lockb",
&base64::engine::general_purpose::STANDARD
.decode(&splitted[1])
.map_err(|_| {
error::Error::InternalErr("Could not decode bun.lockb".to_string())
})?,
)
.await?;
let mut sha_path = sha2::Sha256::new();
sha_path.update(lockb.as_bytes());
let buntar_name = base64::engine::general_purpose::URL_SAFE.encode(sha_path.finalize());
let buntar_path = format!("{BUN_TAR_CACHE_DIR}/{buntar_name}.tar");
let mut skip_install = false;
let mut create_buntar = false;
if tokio::fs::metadata(&buntar_path).await.is_ok() {
if let Err(e) = untar_file(&buntar_path, job_dir) {
tracing::error!("Could not untar buntar: {e}");
let mut buntar_path = "".to_string();
if !annotation.npm_mode {
let _ = write_file_binary(
job_dir,
"bun.lockb",
&base64::engine::general_purpose::STANDARD
.decode(&splitted[1])
.map_err(|_| {
error::Error::InternalErr("Could not decode bun.lockb".to_string())
})?,
)
.await?;
let mut sha_path = sha2::Sha256::new();
sha_path.update(lockb.as_bytes());
let buntar_name =
base64::engine::general_purpose::URL_SAFE.encode(sha_path.finalize());
buntar_path = format!("{BUN_DEPSTAR_CACHE_DIR}/{buntar_name}");
#[cfg(unix)]
if tokio::fs::metadata(&buntar_path).await.is_ok() {
if let Err(e) = copy_recursively(&buntar_path, job_dir, None) {
tracing::error!("Could not extract buntar: {e:#}");
} else {
gbuntar_name = Some(buntar_name.clone());
skip_install = true;
}
} else {
gbuntar_name = Some(buntar_name.clone());
skip_install = true;
create_buntar = true;
}
} else {
create_buntar = true;
}
if !skip_install {
install_lockfile(
mem_peak,
@@ -554,13 +808,21 @@ pub async fn handle_bun_job(
)
.await?;
#[cfg(unix)]
if create_buntar {
let f = std::fs::File::create(&buntar_path);
if let Err(e) = f {
tracing::error!("Could not create buntar file {buntar_path}: {e}");
} else if let Err(e) =
tar::Builder::new(f.unwrap()).append_dir_all(".", job_dir)
{
fs::create_dir_all(&buntar_path)?;
if let Err(e) = copy_recursively(
job_dir,
&buntar_path,
Some(&vec![
"main.ts".to_string(),
"package.json".to_string(),
"bun.lockb".to_string(),
"shared".to_string(),
"bunfig.toml".to_string(),
]),
) {
fs::remove_dir_all(&buntar_path)?;
tracing::error!("Could not create buntar: {e}");
}
}
@@ -591,13 +853,23 @@ pub async fn handle_bun_job(
// }
}
let _ = write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?).await?;
let mut init_logs = if codebase.is_some() {
"\n\n--- NODE SNAPSHOT EXECUTION ---\n".to_string()
let mut init_logs = if annotation.native_mode {
"\n\n--- NATIVE CODE EXECUTION ---\n".to_string()
} else if has_bundle_cache {
if annotation.nodejs_mode {
"\n\n--- NODE BUNDLE SNAPSHOT EXECUTION ---\n".to_string()
} else {
"\n\n--- BUN BUNDLE SNAPSHOT EXECUTION ---\n".to_string()
}
} else if codebase.is_some() {
"\n\n--- NODE CODEBASE SNAPSHOT EXECUTION ---\n".to_string()
} else if annotation.native_mode {
"\n\n--- NATIVE CODE EXECUTION ---\n".to_string()
} else if annotation.nodejs_mode {
write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?).await?;
"\n\n--- NODE CODE EXECUTION ---\n".to_string()
} else {
write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?).await?;
"\n\n--- BUN CODE EXECUTION ---\n".to_string()
};
@@ -608,9 +880,14 @@ pub async fn handle_bun_job(
);
}
append_logs(&job.id, &job.workspace_id, init_logs, db).await;
if has_bundle_cache {
init_logs = format!("\n{}{}", cache_logs, init_logs);
}
let write_wrapper_f = async {
if !has_bundle_cache && annotation.native_mode {
return Ok(()) as error::Result<()>;
}
// let mut start = Instant::now();
let args =
windmill_parser_ts::parse_deno_signature(inner_content, true, main_override.clone())?
@@ -633,7 +910,7 @@ pub async fn handle_bun_job(
// we cannot use Bun.read and Bun.write because it results in an EBADF error on cloud
let main_name = main_override.unwrap_or("main".to_string());
let main_import = if codebase.is_some() {
let main_import = if codebase.is_some() || has_bundle_cache {
"./main.js"
} else {
"./main.ts"
@@ -662,6 +939,7 @@ async function run() {{
try {{
await run();
}} catch(e) {{
console.error(e);
let err = {{ message: e.message, name: e.name, stack: e.stack }};
let step_id = process.env.WM_FLOW_STEP_ID;
if (step_id) {{
@@ -677,6 +955,9 @@ try {{
};
let reserved_variables_args_out_f = async {
if annotation.native_mode {
return Ok(HashMap::new()) as error::Result<HashMap<String, String>>;
}
let args_and_out_f = async {
create_args_and_out_file(&client, job, job_dir, db).await?;
Ok(()) as Result<()>
@@ -690,15 +971,42 @@ try {{
Ok(reserved_variables) as error::Result<HashMap<String, String>>
};
let build_cache = !has_bundle_cache
&& !annotation.nobundling
&& !codebase.is_some()
&& (requirements_o.is_some() || annotation.native_mode);
let write_loader_f = async {
if !codebase.is_some() {
if build_cache {
build_loader(
job_dir,
base_internal_url,
&client.get_token().await,
&job.workspace_id,
&job.script_path(),
annotation.nodejs_mode,
if annotation.nodejs_mode {
LoaderMode::NodeBundle
} else if annotation.native_mode {
LoaderMode::BrowserBundle
} else {
LoaderMode::BunBundle
},
)
.await?;
Ok(())
} else if !codebase.is_some() && !has_bundle_cache {
build_loader(
job_dir,
base_internal_url,
&client.get_token().await,
&job.workspace_id,
&job.script_path(),
if annotation.nodejs_mode {
LoaderMode::Node
} else {
LoaderMode::Bun
},
)
.await
} else {
@@ -711,21 +1019,103 @@ try {{
write_wrapper_f,
write_loader_f
)?;
if annotation.nodejs_mode && !codebase.is_some() {
generate_wrapper_mjs(
job_dir,
&job.workspace_id,
&job.id,
worker_name,
db,
if !codebase.is_some() && !has_bundle_cache {
if build_cache {
generate_bun_bundle(
job_dir,
&job.workspace_id,
&job.id,
worker_name,
db,
job.timeout,
mem_peak,
canceled_by,
&common_bun_proc_envs,
)
.await?;
if !local_path.is_empty() {
match save_cache(&local_path, &remote_path, &format!("{job_dir}/main.js")).await {
Err(e) => {
let em = format!("could not save {local_path} to bundle cache: {e:?}");
tracing::error!(em)
}
Ok(logs) => {
init_logs.push_str(&"\n");
init_logs.push_str(&logs);
init_logs.push_str(&"\n");
tracing::info!("saved bun bundle cache: {logs}")
}
}
}
if !annotation.native_mode {
let ex_wrapper = read_file_content(&format!("{job_dir}/wrapper.mjs")).await?;
write_file(
job_dir,
"wrapper.mjs",
&ex_wrapper.replace(
"import * as Main from \"./main.ts\"",
"import * as Main from \"./main.js\"",
),
)
.await?;
write_file(job_dir, "package.json", r#"{ "type": "module" }"#).await?;
}
fs::remove_file(format!("{job_dir}/main.ts"))?;
has_bundle_cache = true;
} else if annotation.nodejs_mode {
generate_wrapper_mjs(
job_dir,
&job.workspace_id,
&job.id,
worker_name,
db,
job.timeout,
mem_peak,
canceled_by,
&common_bun_proc_envs,
)
.await?;
}
}
if annotation.native_mode {
let env_code = format!(
"const process = {{ env: {{}} }};\nconst BASE_URL = '{base_internal_url}';\nconst BASE_INTERNAL_URL = '{base_internal_url}';\nprocess.env['BASE_URL'] = BASE_URL;process.env['BASE_INTERNAL_URL'] = BASE_INTERNAL_URL;\n{}",
reserved_variables
.iter()
.map(|(k, v)| format!("process.env['{}'] = '{}';\n", k, v))
.collect::<Vec<String>>()
.join("\n"));
let js_code = read_file_content(&format!("{job_dir}/main.js")).await?;
let started_at = Instant::now();
let result = crate::js_eval::eval_fetch_timeout(
env_code,
inner_content.clone(),
js_code,
job.args.as_ref(),
job.id,
job.timeout,
db,
mem_peak,
canceled_by,
&common_bun_proc_envs,
worker_name,
&job.workspace_id,
false,
)
.await?;
tracing::info!(
"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);
}
append_logs(&job.id, &job.workspace_id, init_logs, db).await;
//do not cache local dependencies
let child = if !*DISABLE_NSJAIL {
@@ -767,13 +1157,14 @@ try {{
&NODE_PATH,
"/tmp/nodejs/wrapper.mjs",
]
} else if codebase.is_some() {
} else if codebase.is_some() || has_bundle_cache {
vec![
"--config",
"run.config.proto",
"--",
&BUN_PATH,
"run",
"--preserve-symlinks",
"/tmp/bun/wrapper.mjs",
]
} else {
@@ -812,7 +1203,7 @@ try {{
.envs(envs)
.envs(reserved_variables)
.envs(common_bun_proc_envs)
.args(vec![&script_path])
.args(vec!["--preserve-symlinks", &script_path])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
bun_cmd
@@ -820,7 +1211,7 @@ try {{
let script_path = format!("{job_dir}/wrapper.mjs");
let mut bun_cmd = Command::new(&*BUN_PATH);
let args = if codebase.is_some() {
let args = if codebase.is_some() || has_bundle_cache {
vec!["run", &script_path]
} else {
vec![
@@ -876,7 +1267,7 @@ pub async fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String
(String::from("PATH"), PATH_ENV.clone()),
(String::from("HOME"), HOME_ENV.clone()),
(String::from("TZ"), TZ_ENV.clone()),
(String::from("DISABLE_COLORS"), "0".to_string()),
(String::from("FORCE_COLOR"), "1".to_string()),
(String::from("DO_NOT_TRACK"), "1".to_string()),
(
String::from("BASE_URL"),
@@ -925,7 +1316,7 @@ pub async fn start_worker(
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url).await;
let mut annotation = get_annotation(inner_content);
let mut annotation = windmill_common::worker::get_annotation(inner_content);
//TODO: remove this when bun dedicated workers work without issues
annotation.nodejs_mode = true;
@@ -1079,7 +1470,11 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
token,
w_id,
script_path,
annotation.nodejs_mode,
if annotation.nodejs_mode {
LoaderMode::Node
} else {
LoaderMode::Bun
},
)
.await?;
}
+19 -113
View File
@@ -12,7 +12,7 @@ use windmill_common::{
error::{self, Error},
jobs::QueuedJob,
utils::calculate_hash,
worker::CLOUD_HOSTED,
worker::{save_cache, CLOUD_HOSTED},
};
use windmill_parser_go::{parse_go_imports, REQUIRE_PARSE};
use windmill_queue::{append_logs, CanceledBy};
@@ -33,111 +33,7 @@ lazy_static::lazy_static! {
static ref GO_PATH: String = std::env::var("GO_PATH").unwrap_or_else(|_| "/usr/bin/go".to_string());
}
pub async fn save_cache(
bin_path: &str,
job_dir: &str,
_hash: &str,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
) -> windmill_common::error::Result<()> {
let job_main_path = format!("{job_dir}/main");
let mut _cached_to_s3 = false;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
use object_store::path::Path;
let hash_path = hash_to_os_path(_hash);
if let Err(e) = os
.put(
&Path::from(hash_path.clone()),
std::fs::read(&job_main_path)?.into(),
)
.await
{
tracing::error!(
"Failed to put go bin to object store: {hash_path}. Error: {:?}",
e
);
} else {
_cached_to_s3 = true;
}
}
if !*CLOUD_HOSTED {
tokio::fs::copy(&job_main_path, bin_path).await?;
append_logs(
&job.id,
&job.workspace_id,
format!(
"\nwrite cached binary: {} (backed by object store: {_cached_to_s3})\n",
bin_path
),
db,
)
.await;
} else if _cached_to_s3 {
append_logs(
&job.id,
&job.workspace_id,
format!("write cached binary to object store {}\n", bin_path),
db,
)
.await;
}
Ok(())
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
async fn write_binary_file(main_path: &str, byts: &mut bytes::Bytes) -> error::Result<()> {
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
use tokio::io::AsyncWriteExt;
let mut file = File::create(main_path).await?;
file.write_all_buf(byts).await?;
file.set_permissions(Permissions::from_mode(0o755)).await?;
file.flush().await?;
Ok(())
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
fn hash_to_os_path(hash: &str) -> String {
format!("gobin/{hash}")
}
async fn load_cache(bin_path: &str, _hash: &str) -> (bool, String) {
if tokio::fs::metadata(&bin_path).await.is_ok() {
(true, format!("loaded bin from local cache: {}\n", bin_path))
} else {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
use windmill_common::s3_helpers::attempt_fetch_bytes;
if let Ok(mut x) = attempt_fetch_bytes(os, &hash_to_os_path(_hash)).await {
if let Err(e) = write_binary_file(bin_path, &mut x).await {
tracing::error!("could not write binary file: {e:?}");
return (
false,
"error writing binary file from object store".to_string(),
);
}
tracing::info!("loaded bin from object store {}", bin_path);
return (true, format!("loaded bin from object store {}", bin_path));
}
}
(false, "".to_string())
}
}
pub const GO_OBJECT_STORE_PREFIX: &str = "gobin/";
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_go_job(
mem_peak: &mut i32,
@@ -163,9 +59,9 @@ pub async fn handle_go_job(
.map(|x| x.to_string())
.unwrap_or_default()
));
let bin_path = format!("{}/{hash}", GO_BIN_CACHE_DIR,);
let (cache, cache_logs) = load_cache(&bin_path, &hash).await;
let bin_path = format!("{}/{hash}", GO_BIN_CACHE_DIR);
let remote_path = format!("{GO_OBJECT_STORE_PREFIX}{hash}");
let (cache, cache_logs) = windmill_common::worker::load_cache(&bin_path, &remote_path).await;
let (skip_go_mod, skip_tidy) = if cache {
create_dir(job_dir).await?;
@@ -309,13 +205,23 @@ func Run(req Req) (interface{{}}, error){{
)
.await?;
if let Err(e) = save_cache(&bin_path, &job_dir, &hash, &job, db).await {
tracing::error!("could not save {bin_path} to go cache: {e:?}");
match save_cache(
&bin_path,
&format!("{GO_OBJECT_STORE_PREFIX}{hash}"),
&format!("{job_dir}/main"),
)
.await
{
Err(e) => {
let em = format!("could not save {bin_path} to go cache: {e:?}");
tracing::error!(em);
em
}
Ok(logs) => logs,
}
"".to_string()
} else {
let target = format!("{job_dir}/main");
tokio::fs::symlink(&bin_path, &target).await.map_err(|e| {
std::os::unix::fs::symlink(&bin_path, &target).map_err(|e| {
Error::ExecutionErr(format!(
"could not copy cached binary from {bin_path} to {job_dir}/main: {e:?}"
))
+121 -43
View File
@@ -65,9 +65,9 @@ impl ContainerRootCertStoreProvider {
fn add_certificate(&mut self, cert_path: String) -> io::Result<()> {
let cert_file = std::fs::File::open(cert_path)?;
let mut reader = BufReader::new(cert_file);
let pem_file = rustls_pemfile::certs(&mut reader)?;
let pem_file = rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()?;
self.root_cert_store.add_parsable_certificates(&pem_file);
self.root_cert_store.add_parsable_certificates(pem_file);
Ok(())
}
}
@@ -189,22 +189,25 @@ pub async fn eval_timeout(
}
if by_id.is_some() && authed_client.is_some() {
if let Some(x) = RE_FULL
.captures(&expr)
.and_then(|x| x.get(1).map(|y| y.as_str()))
{
// tracing::error!("{:?}", x.split(".").collect::<Vec<_>>());
let arr = x.split(".").collect::<Vec<_>>();
let mut iter = arr.iter();
iter.next();
if let Some(id) = iter.next() {
let path = iter.join(".");
let query = if path.is_empty() { None } else { Some(path) };
return authed_client
.unwrap()
.get_result_by_id(&by_id.as_ref().unwrap().flow_job.to_string(), id, query)
.await;
}
if let Some((id, idx_o, rest)) = RE_FULL.captures(&expr).map(|x| {
(
x.get(1).unwrap().as_str(),
x.get(2).map(|y| y.as_str()),
x.get(3).map(|y| y.as_str()),
)
}) {
let query = if let Some(idx) = idx_o {
match rest {
Some(rest) => Some(format!("{}{}", idx, rest)),
None => Some(idx.to_string()),
}
} else {
rest.map(|x| x.trim_start_matches('.').to_string())
};
return authed_client
.unwrap()
.get_result_by_id(&by_id.as_ref().unwrap().flow_job.to_string(), id, query)
.await;
}
}
@@ -334,10 +337,11 @@ fn replace_with_await(expr: String, fn_name: &str) -> String {
}
lazy_static! {
static ref RE: Regex =
Regex::new(r#"(?m)(?P<r>results(?:(?:\.(?:[a-z]|[A-Z]|_|[1-9])+)|(?:\[\".*?\"\])))"#)
.unwrap();
Regex::new(r#"(?m)(?P<r>results(?:(?:\.[a-zA-Z_0-9]+)|(?:\[\".*?\"\])))"#).unwrap();
static ref RE_FULL: Regex =
Regex::new(r"(?m)^results((?:\.(?:(?:[a-z]|[A-Z]|_|[1-9])+))+)$").unwrap();
Regex::new(r"(?m)^results\.([a-zA-Z_0-9]+)(?:\[(\d+)\])?((?:\.[a-zA-Z_0-9]+)+)?$").unwrap();
static ref RE_PROXY: Regex =
Regex::new(r"^(https?)://(([^:@\s]+):([^:@\s]+)@)?([^:@\s]+)(:(\d+))?$").unwrap();
}
fn replace_with_await_result(expr: String) -> String {
@@ -626,6 +630,51 @@ pub struct LogString {
pub s: String,
}
pub struct NativeAnnotation {
pub useragent: Option<String>,
pub proxy: Option<(String, Option<(String, String)>)>,
}
pub fn get_annotation(inner_content: &str) -> NativeAnnotation {
let mut res = NativeAnnotation { useragent: None, proxy: None };
let anns = inner_content
.lines()
.take_while(|x| x.starts_with("//"))
.map(|x| x.to_string().trim_start_matches("//").trim().to_string())
.collect_vec();
for ann in anns.iter() {
if ann.starts_with("useragent") {
res.useragent = Some(ann.trim_start_matches("useragent").trim().to_string());
} else if ann.starts_with("proxy") {
res.proxy = capture_proxy(ann.trim_start_matches("proxy").trim());
}
}
res
}
fn capture_proxy(s: &str) -> Option<(String, Option<(String, String)>)> {
RE_PROXY.captures(s).map(|x| {
(
format!(
"{}://{}{}",
x.get(1).map(|x| x.as_str()).unwrap_or_default(),
x.get(5).map(|x| x.as_str()).unwrap_or_default(),
x.get(7)
.map(|x| format!(":{}", x.as_str()))
.unwrap_or_default(),
),
x.get(3).map(|y| {
(
y.as_str().to_string(),
x.get(4).map(|x| x.as_str().to_string()).unwrap_or_default(),
)
}),
)
})
}
pub async fn eval_fetch_timeout(
env_code: String,
ts_expr: String,
@@ -638,6 +687,7 @@ pub async fn eval_fetch_timeout(
canceled_by: &mut Option<CanceledBy>,
worker_name: &str,
w_id: &str,
load_client: bool,
) -> anyhow::Result<(Box<RawValue>, String)> {
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
@@ -650,20 +700,45 @@ pub async fn eval_fetch_timeout(
})
.collect::<Vec<_>>();
let ann = get_annotation(&ts_expr);
#[cfg(not(feature = "enterprise"))]
if ann.proxy.is_some() {
return Err(Error::ExecutionErr("Proxy is an EE feature".to_string()).into());
}
let mut extra_logs = String::new();
if ann.useragent.is_some() {
extra_logs.push_str(&format!("useragent: {}\n", ann.useragent.as_ref().unwrap()));
}
if ann.proxy.is_some() {
let (proxy, auth) = ann.proxy.as_ref().unwrap();
extra_logs.push_str(&format!(
"proxy: {proxy} (basic auth: {})\n",
auth.is_some()
));
}
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() };
let deno_fetch_options = if let Some(cert_path) = env::var("DENO_CERT").ok() {
let mut cert_store_provider = ContainerRootCertStoreProvider::new();
cert_store_provider.add_certificate(cert_path)?;
deno_fetch::Options {
root_cert_store_provider: Some(Arc::new(cert_store_provider)),
..Default::default()
}
} else {
Default::default()
let fetch_options = deno_fetch::Options {
root_cert_store_provider: if let Some(cert_path) = env::var("DENO_CERT").ok() {
let mut cert_store_provider = ContainerRootCertStoreProvider::new();
cert_store_provider.add_certificate(cert_path)?;
Some(Arc::new(cert_store_provider))
} else {
None
},
user_agent: ann.useragent.unwrap_or_else(|| "windmill/beta".to_string()),
proxy: ann.proxy.map(|x| deno_tls::Proxy {
url: x.0,
basic_auth: x
.1
.map(|(username, password)| deno_tls::BasicAuth { username, password }),
}),
..Default::default()
};
let exts: Vec<Extension> = vec![
@@ -674,7 +749,7 @@ pub async fn eval_fetch_timeout(
Arc::new(BlobStore::default()),
None,
),
deno_fetch::deno_fetch::init_ops::<PermissionsContainer>(deno_fetch_options),
deno_fetch::deno_fetch::init_ops::<PermissionsContainer>(fetch_options),
deno_net::deno_net::init_ops::<PermissionsContainer>(None, None),
ext,
];
@@ -732,7 +807,7 @@ pub async fn eval_fetch_timeout(
let future = async {
tokio::select! {
r = eval_fetch(&mut js_runtime, &js_expr, Some(env_code)) => Ok(r),
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()))
}
};
@@ -752,7 +827,7 @@ pub async fn eval_fetch_timeout(
})
});
let r = run_future_with_polling_update_job_poller(
let (res, logs) = run_future_with_polling_update_job_poller(
job_id,
job_timeout,
db,
@@ -769,8 +844,8 @@ pub async fn eval_fetch_timeout(
}
e
})?;
*mem_peak = (r.0.get().len() / 1000) as i32;
Ok(r)
*mem_peak = (res.get().len() / 1000) as i32;
Ok((res, format!("{extra_logs}{logs}")))
}
const WINDMILL_CLIENT: &str = include_str!("./windmill-client.js");
@@ -779,14 +854,17 @@ async fn eval_fetch(
js_runtime: &mut JsRuntime,
expr: &str,
env_code: Option<String>,
load_client: bool,
) -> anyhow::Result<Box<RawValue>> {
if let Some(env_code) = env_code.as_ref() {
let _ = js_runtime
.load_side_es_module_from_code(
&deno_core::resolve_url("file:///windmill.ts")?,
format!("{env_code}\n{}", WINDMILL_CLIENT.to_string()),
)
.await?;
if load_client {
if let Some(env_code) = env_code.as_ref() {
let _ = js_runtime
.load_side_es_module_from_code(
&deno_core::resolve_url("file:///windmill.ts")?,
format!("{env_code}\n{}", WINDMILL_CLIENT.to_string()),
)
.await?;
}
}
let _ = js_runtime
+88 -39
View File
@@ -1,17 +1,23 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use base64::Engine;
use futures::{future::BoxFuture, FutureExt};
use itertools::Itertools;
use mysql_async::{
consts::ColumnType, prelude::*, FromValueError, OptsBuilder, Params, Row, SslOpts,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, value::RawValue, Value};
use sqlx::types::Json;
use tokio::sync::Mutex;
use windmill_common::{
error::{to_anyhow, Error},
jobs::QueuedJob,
};
use windmill_parser_sql::{parse_db_resource, parse_mysql_sig, RE_ARG_MYSQL_NAMED};
use windmill_parser_sql::{
parse_db_resource, parse_mysql_sig, parse_sql_blocks, parse_sql_statement_named_params,
RE_ARG_MYSQL_NAMED,
};
use windmill_queue::CanceledBy;
use crate::{
@@ -29,6 +35,59 @@ struct MysqlDatabase {
ssl: Option<bool>,
}
pub fn do_mysql_inner<'a>(
query: &'a str,
all_statement_values: &Params,
conn: Arc<Mutex<mysql_async::Conn>>,
column_order: Option<&'a mut Option<Vec<String>>>,
) -> windmill_common::error::Result<BoxFuture<'a, anyhow::Result<Vec<Value>>>> {
let param_names = parse_sql_statement_named_params(query, ':')
.into_iter()
.map(|x| x.into_bytes())
.collect_vec();
let statement_values = if let Params::Named(m) = all_statement_values {
Params::Named(
m.into_iter()
.filter(|(k, _)| param_names.contains(&k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
)
} else {
all_statement_values.clone()
};
let result_f = async move {
let rows: Vec<Row> = conn
.lock()
.await
.exec(query, statement_values)
.await
.map_err(to_anyhow)?;
if let Some(column_order) = column_order {
*column_order = Some(
rows.first()
.map(|x| {
x.columns()
.iter()
.map(|x| x.name_str().to_string())
.collect::<Vec<String>>()
})
.unwrap_or_default(),
);
}
Ok(rows
.into_iter()
.map(|x| convert_row_to_value(x))
.collect::<Vec<serde_json::Value>>())
as Result<Vec<serde_json::Value>, anyhow::Error>
};
Ok(result_f.boxed())
}
pub async fn do_mysql(
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
@@ -91,14 +150,11 @@ pub async fn do_mysql(
opts
};
let pool = mysql_async::Pool::new(opts);
let mut conn = pool.get_conn().await.map_err(to_anyhow)?;
let sig = parse_mysql_sig(&query)
let sig = parse_mysql_sig(query)
.map_err(|x| Error::ExecutionErr(x.to_string()))?
.args;
let using_named_params = RE_ARG_MYSQL_NAMED.captures_iter(&query).count() > 0;
let using_named_params = RE_ARG_MYSQL_NAMED.captures_iter(query).count() > 0;
let mut statement_values: Params = match using_named_params {
true => Params::Named(HashMap::new()),
@@ -106,10 +162,8 @@ pub async fn do_mysql(
};
for arg in &sig {
let arg_t = arg.otyp.clone().unwrap_or_else(|| "text".to_string());
let arg_n = arg.clone().name;
let mysql_v = match job
.args
.as_ref()
let arg_n = arg.name.clone();
let mysql_v = match job_args
.and_then(|x| {
x.get(arg.name.as_str())
.map(|x| serde_json::from_str::<serde_json::Value>(x.get()).ok())
@@ -172,35 +226,30 @@ pub async fn do_mysql(
}
}
let result_f = async {
let rows: Vec<Row> = conn
.exec(
query,
match statement_values {
Params::Positional(v) => Params::Positional(v),
Params::Named(m) => Params::Named(m),
_ => Params::Empty,
},
)
.await
.map_err(to_anyhow)?;
let pool = mysql_async::Pool::new(opts);
let conn = pool.get_conn().await.map_err(to_anyhow)?;
let conn_a = Arc::new(Mutex::new(conn));
*column_order = Some(
rows.first()
.map(|x| {
x.columns()
.iter()
.map(|x| x.name_str().to_string())
.collect::<Vec<String>>()
})
.unwrap_or_default(),
);
let queries = parse_sql_blocks(query);
Ok(rows
.into_iter()
.map(|x| convert_row_to_value(x))
.collect::<Vec<serde_json::Value>>())
as Result<Vec<serde_json::Value>, anyhow::Error>
let result_f = if queries.len() > 1 {
let futures = queries
.iter()
.map(|x| do_mysql_inner(x, &statement_values, conn_a.clone(), None))
.collect::<windmill_common::error::Result<Vec<_>>>()?;
let f = async {
let mut res: Vec<serde_json::Value> = vec![];
for fut in futures {
let r = fut.await?;
res.push(serde_json::to_value(r).map_err(to_anyhow)?);
}
Ok(res)
};
f.boxed()
} else {
do_mysql_inner(query, &statement_values, conn_a.clone(), Some(column_order))?
};
let result = run_future_with_polling_update_job_poller(
@@ -215,7 +264,7 @@ pub async fn do_mysql(
)
.await?;
drop(conn);
drop(conn_a);
pool.disconnect().await.map_err(to_anyhow)?;
+130 -73
View File
@@ -1,12 +1,15 @@
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context;
use base64::{engine, Engine as _};
use chrono::Utc;
use futures::TryStreamExt;
use futures::future::BoxFuture;
use futures::{FutureExt, TryStreamExt};
use itertools::Itertools;
use native_tls::{Certificate, TlsConnector};
use postgres_native_tls::MakeTlsConnector;
use rust_decimal::{prelude::FromPrimitive, Decimal};
@@ -16,6 +19,7 @@ use serde_json::Map;
use serde_json::Value;
use tokio::sync::Mutex;
use tokio_postgres::types::IsNull;
use tokio_postgres::Client;
use tokio_postgres::{
types::{to_sql_checked, ToSql},
NoTls, Row,
@@ -28,8 +32,10 @@ use uuid::Uuid;
use windmill_common::error::{self, Error};
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
use windmill_common::{error::to_anyhow, jobs::QueuedJob};
use windmill_parser::Typ;
use windmill_parser_sql::{parse_db_resource, parse_pgsql_sig};
use windmill_parser::{Arg, Typ};
use windmill_parser_sql::{
parse_db_resource, parse_pg_statement_arg_indices, parse_pgsql_sig, parse_sql_blocks,
};
use windmill_queue::CanceledBy;
use crate::common::{build_args_values, run_future_with_polling_update_job_poller, sizeof_val};
@@ -56,6 +62,87 @@ lazy_static! {
pub static ref RUNNING: AtomicBool = AtomicBool::new(false);
}
fn do_postgresql_inner<'a>(
mut query: String,
param_idx_to_arg_and_value: &HashMap<i32, (&Arg, Option<&Value>)>,
client: &'a Client,
column_order: Option<&'a mut Option<Vec<String>>>,
siz: &'a AtomicUsize,
) -> error::Result<BoxFuture<'a, anyhow::Result<Vec<Value>>>> {
let mut query_params = vec![];
let arg_indices = parse_pg_statement_arg_indices(&query);
let mut i = 1;
for oidx in arg_indices.iter().sorted() {
if let Some((arg, value)) = param_idx_to_arg_and_value.get(&oidx) {
if *oidx as usize != i {
query = query.replace(&format!("${}", oidx), &format!("${}", i));
}
let value = value.unwrap_or_else(|| &serde_json::Value::Null);
let arg_t = arg
.otyp
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing otyp for pg arg"))?;
let typ = &arg.typ;
let param = convert_val(value, arg_t, typ)?;
query_params.push(param);
i += 1;
}
}
let result_f = async move {
// Now we can execute a simple statement that just returns its parameter.
let rows = client
.query_raw(&query, query_params)
.await
.map_err(to_anyhow)?;
let rows = rows.try_collect::<Vec<Row>>().await.map_err(to_anyhow)?;
if let Some(column_order) = column_order {
*column_order = Some(
rows.first()
.map(|x| {
x.columns()
.iter()
.map(|x| x.name().to_string())
.collect::<Vec<String>>()
})
.unwrap_or_default(),
);
}
let mut res: Vec<serde_json::Value> = vec![];
for row in rows.into_iter() {
let r = postgres_row_to_json_value(row);
if let Ok(v) = r.as_ref() {
let size = sizeof_val(v);
siz.fetch_add(size, Ordering::Relaxed);
}
if *CLOUD_HOSTED {
let siz = siz.load(Ordering::Relaxed);
if siz > MAX_RESULT_SIZE * 4 {
return Err(anyhow::anyhow!(
"Query result too large for cloud (size = {} > {})",
siz,
MAX_RESULT_SIZE & 4
));
}
}
if let Ok(v) = r {
res.push(v);
} else {
return Err(to_anyhow(r.err().unwrap()));
}
}
Ok(res)
};
Ok(result_f.boxed())
}
pub async fn do_postgresql(
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
@@ -175,34 +262,7 @@ pub async fn do_postgresql(
Some((client, handle))
};
let mut statement_values: Vec<serde_json::Value> = vec![];
let sig = parse_pgsql_sig(&query)
.map_err(|x| Error::ExecutionErr(x.to_string()))?
.args;
for arg in &sig {
statement_values.push(
pg_args
.get(&arg.name)
.map(|x| x.to_owned())
.unwrap_or_else(|| serde_json::Value::Null),
);
}
let query_params = statement_values
.iter()
.enumerate()
.map(|(i, value)| {
let arg_t = &sig[i]
.otyp
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing otyp for pg arg"))?
.to_owned();
let typ = &sig[i].typ;
convert_val(value, arg_t, typ)
})
.collect::<windmill_common::error::Result<Vec<_>>>()?;
let queries = parse_sql_blocks(query);
let (client, handle) = if let Some((client, handle)) = new_client.as_ref() {
(client, Some(handle))
@@ -211,52 +271,49 @@ pub async fn do_postgresql(
(client, None)
};
let result_f = async {
// Now we can execute a simple statement that just returns its parameter.
let rows = client
.query_raw(query, query_params)
.await
.map_err(to_anyhow)?;
let sig = parse_pgsql_sig(&query).map_err(|x| Error::ExecutionErr(x.to_string()))?;
let param_idx_to_arg_and_value = sig
.args
.iter()
.filter_map(|x| x.oidx.map(|oidx| (oidx, (x, pg_args.get(&x.name)))))
.collect::<HashMap<_, _>>();
let rows = rows.try_collect::<Vec<Row>>().await.map_err(to_anyhow)?;
let size = AtomicUsize::new(0);
let result_f = if queries.len() > 1 {
let futures = queries
.iter()
.map(|x| {
do_postgresql_inner(
x.to_string(),
&param_idx_to_arg_and_value,
client,
None,
&size,
)
})
.collect::<error::Result<Vec<_>>>()?;
*column_order = Some(
rows.first()
.map(|x| {
x.columns()
.iter()
.map(|x| x.name().to_string())
.collect::<Vec<String>>()
})
.unwrap_or_default(),
);
let mut siz = 0;
let mut res: Vec<serde_json::Value> = vec![];
for row in rows.into_iter() {
let r = postgres_row_to_json_value(row);
if let Ok(v) = r.as_ref() {
let size = sizeof_val(v);
siz += size;
let f = async {
let mut res: Vec<serde_json::Value> = vec![];
for fut in futures {
let r = fut.await?;
res.push(serde_json::to_value(r).map_err(to_anyhow)?);
}
if *CLOUD_HOSTED && siz > MAX_RESULT_SIZE * 4 {
return Err(anyhow::anyhow!(
"Query result too large for cloud (size = {} > {})",
siz,
MAX_RESULT_SIZE & 4
));
}
if let Ok(v) = r {
res.push(v);
} else {
return Err(to_anyhow(r.err().unwrap()));
}
}
Ok(res)
};
Ok((res, siz))
f.boxed()
} else {
do_postgresql_inner(
query.to_string(),
&param_idx_to_arg_and_value,
client,
Some(column_order),
&size,
)?
};
let (result, size) = run_future_with_polling_update_job_poller(
let result = run_future_with_polling_update_job_poller(
job.id,
job.timeout,
db,
@@ -268,7 +325,7 @@ pub async fn do_postgresql(
)
.await?;
*mem_peak = size as i32;
*mem_peak = size.load(Ordering::Relaxed) as i32;
RUNNING.store(false, std::sync::atomic::Ordering::Relaxed);
@@ -286,13 +286,14 @@ pub async fn handle_python_job(
create_args_and_out_file(&client, job, job_dir, db).await?;
let os_main_override = if let Some(main_override) = main_name.as_ref() {
format!("import os\nos.environ[\"MAIN_OVERRIDE\"] = \"{main_override}\"\n")
format!("os.environ[\"MAIN_OVERRIDE\"] = \"{main_override}\"\n")
} else {
String::new()
};
let main_override = main_name.unwrap_or_else(|| "main".to_string());
let wrapper_content: String = format!(
r#"
import os
import json
{import_loader}
{import_base64}
@@ -318,6 +319,9 @@ def to_b_64(v: bytes):
return b64.decode('ascii')
replace_nan = re.compile(r'(?:\bNaN\b|\\u0000)')
result_json = os.path.join(os.path.abspath(os.path.dirname(__file__)), "result.json")
try:
res = inner_script.{main_override}(**args)
typ = type(res)
@@ -333,14 +337,13 @@ try:
if type(v).__name__ == 'bytes':
res[k] = to_b_64(v)
res_json = re.sub(replace_nan, ' null ', json.dumps(res, separators=(',', ':'), default=str).replace('\n', ''))
with open("result.json", 'w') as f:
with open(result_json, 'w') as f:
f.write(res_json)
except BaseException as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
tb = traceback.format_tb(exc_traceback)
with open("result.json", 'w') as f:
with open(result_json, 'w') as f:
err = {{ "message": str(e), "name": e.__class__.__name__, "stack": '\n'.join(tb[1:]) }}
import os
flow_node_id = os.environ.get('WM_FLOW_STEP_ID')
if flow_node_id:
err['step_id'] = flow_node_id
+1
View File
@@ -29,6 +29,7 @@ globalThis.atob = base64.atob;
globalThis.btoa = base64.btoa;
globalThis.fetch = fetch.fetch;
globalThis.Request = request.Request;
globalThis.Response = response.Response;
globalThis.Blob = file.Blob;
globalThis.URL = url.URL;
globalThis.FormData = formData.FormData;
+135 -85
View File
@@ -1,15 +1,17 @@
use base64::{engine, Engine as _};
use chrono::Datelike;
use core::fmt::Write;
use futures::TryFutureExt;
use futures::future::BoxFuture;
use futures::{FutureExt, TryFutureExt};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use serde_json::{json, value::RawValue, Value};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use windmill_common::error::to_anyhow;
use windmill_common::jobs::QueuedJob;
use windmill_common::{error::Error, worker::to_raw_value};
use windmill_parser_sql::{parse_db_resource, parse_snowflake_sig};
use windmill_parser_sql::{parse_db_resource, parse_snowflake_sig, parse_sql_blocks};
use windmill_queue::{CanceledBy, HTTP_CLIENT};
use serde::{Deserialize, Serialize};
@@ -63,6 +65,104 @@ struct SnowflakeError {
message: String,
}
fn do_snowflake_inner<'a>(
query: &'a str,
job_args: &HashMap<String, Value>,
mut body: serde_json::Map<String, Value>,
account_identifier: &'a str,
token: &'a str,
column_order: Option<&'a mut Option<Vec<String>>>,
) -> windmill_common::error::Result<BoxFuture<'a, windmill_common::error::Result<Box<RawValue>>>> {
body.insert("statement".to_string(), json!(query));
let mut bindings = serde_json::Map::new();
let sig = parse_snowflake_sig(&query)
.map_err(|x| Error::ExecutionErr(x.to_string()))?
.args;
let mut i = 1;
for arg in &sig {
let arg_t = arg.otyp.clone().unwrap_or_else(|| "string".to_string());
let arg_v = job_args.get(&arg.name).cloned().unwrap_or(json!(""));
let snowflake_v = convert_typ_val(arg_t, arg_v);
bindings.insert(i.to_string(), snowflake_v);
i += 1;
}
if i > 1 {
body.insert("bindings".to_string(), json!(bindings));
}
let result_f = async move {
let response = HTTP_CLIENT
.post(format!(
"https://{}.snowflakecomputing.com/api/v2/statements/",
account_identifier.to_uppercase()
))
.bearer_auth(token)
.header("X-Snowflake-Authorization-Token-Type", "KEYPAIR_JWT")
.json(&body)
.send()
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
match response.error_for_status_ref() {
Ok(_) => {
let result = response
.json::<SnowflakeResponse>()
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
if result.resultSetMetaData.numRows > 10000 {
return Err(Error::ExecutionErr(
"More than 10000 rows were requested, use LIMIT 10000 to limit the number of rows".to_string(),
));
}
if let Some(column_order) = column_order {
*column_order = Some(
result
.resultSetMetaData
.rowType
.iter()
.map(|x| x.name.clone())
.collect::<Vec<String>>(),
);
}
let rows = to_raw_value(
&result
.data
.iter()
.map(|row| {
let mut row_map = serde_json::Map::new();
row.iter()
.zip(result.resultSetMetaData.rowType.iter())
.for_each(|(val, row_type)| {
row_map.insert(
row_type.name.clone(),
parse_val(&val, &row_type.r#type),
);
});
row_map
})
.collect::<Vec<_>>(),
);
Ok(rows)
}
Err(e) => {
let resp = response.text().await.unwrap_or("".to_string());
match serde_json::from_str::<SnowflakeError>(&resp) {
Ok(sf_err) => Err(Error::ExecutionErr(sf_err.message)),
Err(_) => Err(Error::ExecutionErr(e.to_string())),
}
}
}
};
Ok(result_f.boxed())
}
pub async fn do_snowflake(
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
@@ -132,21 +232,6 @@ pub async fn do_snowflake(
tracing::debug!("Snowflake token: {}", token);
let mut bindings = serde_json::Map::new();
let sig = parse_snowflake_sig(&query)
.map_err(|x| Error::ExecutionErr(x.to_string()))?
.args;
let mut i = 1;
for arg in &sig {
let arg_t = arg.otyp.clone().unwrap_or_else(|| "string".to_string());
let arg_v = snowflake_args.get(&arg.name).cloned().unwrap_or(json!(""));
let snowflake_v = convert_typ_val(arg_t, arg_v);
bindings.insert(i.to_string(), snowflake_v);
i += 1;
}
let mut body = serde_json::Map::new();
if database.schema.is_some() {
body.insert(
@@ -172,79 +257,44 @@ pub async fn do_snowflake(
json!(database.database.unwrap().to_uppercase()),
);
}
body.insert("statement".to_string(), json!(query));
body.insert("timeout".to_string(), json!(10)); // in seconds
if i > 1 {
body.insert("bindings".to_string(), json!(bindings));
}
let queries = parse_sql_blocks(query);
let result_f = async {
let response = HTTP_CLIENT
.post(format!(
"https://{}.snowflakecomputing.com/api/v2/statements/",
database.account_identifier.to_uppercase()
))
.bearer_auth(token)
.header("X-Snowflake-Authorization-Token-Type", "KEYPAIR_JWT")
.json(&body)
.send()
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let result_f = if queries.len() > 1 {
let futures = queries
.iter()
.map(|x| {
do_snowflake_inner(
x,
&snowflake_args,
body.clone(),
&database.account_identifier,
&token,
None,
)
})
.collect::<windmill_common::error::Result<Vec<_>>>()?;
match response.error_for_status_ref() {
Ok(_) => {
let result = response
.json::<SnowflakeResponse>()
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
tracing::info!("Snowflake response: {:?}", result);
if result.resultSetMetaData.numRows > 10000 {
return Err(Error::ExecutionErr(
"More than 10000 rows were requested, use LIMIT 10000 to limit the number of rows".to_string(),
));
}
*column_order = Some(
result
.resultSetMetaData
.rowType
.iter()
.map(|x| x.name.clone())
.collect::<Vec<String>>(),
);
let rows = to_raw_value(
&result
.data
.iter()
.map(|row| {
let mut row_map = serde_json::Map::new();
row.iter()
.zip(result.resultSetMetaData.rowType.iter())
.for_each(|(val, row_type)| {
row_map.insert(
row_type.name.clone(),
parse_val(&val, &row_type.r#type),
);
});
row_map
})
.collect::<Vec<_>>(),
);
Ok(rows)
let f = async {
let mut res: Vec<Box<RawValue>> = vec![];
for fut in futures {
let r = fut.await?;
res.push(r);
}
Err(e) => {
let resp = response.text().await.unwrap_or("".to_string());
match serde_json::from_str::<SnowflakeError>(&resp) {
Ok(sf_err) => Err(Error::ExecutionErr(sf_err.message)),
Err(_) => Err(Error::ExecutionErr(e.to_string())),
}
}
}
Ok(to_raw_value(&res))
};
f.boxed()
} else {
do_snowflake_inner(
query,
&snowflake_args,
body.clone(),
&database.account_identifier,
&token,
Some(column_order),
)?
};
let r = run_future_with_polling_update_job_poller(
job.id,
+50 -142
View File
@@ -226,6 +226,8 @@ pub async fn create_token_for_owner(
pub const TMP_LOGS_DIR: &str = concatcp!(TMP_DIR, "/logs");
pub const ROOT_CACHE_DIR: &str = concatcp!(TMP_DIR, "/cache/");
pub const ROOT_CACHE_NOMOUNT_DIR: &str = concatcp!(TMP_DIR, "/cache_nomount/");
pub const LOCK_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "lock");
pub const PIP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "pip");
pub const TAR_PIP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/pip");
@@ -234,8 +236,9 @@ pub const DENO_CACHE_DIR_DEPS: &str = concatcp!(ROOT_CACHE_DIR, "deno/deps");
pub const DENO_CACHE_DIR_NPM: &str = concatcp!(ROOT_CACHE_DIR, "deno/npm");
pub const GO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "go");
pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun");
pub const BUN_TAR_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "buntar");
pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "bun");
pub const BUN_BUNDLE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun");
pub const BUN_DEPSTAR_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "buntar");
pub const HUB_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub");
pub const GO_BIN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "gobin");
@@ -284,10 +287,14 @@ lazy_static::lazy_static! {
pub static ref JOB_TOKEN: Option<String> = std::env::var("JOB_TOKEN").ok();
static ref SLEEP_QUEUE: u64 = std::env::var("SLEEP_QUEUE")
pub static ref SLEEP_QUEUE: u64 = std::env::var("SLEEP_QUEUE")
.ok()
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or(DEFAULT_SLEEP_QUEUE);
.unwrap_or(DEFAULT_SLEEP_QUEUE * std::env::var("NUM_WORKERS")
.ok()
.map(|x| x.parse().ok())
.flatten()
.unwrap_or(2) / 2);
pub static ref DISABLE_NUSER: bool = std::env::var("DISABLE_NUSER")
@@ -570,10 +577,10 @@ async fn handle_receive_completed_job<
R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static,
>(
jc: JobCompleted,
base_internal_url: String,
db: Pool<Postgres>,
worker_dir: String,
same_worker_tx: Sender<SameWorkerPayload>,
base_internal_url: &str,
db: &Pool<Postgres>,
worker_dir: &str,
same_worker_tx: &Sender<SameWorkerPayload>,
rsmq: Option<R>,
worker_name: &str,
worker_save_completed_job_duration: Option<Histo>,
@@ -594,7 +601,7 @@ async fn handle_receive_completed_job<
if let Err(err) = process_completed_job(
jc,
&client,
&db,
db,
&worker_dir,
same_worker_tx.clone(),
rsmq.clone(),
@@ -606,7 +613,7 @@ async fn handle_receive_completed_job<
.await
{
handle_job_error(
&db,
db,
&client,
job.as_ref(),
mem_peak,
@@ -1186,140 +1193,40 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
if let Some(wj) = worker_job_completed_channel_queue2.as_ref() {
wj.dec();
}
let base_internal_url2 = base_internal_url2.clone();
let worker_dir2 = worker_dir2.clone();
let db2 = db2.clone();
let same_worker_tx2 = same_worker_tx2.clone();
let rsmq2 = rsmq2.clone();
let worker_name = worker_name2.clone();
if matches!(jc.job.job_kind, JobKind::Noop) {
thread_count.fetch_add(1, Ordering::SeqCst);
let thread_count = thread_count.clone();
loop {
if thread_count.load(Ordering::Relaxed) < 4 {
break;
}
tokio::time::sleep(Duration::from_millis(3)).await;
}
#[cfg(feature = "benchmark")]
let send_duration = send_duration2.clone();
#[cfg(feature = "benchmark")]
let process_duration = process_duration.clone();
#[cfg(feature = "benchmark")]
let completed_jobs = completed_jobs.clone();
#[cfg(feature = "benchmark")]
let main_duration = main_duration2.clone();
#[cfg(feature = "prometheus")]
let worker_save_completed_job_duration2 =
worker_save_completed_job_duration.clone();
#[cfg(feature = "prometheus")]
let worker_flow_transition_duration2 =
worker_flow_transition_duration.clone();
let killpill_tx = killpill_tx2.clone();
let job_completed_sender = job_completed_sender.clone();
tokio::spawn(async move {
#[cfg(feature = "benchmark")]
let process_start = Instant::now();
let is_dependency_job = matches!(
jc.job.job_kind,
JobKind::Dependencies | JobKind::FlowDependencies
);
handle_receive_completed_job(
jc,
base_internal_url2,
db2.clone(),
worker_dir2,
same_worker_tx2,
rsmq2,
&worker_name,
worker_save_completed_job_duration2.clone(),
worker_flow_transition_duration2.clone(),
job_completed_sender.clone(),
)
.await;
#[cfg(feature = "benchmark")]
{
let n = completed_jobs.fetch_add(1, Ordering::SeqCst);
if (n + 1) % 1000 == 0 || n == (jobs - 1) as usize {
let duration_s = start.elapsed().as_secs_f64();
let jobs_per_sec = n as f64 / duration_s;
tracing::info!(
"completed {} jobs in {}s, {} jobs/s",
n + 1,
duration_s,
jobs_per_sec
);
tracing::info!(
"main loop without send {}s",
main_duration.load(Ordering::SeqCst) as f64 / 1000.0
);
tracing::info!(
"send job completed / send dedicated job duration {}s",
send_duration.load(Ordering::SeqCst) as f64 / 1000.0
);
tracing::info!(
"job completed process duration {}s",
process_duration.load(Ordering::SeqCst) as f64 / 1000.0
);
}
process_duration.fetch_add(
process_start.elapsed().as_millis() as usize,
Ordering::SeqCst,
);
}
thread_count.fetch_sub(1, Ordering::SeqCst);
if is_dependency_job && is_dedicated_worker {
tracing::error!("Dedicated worker executed a dependency job, a new script has been deployed. Exiting expecting to be restarted.");
sqlx::query!("UPDATE config SET config = config WHERE name = $1", format!("worker__{}", *WORKER_GROUP))
.execute(&db2)
.await
.expect("update config to trigger restart of all dedicated workers at that config");
killpill_tx.send(()).unwrap_or_default();
}
});
} else {
let is_init_script_and_failure =
!jc.success && jc.job.tag.as_str() == INIT_SCRIPT_TAG;
let is_dependency_job = matches!(
jc.job.job_kind,
JobKind::Dependencies | JobKind::FlowDependencies);
handle_receive_completed_job(
jc,
base_internal_url2,
db2.clone(),
worker_dir2,
same_worker_tx2,
rsmq2,
&worker_name,
worker_save_completed_job_duration2.clone(),
worker_flow_transition_duration2.clone(),
job_completed_sender.clone(),
)
.await;
if is_init_script_and_failure {
tracing::error!("init script errored, exiting");
killpill_tx2.send(()).unwrap_or_default();
}
if is_dependency_job && is_dedicated_worker {
tracing::error!("Dedicated worker executed a dependency job, a new script has been deployed. Exiting expecting to be restarted.");
sqlx::query!("UPDATE config SET config = config WHERE name = $1", format!("worker__{}", *WORKER_GROUP))
.execute(&db2)
.await
.expect("update config to trigger restart of all dedicated workers at that config");
killpill_tx2.send(()).unwrap_or_default();
}
let is_init_script_and_failure =
!jc.success && jc.job.tag.as_str() == INIT_SCRIPT_TAG;
let is_dependency_job = matches!(
jc.job.job_kind,
JobKind::Dependencies | JobKind::FlowDependencies);
handle_receive_completed_job(
jc,
&base_internal_url2,
&db2,
&worker_dir2,
&same_worker_tx2,
rsmq2,
&worker_name2,
worker_save_completed_job_duration2.clone(),
worker_flow_transition_duration2.clone(),
job_completed_sender.clone(),
)
.await;
if is_init_script_and_failure {
tracing::error!("init script errored, exiting");
killpill_tx2.send(()).unwrap_or_default();
}
if is_dependency_job && is_dedicated_worker {
tracing::error!("Dedicated worker executed a dependency job, a new script has been deployed. Exiting expecting to be restarted.");
sqlx::query!("UPDATE config SET config = config WHERE name = $1", format!("worker__{}", *WORKER_GROUP))
.execute(&db2)
.await
.expect("update config to trigger restart of all dedicated workers at that config");
killpill_tx2.send(()).unwrap_or_default();
}
}
SendResult::UpdateFlow {
flow,
@@ -2336,6 +2243,7 @@ async fn do_nativets(
canceled_by,
worker_name,
&job.workspace_id,
true,
)
.await?;
Ok((result.0, result.1))
@@ -3119,7 +3027,7 @@ mount {{
)
.await
}
Some(ScriptLang::Bun) => {
Some(ScriptLang::Bun) | Some(ScriptLang::Bunnative) => {
handle_bun_job(
requirements_o,
codebase,
+94 -13
View File
@@ -12,7 +12,7 @@ use windmill_common::flows::{FlowModule, FlowModuleValue};
use windmill_common::get_latest_deployed_hash_for_path;
use windmill_common::jobs::JobPayload;
use windmill_common::scripts::ScriptHash;
use windmill_common::worker::{to_raw_value, to_raw_value_owned};
use windmill_common::worker::{get_annotation, to_raw_value, to_raw_value_owned};
use windmill_common::{
error::{self, to_anyhow},
flows::FlowValue,
@@ -187,14 +187,16 @@ fn parse_bun_relative_imports(raw_code: &str, script_path: &str) -> error::Resul
Ok(relative_imports)
}
fn extract_relative_imports(
pub fn extract_relative_imports(
raw_code: &str,
script_path: &str,
language: &Option<ScriptLang>,
) -> Option<Vec<String>> {
match language {
Some(ScriptLang::Python3) => parse_relative_imports(&raw_code, script_path).ok(),
Some(ScriptLang::Bun) => parse_bun_relative_imports(&raw_code, script_path).ok(),
Some(ScriptLang::Bun) | Some(ScriptLang::Bunnative) => {
parse_bun_relative_imports(&raw_code, script_path).ok()
}
_ => None,
}
}
@@ -232,6 +234,24 @@ pub async fn handle_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync +
.is_some_and(|y| y.to_string().as_str() == "true")
})
.unwrap_or(false);
let npm_mode = if job
.language
.as_ref()
.map(|v| v == &ScriptLang::Bun)
.unwrap_or(false)
{
Some(
job.args
.as_ref()
.map(|x| {
x.get("npm_mode")
.is_some_and(|y| y.to_string().as_str() == "true")
})
.unwrap_or(false),
)
} else {
None
};
let content = capture_dependency_job(
&job.id,
@@ -252,6 +272,7 @@ pub async fn handle_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync +
token,
script_path,
raw_deps,
npm_mode,
)
.await;
@@ -267,7 +288,7 @@ pub async fn handle_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync +
let hash = job.script_hash.unwrap_or(ScriptHash(0));
let w_id = &job.workspace_id;
sqlx::query!(
"UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3",
"UPDATE script SET lock = $1, created_at = now() WHERE hash = $2 AND workspace_id = $3",
&content,
&hash.0,
w_id
@@ -681,7 +702,7 @@ async fn lock_modules<'c>(
lock,
path,
content,
language,
mut language,
input_transforms,
tag,
custom_concurrency_key,
@@ -833,8 +854,11 @@ async fn lock_modules<'c>(
}
} else {
if lock.as_ref().is_some_and(|x| !x.trim().is_empty()) {
new_flow_modules.push(e);
continue;
let skip_creating_new_lock = skip_creating_new_lock(&language, &content);
if skip_creating_new_lock {
new_flow_modules.push(e);
continue;
}
}
}
@@ -858,6 +882,7 @@ async fn lock_modules<'c>(
&path.clone().unwrap_or_else(|| job_path.to_string())
),
false,
None,
)
.await;
//
@@ -891,6 +916,14 @@ async fn lock_modules<'c>(
append_logs(&job.id, &job.workspace_id, logs, db).await;
}
if language == ScriptLang::Bun || language == ScriptLang::Bunnative {
let anns = get_annotation(&content);
if anns.native_mode && language == ScriptLang::Bun {
language = ScriptLang::Bunnative;
} else if !anns.native_mode && language == ScriptLang::Bunnative {
language = ScriptLang::Bun;
};
}
e.value = windmill_common::worker::to_raw_value(&FlowModuleValue::RawScript {
lock: Some(new_lock),
path,
@@ -932,6 +965,18 @@ async fn lock_modules<'c>(
Ok((new_flow_modules, tx, modified_ids))
}
fn skip_creating_new_lock(language: &ScriptLang, content: &str) -> bool {
if language == &ScriptLang::Bun || language == &ScriptLang::Bunnative {
let anns = get_annotation(&content);
if anns.native_mode && language == &ScriptLang::Bun {
return false;
} else if !anns.native_mode && language == &ScriptLang::Bunnative {
return false;
};
}
true
}
#[async_recursion]
async fn lock_modules_app(
value: Value,
@@ -965,10 +1010,12 @@ async fn lock_modules_app(
if v.get("lock")
.is_some_and(|x| !x.as_str().unwrap().trim().is_empty())
{
logs.push_str(
"Found already locked inline script. Skipping lock...\n",
);
return Ok(Value::Object(m.clone()));
if skip_creating_new_lock(&language, &content) {
logs.push_str(
"Found already locked inline script. Skipping lock...\n",
);
return Ok(Value::Object(m.clone()));
}
}
logs.push_str("Found lockable inline script. Generating lock...\n");
let new_lock = capture_dependency_job(
@@ -986,11 +1033,27 @@ async fn lock_modules_app(
token,
&format!("{}/app", job.script_path()),
false,
None,
)
.await;
match new_lock {
Ok(new_lock) => {
append_logs(&job.id, &job.workspace_id, logs, db).await;
let anns = get_annotation(&content);
let nlang = if anns.native_mode && language == ScriptLang::Bun {
Some(ScriptLang::Bunnative)
} else if !anns.native_mode && language == ScriptLang::Bunnative
{
Some(ScriptLang::Bun)
} else {
None
};
if let Some(nlang) = nlang {
v.insert(
"language".to_string(),
serde_json::Value::String(nlang.as_str().to_string()),
);
}
v.insert(
"lock".to_string(),
serde_json::Value::String(new_lock),
@@ -1177,6 +1240,7 @@ async fn capture_dependency_job(
token: &str,
script_path: &str,
raw_deps: bool,
npm_mode: Option<bool>,
) -> error::Result<String> {
match job_language {
ScriptLang::Python3 => {
@@ -1271,7 +1335,9 @@ async fn capture_dependency_job(
)
.await
}
ScriptLang::Bun => {
ScriptLang::Bun | ScriptLang::Bunnative => {
let npm_mode = npm_mode
.unwrap_or_else(|| windmill_common::worker::get_annotation(job_raw_code).npm_mode);
if !raw_deps {
let _ = write_file(job_dir, "main.ts", job_raw_code).await?;
}
@@ -1292,9 +1358,24 @@ async fn capture_dependency_job(
} else {
None
},
false,
npm_mode,
)
.await?;
if req.is_some() && !raw_deps {
crate::bun_executor::prebundle_script(
job_raw_code,
req.clone(),
script_path,
job_id,
w_id,
db,
&job_dir,
base_internal_url,
worker_name,
&token,
)
.await?;
}
Ok(req.unwrap_or_else(String::new))
}
ScriptLang::Php => {
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.366.3";
export const VERSION = "v1.372.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -34,7 +34,7 @@ addEventListener("error", (event) => {
}
});
export const VERSION = "v1.366.3";
export const VERSION = "v1.372.0";
let command: any = new Command()
.name("wmill")
+5 -1
View File
@@ -157,9 +157,12 @@ export async function handleFile(
log.info(`Starting building the bundle for ${path}`);
const out = await esbuild.build({
entryPoints: [path],
format: "esm",
format: "cjs",
bundle: true,
write: false,
platform: "node",
packages: "bundle",
target: "node20.15.1",
});
bundleContent = out.outputFiles[0].text;
log.info(`Finished building the bundle for ${path}`);
@@ -429,6 +432,7 @@ export const exts = [
".sql",
".gql",
".ps1",
".php",
];
export function removeExtensionToPath(path: string): string {
+2 -2
View File
@@ -292,7 +292,7 @@ export function newPathAssigner(defaultTs: "bun" | "deno"): PathAssigner {
const seen_names = new Set<string>();
function assignPath(
summary: string | undefined,
language: RawScript["language"]
language: RawScript["language"] | "frontend" | "bunnative"
): [string, string] {
let name;
@@ -313,7 +313,7 @@ export function newPathAssigner(defaultTs: "bun" | "deno"): PathAssigner {
let ext;
if (language == "python3") ext = "py";
else if (language == defaultTs) ext = "ts";
else if (language == defaultTs || language == "bunnative") ext = "ts";
else if (language == "bun") ext = "bun.ts";
else if (language == "deno") ext = "deno.ts";
else if (language == "go") ext = "go";
+5 -3
View File
@@ -70,16 +70,18 @@ services:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 2
replicas: 1
resources:
limits:
cpus: "0.1"
memory: 128M
cpus: "1"
memory: 2048M
restart: unless-stopped
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=worker
- WORKER_GROUP=native
- NUM_WORKERS=8
- SLEEP_QUEUE=200
depends_on:
db:
condition: service_healthy
+6 -6
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.366.3",
"version": "1.372.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.366.3",
"version": "1.372.0",
"license": "AGPL-3.0",
"dependencies": {
"@aws-crypto/sha256-js": "^4.0.0",
@@ -52,7 +52,7 @@
"vscode-languageclient": "~9.0.1",
"vscode-uri": "~3.0.8",
"vscode-ws-jsonrpc": "~3.1.0",
"windmill-parser-wasm": "^1.364.4",
"windmill-parser-wasm": "^1.367.2",
"windmill-sql-datatype-parser-wasm": "^1.318.0",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.0",
@@ -10261,9 +10261,9 @@
}
},
"node_modules/windmill-parser-wasm": {
"version": "1.364.4",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.364.4.tgz",
"integrity": "sha512-zAW29GvUXdctOJOXSN8Z+yNfG67J5rbmI5FZD8esw8MyIIrha0ajqgtUYIKeuT+Q2kdBLtVc7AVo9/Se2Q4D2g=="
"version": "1.367.2",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.367.2.tgz",
"integrity": "sha512-If/IXXXADC0jWq4Vj6IN1IRZ1GTWCpE4oMnyZAkVzZUJ2ZbQLw5maLhAmm4iAMpmMHRb4U4kd68D1h30Iy4MNQ=="
},
"node_modules/windmill-sql-datatype-parser-wasm": {
"version": "1.318.0",

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