Compare commits

...

123 Commits

Author SHA1 Message Date
Ruben Fiszel
0afb1a58cb remove cache 2024-12-18 13:10:48 +01:00
Ruben Fiszel
833547a6fb downgrade images 2024-12-18 13:09:25 +01:00
Ruben Fiszel
48385bffd1 update backend tests to 1.82.0 2024-12-18 12:35:22 +01:00
Ruben Fiszel
fb72e89d49 chore(main): release 1.440.1 (#4948)
* chore(main): release 1.440.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-12-18 12:29:23 +01:00
Ruben Fiszel
02a8f1f864 fix(internal): updating rust to 1.82 2024-12-18 12:01:36 +01:00
Ruben Fiszel
bc4d13f5ff nits cleanup 2024-12-18 11:49:44 +01:00
Ruben Fiszel
d8d47d910d chore(main): release 1.440.0 (#4931)
* chore(main): release 1.440.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-12-18 11:40:33 +01:00
Ruben Fiszel
13be0cd1c8 fix: fix relative imports in cached flow scripts 2024-12-18 11:34:07 +01:00
Ruben Fiszel
ee28955ba4 restrict backend ci checks to ci changes 2024-12-18 10:08:06 +01:00
Lucas Abel
a040513791 cache: disable caching for tests (#4943)
As `#[sqlx::test]` spawn a database per test, the cache
could yield unexpected results
2024-12-18 09:57:31 +01:00
Ruben Fiszel
fe4334dfb2 test -> check 2024-12-18 09:51:28 +01:00
Ruben Fiszel
21e20ee8de nit check 2024-12-18 09:39:28 +01:00
Ruben Fiszel
48e68e6406 add fast backend checks 2024-12-18 09:15:27 +01:00
Ruben Fiszel
1b008409e2 flow editor improvements 2024-12-18 00:21:25 +01:00
Ruben Fiszel
33bacb2ee8 improve date setValue 2024-12-17 22:17:58 +01:00
Lucas Abel
51b74e713c cache: improve error reporting (#4940) 2024-12-17 19:44:51 +00:00
Ruben Fiszel
999e6ee94e revert docker build changes 2024-12-17 20:36:53 +01:00
Ruben Fiszel
cd4dfec61a ci improvements (#4938) 2024-12-17 20:01:11 +01:00
Ruben Fiszel
1693f14fe6 ci improvements 2024-12-17 19:56:47 +01:00
Ruben Fiszel
977a33b397 ci improvements 2024-12-17 19:44:33 +01:00
Ruben Fiszel
c1ba227ae4 ci improvements 2024-12-17 19:39:40 +01:00
Ruben Fiszel
7558572d23 ci improvements 2024-12-17 19:32:46 +01:00
Ruben Fiszel
4f57d9d121 ci improvements 2024-12-17 18:51:39 +01:00
Ruben Fiszel
e3413f8ff8 add uv 2024-12-17 17:25:53 +01:00
Ruben Fiszel
84a45166aa all 2024-12-17 17:05:26 +01:00
Ruben Fiszel
07159ea062 all 2024-12-17 16:53:56 +01:00
Ruben Fiszel
eb8ffbdedb all 2024-12-17 16:49:46 +01:00
Ruben Fiszel
2a9620338f all 2024-12-17 16:48:47 +01:00
Ruben Fiszel
2d58b2c5bc all 2024-12-17 16:46:00 +01:00
Ruben Fiszel
67377a8c63 all 2024-12-17 16:40:07 +01:00
Ruben Fiszel
3d1af4cdb9 all 2024-12-17 16:37:43 +01:00
Ruben Fiszel
2faa929d37 all 2024-12-17 16:31:25 +01:00
Ruben Fiszel
862405e90b all 2024-12-17 16:27:29 +01:00
Ruben Fiszel
6c274849a5 all 2024-12-17 16:26:42 +01:00
Ruben Fiszel
1da8dcc655 all 2024-12-17 16:23:40 +01:00
Ruben Fiszel
bc8c6abd00 all 2024-12-17 16:16:56 +01:00
Ruben Fiszel
ab9481c4c5 all 2024-12-17 15:57:52 +01:00
Ruben Fiszel
191db2b739 all 2024-12-17 15:53:46 +01:00
Ruben Fiszel
c6e06da611 all 2024-12-17 15:44:19 +01:00
wendrul
d8806555f1 fix: add workspace selector and fix css for create webhook page (#4939)
* fix: add workspace selector and fix css for create webhook page

* Improve the tooltip message about token generation
2024-12-17 15:41:16 +01:00
Ruben Fiszel
ad81499f8d all 2024-12-17 15:37:55 +01:00
Ruben Fiszel
16ce09fd1b all 2024-12-17 15:36:14 +01:00
Ruben Fiszel
6bcaa73af5 all 2024-12-17 15:26:18 +01:00
Ruben Fiszel
5fb68cf28f all 2024-12-17 15:11:10 +01:00
Ruben Fiszel
204ca7ea46 all 2024-12-17 14:52:38 +01:00
Ruben Fiszel
e9eb49b999 all 2024-12-17 14:49:50 +01:00
Ruben Fiszel
7c8040cc16 all 2024-12-17 14:43:10 +01:00
Ruben Fiszel
fd9a4183d3 all 2024-12-17 14:38:44 +01:00
Ruben Fiszel
1846c8795d all 2024-12-17 14:12:12 +01:00
Ruben Fiszel
9194d20bc1 all 2024-12-17 14:08:27 +01:00
Ruben Fiszel
db90705d41 all 2024-12-17 13:00:51 +01:00
Ruben Fiszel
95c613de6e add workflow_dispatch to build 2024-12-17 11:05:54 +01:00
Lucas Abel
977ac5c3f3 feat(cache): remove persistent raw values from queue (#4866)
* feat(cache): remove persistent raw values from queue

* test: add tests and fix discovered regressions

* fix csharp executor after rebase

* invalidate script cache after locking

* fix lite flow version race

* fix flow dependencies lock race

* update version flag to `v.1.440`
2024-12-16 17:10:16 +01:00
michael
d39193063f Update displayDate to use local time format preferences (#4826)
* update date display time to use local browser locale

* incorporate displaySecond and displayDate

* rewrite

* a

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2024-12-16 11:18:52 +01:00
Ruben Fiszel
e4bffe4741 chore(main): release 1.439.0 (#4925)
* chore(main): release 1.439.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-12-16 10:53:56 +01:00
Ruben Fiszel
aee8984be5 use rc.1 from crate 2024-12-15 23:55:58 +01:00
Ruben Fiszel
ede44b9a90 update openidconnect to 0.5.0 2024-12-15 15:01:13 +01:00
Ruben Fiszel
0fd69efaa3 update tiberius to 0.12.3 2024-12-15 14:50:03 +01:00
Ruben Fiszel
95d7a56ee4 update rustpython to 0.4.0 2024-12-15 14:46:05 +01:00
Ruben Fiszel
820a454e62 update rsa 2024-12-15 14:14:30 +01:00
Ruben Fiszel
278ab4bf57 remove rust embed in dev mode 2024-12-15 13:36:21 +01:00
Ruben Fiszel
34392ef66f split workspace and auth logic 2024-12-15 13:13:09 +01:00
Ruben Fiszel
47e2230cbe add rust to tests 2024-12-15 11:06:56 +01:00
HugoCasa
83a60cbc51 feat: add multipart/form-data support (#4927)
* feat: add multipart/form-data support

* nits
2024-12-15 10:53:12 +01:00
Ruben Fiszel
c320862c51 make mail-send optional 2024-12-15 10:31:44 +01:00
Ruben Fiszel
7762c1d132 make rust optional 2024-12-15 10:00:22 +01:00
Ruben Fiszel
2831e9e81e remove prometheus from fast build 2024-12-15 09:45:00 +01:00
Ruben Fiszel
86234b6c54 unpin zstd 2024-12-15 01:37:06 +01:00
Ruben Fiszel
c09b6c442f improve backend zip lib 2024-12-15 01:30:54 +01:00
Ruben Fiszel
b169ddb9d3 nit 2024-12-15 01:00:44 +01:00
Ruben Fiszel
51c0373b4c feature gates more feature for faster compilation (#4928)
* cleanup

* cleanup

* all

* all

* all

* all
2024-12-15 00:00:37 +01:00
HugoCasa
7dce9ab003 fix unused variable (#4926) 2024-12-14 13:43:51 +01:00
Ruben Fiszel
9cdf90c50f update sqlx 2024-12-14 13:27:42 +01:00
HugoCasa
90ece755f7 more feature flags (#4918)
* more feature flags

* include python in tests

* smtp feature flag

* del

* add back file

* fix openapi int
2024-12-14 13:21:28 +01:00
hkader-tl
5172c13ab8 fix: ECS terraform db url + ami issues (#4924)
Modifications to improve the terraform experience for bringing up a
windmill cluster. Due to the default RDS settings for the version in the
terraform, we do not want to disable ssl in connecting. Additionally,
change the TF to automatically pull the latest 2023 AMI. Given this is
meant to be a quick start example, this seems better than hard-coding
since eventually the hard-coded ami doesn't exist.
2024-12-14 13:21:14 +01:00
Ruben Fiszel
829eae97b9 add debug logs to websockets 2024-12-13 20:37:35 +01:00
Ruben Fiszel
02e31372f5 refresh user tokens less aggressively 2024-12-13 20:09:33 +01:00
Lucas Abel
74a89552c7 cleanup is_flow usage and impl (#4923) 2024-12-13 18:08:01 +01:00
Ruben Fiszel
30256ae6ec chore(main): release 1.438.0 (#4899)
* chore(main): release 1.438.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-12-13 17:58:50 +01:00
Alexander Petric
0e11c43014 small ui fix for result display in psql + adding --return_last_result to psql script helper (#4919) 2024-12-13 17:55:19 +01:00
wendrul
eb3ed7a2c7 fix: add DOTNET_ROOT env variable (#4921) 2024-12-13 17:54:56 +01:00
Ruben Fiszel
3634ade41b fix: flow node default tag 2024-12-13 17:51:08 +01:00
Ruben Fiszel
a7ef9e9f52 fix flow module adder 2024-12-13 16:56:45 +01:00
wendrul
393b838ec9 Target dotnet 9.0 for C# (#4920) 2024-12-13 15:41:39 +01:00
Ruben Fiszel
1b0d4dd46b nit 2024-12-13 12:11:16 +01:00
Henri Courdent
4fb366d11f Harmonization sentence cases (#4888)
Co-authored-by: HugoCasa <hugo@casademont.ch>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2024-12-13 11:31:46 +01:00
wendrul
afac8a73f0 fix: c#: nsjail image, default langs and feature cage imports (#4917)
* fix: nsjail image, default langs and feature cage csharp imports

* Fix unused imports with csharp flag
2024-12-13 09:36:17 +01:00
wendrul
c85d2a4957 feat: add C# support (#4908)
* Add csharp stuff and merge after the rewrite

    * Add csharp, boilerplate + minimal execution
    * Add initial parser
    * Frontend + wasm export of the tree sitter parser
    * Arg spread and use cache

* More types and req parsing

* Add logo

* Parse async and void to fit the wrapper to the main sig

* Csharp editor resource and var buttons

* Lockfile generation

* Add default parsing using serde_json::from_str

* Update init code for c#

* Nsjail for c#

* Remove c_variadic feature

* Remove unused imports

* Add c_variadic only for wasm32 assembly

* Fix c# test

* Add dotnet to the images

* Update dockerfiles

* Update dockerfile again

* Touch Dockerfile

* npm install c# parser

* Add opt mount in nsjail

* Update ee repo ref

* Add csharp to backend image for tests

* Add setting to set nuget package

* Windows compatibility

* Disable csharp test

* ADd feature flag
2024-12-13 00:29:01 +01:00
HugoCasa
3864cfce24 feat(backend): handle xml payload as raw_string (#4915) 2024-12-12 19:18:43 +01:00
Ruben Fiszel
441ee933f5 update ee ref 2024-12-12 18:46:03 +01:00
Ruben Fiszel
8e490b0738 update ee ref 2024-12-12 18:39:57 +01:00
HugoCasa
952cbd182d fix: app custom url diff (#4914)
* fix: app custom url diff

* update ee ref
2024-12-12 18:37:34 +01:00
HugoCasa
762ac30b59 fix(frontend): schedule operator perms + add instance settings in operator menu (#4912)
* fix(frontend): schedule operator perms + add instance settings in operator menu

* fix npm timeout

* forgot
2024-12-12 18:11:44 +01:00
Alexander Petric
aed793e548 updating hub script: adding error logging to git sync (#4913) 2024-12-12 18:11:05 +01:00
pyranota
7398c1c09c Display memory peak while installing wheels with uv (#4889)
* fix no_uv not affecting deploy

Before this fix no_uv, no_uv_compile and no_uv_install were not affecting Dependency jobs

These jobs are only affected if used USE_PIP_COMPILE or USE_PIP_INSTALL env variables

To make it more consistant, no_uv should also affect dep jobs.

Also make ansible use uv by default

* Make it build

* Add no_uv_usage stats

* Provide build-env for Samael through shell.nix

* Run update_sqlx.sh

* Update ee-repo-ref

* Display memory peak while installing wheels with uv

* Invert DISABLE_NSJAIL

* Make it safer

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2024-12-12 11:26:02 +01:00
Ruben Fiszel
0dab23f6bf improve subobjects handling in schemaForm 2024-12-12 02:19:20 +01:00
HugoCasa
a6db5cddf8 add php and mysql feature flags (#4905) 2024-12-12 01:16:25 +01:00
HugoCasa
563a492008 feat: accept direct file upload for webhook/http (s3) (#4903)
* feat: accept direct file upload for webhook/http (s3)

* nit

* nit
2024-12-12 00:33:19 +01:00
Guilhem
2f667d4065 add manual container size breakpoint (#4902)
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2024-12-12 00:32:27 +01:00
pyranota
3618e26326 Add no_uv_usage stats (#4883)
* fix no_uv not affecting deploy

Before this fix no_uv, no_uv_compile and no_uv_install were not affecting Dependency jobs

These jobs are only affected if used USE_PIP_COMPILE or USE_PIP_INSTALL env variables

To make it more consistant, no_uv should also affect dep jobs.

Also make ansible use uv by default

* Make it build

* Add no_uv_usage stats

* Provide build-env for Samael through shell.nix

* Run update_sqlx.sh

* Update ee-repo-ref

* Update sqlx cache

* Update ee-repo-ref

* Update ee repo ref

* Update sqlx cache

* Update ee-repo-ref.txt
2024-12-12 00:32:03 +01:00
HugoCasa
4ce4fba18f fix(frontend): form and content update when script is emptied (#4887)
* fix(frontend): form and content update when script is emptied

* fix no schema on init
2024-12-12 00:31:45 +01:00
Ruben Fiszel
ee5c95002e frontend check fix 2024-12-11 22:44:09 +01:00
Ruben Fiszel
a75c576a65 clear env for uv pip-compile 2024-12-11 17:06:05 +01:00
Lucas Abel
1b922f3ba9 fix s3_storage decl not being the same as in ee (#4901) 2024-12-11 14:39:45 +01:00
Lucas Abel
ca1ff291d7 fix run rendering of flownode job (#4900) 2024-12-11 14:33:42 +01:00
Lucas Abel
af5cca1b00 feat(cache): re-work job results cache (#4898) 2024-12-11 12:01:48 +01:00
Lucas Abel
343cd02ad5 nix: update flake (#4895) 2024-12-11 08:57:14 +01:00
Lucas Abel
e77ae1c25b fmt: re-format Rust leftovers (#4897) 2024-12-11 08:57:00 +01:00
Ruben Fiszel
cbdb6ab8e2 chore(main): release 1.437.1 (#4891)
* chore(main): release 1.437.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-12-10 19:45:16 +01:00
Ruben Fiszel
bcd2cfe674 fix(frontend): fix newly deployed apps with frontend scripts 2024-12-10 19:41:11 +01:00
Ruben Fiszel
7ab42de977 chore(main): release 1.437.0 (#4877)
* chore(main): release 1.437.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-12-10 17:47:48 +01:00
Ruben Fiszel
5695767c2e handle empty requirement line in python 2024-12-10 16:40:12 +01:00
Ruben Fiszel
c173e46a72 fix(python): do not follow symlinks when copying recursively in overlapping sites-package 2024-12-10 15:52:47 +01:00
Ruben Fiszel
1639bb2b35 improve debug logs 2024-12-10 12:35:56 +01:00
HugoCasa
9d180ba61c add xero oauth (#4867)
* add xero oauth

* revert

* Revert "revert"

This reverts commit 46a9ed3456.

* remove xero

* add back xero
2024-12-10 00:43:39 +01:00
Ruben Fiszel
96714b8b9b update sqlx 2024-12-10 00:42:37 +01:00
Ruben Fiszel
7fbf3cb7f5 improve nested field editor 2024-12-10 00:35:14 +01:00
wendrul
4dfbc32109 Don't truncate hostnames when graph is not showing (#4862) 2024-12-10 00:02:16 +01:00
Ruben Fiszel
23875033dd feat: add ctx.name to apps #4885 2024-12-09 23:58:30 +01:00
HugoCasa
7014389d67 fix(parsers): more robust pwsh param parsing (#4884) 2024-12-09 20:55:43 +01:00
Ruben Fiszel
69195b71f3 add home in uv pip compile env 2024-12-09 20:53:43 +01:00
Ruben Fiszel
5366c8a882 chore(main): release 1.436.0 (#4858)
* chore(main): release 1.436.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-12-09 19:11:21 +01:00
HugoCasa
1046c0b055 fix: disabling app custom url (#4868) 2024-12-09 19:09:18 +01:00
277 changed files with 9523 additions and 4973 deletions

View File

@@ -52,4 +52,6 @@ RUN unzip deno.zip && rm deno.zip && mv deno /usr/bin/deno
RUN apt-get update \
&& apt-get install -y postgresql-client --allow-unauthenticated
RUN rustup component add rustfmt
RUN rustup component add rustfmt
COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk
RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet

130
.github/workflows/backend-check.yml vendored Normal file
View File

@@ -0,0 +1,130 @@
name: Backend check
on:
push:
paths:
- "backend/**"
- ".github/workflows/backend-check.yml"
jobs:
check_oss:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
run: SQLX_OFFLINE=true cargo check
check_oss_full:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: install xmlsec1
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
run: SQLX_OFFLINE=true cargo check --all-features
check_ee:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Substitute EE code (EE logic is behind feature flag)
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
run: SQLX_OFFLINE=true cargo check
check_ee_full:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: install xmlsec1
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev
- name: Substitute EE code (EE logic is behind feature flag)
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
timeout-minutes: 16
working-directory: ./backend
run: SQLX_OFFLINE=true cargo check --all-features

View File

@@ -13,40 +13,58 @@ on:
- "backend/**"
- ".github/workflows/backend-test.yml"
defaults:
run:
working-directory: ./backend
jobs:
cargo_test:
runs-on: ubicloud-standard-8
container:
image: ghcr.io/windmill-labs/backend-tests
services:
postgres:
image: postgres
ports:
- 5432:5432
env:
POSTGRES_DB: windmill
POSTGRES_PASSWORD: changeme
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: actions/setup-go@v2
with:
go-version: 1.21.5
- uses: actions/setup-python@v2
with:
python-version: 3.11
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.1.38
- uses: astral-sh/setup-uv@v4
with:
version: "0.4.18"
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.80.0
# - uses: Swatinem/rust-cache@v2
# with:
# workspaces: |
# backend
# backend -> target
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo test
timeout-minutes: 15
timeout-minutes: 16
run:
/usr/bin/deno --version &&
/usr/bin/bun -v &&
go version &&
/usr/local/bin/python3 --version &&
mkdir frontend/build && cd backend && touch
windmill-api/openapi-deref.yaml &&
DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill
DISABLE_EMBEDDING=true RUST_LOG=info cargo test --features
enterprise,deno_core --all -- --nocapture
deno --version && bun -v && go version && python3 --version &&
SQLX_OFFLINE=true
DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill
DISABLE_EMBEDDING=true RUST_LOG=info PYTHON_PATH=$(which python)
DENO_PATH=$(which deno) BUN_PATH=$(which bun) GO_PATH=$(which go)
UV_PATH=$(which uv) cargo test --features
enterprise,deno_core,license,python,rust --all -- --nocapture

View File

@@ -64,7 +64,7 @@ jobs:
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,http_trigger,zip,oauth2,kafka,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -81,7 +81,7 @@ jobs:
platforms: linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,http_trigger,zip,oauth2,kafka,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -111,12 +111,16 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: RHEL9-amd64 build
path: ${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9
path:
${{ steps.extract-ee-amd64.outputs.destination
}}/windmill-ee-amd64-rhel9
- uses: actions/upload-artifact@v4
with:
name: RHEL9-arm64 build
path: ${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel9
path:
${{ steps.extract-ee-arm64.outputs.destination
}}/windmill-ee-arm64-rhel9
# - name: Attach binary to release
# uses: softprops/action-gh-release@v2

View File

@@ -1,70 +0,0 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
name: Build windmill-staging
on:
workflow_dispatch:
permissions: write-all
jobs:
build_ee:
runs-on: ubicloud
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-staging-ee
flavor: |
latest=false
tags: |
type=sha
type=ref,event=branch
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Substitute EE code
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Build and push publicly ee
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka
tags: |
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License

View File

@@ -45,7 +45,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
- name: Rename binary with corresponding architecture
run: |

View File

@@ -19,7 +19,10 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: depot/setup-action@v1
# - uses: depot/setup-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
@@ -40,7 +43,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly
uses: depot/build-push-action@v1
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/DockerfileMultiplayer

View File

@@ -67,7 +67,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=embedding,parquet,openidconnect,deno_core
features=embedding,parquet,openidconnect,deno_core,license,http_trigger,zip,oauth2,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ steps.meta-public.outputs.tags }}

View File

@@ -1,9 +1,11 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.event_name != 'pull_request' && github.repository ||
'windmill-labs/windmill-test' }}
DEV_SHA: ${{ github.event_name != 'pull_request' && 'dev' || format('pr-{0}',
github.event.number) }}
IMAGE_NAME:
${{ github.event_name != 'pull_request' && github.event_name !=
'workflow_dispatch' && github.repository || 'windmill-labs/windmill-test' }}
DEV_SHA:
${{ github.event_name != 'pull_request' && github.event_name !=
'workflow_dispatch' && 'dev' || github.event.inputs.tag || 'test' }}
name: Build windmill:main
on:
@@ -14,6 +16,17 @@ on:
types: [opened, synchronize, reopened]
paths:
- "Dockerfile"
workflow_dispatch:
inputs:
ee:
description: "Build EE image (true, false)"
required: false
default: false
type: boolean
tag:
description: "Tag the image"
required: true
default: "test"
concurrency:
group: ${{ github.ref }}
@@ -23,8 +36,8 @@ permissions: write-all
jobs:
build:
runs-on: ubicloud
if: (github.event_name != 'issue_comment') || (contains(github.event.comment.body, '/buildimage_all') || contains(github.event.comment.body, '/buildimage_base'))
runs-on: ubicloud-standard-8
if: (github.event_name != 'workflow_dispatch') || (inputs.ee == false)
steps:
- uses: actions/checkout@v4
with:
@@ -42,9 +55,9 @@ jobs:
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - uses: depot/setup-action@v1
- name: Login to registry
uses: docker/login-action@v3
@@ -70,13 +83,12 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
- name: Build and push publicly
uses: depot/build-push-action@v1
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=embedding,parquet,openidconnect,jemalloc,deno_core,dind
features=embedding,parquet,openidconnect,jemalloc,deno_core,license,http_trigger,zip,oauth2,dind,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
${{ steps.meta-public.outputs.tags }}
@@ -84,9 +96,10 @@ jobs:
${{ steps.meta-public.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3
build_ee:
runs-on: ubicloud
if: (github.event_name != 'issue_comment') || (contains(github.event.comment.body, '/buildimage_ee') || contains(github.event.comment.body, '/buildimage_nsjail')) || contains(github.event.comment.body, '/buildimage_all')
build_arm:
runs-on: ubicloud-standard-8-arm
if: (github.event_name != 'workflow_dispatch') || (inputs.ee == false)
steps:
- uses: actions/checkout@v4
with:
@@ -104,9 +117,70 @@ jobs:
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - uses: depot/setup-action@v1
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Substitute EE code (EE logic is behind feature flag)
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Docker meta
id: meta-public
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push publicly
uses: docker/build-push-action@v6
with:
context: .
push: true
build-args: |
features=embedding,parquet,openidconnect,jemalloc,deno_core,license,http_trigger,zip,oauth2,dind,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
${{ steps.meta-public.outputs.tags }}
labels: |
${{ steps.meta-public.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3
build_ee:
runs-on: ubicloud
if: (github.event_name != 'workflow_dispatch') || (inputs.ee == true)
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
@@ -132,20 +206,85 @@ jobs:
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Build and push publicly ee
uses: depot/build-push-action@v1
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
# platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,otel,dind
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,otel,dind,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
cache-from: type=gha
cache-to: type=gha
build_ee_arm:
runs-on: ubicloud
if: (github.event_name != 'workflow_dispatch') || (inputs.ee == true)
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Substitute EE code
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Build and push publicly ee
uses: docker/build-push-action@v6
with:
context: .
# platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,otel,dind,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
cache-from: type=gha
cache-to: type=gha
build_ee_312:
runs-on: ubicloud
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
@@ -166,9 +305,9 @@ jobs:
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public-py312
@@ -194,19 +333,21 @@ jobs:
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Build and push publicly ee
uses: depot/build-push-action@v1
uses: docker/build-push-action@v1
with:
context: .
platforms: linux/amd64
# platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,otel,dind
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,otel,dind,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
PYTHON_IMAGE=python:3.12.2-slim-bookworm
tags: |
${{ steps.meta-ee-public-py312.outputs.tags }}
labels: |
${{ steps.meta-ee-public-py312.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
cache-from: type=gha
cache-to: type=gha
# disabled until we make it 100% reliable and add more meaningful tests
# playwright:
@@ -330,7 +471,7 @@ jobs:
# ${{ steps.extract-ee.outputs.destination }}/*
run_integration_test:
runs-on: ubicloud
runs-on: ubicloud-standard-8
needs: [build_ee]
steps:
- uses: actions/checkout@v4
@@ -341,7 +482,7 @@ jobs:
run: cd integration_tests && ./build.sh
- name: Test run
if: ${{ ! startsWith(github.ref, 'refs/tags/v') }}
timeout-minutes: 15
timeout-minutes: 16
env:
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
run: cd integration_tests && ./run.sh
@@ -356,7 +497,9 @@ jobs:
tag_latest:
runs-on: ubicloud
needs: [run_integration_test, build]
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
if:
github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' ||
startsWith(github.ref, 'refs/tags/v'))
steps:
- uses: actions/checkout@v4
with:
@@ -375,7 +518,9 @@ jobs:
tag_latest_ee:
runs-on: ubicloud
needs: [run_integration_test, build_ee]
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
if:
github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' ||
startsWith(github.ref, 'refs/tags/v'))
steps:
- uses: actions/checkout@v4
with:
@@ -436,16 +581,18 @@ jobs:
build_ee_nsjail:
needs: [build_ee]
runs-on: ubicloud
if: (github.event_name != 'issue_comment') || (github.event_name != 'pull_request') || (contains(github.event.comment.body, '/buildimage_nsjail') || contains(github.event.comment.body, '/buildimage_all'))
if:
(github.event_name != 'pull_request') && (github.event_name !=
'workflow_dispatch')
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
@@ -470,10 +617,61 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: depot/build-push-action@v1
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
# platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileNsjail"
tags: |
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
build_ee_nsjail_arm:
needs: [build_ee_arm]
runs-on: ubicloud
if:
(github.event_name != 'pull_request') && (github.event_name !=
'workflow_dispatch')
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-nsjail
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,enable=true,priority=100,prefix=,suffix=,format=short
type=ref,event=branch
type=ref,event=pr
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: docker/build-push-action@v6
with:
context: .
# platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileNsjail"
tags: |
@@ -490,10 +688,10 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
# - uses: depot/setup-action@v1
- name: Login to registry
uses: docker/login-action@v3
@@ -514,10 +712,54 @@ jobs:
type=sha,enable=true,priority=100,prefix=,suffix=,format=short
- name: Build and push publicly ee reports
uses: depot/build-push-action@v1
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
# platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileReports"
tags: |
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
build_ee_reports_privately_arm:
needs: [build_ee_nsjail_arm]
runs-on: ubicloud
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - uses: depot/setup-action@v1
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta-ee-public
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-reports
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,enable=true,priority=100,prefix=,suffix=,format=short
- name: Build and push publicly ee reports
uses: docker/build-push-action@v6
with:
context: .
# platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileReports"
tags: |
@@ -527,9 +769,11 @@ jobs:
org.opencontainers.image.licenses=Windmill-Enterprise-License
publish_ecr_s3:
needs: [build_ee_nsjail]
needs: [build_ee_nsjail_arm]
runs-on: ubicloud-standard-2-arm
if: github.event_name != 'pull_request'
if:
(github.event_name != 'pull_request') && (github.event_name !=
'workflow_dispatch')
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -573,16 +817,16 @@ jobs:
build_ee_cuda:
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
needs: [build_ee]
runs-on: ubicloud
runs-on: ubicloud-standard-16
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
@@ -602,10 +846,10 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: depot/build-push-action@v1
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
# platforms: linux/amd64
push: true
file: "./docker/DockerfileCuda"
tags: |
@@ -623,10 +867,10 @@ jobs:
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
@@ -646,10 +890,10 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: depot/build-push-action@v1
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
# platforms: linux/amd64
push: true
file: "./docker/DockerfileSlim"
tags: |
@@ -666,10 +910,10 @@ jobs:
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
@@ -689,10 +933,10 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: depot/build-push-action@v1
uses: docker/build-push-action@v1
with:
context: .
platforms: linux/amd64
# platforms: linux/amd64
push: true
file: "./docker/DockerfileSlimEe"
tags: |
@@ -710,10 +954,10 @@ jobs:
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-public
@@ -733,10 +977,53 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly
uses: depot/build-push-action@v1
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
# platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileFull"
tags: |
${{ steps.meta-public.outputs.tags }}
labels: |
${{ steps.meta-public.outputs.labels }}
build_full_arm:
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
needs: [build_arm]
runs-on: ubicloud
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-public
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-full
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly
uses: docker/build-push-action@v6
with:
context: .
# platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileFull"
tags: |
@@ -753,10 +1040,8 @@ jobs:
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta
id: meta-ee-public
@@ -776,10 +1061,52 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: depot/build-push-action@v1
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
# platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileFullEe"
tags: |
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
build_ee_full_arm:
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
needs: [build_ee_arm]
runs-on: ubicloud
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta
id: meta-ee-public
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-full
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: docker/build-push-action@v6
with:
context: .
# platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileFullEe"
tags: |

View File

@@ -15,7 +15,6 @@ jobs:
with:
node-version: 18
- name: "npm check"
timeout-minutes: 2
run:
cd frontend && npm ci && npm run generate-backend-client && npm run
timeout-minutes: 5
run: cd frontend && npm ci && npm run generate-backend-client && npm run
check

View File

@@ -47,7 +47,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
- name: Rename binary with corresponding architecture
run: |

View File

@@ -1,5 +1,94 @@
# Changelog
## [1.440.1](https://github.com/windmill-labs/windmill/compare/v1.440.0...v1.440.1) (2024-12-18)
### Bug Fixes
* **internal:** updating rust to 1.82 ([02a8f1f](https://github.com/windmill-labs/windmill/commit/02a8f1f86453a5f8769364ba3798998b8830d086))
## [1.440.0](https://github.com/windmill-labs/windmill/compare/v1.439.0...v1.440.0) (2024-12-18)
### Features
* **cache:** remove persistent raw values from queue ([#4866](https://github.com/windmill-labs/windmill/issues/4866)) ([977ac5c](https://github.com/windmill-labs/windmill/commit/977ac5c3f3c2e8224f4915e483ae60f28ce008fc))
### Bug Fixes
* add workspace selector and fix css for create webhook page ([#4939](https://github.com/windmill-labs/windmill/issues/4939)) ([d880655](https://github.com/windmill-labs/windmill/commit/d8806555f1d78a5fae7ae77acbcdad402e89951d))
* fix relative imports in cached flow scripts ([13be0cd](https://github.com/windmill-labs/windmill/commit/13be0cd1c822d7a809dc96914dd1286510b9f9eb))
## [1.439.0](https://github.com/windmill-labs/windmill/compare/v1.438.0...v1.439.0) (2024-12-15)
### Features
* add multipart/form-data support ([#4927](https://github.com/windmill-labs/windmill/issues/4927)) ([83a60cb](https://github.com/windmill-labs/windmill/commit/83a60cbc517d5ddab24b247fd5e452175d59ad07))
### Bug Fixes
* ECS terraform db url + ami issues ([#4924](https://github.com/windmill-labs/windmill/issues/4924)) ([5172c13](https://github.com/windmill-labs/windmill/commit/5172c13ab8e9aeb1a83c161e7e7f63ebfb40b008))
## [1.438.0](https://github.com/windmill-labs/windmill/compare/v1.437.1...v1.438.0) (2024-12-13)
### Features
* accept direct file upload for webhook/http (s3) ([#4903](https://github.com/windmill-labs/windmill/issues/4903)) ([563a492](https://github.com/windmill-labs/windmill/commit/563a49200898bc32ab114f63c7dd575000f49e60))
* add C# support ([#4908](https://github.com/windmill-labs/windmill/issues/4908)) ([c85d2a4](https://github.com/windmill-labs/windmill/commit/c85d2a495715e9506984b0d26bcf34a25d476c09))
* **backend:** handle xml payload as raw_string ([#4915](https://github.com/windmill-labs/windmill/issues/4915)) ([3864cfc](https://github.com/windmill-labs/windmill/commit/3864cfce246d2b0d1a61b46578ed39221ebbbd31))
* **cache:** re-work job results cache ([#4898](https://github.com/windmill-labs/windmill/issues/4898)) ([af5cca1](https://github.com/windmill-labs/windmill/commit/af5cca1b002ecb6eae14684620e0a695394b6df8))
### Bug Fixes
* add `DOTNET_ROOT` env variable ([#4921](https://github.com/windmill-labs/windmill/issues/4921)) ([eb3ed7a](https://github.com/windmill-labs/windmill/commit/eb3ed7a2c7623a5ee87e245dfda16266ed8e467c))
* app custom url diff ([#4914](https://github.com/windmill-labs/windmill/issues/4914)) ([952cbd1](https://github.com/windmill-labs/windmill/commit/952cbd182de5358cc326d7582f4494c860e3836e))
* c#: nsjail image, default langs and feature cage imports ([#4917](https://github.com/windmill-labs/windmill/issues/4917)) ([afac8a7](https://github.com/windmill-labs/windmill/commit/afac8a73f0a76850ff9eca0e368db0e3ec793328))
* flow node default tag ([3634ade](https://github.com/windmill-labs/windmill/commit/3634ade41b07cbae0fd1ba40b109815b7f57562d))
* **frontend:** form and content update when script is emptied ([#4887](https://github.com/windmill-labs/windmill/issues/4887)) ([4ce4fba](https://github.com/windmill-labs/windmill/commit/4ce4fba18f529f5f81ad6e242059f50254fc509b))
* **frontend:** schedule operator perms + add instance settings in operator menu ([#4912](https://github.com/windmill-labs/windmill/issues/4912)) ([762ac30](https://github.com/windmill-labs/windmill/commit/762ac30b59e1d23d2a043a86c2ce4b069030941d))
## [1.437.1](https://github.com/windmill-labs/windmill/compare/v1.437.0...v1.437.1) (2024-12-10)
### Bug Fixes
* **frontend:** fix newly deployed apps with frontend scripts ([bcd2cfe](https://github.com/windmill-labs/windmill/commit/bcd2cfe674038042f08f22d05286596b3442caf9))
## [1.437.0](https://github.com/windmill-labs/windmill/compare/v1.436.0...v1.437.0) (2024-12-10)
### Features
* add ctx.name to apps [#4885](https://github.com/windmill-labs/windmill/issues/4885) ([2387503](https://github.com/windmill-labs/windmill/commit/23875033dd19d6d585f2433fa7b18a703175e329))
### Bug Fixes
* **parsers:** more robust pwsh param parsing ([#4884](https://github.com/windmill-labs/windmill/issues/4884)) ([7014389](https://github.com/windmill-labs/windmill/commit/7014389d677fba563bec02e08815452b195ecd0a))
* **python:** do not follow symlinks when copying recursively in overlapping sites-package ([c173e46](https://github.com/windmill-labs/windmill/commit/c173e46a724b50d27416bbc169bf2a57fe68808c))
## [1.436.0](https://github.com/windmill-labs/windmill/compare/v1.435.2...v1.436.0) (2024-12-09)
### Features
* add db storage for app inline scripts ([#4837](https://github.com/windmill-labs/windmill/issues/4837)) ([2bc4934](https://github.com/windmill-labs/windmill/commit/2bc4934c4f38ca2631eb99140d89a0b3d3b3a2cf))
* add otlp support ([#4869](https://github.com/windmill-labs/windmill/issues/4869)) ([6d04744](https://github.com/windmill-labs/windmill/commit/6d047449e200d51f060d4107ff5ad0e10af99a66))
* **cache:** refurbish fs backed cache ([#4863](https://github.com/windmill-labs/windmill/issues/4863)) ([3c4408e](https://github.com/windmill-labs/windmill/commit/3c4408e3dbf8a37c0b862805b1991af3d39ac054))
* docker runtime ([2c5d07f](https://github.com/windmill-labs/windmill/commit/2c5d07f3d9069b21b877ea2d085f0a851b1975c1))
* **frontend:** render new job kinds ([#4864](https://github.com/windmill-labs/windmill/issues/4864)) ([691ef64](https://github.com/windmill-labs/windmill/commit/691ef6468823c99ab129c53ae699de1a91f77c97))
### Bug Fixes
* fix `flow_node` uniqueness ([#4850](https://github.com/windmill-labs/windmill/issues/4850)) ([667167a](https://github.com/windmill-labs/windmill/commit/667167a022ed31cb67a6c189cc5bd54b09473f4f))
* handle flow & workspace renames for `flow_node` ([#4861](https://github.com/windmill-labs/windmill/issues/4861)) ([f175158](https://github.com/windmill-labs/windmill/commit/f175158b9ffcc0d9c35938e61dfe3ce56d3b64c2))
## [1.435.2](https://github.com/windmill-labs/windmill/compare/v1.435.1...v1.435.2) (2024-12-05)

View File

@@ -1,5 +1,5 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.80-slim-bookworm
ARG RUST_IMAGE=rust:1.82-slim-bookworm
ARG PYTHON_IMAGE=python:3.11.10-slim-bookworm
FROM ${RUST_IMAGE} AS rust_base
@@ -13,8 +13,6 @@ RUN apt-get -y update \
RUN rustup component add rustfmt
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install cargo-chef --version ^0.1
RUN cargo install sccache --version ^0.8
ENV RUSTC_WRAPPER=sccache SCCACHE_DIR=/backend/sccache
WORKDIR /windmill
@@ -50,9 +48,7 @@ FROM rust_base AS planner
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef prepare --recipe-path recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef prepare --recipe-path recipe.json
FROM rust_base AS builder
ARG features=""
@@ -63,9 +59,7 @@ RUN apt-get update && apt-get install -y libxml2-dev=2.9.* libxmlsec1-dev=1.2.*
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
@@ -76,9 +70,7 @@ COPY --from=frontend /frontend/build /frontend/build
COPY --from=frontend /backend/windmill-api/openapi-deref.yaml ./windmill-api/openapi-deref.yaml
COPY .git/ .git/
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
FROM ${PYTHON_IMAGE}

View File

@@ -0,0 +1,67 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n job_kind AS \"job_kind: JobKind\",\n script_hash AS \"script_hash: ScriptHash\",\n flow_status AS \"flow_status!: Json<Box<RawValue>>\",\n raw_flow AS \"raw_flow: Json<Box<RawValue>>\"\n FROM queue WHERE id = $1 AND workspace_id = $2 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_kind: JobKind",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 1,
"name": "script_hash: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "flow_status!: Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "raw_flow: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
true,
true,
true
]
},
"hash": "0a6a89e6ab3037f02c3c4c84ee02138d5fded1e6360bb992046fe9711b5ea213"
}

View File

@@ -75,7 +75,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\"\n FROM flow_version WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "10cd655d3d2916721b530c7faa7652fb8fae25383b58f6b9e8dc431b76947315"
}

View File

@@ -52,7 +52,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n MAX (created_at) AS last_deploy, \n COUNT (*) AS deploys_count \n , 'python' AS language\n FROM metrics \n WHERE id = 'no_uv_usage_py'\n\n UNION ALL\n \n SELECT \n MAX (created_at) AS last_deploy, \n COUNT (*) AS deploys_count \n , 'ansible' AS language\n FROM metrics \n WHERE id = 'no_uv_usage_ansible'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "last_deploy",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "deploys_count",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "language",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null
]
},
"hash": "240cf4ba63ec39a7ccfa8360824259d2fdc6681bbbd44e3ccde0a3893f6cf9a0"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value FROM app_version WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Json"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "29fce7e56bb4a874b4c1e3a6454273203aab7344abd03df4d86e4c5f5b9a394a"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow.versions[array_upper(flow.versions, 1)] AS \"version!: i64\"\n FROM flow WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version!: i64",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "2b2ee874dbd90beec26713d2effdc5d011d9f1091a13761642d064220add7b41"
}

View File

@@ -61,7 +61,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -0,0 +1,73 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n script_path, script_hash AS \"script_hash: ScriptHash\",\n job_kind AS \"job_kind: JobKind\",\n flow_status AS \"flow_status: Json<Box<RawValue>>\",\n raw_flow AS \"raw_flow: Json<Box<RawValue>>\"\n FROM completed_job WHERE id = $1 and workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_hash: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "job_kind: JobKind",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 3,
"name": "flow_status: Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "raw_flow: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
true,
true,
false,
true,
true
]
},
"hash": "3df03ec2345c905f03450e1e3f0c3ce7f41a22e72c5180ef8cc910305e4d0fce"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO metrics (id, value) \n VALUES ('no_uv_usage_ansible', $1)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "4a804ee30bfe86c4e2c15a9f6511be5adf0dd22cb942fac64b439fb4e20df447"
}

View File

@@ -57,7 +57,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM usr where username = $1 AND workspace_id = $2",
"query": "SELECT usr.*, password.super_admin, password.name FROM usr LEFT JOIN password ON usr.email = password.email Where usr.username = $1 AND workspace_id = $2\n ",
"describe": {
"columns": [
{
@@ -42,6 +42,16 @@
"ordinal": 7,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "super_admin",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
@@ -58,8 +68,10 @@
false,
false,
false,
true,
false,
true
]
},
"hash": "3d363466d79075df3f74f946eff43ca89faefca3bcdf2c533425ca3868b0369a"
"hash": "6aabe704395c9be30c86d15a5d22f3509b4fcea56227b019588837132b64d58b"
}

View File

@@ -28,7 +28,8 @@
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode"
"flownode",
"appscript"
]
}
}
@@ -54,7 +55,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT hash FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND lock IS not NULL AND lock_error_logs IS NULL",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "700f987ec9b2a17c9b8304d598269f8fb58a432934163588af5ea84481c1c087"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\"\n FROM flow_version WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "7a9a711c7cb05ed8a61116586fb68ef270567ebd70266f07139f3e3e34940699"
}

View File

@@ -30,7 +30,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -1,27 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO flow_node (path, workspace_id, hash_v2, lock, code, flow)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (hash_v2) DO UPDATE SET path = EXCLUDED.path -- trivial update to return the id\n RETURNING id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bpchar",
"Text",
"Text",
"Jsonb"
]
},
"nullable": [
false
]
},
"hash": "83cc9e432aea1450f79e9fce04eca0e75f30faa8c39ad4754bd568ae6f210806"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\"\n FROM flow_version_lite WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true
]
},
"hash": "85085cdc08c262c4750566a0a0b9754017b890ea8c1161a5892b6c29e663ee0e"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource\n (workspace_id, path, value, resource_type, created_by, edited_at)\n VALUES ($1, $2, $3, $4, $5, now()) ON CONFLICT (workspace_id, path)\n DO UPDATE SET value = $3, edited_at = now()",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "8bd028c8b5f8a4d566f89eebc2e63fd04beaf2b0b49e07c7df42ecddd70737f3"
}

View File

@@ -0,0 +1,71 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock AS \"lock: String\", content AS \"code!: String\",\n language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase AS \"codebase: String\" FROM script WHERE hash = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock: String",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "code!: String",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "language: Option<ScriptLang>",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible"
]
}
}
}
},
{
"ordinal": 3,
"name": "envs: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 4,
"name": "codebase: String",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
false,
false,
true,
true
]
},
"hash": "9c1a1d0feb79f750c7143fabb0cfa7dab8dd683cd294c27d0549bd8d78ab60a0"
}

View File

@@ -57,7 +57,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -28,7 +28,8 @@
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode"
"flownode",
"appscript"
]
}
}
@@ -54,7 +55,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock AS \"lock: String\", code AS \"code: String\", flow AS \"flow: Json<Box<RawValue>>\" FROM flow_node WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock: String",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "code: String",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "flow: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "9ee2f67042c1bed1e7d13eb7d07e78991e5d7cf01fc7993531dbedf33dac2e0b"
}

View File

@@ -38,7 +38,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_code, raw_lock, raw_flow AS \"raw_flow: Json<Box<RawValue>>\" FROM job WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "raw_code",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "raw_lock",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "raw_flow: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "bc8ac03254669951654cda4bcfa12491341e745aef5e0e5090c2f4e4a4dc54fb"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT true FROM token WHERE token = $1 and expiration IS NOT NULL and expiration > now() + $2::int * '1 sec'::interval",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Int4"
]
},
"nullable": [
null
]
},
"hash": "bfff3d8df18db198d6ebba8a049b00147fc8bcd42f3df37ef81b9ded80974bd0"
}

View File

@@ -1,18 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource\n (workspace_id, path, value, resource_type, created_by, edited_at)\n VALUES ($1, $2, $3, $4, $5, now()) ON CONFLICT (workspace_id, path)\n DO UPDATE SET value = $3, edited_at = now()",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "c4ee16065fa021cf6443c2f29f1ca986b9730eae9df26dd33c3ea21e35df62c4"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\"\n FROM flow_version_lite WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true
]
},
"hash": "c7de95ef4934752b62d6d01bf663ab6a104bbcfb827a4a9e4e03e38732375d55"
}

View File

@@ -0,0 +1,72 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock AS \"lock: String\", content AS \"code!: String\",\n language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase AS \"codebase: String\" FROM script WHERE hash = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock: String",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "code!: String",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "language: Option<ScriptLang>",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp"
]
}
}
}
},
{
"ordinal": 3,
"name": "envs: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 4,
"name": "codebase: String",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
false,
false,
true,
true
]
},
"hash": "d061e7ca73987036928e17245360fd7f2969aab0c422a00bbdade6a7236aa75d"
}

View File

@@ -1,27 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH existing AS (\n SELECT id FROM flow_node\n WHERE hash = $1 AND path = $2 AND workspace_id = $3\n AND (code IS NOT DISTINCT FROM $4)\n AND (lock IS NOT DISTINCT FROM $5)\n AND (flow IS NOT DISTINCT FROM $6)\n LIMIT 1\n ),\n inserted AS (\n INSERT INTO flow_node (hash, path, workspace_id, code, lock, flow)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT DO NOTHING\n RETURNING id\n )\n SELECT id FROM existing\n UNION ALL\n SELECT id FROM inserted\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8",
"Text",
"Text",
"Text",
"Text",
"Jsonb"
]
},
"nullable": [
null
]
},
"hash": "dc32e9e81eab9cda88875020acc414271b96d251bcf54c153c45370d917eea9d"
}

View File

@@ -37,7 +37,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO metrics (id, value) \n VALUES ('no_uv_usage_py', $1)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "ed318070b26861fda2d591a4356fdbeb6c7fdc965be43bddb010fd8299af1286"
}

View File

@@ -81,7 +81,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

View File

@@ -57,7 +57,8 @@
"php",
"bunnative",
"rust",
"ansible"
"ansible",
"csharp"
]
}
}

760
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.435.2"
version = "1.440.1"
authors.workspace = true
edition.workspace = true
@@ -21,6 +21,7 @@ members = [
"./parsers/windmill-parser-wasm",
"./parsers/windmill-parser-go",
"./parsers/windmill-parser-rust",
"./parsers/windmill-parser-csharp",
"./parsers/windmill-parser-bash",
"./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-imports",
@@ -29,7 +30,7 @@ members = [
]
[workspace.package]
version = "1.435.2"
version = "1.440.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -46,25 +47,39 @@ lto = "thin"
[features]
default = []
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise", "windmill-indexer/enterprise"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
enterprise_saml = ["windmill-api/enterprise_saml"]
stripe = ["windmill-api/stripe"]
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark", "windmill-common/benchmark"]
flamegraph = ["windmill-common/flamegraph", "windmill-worker/flamegraph"]
loki = ["windmill-common/loki"]
embedding = ["windmill-api/embedding"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet", "windmill-indexer/parquet", "dep:object_store"]
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet", "dep:object_store"]
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus", "dep: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 = ["dep:windmill-indexer", "windmill-api/tantivy"]
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy", "windmill-indexer/enterprise", "windmill-indexer/parquet"]
sqlx = ["windmill-worker/sqlx"]
deno_core = ["windmill-worker/deno_core", "dep:deno_core"]
kafka = ["windmill-api/kafka"]
otel = ["windmill-common/otel", "windmill-worker/otel"]
dind = ["windmill-worker/dind"]
php = ["windmill-worker/php"]
rust = ["windmill-worker/rust"]
mysql = ["windmill-worker/mysql"]
mssql = ["windmill-worker/mssql"]
bigquery = ["windmill-worker/bigquery"]
websocket = ["windmill-api/websocket"]
python = ["windmill-worker/python"]
smtp = ["windmill-api/smtp", "windmill-common/smtp"]
csharp = ["windmill-worker/csharp"]
license = ["windmill-api/license"]
oauth2 = ["windmill-api/oauth2"]
http_trigger = ["windmill-api/http_trigger"]
zip = ["windmill-api/zip"]
static_frontend = ["windmill-api/static_frontend"]
[dependencies]
anyhow.workspace = true
@@ -88,7 +103,7 @@ sha2.workspace = true
url.workspace = true
lazy_static.workspace = true
once_cell.workspace = true
prometheus.workspace = true
prometheus = { workspace = true, optional = true }
uuid.workspace = true
gethostname.workspace = true
serde_json.workspace = true
@@ -129,6 +144,7 @@ windmill-parser-py-imports = { path = "./parsers/windmill-parser-py-imports" }
windmill-parser-go = { path = "./parsers/windmill-parser-go" }
windmill-parser-rust = { path = "./parsers/windmill-parser-rust" }
windmill-parser-yaml = { path = "./parsers/windmill-parser-yaml" }
windmill-parser-csharp = { path = "./parsers/windmill-parser-csharp" }
windmill-parser-bash = { path = "./parsers/windmill-parser-bash" }
windmill-parser-sql = { path = "./parsers/windmill-parser-sql" }
windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
@@ -165,7 +181,7 @@ rand = "^0"
rand_core = { version = "^0", features = ["std"] }
magic-crypt = "^3"
git-version = "^0"
rustpython-parser = { git = "https://github.com/RustPython/Parser", rev = "9ce55aefdeb35e2f706ce0b02d5a2dfe6295fc57" }
rustpython-parser = "^0"
php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb411dec09450946ef57920b7ffced7f6495d" }
cron = "^0"
mail-send = { version = "0.4.0", features = ["builder"], default-features=false }
@@ -224,13 +240,13 @@ lazy_static = "1.4.0"
serde_derive = "1.0.147"
const_format = { version = "0.2", features = ["rust_1_64", "rust_1_51"] }
dyn-iter = "0.2.0"
rsa = "0.7.2"
rsa = "^0"
async-stripe = { version = "0.39.1", features = [
"runtime-tokio-hyper",
"checkout",
"billing",
] }
async_zip = { version = "0.0.11", features = ["full"] }
async_zip = { version = "0.0.17", features = ["tokio", "tokio-fs", "deflate", "chrono"] }
once_cell = "1.17.1"
gosyn = "0.2.6"
bytes = "1.4.0"
@@ -260,7 +276,7 @@ tokenizers = "0.14.1"
candle-core = "0.3.0"
candle-transformers = "0.3.0"
candle-nn = "0.3.0"
tiberius = { git = "https://github.com/prisma/tiberius", rev = "8f66a699dfa041e7b5f736c7e94f92c945453c9e", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"]}
tiberius = { version = "0.12.3", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"]}
pin-project = "1"
indexmap = { version = "2.2.5", features = ["serde"]}
tokio-native-tls = "^0"
@@ -271,8 +287,7 @@ rdkafka = { version = "0.36.2", features = ["cmake-build", "ssl-vendored"] }
datafusion = "39.0.0"
object_store = { version = "0.10.0", features = ["aws", "azure"] }
openidconnect = { version = "3.4.0" }
zstd = "=0.12.4"
openidconnect = { version = "4.0.0-rc.1" }
aws-config = "^1"
aws-sdk-sts = "^1"
@@ -312,3 +327,5 @@ quote = "1.0.36"
regex-lite = "0.1.6"
yaml-rust = "0.4.5"
tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] }
tree-sitter = {version = "0.23.0", features = []}
tree-sitter-c-sharp = "0.23.0"

View File

@@ -1 +1 @@
a07bc62582c809457f1c945d6cda145770b94d04
324617e19a5906fe57c911c8df4fb63786a1634a

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'csharp';
UPDATE config set config = jsonb_set(config, '{worker_tags}', config->'worker_tags' || '["csharp"]'::jsonb) where name = 'worker__default' and config @> '{"worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun", "php", "rust", "ansible"]}'::jsonb AND NOT config->'worker_tags' @> '"csharp"'::jsonb;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
delete from app_version_lite;

View File

@@ -172,5 +172,13 @@
"user-library-read"
]
},
"xero": {
"auth_url": "https://login.xero.com/identity/connect/authorize",
"token_url": "https://identity.xero.com/connect/token",
"scopes": [
"offline_access",
"accounting.transactions"
]
},
"snowflake_oauth": {}
}

View File

@@ -9,7 +9,7 @@ use regex_lite::Regex;
use serde_json::json;
use std::collections::HashMap;
use std::{collections::HashMap, str::FromStr};
use windmill_parser::{Arg, MainArgSignature, Typ};
pub fn parse_bash_sig(code: &str) -> anyhow::Result<MainArgSignature> {
@@ -84,36 +84,94 @@ fn parse_bash_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
Ok(Some(args))
}
enum ParserState {
Normal,
InSingleQuote,
InDoubleQuote,
}
fn split_pwsh_args(code: &str) -> Vec<&str> {
let mut chars = code.char_indices().peekable();
let mut state = ParserState::Normal;
let mut splits = vec![];
let mut last_idx = 0;
while let Some((idx, char)) = chars.next() {
match (&state, char) {
(ParserState::Normal, '\'') => {
state = ParserState::InSingleQuote;
}
(ParserState::Normal, '"') => {
state = ParserState::InDoubleQuote;
}
(ParserState::InSingleQuote, '\'') => {
state = ParserState::Normal;
}
(ParserState::InDoubleQuote, '"') => {
state = ParserState::Normal;
}
(ParserState::Normal, ',') => {
splits.push(&code[last_idx..idx]);
last_idx = idx + 1; // skip the comma
}
_ => {}
}
}
if last_idx < code.len() {
splits.push(&code[last_idx..]);
}
splits
}
fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let param_wrapper = RE_POWERSHELL_PARAM.captures(code);
let mut args = vec![];
if let Some(param_wrapper) = param_wrapper {
let param_wrapper = param_wrapper.get(1).unwrap().as_str();
for cap in RE_POWERSHELL_ARGS.captures_iter(param_wrapper) {
let typ = cap
.get(1)
.map(|x| x.as_str().to_string())
.unwrap_or("string".to_string());
let name = cap.get(2).unwrap().as_str().to_string();
let default = cap
.get(3)
.or(cap.get(4))
.map(|x| json!(x.as_str().to_string()));
let params = split_pwsh_args(param_wrapper);
for param in params {
if let Some(cap) = RE_POWERSHELL_ARGS.captures(param) {
let typ = cap.get(1).map(|x| x.as_str().to_string());
let name = cap.get(2).unwrap().as_str().to_string();
args.push(Arg {
name: name,
typ: match typ.as_str() {
"string" => Typ::Str(None),
"int" | "long" => Typ::Int,
"decimal" | "double" | "single" => Typ::Float,
"datetime" | "DateTime" => Typ::Datetime,
_ => Typ::Str(None),
},
default: default.clone(),
otyp: None,
has_default: default.is_some(),
oidx: None,
});
let mut parsed_typ = if let Some(typ) = typ {
match typ.as_str() {
"string" => Some(Typ::Str(None)),
"int" | "long" => Some(Typ::Int),
"decimal" | "double" | "single" => Some(Typ::Float),
"datetime" | "DateTime" => Some(Typ::Datetime),
_ => None,
}
} else {
None
};
let default = if let Some(x) = cap.get(3) {
Some(json!(x.as_str().to_string()))
} else if let Some(x) = cap.get(4) {
if parsed_typ.is_none() {
if x.as_str().parse::<i64>().is_ok() {
parsed_typ = Some(Typ::Int);
} else if x.as_str().parse::<f64>().is_ok() {
parsed_typ = Some(Typ::Float);
}
}
serde_json::Number::from_str(x.as_str())
.ok()
.map(serde_json::Value::Number)
} else {
None
};
args.push(Arg {
name: name,
typ: parsed_typ.unwrap_or(Typ::Str(None)),
default: default.clone(),
otyp: None,
has_default: default.is_some(),
oidx: None,
});
}
}
}
Ok(Some(args))
@@ -192,4 +250,77 @@ non_required="${5:-}"
Ok(())
}
#[test]
fn test_parse_powershell_sig() -> anyhow::Result<()> {
let code = r#"param($Msg, [string]$Msg2, $Dflt = "default value, with comma", [int]$Nb = 3 , $Nb2 = 5.0, $Nb3 = 5, $Wahoo = $env:WAHOO)"#;
assert_eq!(
parse_powershell_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: None,
name: "Msg".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "Msg2".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "Dflt".to_string(),
typ: Typ::Str(None),
default: Some(json!("default value, with comma")),
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "Nb".to_string(),
typ: Typ::Int,
default: Some(json!(3)),
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "Nb2".to_string(),
typ: Typ::Float,
default: Some(json!(5.0)),
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "Nb3".to_string(),
typ: Typ::Int,
default: Some(json!(5)),
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "Wahoo".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
}
],
no_main_func: None,
has_preprocessor: None
}
);
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
[package]
name = "windmill-parser-csharp"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
name = "windmill_parser_csharp"
path = "./src/lib.rs"
[dependencies]
windmill-parser.workspace = true
tree-sitter.workspace = true
tree-sitter-c-sharp.workspace = true
anyhow.workspace = true
wasm-bindgen.workspace = true
serde_json.workspace = true
# convert_case.workspace = true
# lazy_static.workspace = true
# regex.workspace = true

View File

@@ -0,0 +1,314 @@
#![cfg_attr(target_arch = "wasm32", feature(c_variadic))]
#[cfg(target_arch = "wasm32")]
pub mod wasm_libc;
use anyhow::anyhow;
use tree_sitter::Node;
use windmill_parser::Arg;
use windmill_parser::MainArgSignature;
use windmill_parser::Typ;
#[derive(Debug)]
pub struct CsharpMainSigMeta {
pub is_async: bool,
pub is_public: bool,
pub returns_void: bool,
pub class_name: Option<String>,
pub main_sig: MainArgSignature,
}
fn csharp_param_default_value<'a>(def: Node<'a>, code: &str) -> Option<serde_json::Value> {
def.utf8_text(code.as_bytes())
.ok()
.and_then(|content| serde_json::from_str(content).ok())
}
pub fn parse_csharp_sig_meta(code: &str) -> anyhow::Result<CsharpMainSigMeta> {
let mut parser = tree_sitter::Parser::new();
let language = tree_sitter_c_sharp::LANGUAGE;
parser
.set_language(&language.into())
.map_err(|e| anyhow!("Error setting c# as language: {e}"))?;
// Parse code
let tree = parser.parse(code, None).expect("Failed to parse code");
let root_node = tree.root_node();
// Traverse the AST to find the Main method signature
let main_sig = find_main_signature(root_node, code);
let no_main_func = Some(main_sig.is_none());
let mut is_async = false;
let mut is_public = false;
let mut returns_void = false;
let mut class_name = None;
let mut args = vec![];
if let Some((sig, name)) = main_sig {
class_name = name;
for sig_node in sig.children(&mut sig.walk()) {
if sig_node.kind() == "modifier" && sig_node.utf8_text(code.as_bytes())? == "async" {
is_async = true;
}
if sig_node.kind() == "modifier" && sig_node.utf8_text(code.as_bytes())? == "public" {
is_public = true;
}
}
if let Some(return_type) = sig.child_by_field_name("returns") {
let return_type = return_type.utf8_text(code.as_bytes())?;
if return_type == "void" || (is_async && return_type == "Task") {
returns_void = true;
}
}
if let Some(param_list) = sig.child_by_field_name("parameters") {
for p_list_node in param_list.children(&mut param_list.walk()) {
if p_list_node.kind() == "parameter" {
let mut default = None;
for a in p_list_node.children(&mut p_list_node.walk()) {
if a.kind() == "=" {
if let Some(node) = a.next_sibling() {
default = csharp_param_default_value(node, code);
}
}
}
let (otyp, typ, name) = parse_csharp_typ(p_list_node, code)?;
args.push(Arg { name, otyp, typ, default, has_default: false, oidx: None });
}
}
}
}
let main_sig = MainArgSignature {
star_args: false,
star_kwargs: false,
args,
has_preprocessor: None,
no_main_func,
};
Ok(CsharpMainSigMeta { is_async, returns_void, class_name, main_sig, is_public })
}
pub fn parse_csharp_signature(code: &str) -> anyhow::Result<MainArgSignature> {
Ok(parse_csharp_sig_meta(code)?.main_sig)
}
fn find_typ<'a>(typ_node: Node<'a>, code: &str) -> anyhow::Result<Typ> {
match typ_node.kind() {
"predefined_type" => {
match typ_node.utf8_text(code.as_bytes()) {
Ok("string") => Ok(Typ::Str(None)),
Ok("sbyte") | Ok("System.SByte") => Ok(Typ::Bytes),
Ok("byte") | Ok("System.Byte") => Ok(Typ::Bytes),
Ok("short") | Ok("System.Int16") => Ok(Typ::Int),
Ok("ushort") | Ok("System.UInt16") => Ok(Typ::Int),
Ok("int") | Ok("System.Int32") => Ok(Typ::Int),
Ok("uint") | Ok("System.UInt32") => Ok(Typ::Int),
Ok("long") | Ok("System.Int64") => Ok(Typ::Int),
Ok("ulong") | Ok("System.UInt64") => Ok(Typ::Int),
Ok("char") | Ok("System.Char") => Ok(Typ::Str(None)),
Ok("float") | Ok("System.Single") => Ok(Typ::Float),
Ok("double") | Ok("System.Double") => Ok(Typ::Float),
Ok("bool") | Ok("System.Boolean") => Ok(Typ::Bool),
Ok("decimal") | Ok("System.Decimal") => Ok(Typ::Float),
Ok("object") => Ok(Typ::Object(vec![])), // TODO: Complete the object type
Ok(s) => Err(anyhow!("Unknown type `{s}`")),
Err(e) => Err(anyhow!("Error getting type name: {}", e)),
}
}
"array_type" => {
let new_typ_node = typ_node
.child_by_field_name("type")
.ok_or(anyhow!("Failed to find inner type of array type"))?;
Ok(Typ::List(Box::new(find_typ(new_typ_node, code)?)))
}
"identifier" => Ok(Typ::Unknown),
"generic_name" => Ok(Typ::Unknown),
"pointer_type" => Ok(Typ::Int),
"nullable_type" => {
let new_typ_node = typ_node
.child_by_field_name("type")
.ok_or(anyhow!("Failed to find inner type of nullable_type"))?;
Ok(find_typ(new_typ_node, code)?)
}
wc => Err(anyhow!(
"Unexpected C# type node kind: {} for '{}'. This type is not handeled by Windmill, please open an issue if this seems to be an error",
wc,
typ_node.utf8_text(code.as_bytes())?
)),
}
}
fn parse_csharp_typ<'a>(
param_node: Node<'a>,
code: &str,
) -> anyhow::Result<(Option<String>, Typ, String)> {
let name = param_node
.child_by_field_name("name")
.and_then(|n| n.utf8_text(code.as_bytes()).ok())
.unwrap_or("");
let otyp_node = param_node.child_by_field_name("type");
let otyp = otyp_node
.and_then(|n| n.utf8_text(code.as_bytes()).ok())
.map(|s| s.to_string());
let typ = find_typ(otyp_node.unwrap(), code)?;
Ok((otyp, typ, name.to_string()))
}
// Function to find the Main method's signature
fn find_main_signature<'a>(root_node: Node<'a>, code: &str) -> Option<(Node<'a>, Option<String>)> {
let mut cursor = root_node.walk();
for x in root_node.children(&mut cursor) {
if x.kind() == "class_declaration" {
let class_name = x
.child_by_field_name("name")
.and_then(|n| n.utf8_text(code.as_bytes()).ok().map(|s| s.to_string()));
for c in x.children(&mut x.walk()) {
if c.kind() == "declaration_list" {
for w in c.children(&mut c.walk()) {
if w.kind() == "method_declaration" {
for child in w.children(&mut w.walk()) {
if child
.utf8_text(code.as_bytes())
.map(|name| name == "Main")
.unwrap_or(false)
{
return Some((w, class_name));
}
}
}
}
}
}
}
}
return None;
}
pub fn parse_csharp_reqs(code: &str) -> (Vec<(String, Option<String>)>, Vec<usize>) {
let mut nuget_reqs = Vec::new();
let mut pkg_lines = Vec::new();
for (i, line) in code.split("\n").enumerate() {
if line.starts_with('#') {
if let Some(req) = parse_nuget_req(&line) {
pkg_lines.push(i);
nuget_reqs.push(req);
}
} else {
break; // Stop processing after the first non-comment line
}
}
(nuget_reqs, pkg_lines)
}
fn parse_nuget_req(line: &str) -> Option<(String, Option<String>)> {
// Check if the line starts with `#r "nuget:`
if let Some(start) = line.find("#r \"nuget:") {
// Extract the content after `#r "nuget:`
let start_idx = start + 10;
let end_idx = line[start_idx..].find('"')?;
let line = &line[start_idx..start_idx + end_idx];
let mut splitted = line.split(",");
if let Some(pkg) = splitted.next() {
return Some((
pkg.trim().to_string(),
splitted.next().map(|s| s.trim().to_string()),
));
}
}
None
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_csharp_sig_and_meta() {
let code = r#"
using System;
class LilProgram
{
public async static string Main(string myString = "World", int myInt = 2, string[] jj = ["asd", "ss"])
{
Console.Writeline("Hello!!");
return "yeah";
}
}"#;
let sig_meta = parse_csharp_sig_meta(code).unwrap();
assert_eq!(sig_meta.class_name, Some("LilProgram".to_string()));
assert_eq!(sig_meta.is_async, true);
let ret = sig_meta.main_sig;
assert_eq!(ret.args.len(), 3);
}
#[test]
fn test_parse_csharp_sig() {
let code = r#"
using System;
class LilProgram
{
public static string Main(string myString = "World", int myInt, string[] jj)
{
Console.Writeline("Hello!!");
return "yeah";
}
}"#;
let ret = parse_csharp_signature(code).unwrap();
assert_eq!(ret.args.len(), 3);
assert_eq!(ret.args[0].name, "myString");
assert_eq!(ret.args[0].otyp, Some("string".to_string()));
assert_eq!(ret.args[0].typ, Typ::Str(None));
assert_eq!(ret.args[1].name, "myInt");
assert_eq!(ret.args[1].otyp, Some("int".to_string()));
assert_eq!(ret.args[1].typ, Typ::Int);
assert_eq!(ret.args[2].name, "jj");
assert_eq!(ret.args[2].otyp, Some("string[]".to_string()));
assert_eq!(ret.args[2].typ, Typ::List(Box::new(Typ::Str(None))));
}
#[test]
fn test_parse_csharp_reqs() {
let file_content = r#"#r "nuget: AutoMapper, 6.1.0"
#r "nuget: Newtonsoft.Json, 13.0.1"
#r "nuget: Serilog, 2.10.0"
#r "nuget: Serilog, 2.10.0"
using System;
"#;
let requirements = parse_csharp_reqs(file_content).0;
assert_eq!(requirements.len(), 3);
assert_eq!(
requirements[0],
("AutoMapper".to_string(), Some("6.1.0".to_string()))
);
assert_eq!(
requirements[1],
("Newtonsoft.Json".to_string(), Some("13.0.1".to_string()))
);
assert_eq!(
requirements[2],
("Serilog".to_string(), Some("2.10.0".to_string()))
);
}
}

View File

@@ -0,0 +1,208 @@
use std::{
alloc::{self, Layout},
ffi::{c_char, c_int, c_void},
mem::align_of,
ptr,
};
use std::collections::BTreeMap;
use std::sync::{Mutex, OnceLock};
use wasm_bindgen::prelude::*;
/* -------------------------------- stdlib.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn abort() {
panic!("Aborted from C");
}
macro_rules! console_log {
($($t:tt)*) => (unsafe { log(&format_args!($($t)*).to_string()) })
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(a: &str);
}
#[no_mangle]
pub unsafe extern "C" fn malloc(size: usize) -> *mut c_void {
if size == 0 {
return ptr::null_mut();
}
let (layout, offset_to_data) = layout_for_size_prepended(size);
let buf = alloc::alloc(layout);
store_layout(buf, layout, offset_to_data)
}
#[no_mangle]
pub unsafe extern "C" fn calloc(count: usize, size: usize) -> *mut c_void {
if count == 0 || size == 0 {
return ptr::null_mut();
}
let (layout, offset_to_data) = layout_for_size_prepended(size * count);
let buf = alloc::alloc_zeroed(layout);
store_layout(buf, layout, offset_to_data)
}
#[no_mangle]
pub unsafe extern "C" fn realloc(buf: *mut c_void, new_size: usize) -> *mut c_void {
if buf.is_null() {
malloc(new_size)
} else if new_size == 0 {
free(buf);
ptr::null_mut()
} else {
let (old_buf, old_layout) = retrieve_layout(buf);
let (new_layout, offset_to_data) = layout_for_size_prepended(new_size);
let new_buf = alloc::realloc(old_buf, old_layout, new_layout.size());
store_layout(new_buf, new_layout, offset_to_data)
}
}
#[no_mangle]
pub unsafe extern "C" fn free(buf: *mut c_void) {
if buf.is_null() {
return;
}
let (buf, layout) = retrieve_layout(buf);
alloc::dealloc(buf, layout);
}
// In all these allocations, we store the layout before the data for later retrieval.
// This is because we need to know the layout when deallocating the memory.
// Here are some helper methods for that:
/// Given a pointer to the data, retrieve the layout and the pointer to the layout.
unsafe fn retrieve_layout(buf: *mut c_void) -> (*mut u8, Layout) {
let (_, layout_offset) = Layout::new::<Layout>()
.extend(Layout::from_size_align(0, align_of::<*const u8>() * 2).unwrap())
.unwrap();
let buf = (buf as *mut u8).offset(-(layout_offset as isize));
let layout = *(buf as *mut Layout);
(buf, layout)
}
/// Calculate a layout for a given size with space for storing a layout at the start.
/// Returns the layout and the offset to the data.
fn layout_for_size_prepended(size: usize) -> (Layout, usize) {
Layout::new::<Layout>()
.extend(Layout::from_size_align(size, align_of::<*const u8>() * 2).unwrap())
.unwrap()
}
/// Store a layout in the pointer, returning a pointer to where the data should be stored.
unsafe fn store_layout(buf: *mut u8, layout: Layout, offset_to_data: usize) -> *mut c_void {
*(buf as *mut Layout) = layout;
(buf as *mut u8).offset(offset_to_data as isize) as *mut c_void
}
/* -------------------------------- string.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn strncmp(ptr1: *const c_void, ptr2: *const c_void, n: usize) -> c_int {
let s1 = std::slice::from_raw_parts(ptr1 as *const u8, n);
let s2 = std::slice::from_raw_parts(ptr2 as *const u8, n);
for (a, b) in s1.iter().zip(s2.iter()) {
if *a != *b || *a == 0 {
return (*a as i32) - (*b as i32);
}
}
0
}
/* -------------------------------- wctype.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn iswspace(c: c_int) -> bool {
char::from_u32(c as u32).map_or(false, |c| c.is_whitespace())
}
#[no_mangle]
pub unsafe extern "C" fn iswalnum(c: c_int) -> bool {
char::from_u32(c as u32).map_or(false, |c| c.is_alphanumeric())
}
/* --------------------------------- time.h --------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn clock() -> u64 {
panic!("clock is not supported");
}
/* --------------------------------- ctype.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn isprint(c: c_int) -> bool {
c >= 32 && c <= 126
}
/* --------------------------------- stdio.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn fprintf(_file: *mut c_void, _format: *const c_void, _args: ...) -> c_int {
panic!("fprintf is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fputs(_s: *const c_void, _file: *mut c_void) -> c_int {
panic!("fputs is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fputc(_c: c_int, _file: *mut c_void) -> c_int {
panic!("fputc is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fdopen(_fd: c_int, _mode: *const c_void) -> *mut c_void {
panic!("fdopen is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fclose(_file: *mut c_void) -> c_int {
panic!("fclose is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fwrite(
_ptr: *const c_void,
_size: usize,
_nmemb: usize,
_stream: *mut c_void,
) -> usize {
panic!("fwrite is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn vsnprintf(
_buf: *mut c_char,
_size: usize,
_format: *const c_char,
_args: ...
) -> c_int {
panic!("vsnprintf is not supported");
}
#[no_mangle]
pub extern "C" fn clock_gettime(ptr: usize, new_size: usize) {
panic!("asdasd");
}
// int snprintf( char* restrict buffer, size_t bufsz, const char* restrict format, ... );
#[no_mangle]
pub extern "C" fn snprintf() {
panic!("snprintf is not supported");
}
#[no_mangle]
pub extern "C" fn __assert_fail(_: *const i32, _: *const i32, _: *const i32, _: *const i32) {
panic!("oh no");
}

View File

@@ -26,6 +26,7 @@ php-parser = [ "dep:windmill-parser-php"]
rust-parser = [ "dep:windmill-parser-rust"]
graphql-parser = [ "dep:windmill-parser-graphql"]
ansible-parser = [ "dep:windmill-parser-yaml"]
csharp-parser = [ "dep:windmill-parser-csharp"]
[dependencies]
anyhow.workspace = true
@@ -39,6 +40,7 @@ windmill-parser-php = { workspace = true, optional = true }
windmill-parser-graphql = { workspace = true, optional = true }
windmill-parser-rust = { workspace = true, optional = true }
windmill-parser-yaml = { workspace = true, optional = true }
windmill-parser-csharp = { workspace = true, optional = true }
wasm-bindgen.workspace = true
serde_json.workspace = true
getrandom = { workspace = true, features = ["js"] }

View File

@@ -48,3 +48,9 @@ OUT_DIR="pkg-yaml"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json
# C# (needs some more stuff to compile C tree sitter into wasm)
# TODO: hasn't been tested on mac, might need fixing
OUT_DIR="pkg-csharp"
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser"
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json

View File

@@ -48,3 +48,8 @@ OUT_DIR="pkg-yaml"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json
# C# (needs some more stuff to compile C tree sitter into wasm)
OUT_DIR="pkg-csharp"
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser"
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json

View File

@@ -24,3 +24,6 @@ popd
pushd "pkg-yaml" && npm publish ${args}
popd
pushd "pkg-csharp" && npm publish ${args}
popd

View File

@@ -135,3 +135,9 @@ pub fn parse_rust(code: &str) -> String {
pub fn parse_ansible(code: &str) -> String {
wrap_sig(windmill_parser_yaml::parse_ansible_sig(code))
}
#[cfg(feature = "csharp-parser")]
#[wasm_bindgen]
pub fn parse_csharp(code: &str) -> String {
wrap_sig(windmill_parser_csharp::parse_csharp_signature(code))
}

View File

@@ -0,0 +1,4 @@
#pragma once
#define assert(ignore) ((void)0)
#define static_assert(cnd, msg) assert(cnd && msg)

View File

@@ -0,0 +1,3 @@
#pragma once
int isprint(int c);

View File

@@ -0,0 +1,3 @@
#pragma once
#define PRId32 "d"

View File

@@ -0,0 +1,5 @@
#pragma once
#define bool _Bool
#define true 1
#define false 0

View File

@@ -0,0 +1,19 @@
#pragma once
typedef signed char int8_t;
typedef short int16_t;
typedef long int32_t;
typedef long long int64_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned long uint32_t;
typedef unsigned long long uint64_t;
typedef unsigned long size_t;
typedef unsigned int uintptr_t;
#define UINT8_MAX 0xff
#define UINT16_MAX 0xffff
#define UINT32_MAX 0xffffffff

View File

@@ -0,0 +1,19 @@
#pragma once
// just some filler type
#define FILE void
#define stdin NULL
#define stdout NULL
#define stderr NULL
int fprintf(FILE *__restrict__, const char *__restrict__, ...);
int fputs(const char *__restrict, FILE *__restrict);
int fputc(int, FILE *);
FILE *fdopen(int, const char *);
int fclose(FILE *);
int vsnprintf(char *s, unsigned long n, const char *format, ...);
#define sprintf(str, ...) 0
#define snprintf(str, len, ...) 0

View File

@@ -0,0 +1,12 @@
#pragma once
#include <stdint.h>
#define NULL ((void*)0)
void* malloc(size_t size);
void* calloc(size_t nmemb, size_t size);
void free(void* ptr);
void* realloc(void* ptr, size_t size);
void abort(void);

View File

@@ -0,0 +1,7 @@
#pragma once
void *memcpy(void *dest, const void *src, unsigned long n);
void *memmove(void *dest, const void *src, unsigned long n);
void *memset(void *s, int c, unsigned long n);
int memcmp(const void *ptr1, const void *ptr2, unsigned long n);
int strncmp(const char *s1, const char *s2, unsigned long n);

View File

@@ -0,0 +1,5 @@
#pragma once
typedef unsigned long clock_t;
#define CLOCKS_PER_SEC ((clock_t)1000000)
clock_t clock(void);

View File

@@ -0,0 +1,3 @@
#pragma once
int dup(int);

View File

@@ -0,0 +1,7 @@
#pragma once
typedef __WCHAR_TYPE__ wchar_t;
typedef __WINT_TYPE__ wint_t;
int iswspace(wchar_t ch);
int iswalnum(wint_t _wc);

View File

@@ -9,8 +9,8 @@
use anyhow::Context;
use monitor::{
load_base_url, load_otel, reload_delete_logs_periodically_setting, reload_indexer_config,
reload_timeout_wait_result_setting, send_current_log_file_to_object_store,
send_logs_to_object_store,
reload_nuget_config_setting, reload_timeout_wait_result_setting,
send_current_log_file_to_object_store, send_logs_to_object_store,
};
use rand::Rng;
use sqlx::{postgres::PgListener, Pool, Postgres};
@@ -37,9 +37,10 @@ use windmill_common::{
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING,
OAUTH_SETTING, OTEL_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING, PIP_INDEX_URL_SETTING,
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING,
TIMEOUT_WAIT_RESULT_SETTING,
},
scripts::ScriptLang,
stats_ee::schedule_stats,
@@ -66,15 +67,15 @@ use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING;
use windmill_worker::{
get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR,
BUN_DEPSTAR_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM,
GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR, POWERSHELL_CACHE_DIR,
PY311_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TAR_PY311_CACHE_DIR, TMP_LOGS_DIR,
UV_CACHE_DIR,
BUN_DEPSTAR_CACHE_DIR, CSHARP_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS,
DENO_CACHE_DIR_NPM, GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR,
POWERSHELL_CACHE_DIR, PY311_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TAR_PY311_CACHE_DIR,
TMP_LOGS_DIR, UV_CACHE_DIR,
};
use crate::monitor::{
initial_load, load_keep_job_dir, load_metrics_debug_enabled, load_require_preexisting_user,
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db, monitor_pool,
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db,
reload_base_url_setting, reload_bunfig_install_scopes_setting,
reload_critical_alert_mute_ui_setting, reload_critical_error_channels_setting,
reload_extra_pip_index_url_setting, reload_hub_base_url_setting,
@@ -141,7 +142,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
for path in paths.values() {
tracing::info!("Caching hub script at {path}");
let res = get_hub_script_content_and_requirements(Some(path.to_string()), None).await?;
let res = get_hub_script_content_and_requirements(Some(path), None).await?;
if res
.language
.as_ref()
@@ -188,7 +189,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
if let Err(e) = windmill_worker::prebundle_bun_script(
&res.content,
Some(lockfile),
Some(&lockfile),
&path,
&job_id,
"admins",
@@ -282,9 +283,6 @@ async fn windmill_main() -> anyhow::Result<()> {
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]
println!("jemalloc enabled");
#[cfg(feature = "flamegraph")]
let _guard = windmill_common::tracing_init::setup_flamegraph();
let cli_arg = std::env::args().nth(1).unwrap_or_default();
match cli_arg.as_str() {
@@ -361,7 +359,6 @@ async fn windmill_main() -> anyhow::Result<()> {
.unwrap_or_else(|| "local")
.to_string();
#[cfg(not(feature = "flamegraph"))]
let _guard = windmill_common::tracing_init::initialize_tracing(&hostname, &mode, &environment);
let num_version = sqlx::query_scalar!("SELECT version()").fetch_one(&db).await;
@@ -488,7 +485,8 @@ Windmill Community Edition {GIT_VERSION}
)
.await;
monitor_pool(&db).await;
#[cfg(feature = "prometheus")]
crate::monitor::monitor_pool(&db).await;
send_logs_to_object_store(&db, &hostname, &mode);
@@ -611,6 +609,7 @@ Windmill Community Edition {GIT_VERSION}
server_killpill_rx,
base_internal_tx,
server_mode,
#[cfg(feature = "smtp")]
base_internal_url.clone(),
)
.await?;
@@ -769,6 +768,9 @@ Windmill Community Edition {GIT_VERSION}
BUNFIG_INSTALL_SCOPES_SETTING => {
reload_bunfig_install_scopes_setting(&db).await
},
NUGET_CONFIG_SETTING => {
reload_nuget_config_setting(&db).await
},
KEEP_JOB_DIR_SETTING => {
load_keep_job_dir(&db).await;
},
@@ -1023,6 +1025,7 @@ pub async fn run_workers(
GO_CACHE_DIR,
GO_BIN_CACHE_DIR,
RUST_CACHE_DIR,
CSHARP_CACHE_DIR,
HUB_CACHE_DIR,
POWERSHELL_CACHE_DIR,
] {

View File

@@ -1,5 +1,6 @@
#[cfg(feature = "oauth2")]
use std::collections::HashMap;
use std::{
collections::HashMap,
fmt::Display,
ops::Mul,
str::FromStr,
@@ -22,12 +23,15 @@ use tokio::{
#[cfg(feature = "embedding")]
use windmill_api::embeddings::update_embeddings_db;
use windmill_api::{
jobs::TIMEOUT_WAIT_RESULT,
oauth2_ee::{build_oauth_clients, OAuthClient},
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN,
jobs::TIMEOUT_WAIT_RESULT, DEFAULT_BODY_LIMIT, IS_SECURE, REQUEST_SIZE_LIMIT, SAML_METADATA,
SCIM_TOKEN,
};
#[cfg(feature = "enterprise")]
use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts};
#[cfg(feature = "oauth2")]
use windmill_common::global_settings::OAUTH_SETTING;
use windmill_common::{
auth::JWT_SECRET,
ee::CriticalErrorChannel,
@@ -39,7 +43,7 @@ use windmill_common::{
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,
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING,
OTEL_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, TIMEOUT_WAIT_RESULT_SETTING,
@@ -65,7 +69,7 @@ use windmill_queue::cancel_job;
use windmill_worker::{
create_token_for_owner, handle_job_error, AuthedClient, SameWorkerPayload, SameWorkerSender,
SendResult, BUNFIG_INSTALL_SCOPES, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, NPM_CONFIG_REGISTRY,
PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
NUGET_CONFIG, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
};
#[cfg(feature = "parquet")]
@@ -82,22 +86,8 @@ use crate::ee::verify_license_key;
use crate::ee::set_license_key;
#[cfg(feature = "prometheus")]
lazy_static::lazy_static! {
static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT")
.ok()
.and_then(|x| x.parse::<String>().ok())
.unwrap_or_else(|| "60".to_string());
static ref FLOW_ZOMBIE_TRANSITION_TIMEOUT: String = std::env::var("FLOW_ZOMBIE_TRANSITION_TIMEOUT")
.ok()
.and_then(|x| x.parse::<String>().ok())
.unwrap_or_else(|| "60".to_string());
pub static ref RESTART_ZOMBIE_JOBS: bool = std::env::var("RESTART_ZOMBIE_JOBS")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(true);
static ref QUEUE_ZOMBIE_RESTART_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_zombie_restart_count",
@@ -116,6 +106,26 @@ lazy_static::lazy_static! {
&["tag"]
).unwrap();
}
lazy_static::lazy_static! {
static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT")
.ok()
.and_then(|x| x.parse::<String>().ok())
.unwrap_or_else(|| "60".to_string());
static ref FLOW_ZOMBIE_TRANSITION_TIMEOUT: String = std::env::var("FLOW_ZOMBIE_TRANSITION_TIMEOUT")
.ok()
.and_then(|x| x.parse::<String>().ok())
.unwrap_or_else(|| "60".to_string());
pub static ref RESTART_ZOMBIE_JOBS: bool = std::env::var("RESTART_ZOMBIE_JOBS")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(true);
static ref QUEUE_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
}
@@ -188,6 +198,7 @@ pub async fn initial_load(
reload_pip_index_url_setting(&db).await;
reload_npm_config_registry_setting(&db).await;
reload_bunfig_install_scopes_setting(&db).await;
reload_nuget_config_setting(&db).await;
}
}
@@ -916,6 +927,16 @@ pub async fn reload_bunfig_install_scopes_setting(db: &DB) {
.await;
}
pub async fn reload_nuget_config_setting(db: &DB) {
reload_option_setting_with_tracing(
db,
NUGET_CONFIG_SETTING,
"NUGET_CONFIG",
NUGET_CONFIG.clone(),
)
.await;
}
pub async fn reload_retention_period_setting(db: &DB) {
if let Err(e) = reload_setting(
db,
@@ -1148,6 +1169,7 @@ pub async fn reload_setting<T: FromStr + DeserializeOwned + Display>(
Ok(())
}
#[cfg(feature = "prometheus")]
pub async fn monitor_pool(db: &DB) {
if METRICS_ENABLED.load(Ordering::Relaxed) {
let db = db.clone();
@@ -1273,6 +1295,7 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
if metrics_enabled || save_metrics {
let queue_counts = windmill_common::queue::get_queue_counts(db).await;
#[cfg(feature = "prometheus")]
if metrics_enabled {
for q in QUEUE_COUNT_TAGS.read().await.iter() {
if queue_counts.get(q).is_none() {
@@ -1281,11 +1304,13 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
}
}
#[allow(unused_mut)]
let mut tags_to_watch = vec![];
for q in queue_counts {
let count = q.1;
let tag = q.0;
#[cfg(feature = "prometheus")]
if metrics_enabled {
let metric = (*QUEUE_COUNT).with_label_values(&[&tag]);
metric.set(count as i64);
@@ -1429,10 +1454,15 @@ pub async fn load_base_url(db: &DB) -> error::Result<String> {
}
pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
#[cfg(feature = "oauth2")]
let q_oauth = load_value_from_global_settings(db, OAUTH_SETTING).await?;
#[cfg(feature = "oauth2")]
let oauths = if let Some(q) = q_oauth {
if let Ok(v) = serde_json::from_value::<Option<HashMap<String, OAuthClient>>>(q.clone()) {
if let Ok(v) = serde_json::from_value::<
Option<HashMap<String, windmill_api::oauth2_ee::OAuthClient>>,
>(q.clone())
{
v
} else {
tracing::error!("Could not parse oauth setting as a json, found: {:#?}", &q);
@@ -1445,9 +1475,10 @@ pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
let base_url = load_base_url(db).await?;
let is_secure = base_url.starts_with("https://");
#[cfg(feature = "oauth2")]
{
let mut l = OAUTH_CLIENTS.write().await;
*l = build_oauth_clients(&base_url, oauths)
let mut l = windmill_api::OAUTH_CLIENTS.write().await;
*l = windmill_api::oauth2_ee::build_oauth_clients(&base_url, oauths)
.map_err(|e| tracing::error!("Error building oauth clients (is the oauth.json mounted and in correct format? Use '{}' as minimal oauth.json): {}", "{}", e))
.unwrap();
}
@@ -1473,9 +1504,11 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
.ok()
.unwrap_or_else(|| vec![]);
#[cfg(feature = "prometheus")]
if METRICS_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
QUEUE_ZOMBIE_RESTART_COUNT.inc_by(restarted.len() as _);
}
let base_url = BASE_URL.read().await.clone();
for r in restarted {
let last_ping = if let Some(x) = r.last_ping {
@@ -1512,6 +1545,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
.ok()
.unwrap_or_else(|| vec![]);
#[cfg(feature = "prometheus")]
if METRICS_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
QUEUE_ZOMBIE_DELETE_COUNT.inc_by(timeouts.len() as _);
}

54
backend/tests/fixtures/hello.sql vendored Normal file
View File

@@ -0,0 +1,54 @@
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'system',
'
export function main(world: string) {
const greet = `Hello ${world}!`;
console.log(greet)
return greet
}
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
'',
'',
'f/system/hello', 123412, 'deno', '');
INSERT INTO public.flow(workspace_id, summary, description, path, versions, schema, value, edited_by) VALUES (
'test-workspace',
'',
'',
'f/system/hello_flow',
'{1443253234253453}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
'{"modules": [{"id": "a", "value": {"path": "f/system/hello", "type": "script", "input_transforms": {"world": {"expr": "flow_input.world", "type": "javascript"}}}}]}',
'system'
);
INSERT INTO public.flow_version(id, workspace_id, path, schema, value, created_by) VALUES (
1443253234253453,
'test-workspace',
'f/system/hello_flow',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
'{"modules": [{"id": "a", "value": {"path": "f/system/failing_script", "type": "script", "input_transforms": {"fail": {"expr": "flow_input.fail", "type": "javascript"}}}}]}',
'system'
);
INSERT INTO public.flow(workspace_id, summary, description, path, versions, schema, value, edited_by) VALUES (
'test-workspace',
'',
'',
'f/system/hello_with_nodes_flow',
'{1443253234253454}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
E'{"modules":[{"id":"a","value":{"type":"forloopflow","modules":[{"id":"b","value":{"type":"rawscript","content":"export function main(world: string) {\\n const greet = `Hello ${world}!`;\\n console.log(greet)\\n return greet\\n}\\n","language":"deno","input_transforms":{"world":{"type":"javascript","expr":"flow_input.iter.value"}},"is_trigger":false}},{"id":"c","value":{"type":"rawscript","content":"export function main(hello: string) {\\n const dareyou = `Did you just say \\"${hello}\\"??!`;\\n console.log(dareyou)\\n return dareyou\\n}","language":"deno","input_transforms":{"hello":{"type":"javascript","value":"${results.b}","expr":"`${results.b}`"}},"is_trigger":false}}],"iterator":{"type":"javascript","expr":"[\'foo\', \'bar\', \'baz\']"},"skip_failures":true,"parallel":false}}],"same_worker":false}',
'system'
);
INSERT INTO public.flow_version(id, workspace_id, path, schema, value, created_by) VALUES (
1443253234253454,
'test-workspace',
'f/system/hello_with_nodes_flow',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
E'{"modules":[{"id":"a","value":{"type":"forloopflow","modules":[{"id":"b","value":{"type":"rawscript","content":"export function main(world: string) {\\n const greet = `Hello ${world}!`;\\n console.log(greet)\\n return greet\\n}\\n","language":"deno","input_transforms":{"world":{"type":"javascript","expr":"flow_input.iter.value"}},"is_trigger":false}},{"id":"c","value":{"type":"rawscript","content":"export function main(hello: string) {\\n const dareyou = `Did you just say \\"${hello}\\"??!`;\\n console.log(dareyou)\\n return dareyou\\n}","language":"deno","input_transforms":{"hello":{"type":"javascript","value":"${results.b}","expr":"`${results.b}`"}},"is_trigger":false}}],"iterator":{"type":"javascript","expr":"[\'foo\', \'bar\', \'baz\']"},"skip_failures":true,"parallel":false}}],"same_worker":false}',
'system'
);

View File

@@ -9,7 +9,7 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/same_folder_script', -28028598712388162, 'bun', '');
'f/system/same_folder_script', 12340, 'bun', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -22,7 +22,7 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/different_folder_script', -28028598712388161, 'bun', '');
'f/system_relative/different_folder_script', 12341, 'bun', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
@@ -41,4 +41,4 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/nested_script', -28028598712388160, 'bun', '');
'f/system_relative/nested_script', 12342, 'bun', '');

View File

@@ -9,7 +9,7 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/same_folder_script', -28028598712388162, 'deno', '');
'f/system/same_folder_script', 12343, 'deno', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -22,7 +22,7 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/different_folder_script', -28028598712388161, 'deno', '');
'f/system_relative/different_folder_script', 12344, 'deno', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
@@ -41,4 +41,4 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/nested_script', -28028598712388160, 'deno', '');
'f/system_relative/nested_script', 12345, 'deno', '');

View File

@@ -8,7 +8,7 @@ def main():
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/same_folder_script', -28028598712388162, 'python3', '');
'f/system/same_folder_script', 12346, 'python3', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -20,7 +20,7 @@ def main():
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/different_folder_script', -28028598712388161, 'python3', '');
'f/system_relative/different_folder_script', 12347, 'python3', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
@@ -38,4 +38,4 @@ def main():
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/nested_script', -28028598712388160, 'python3', '');
'f/system_relative/nested_script', 12348, 'python3', '');

View File

@@ -13,7 +13,7 @@ export async function main(fail: boolean = true) {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"fail":{"default":true,"description":"","type":"boolean"}},"required":[],"type":"object"}',
'',
'',
'f/system/failing_script', -28028598712388162, 'deno', '');
'f/system/failing_script', 12349, 'deno', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -26,7 +26,7 @@ export async function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"path":{"default":null,"description":"","type":"string"},"schedule_path":{"default":null,"description":"","type":"string"},"error":{"default":null,"description":"","properties":{},"type":"object"}},"required":["path","schedule_path","error"],"type":"object"}',
'',
'',
'f/system/schedule_error_handler', -28028598712388161, 'deno', '');
'f/system/schedule_error_handler', 123410, 'deno', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -39,21 +39,21 @@ export async function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"path":{"default":null,"description":"","type":"string"},"schedule_path":{"default":null,"description":"","type":"string"},"previous_job_error":{"default":null,"description":"","type":"string"},"result":{"default":null,"description":"","type":"string"}},"required":["path","schedule_path","previous_job_error","result"],"type":"object"}',
'',
'',
'f/system/schedule_recovery_handler', -28028598712388160, 'deno', '');
'f/system/schedule_recovery_handler', 123411, 'deno', '');
INSERT INTO public.flow(workspace_id, summary, description, path, versions, schema, value, edited_by) VALUES (
'test-workspace',
'',
'',
'f/system/failing_flow',
'{1}',
'{1443253234253452}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"fail":{"default":true,"description":"","type":"boolean","format":""}},"required":[],"type":"object"}',
'{"modules": [{"id": "a", "value": {"path": "f/system/failing_script", "type": "script", "input_transforms": {"fail": {"expr": "flow_input.fail", "type": "javascript"}}}}]}',
'system'
);
INSERT INTO public.flow_version(id, workspace_id, path, schema, value, created_by) VALUES (
1,
1443253234253452,
'test-workspace',
'f/system/failing_flow',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"fail":{"default":true,"description":"","type":"boolean","format":""}},"required":[],"type":"object"}',

View File

@@ -1,5 +1,6 @@
use serde::de::DeserializeOwned;
use std::str::FromStr;
use std::future::Future;
use std::{str::FromStr, sync::Arc};
use windmill_api_client::types::{NewScript, NewScriptLanguage};
#[cfg(feature = "enterprise")]
@@ -11,6 +12,7 @@ use serde::Deserialize;
use serde_json::json;
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres};
use tokio::sync::RwLock;
#[cfg(feature = "enterprise")]
use tokio::time::{timeout, Duration};
@@ -29,6 +31,9 @@ use windmill_common::{
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
jobs::{JobKind, JobPayload, RawCode},
scripts::{ScriptHash, ScriptLang},
worker::{
MIN_VERSION_IS_AT_LEAST_1_427, MIN_VERSION_IS_AT_LEAST_1_432, MIN_VERSION_IS_AT_LEAST_1_440,
},
};
use windmill_queue::PushIsolationLevel;
@@ -134,6 +139,7 @@ impl ApiServer {
rx,
port_tx,
false,
#[cfg(feature = "smtp")]
format!("http://localhost:{}", addr.port()),
));
@@ -1798,6 +1804,48 @@ fn main(world: String) -> Result<String, String> {
assert_eq!(result, serde_json::json!("Hello Hyrule!"));
}
// #[sqlx::test(fixtures("base"))]
// async fn test_csharp_job(db: Pool<Postgres>) {
// initialize_tracing().await;
// let server = ApiServer::start(db.clone()).await;
// let port = server.addr.port();
//
// let content = r#"
// using System;
//
// class Script
// {
// public static string Main(string world, int b = 2)
// {
// Console.WriteLine($"Hello {world} - {b}. This is a log line");
// return $"Hello {world} - {b}";
// }
// }
// "#
// .to_owned();
//
// let result = RunJob::from(JobPayload::Code(RawCode {
// hash: None,
// content,
// path: None,
// lock: None,
// language: ScriptLang::CSharp,
// custom_concurrency_key: None,
// concurrent_limit: None,
// concurrency_time_window_s: None,
// cache_ttl: None,
// dedicated_worker: None,
// }))
// .arg("world", json!("Arakis"))
// .arg("b", json!(3))
// .run_until_complete(&db, port)
// .await
// .json_result()
// .unwrap();
//
// assert_eq!(result, serde_json::json!("Hello Arakis - 3"));
// }
#[sqlx::test(fixtures("base"))]
async fn test_bash_job(db: Pool<Postgres>) {
initialize_tracing().await;
@@ -3689,3 +3737,474 @@ async fn test_result_format(db: Pool<Postgres>) {
.unwrap();
assert_eq!(result.get(), correct_result);
}
async fn test_for_versions<F: Future<Output = ()>>(
version_flags: impl Iterator<Item = Arc<RwLock<bool>>>,
test: impl Fn() -> F,
) {
for version_flag in version_flags {
*version_flag.write().await = true;
test().await;
}
}
mod job_payload {
use super::*;
use lazy_static::lazy_static;
use windmill_common::cache;
use windmill_common::flows::FlowNodeId;
lazy_static! {
static ref VERSION_FLAGS: [Arc<RwLock<bool>>; 3] = [
MIN_VERSION_IS_AT_LEAST_1_427.clone(),
MIN_VERSION_IS_AT_LEAST_1_432.clone(),
MIN_VERSION_IS_AT_LEAST_1_440.clone(),
];
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_script_hash_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(123412),
path: "f/system/hello".to_string(),
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
language: ScriptLang::Deno,
priority: None,
apply_preprocessor: false,
})
.arg("world", json!("foo"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Hello foo!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_flow_script_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
// Deploy the flow to produce the "lite" version.
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
let flow_data = cache::flow::fetch_version_lite(&db, 1443253234253454)
.await
.unwrap();
let flow_value = flow_data.value().unwrap();
let flow_scripts = {
async fn load(db: &Pool<Postgres>, modules: &[FlowModule]) -> Vec<FlowNodeId> {
let mut res = vec![];
for module in modules {
let value =
serde_json::from_str::<FlowModuleValue>(module.value.get()).unwrap();
match value {
FlowModuleValue::FlowScript { id, .. } => res.push(id),
FlowModuleValue::ForloopFlow { modules_node: Some(flow_node), .. } => {
let flow_data = cache::flow::fetch_flow(db, flow_node).await.unwrap();
res.extend(
Box::pin(load(db, &flow_data.value().unwrap().modules)).await,
);
}
_ => {}
}
}
res
}
load(&db, &flow_value.modules).await
};
assert_eq!(flow_scripts.len(), 2);
let test = || async {
let result = RunJob::from(JobPayload::FlowScript {
id: flow_scripts[0],
language: ScriptLang::Deno,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
path: "f/system/hello/test-0".into(),
})
.arg("world", json!("foo"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Hello foo!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
let test = || async {
let result = RunJob::from(JobPayload::FlowScript {
id: flow_scripts[1],
language: ScriptLang::Deno,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
path: "f/system/hello/test-0".into(),
})
.arg("hello", json!("You know nothing Jean Neige"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result,
json!("Did you just say \"You know nothing Jean Neige\"??!")
);
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_flow_node_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
// Deploy the flow to produce the "lite" version.
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
let flow_data = cache::flow::fetch_version_lite(&db, 1443253234253454)
.await
.unwrap();
let flow_value = flow_data.value().unwrap();
let forloop_module =
serde_json::from_str::<FlowModuleValue>(flow_value.modules[0].value.get()).unwrap();
let FlowModuleValue::ForloopFlow { modules_node: Some(id), .. } = forloop_module else {
panic!("Expected a forloop module with a flow node");
};
let test = || async {
let result = RunJob::from(JobPayload::FlowNode {
id,
path: "f/system/hello_with_nodes_flow/forloop-0".into(),
})
.arg("iter", json!({ "value": "tests", "index": 0 }))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Did you just say \"Hello tests!\"??!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dependencies_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::Dependencies {
path: "f/system/hello".to_string(),
hash: ScriptHash(123412),
language: ScriptLang::Deno,
dedicated_worker: None,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result.get("status").unwrap(),
&json!("Successful lock file generation")
);
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
// Just test that deploying a flow work as expected.
#[sqlx::test(fixtures("base", "hello"))]
async fn test_flow_dependencies_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result.get("status").unwrap(),
&json!("Successful lock file generation")
);
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_raw_flow_dependencies_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::RawFlowDependencies {
path: "none".to_string(),
flow_value: serde_json::from_value(json!({
"modules": [{
"id": "a",
"value": {
"type": "rawscript",
"content": r#"export function main(world: string) {
const greet = `Hello ${world}!`;
console.log(greet)
return greet
}"#,
"language": "deno",
"input_transforms": {
"world": { "type": "javascript", "expr": "flow_input.world" }
}
}
}],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": { "world": { "type": "string" } },
"type": "object",
"order": [ "world" ]
}
}))
.unwrap(),
})
.arg("skip_flow_update", json!(true))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
let result = RunJob::from(JobPayload::RawFlow {
value: serde_json::from_value::<FlowValue>(
result.get("updated_flow_value").unwrap().clone(),
)
.unwrap(),
path: None,
restarted_from: None,
})
.arg("world", json!("Jean Neige"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Hello Jean Neige!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_raw_script_dependencies_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::RawScriptDependencies {
script_path: "none".into(),
content: r#"export function main(world: string) {
const greet = `Hello ${world}!`;
console.log(greet)
return greet
}"#
.into(),
language: ScriptLang::Deno,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result,
json!({ "lock": "", "status": "Successful lock file generation" })
);
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_flow_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::Flow {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result,
json!([
"Did you just say \"Hello foo!\"??!",
"Did you just say \"Hello bar!\"??!",
"Did you just say \"Hello baz!\"??!",
])
);
};
// Test the not "lite" flow.
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
// Deploy the flow to produce the "lite" version.
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
// Test the "lite" flow.
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_restarted_flow_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let completed_job_id = RunJob::from(JobPayload::Flow {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
})
.run_until_complete(&db, port)
.await
.id;
let result = RunJob::from(JobPayload::RestartedFlow {
completed_job_id,
step_id: "a".into(),
branch_or_iteration_n: None,
})
.arg("iter", json!({ "value": "tests", "index": 0 }))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result,
json!([
"Did you just say \"Hello foo!\"??!",
"Did you just say \"Hello bar!\"??!",
"Did you just say \"Hello baz!\"??!",
])
);
};
// Test the not "lite" flow.
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
// Deploy the flow to produce the "lite" version.
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
// Test the "lite" flow.
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_raw_flow_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::RawFlow {
value: serde_json::from_value(json!({
"modules": [{
"id": "a",
"value": {
"type": "rawscript",
"content": r#"export function main(world: string) {
const greet = `Hello ${world}!`;
console.log(greet)
return greet
}"#,
"language": "deno",
"input_transforms": {
"world": { "type": "javascript", "expr": "flow_input.world" }
}
}
}],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": { "world": { "type": "string" } },
"type": "object",
"order": [ "world" ]
}
}))
.unwrap(),
path: None,
restarted_from: None,
})
.arg("world", json!("Jean Neige"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Hello Jean Neige!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
}

View File

@@ -10,7 +10,7 @@ path = "src/lib.rs"
[features]
default = []
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "dep:openidconnect"]
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise"]
stripe = ["dep:async-stripe"]
enterprise_saml = ["dep:samael"]
benchmark = []
@@ -20,13 +20,19 @@ prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:pr
openidconnect = ["dep:openidconnect"]
tantivy = ["dep:windmill-indexer"]
kafka = ["dep:rdkafka"]
websocket = ["dep:tokio-tungstenite"]
smtp = ["dep:mail-parser", "dep:openssl", "windmill-common/smtp"]
license = ["dep:rsa"]
zip = ["dep:async_zip"]
oauth2 = ["dep:async-oauth2"]
http_trigger = ["dep:matchit"]
static_frontend = ["dep:rust-embed", "dep:mime_guess"]
[dependencies]
windmill-queue.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
windmill-parser.workspace = true
windmill-parser-py-imports.workspace = true
windmill-parser-ts.workspace = true
windmill-git-sync.workspace = true
windmill-indexer = { workspace = true, optional = true }
@@ -44,7 +50,7 @@ itertools.workspace = true
reqwest.workspace = true
serde.workspace = true
sqlx.workspace = true
async-oauth2.workspace = true
async-oauth2 = { workspace = true, optional = true }
tracing.workspace = true
sql-builder.workspace = true
serde_json.workspace = true
@@ -55,16 +61,16 @@ base64.workspace = true
base32.workspace = true
serde_urlencoded.workspace = true
cron.workspace = true
mime_guess.workspace = true
rust-embed.workspace = true
mime_guess = { workspace = true, optional = true }
rust-embed = { workspace = true, optional = true }
tracing-subscriber.workspace = true
quick_cache.workspace = true
rand.workspace = true
time.workspace = true
native-tls.workspace = true
tokio-native-tls.workspace = true
openssl.workspace = true
mail-parser = { workspace = true, features = ["serde_support"] }
openssl = { workspace = true, optional = true }
mail-parser = { workspace = true, features = ["serde_support"], optional = true }
magic-crypt.workspace = true
tempfile.workspace = true
tokio-util.workspace = true
@@ -76,12 +82,12 @@ urlencoding.workspace = true
async-stripe = { workspace = true, optional = true }
lazy_static.workspace = true
prometheus = { workspace = true, optional = true }
async_zip.workspace = true
async_zip = { workspace = true, optional = true }
regex.workspace = true
bytes.workspace = true
samael = { workspace = true, optional = true }
async-recursion.workspace = true
rsa.workspace = true
rsa = { workspace = true, optional = true}
uuid.workspace = true
tinyvector = { workspace = true, optional = true}
hf-hub = { workspace = true, optional = true}
@@ -94,8 +100,8 @@ object_store = { workspace = true, optional = true}
openidconnect = { workspace = true, optional = true}
url = { workspace = true, optional = true}
jsonwebtoken = { workspace = true }
matchit.workspace = true
tokio-tungstenite.workspace = true
matchit = { workspace = true, optional = true }
tokio-tungstenite = { workspace = true, optional = true}
rdkafka = { workspace = true, optional = true }
const_format.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.435.2
version: 1.440.1
title: Windmill API
contact:
@@ -1121,9 +1121,15 @@ paths:
operationId: refreshUserToken
tags:
- user
parameters:
- name: if_expiring_in_less_than_s
in: query
required: false
schema:
type: integer
responses:
"200":
description: free usage
description: new token
content:
text/plain:
schema:
@@ -10692,6 +10698,7 @@ components:
php,
rust,
ansible,
csharp,
]
kind:
type: string
@@ -10795,6 +10802,7 @@ components:
php,
rust,
ansible,
csharp,
]
kind:
type: string
@@ -11019,6 +11027,7 @@ components:
php,
rust,
ansible,
csharp,
]
email:
type: string
@@ -11141,6 +11150,7 @@ components:
php,
rust,
ansible,
csharp,
]
is_skipped:
type: boolean
@@ -11216,6 +11226,8 @@ components:
type: string
is_admin:
type: boolean
name:
type: string
is_super_admin:
type: boolean
created_at:
@@ -11694,6 +11706,7 @@ components:
php,
rust,
ansible,
csharp,
]
tag:
type: string
@@ -12934,6 +12947,8 @@ components:
type: object
additionalProperties:
type: boolean
custom_path:
type: string
required:
- id
- workspace_id
@@ -12955,8 +12970,6 @@ components:
draft_only:
type: boolean
draft: {}
custom_path:
type: string
AppHistory:
type: object
@@ -13310,6 +13323,7 @@ components:
php,
rust,
ansible,
csharp,
]
required:
- raw_code

View File

@@ -20,7 +20,7 @@ use crate::{
#[cfg(feature = "parquet")]
use crate::{
job_helpers_ee::{
get_random_file_name, get_s3_resource, get_workspace_s3_resource, upload_file_internal,
get_random_file_name, get_s3_resource, get_workspace_s3_resource, upload_file_from_req,
UploadFileResponse,
},
users::fetch_api_authed_from_permissioned_as,
@@ -58,7 +58,8 @@ use windmill_common::{
jobs::{get_payload_tag_from_prefixed_path, JobPayload, RawCode},
users::username_to_permissioned_as,
utils::{
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin,
Pagination, StripPath,
},
variables::{build_crypt, build_crypt_with_key_suffix},
worker::{to_raw_value, CLOUD_HOSTED},
@@ -100,11 +101,6 @@ pub fn global_service() -> Router {
.route("/hub/get/:id", get(get_hub_app_by_id))
}
#[cfg(not(feature = "enterprise"))]
pub fn global_unauthed_service() -> Router {
Router::new()
}
#[derive(FromRow, Deserialize, Serialize)]
pub struct ListableApp {
pub id: i64,
@@ -144,6 +140,8 @@ pub struct AppWithLastVersion {
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub extra_perms: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_path: Option<String>,
}
#[derive(Serialize, FromRow)]
@@ -173,8 +171,6 @@ pub struct AppWithLastVersionAndDraft {
pub draft: Option<sqlx::types::Json<Box<RawValue>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_path: Option<String>,
}
#[derive(Serialize)]
@@ -377,7 +373,7 @@ async fn get_app(
let app_o = if query.with_starred_info.unwrap_or(false) {
sqlx::query_as::<_, AppWithLastVersionAndStarred>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, favorite.path IS NOT NULL as starred
FROM app
@@ -397,7 +393,7 @@ async fn get_app(
.await?
} else {
sqlx::query_as::<_, AppWithLastVersionAndStarred>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, NULL as starred
FROM app, app_version
@@ -423,7 +419,7 @@ async fn get_app_lite(
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, coalesce(app_version_lite.value::json, app_version.value) as value,
app_version.created_at, app_version.created_by, NULL as starred
FROM app, app_version
@@ -450,8 +446,8 @@ async fn get_app_w_draft(
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersionAndDraft>(
r#"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value, app.custom_path,
r#"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by,
app.draft_only, draft.value as "draft"
from app
@@ -558,12 +554,11 @@ async fn update_app_history(
return Ok(());
}
async fn custom_path_exists(
Extension(db): Extension<DB>,
Path((w_id, custom_path)): Path<(String, String)>,
) -> JsonResult<bool> {
let exists =
let exists =
sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))",
custom_path,
@@ -582,7 +577,7 @@ async fn get_app_by_id(
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by from app, app_version
WHERE app_version.id = $1 AND app.id = app_version.app_id AND app.workspace_id = $2",
@@ -613,7 +608,7 @@ async fn get_public_app_by_secret(
let id: i64 = bytes.parse().map_err(to_anyhow)?;
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
null as extra_perms, coalesce(app_version_lite.value::json, app_version.value::json) as value,
app_version.created_at, app_version.created_by from app, app_version
LEFT JOIN app_version_lite ON app_version_lite.id = app_version.id
@@ -658,7 +653,6 @@ async fn get_public_app_by_secret(
Ok(Json(app))
}
async fn get_public_resource(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -742,7 +736,6 @@ async fn create_app(
}
if let Some(custom_path) = &app.custom_path {
require_admin(authed.is_admin, &authed.username)?;
let exists = sqlx::query_scalar!(
@@ -778,7 +771,9 @@ async fn create_app(
app.summary,
json!(app.policy),
app.draft_only,
app.custom_path,
app.custom_path
.map(|s| if s.is_empty() { None } else { Some(s) })
.flatten()
)
.fetch_one(&mut *tx)
.await?;
@@ -1019,26 +1014,29 @@ async fn update_app(
}
if let Some(ncustom_path) = &ns.custom_path {
require_admin(authed.is_admin, &authed.username)?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2) AND NOT (path = $3 AND workspace_id = $4))",
ncustom_path,
if *CLOUD_HOSTED { Some(&w_id) } else { None },
path,
w_id
)
.fetch_one(&mut *tx)
.await?.unwrap_or(false);
if ncustom_path.is_empty() {
sqlb.set("custom_path", "NULL");
} else {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2) AND NOT (path = $3 AND workspace_id = $4))",
ncustom_path,
if *CLOUD_HOSTED { Some(&w_id) } else { None },
path,
w_id
)
.fetch_one(&mut *tx)
.await?.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with custom path {} already exists",
ncustom_path
)));
if exists {
return Err(Error::BadRequest(format!(
"App with custom path {} already exists",
ncustom_path
)));
}
sqlb.set_str("custom_path", ncustom_path);
}
sqlb.set_str("custom_path", ncustom_path);
}
if let Some(mut npolicy) = ns.policy {
@@ -1302,15 +1300,8 @@ async fn execute_component(
force_viewer_allow_user_resources: Some(allow_user_resources),
..
} => (
&Policy {
execution_mode: ExecutionMode::Viewer,
..Default::default()
},
&PolicyTriggerableInputs {
static_inputs,
one_of_inputs,
allow_user_resources,
},
&Policy { execution_mode: ExecutionMode::Viewer, ..Default::default() },
&PolicyTriggerableInputs { static_inputs, one_of_inputs, allow_user_resources },
),
// 2. "run" mode.
_ => {
@@ -1332,8 +1323,8 @@ async fn execute_component(
let policy = if let Some(id) = payload.version {
let cache = cache::anon!({ u64 => Arc<Policy> } in "policy" <= 1000);
arc_policy = policy_fut
.map_ok(Arc::new)
.cached(cache, &(id as u64))
.map_ok(sqlx::types::Json) // cache as json.
.cached(cache, id as u64, |sqlx::types::Json(x)| Arc::new(x))
.await?;
&*arc_policy
} else {
@@ -1361,8 +1352,8 @@ async fn execute_component(
)
.fetch_one(&db)
.map_err(Into::<Error>::into)
.map_ok(Arc::new)
.cached(cache, &(*id as u64))
.map_ok(sqlx::types::Json) // cache as json.
.cached(cache, *id as u64, |sqlx::types::Json(x)| Arc::new(x))
.await?
}
_ => unreachable!(),
@@ -1685,7 +1676,7 @@ async fn upload_s3_file_from_app(
])
.into();
upload_file_internal(s3_client, &file_key, request, options).await?;
upload_file_from_req(s3_client, &file_key, request, options).await?;
return Ok(Json(UploadFileResponse { file_key }));
}

View File

@@ -0,0 +1,525 @@
use std::collections::HashMap;
#[cfg(feature = "parquet")]
use crate::job_helpers_ee::get_workspace_s3_resource;
use axum::{
extract::{FromRequest, FromRequestParts, Multipart, Query, Request},
http::{HeaderMap, Uri},
response::{IntoResponse, Response},
};
use bytes::Bytes;
use http::{header::CONTENT_TYPE, request::Parts, StatusCode};
#[cfg(feature = "parquet")]
use object_store::{Attribute, Attributes};
use serde::Deserialize;
use serde_json::value::RawValue;
use sqlx::types::JsonRawValue;
#[cfg(feature = "parquet")]
use windmill_common::s3_helpers::build_object_store_client;
use windmill_common::{error::Error, worker::to_raw_value, DB};
use windmill_queue::PushArgsOwned;
use crate::db::ApiAuthed;
#[cfg(feature = "parquet")]
use crate::job_helpers_ee::{get_random_file_name, upload_file_internal};
#[derive(Default)]
pub struct WebhookArgs {
pub args: PushArgsOwned,
pub multipart: Option<Multipart>,
pub wrap_body: Option<bool>,
}
impl WebhookArgs {
#[cfg(not(feature = "parquet"))]
pub async fn to_push_args_owned(
self,
_authed: &ApiAuthed,
_db: &DB,
_w_id: &str,
) -> Result<PushArgsOwned, Error> {
if self.multipart.is_some() {
return Err(Error::BadRequest(format!(
"multipart/form-data requires the parquet feature"
)));
}
Ok(self.args)
}
#[cfg(feature = "parquet")]
pub async fn to_push_args_owned(
mut self,
authed: &ApiAuthed,
db: &DB,
w_id: &str,
) -> Result<PushArgsOwned, Error> {
use futures::TryStreamExt;
if let Some(mut multipart) = self.multipart {
{
let (_, s3_resource) =
get_workspace_s3_resource(authed, db, None, "", w_id, None).await?;
if let Some(s3_resource) = s3_resource {
let s3_client = build_object_store_client(&s3_resource).await?;
let mut body = HashMap::new();
while let Some(field) = multipart.next_field().await.map_err(|e| {
Error::BadRequest(format!(
"Error reading multipart field: {}",
e.body_text()
))
})? {
if let Some(name) = field.name().map(|x| x.to_string()) {
if let Some(content_type) = field.content_type() {
let ext = field
.file_name()
.map(|x| x.split('.').last())
.flatten()
.map(|x| x.to_string());
let file_key = get_random_file_name(ext);
let options = Attributes::from_iter(vec![
(Attribute::ContentType, content_type.to_string()),
(
Attribute::ContentDisposition,
if let Some(filename) = field.file_name() {
format!("inline; filename=\"{}\"", filename)
} else {
"inline".to_string()
},
),
])
.into();
let bytes_stream = field.into_stream().map_err(|err| {
std::io::Error::new(std::io::ErrorKind::Other, err)
});
upload_file_internal(
s3_client.clone(),
&file_key,
bytes_stream,
options,
)
.await?;
body.insert(
name,
to_raw_value(&serde_json::json!({
"s3": &file_key
})),
);
} else {
body.insert(
name,
to_raw_value(&field.text().await.unwrap_or_default()),
);
}
}
}
if self.wrap_body.unwrap_or(false) {
self.args
.args
.insert("body".to_string(), to_raw_value(&body));
} else {
self.args.args.extend(body);
}
return Ok(self.args);
}
}
return Err(Error::BadRequest(format!(
"You need to connect your workspace to an S3 bucket to use multipart/form-data"
)));
}
Ok(self.args)
}
}
#[derive(Deserialize)]
pub struct RequestQuery {
pub raw: Option<bool>,
pub wrap_body: Option<bool>,
pub include_header: Option<String>,
}
async fn req_to_string<S: Send + Sync>(
req: Request<axum::body::Body>,
_state: &S,
) -> Result<String, Response> {
let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
String::from_utf8(bytes.to_vec())
.map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())
}
#[axum::async_trait]
impl<S> FromRequest<S, axum::body::Body> for WebhookArgs
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(
req: Request<axum::body::Body>,
_state: &S,
) -> Result<Self, Self::Rejection> {
let (content_type, mut extra, use_raw, wrap_body) = {
let headers_map = req.headers();
let content_type_header = headers_map.get(CONTENT_TYPE);
let content_type = content_type_header.and_then(|value| value.to_str().ok());
let uri = req.uri();
let query = Query::<RequestQuery>::try_from_uri(uri).unwrap().0;
let mut extra = build_extra(&headers_map, query.include_header);
let query_decode = DecodeQueries::from_uri(uri);
if let Some(DecodeQueries(queries)) = query_decode {
extra.extend(queries);
}
let raw = query.raw.as_ref().is_some_and(|x| *x);
let wrap_body = query.wrap_body.as_ref().is_some_and(|x| *x);
(content_type, extra, raw, wrap_body)
};
let no_content_type = content_type.is_none();
if no_content_type || content_type.unwrap().starts_with("application/json") {
let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
if no_content_type && bytes.is_empty() {
if use_raw {
extra.insert("raw_string".to_string(), to_raw_value(&"".to_string()));
}
let mut args = HashMap::new();
if wrap_body {
args.insert("body".to_string(), to_raw_value(&serde_json::json!({})));
}
return Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: args },
..Default::default()
});
}
let str = String::from_utf8(bytes.to_vec())
.map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())?;
PushArgsOwned::from_json(extra, use_raw, wrap_body, str)
.await
.map(|args| Self { args, ..Default::default() })
} else if content_type
.unwrap()
.starts_with("application/cloudevents+json")
{
let str = req_to_string(req, _state).await?;
PushArgsOwned::from_ce_json(extra, use_raw, str)
.await
.map(|args| Self { args, ..Default::default() })
} else if content_type
.unwrap()
.starts_with("application/cloudevents-batch+json")
{
Err(
Error::BadRequest(format!("Cloud events batching is not supported yet"))
.into_response(),
)
} else if content_type.unwrap().starts_with("text/plain") {
let str = req_to_string(req, _state).await?;
extra.insert("raw_string".to_string(), to_raw_value(&str));
Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: HashMap::new() },
..Default::default()
})
} else if content_type
.unwrap()
.starts_with("application/x-www-form-urlencoded")
{
let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
if use_raw {
let raw_string = 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(&raw_string));
}
let payload: HashMap<String, Option<String>> = serde_urlencoded::from_bytes(&bytes)
.map_err(|e| {
Error::BadRequest(format!("invalid urlencoded data: {}", e)).into_response()
})?;
let payload = payload
.into_iter()
.map(|(k, v)| (k, to_raw_value(&v)))
.collect::<HashMap<_, _>>();
return Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: payload },
..Default::default()
});
} else if content_type.unwrap().starts_with("application/xml")
|| content_type.unwrap().starts_with("text/xml")
{
let str = req_to_string(req, _state).await?;
extra.insert("raw_string".to_string(), to_raw_value(&str));
Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: HashMap::new() },
..Default::default()
})
} else if content_type.unwrap().starts_with("multipart/form-data") {
let multipart = Multipart::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: HashMap::new() },
multipart: Some(multipart),
wrap_body: Some(wrap_body),
})
} else {
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
}
}
}
lazy_static::lazy_static! {
static ref INCLUDE_HEADERS: Vec<String> = std::env::var("INCLUDE_HEADERS")
.ok().map(|x| x
.split(',')
.map(|s| s.to_string())
.collect()).unwrap_or_default();
}
pub fn build_extra(
headers: &HeaderMap,
include_header: Option<String>,
) -> HashMap<String, Box<RawValue>> {
let mut args = HashMap::new();
let whitelist = include_header
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
to_raw_value(&v.to_str().unwrap().to_string()),
);
}
});
args
}
#[derive(Deserialize)]
pub struct IncludeQuery {
pub include_query: Option<String>,
}
pub struct DecodeQueries(pub HashMap<String, Box<RawValue>>);
#[axum::async_trait]
impl<S> FromRequestParts<S> for DecodeQueries
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(DecodeQueries::from_uri(&parts.uri).unwrap_or_else(|| DecodeQueries(HashMap::new())))
}
}
impl DecodeQueries {
pub fn from_uri(uri: &Uri) -> Option<Self> {
let query = uri.query();
if query.is_none() {
return None;
}
let query = query.unwrap();
let include_query = serde_urlencoded::from_str::<IncludeQuery>(query)
.map(|x| x.include_query)
.ok()
.flatten()
.unwrap_or_default();
let parse_query_args = include_query
.split(",")
.map(|s| s.to_string())
.collect::<Vec<_>>();
let mut args = HashMap::new();
if !parse_query_args.is_empty() {
let queries =
serde_urlencoded::from_str::<HashMap<String, String>>(query).unwrap_or_default();
parse_query_args.iter().for_each(|h| {
if let Some(v) = queries.get(h) {
args.insert(h.to_string(), to_raw_value(v));
}
});
}
Some(DecodeQueries(args))
}
}
// impl<'c> PushArgs<'c> {
// pub fn insert<K: Into<String>, V: Into<Box<RawValue>>>(&mut self, k: K, v: V) {
// self.extra.insert(k.into(), v.into());
// }
// }
fn restructure_cloudevents_metadata(
mut p: HashMap<String, Box<RawValue>>,
) -> Result<HashMap<String, Box<RawValue>>, Error> {
let data = p
.remove("data")
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null));
let str = data.to_string();
let wrap_body = str.len() > 0 && str.chars().next().unwrap() != '{';
if wrap_body {
let args = serde_json::from_str::<Option<Box<RawValue>>>(&str)
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null));
let mut hm = HashMap::new();
hm.insert("body".to_string(), args);
hm.insert("WEBHOOK__METADATA__".to_string(), to_raw_value(&p));
Ok(hm)
} else {
let mut hm = serde_json::from_str::<Option<HashMap<String, Box<JsonRawValue>>>>(&str)
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?
.unwrap_or_else(HashMap::new);
hm.insert("WEBHOOK__METADATA__".to_string(), to_raw_value(&p));
Ok(hm)
}
}
trait PushArgsOwnedExt: Sized {
async fn from_json(
extra: HashMap<String, Box<RawValue>>,
use_raw: bool,
force_wrap_body: bool,
str: String,
) -> Result<Self, Response>;
async fn from_ce_json(
extra: HashMap<String, Box<RawValue>>,
use_raw: bool,
str: String,
) -> Result<Self, Response>;
}
impl PushArgsOwnedExt for PushArgsOwned {
async fn from_json(
mut extra: HashMap<String, Box<RawValue>>,
use_raw: bool,
force_wrap_body: bool,
str: String,
) -> Result<Self, Response> {
if use_raw {
extra.insert("raw_string".to_string(), to_raw_value(&str));
}
let wrap_body = force_wrap_body || str.len() > 0 && str.chars().next().unwrap() != '{';
if wrap_body {
let args = serde_json::from_str::<Option<Box<RawValue>>>(&str)
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)).into_response())?
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null));
let mut hm = HashMap::new();
hm.insert("body".to_string(), args);
Ok(PushArgsOwned { extra: Some(extra), args: hm })
} else {
let hm = serde_json::from_str::<Option<HashMap<String, Box<JsonRawValue>>>>(&str)
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)).into_response())?
.unwrap_or_else(HashMap::new);
Ok(PushArgsOwned { extra: Some(extra), args: hm })
}
}
async fn from_ce_json(
mut extra: HashMap<String, Box<RawValue>>,
use_raw: bool,
str: String,
) -> Result<Self, Response> {
if use_raw {
extra.insert("raw_string".to_string(), to_raw_value(&str));
}
let hm = serde_json::from_str::<HashMap<String, Box<RawValue>>>(&str).map_err(|e| {
Error::BadRequest(format!("invalid cloudevents+json: {}", e)).into_response()
})?;
let hm = restructure_cloudevents_metadata(hm).map_err(|e| e.into_response())?;
Ok(PushArgsOwned { extra: Some(extra), args: hm })
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[tokio::test]
async fn test_cloudevents_json_payload() {
let r1 = r#"
{
"specversion" : "1.0",
"type" : "com.example.someevent",
"source" : "/mycontext",
"subject": null,
"id" : "C234-1234-1234",
"time" : "2018-04-05T17:31:00Z",
"comexampleextension1" : "value",
"comexampleothervalue" : 5,
"datacontenttype" : "application/json",
"data" : {
"appinfoA" : "abc",
"appinfoB" : 123,
"appinfoC" : true
}
}
"#;
let r2 = r#"
{
"specversion" : "1.0",
"type" : "com.example.someevent",
"source" : "/mycontext",
"subject": null,
"id" : "C234-1234-1234",
"time" : "2018-04-05T17:31:00Z",
"comexampleextension1" : "value",
"comexampleothervalue" : 5,
"datacontenttype" : "application/json",
"data" : 1.5
}
"#;
let extra = HashMap::new();
let a1 = PushArgsOwned::from_ce_json(extra.clone(), false, r1.to_string())
.await
.expect("Failed to parse the cloudevent");
let a2 = PushArgsOwned::from_ce_json(extra.clone(), false, r2.to_string())
.await
.expect("Failed to parse the cloudevent");
a1.args.get("WEBHOOK__METADATA__").expect(
"CloudEvents should generate a neighboring `webhook-metadata` field in PushArgs",
);
assert_eq!(
a2.args
.get("body")
.expect("Cloud events with a data field with no wrapping curly brackets should be inside of a `body` field in PushArgs")
.to_string(),
"1.5"
);
}
}

View File

@@ -0,0 +1,587 @@
#[cfg(feature = "enterprise")]
use crate::ee::ExternalJwks;
use axum::{
async_trait,
extract::{FromRequestParts, OriginalUri, Query},
Extension,
};
use chrono::TimeZone;
use http::{request::Parts, StatusCode};
use quick_cache::sync::Cache;
use serde::Deserialize;
use tower_cookies::Cookies;
use tracing::Span;
use crate::db::{ApiAuthed, DB};
use std::sync::{
atomic::{AtomicI64, AtomicU64, Ordering},
Arc,
};
#[cfg(feature = "enterprise")]
use tokio::sync::RwLock;
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, JWT_SECRET},
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
};
#[derive(Clone)]
pub struct ExpiringAuthCache {
pub authed: ApiAuthed,
pub expiry: chrono::DateTime<chrono::Utc>,
}
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>,
#[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) {
self.cache.remove(&(w_id.to_string(), token));
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<ApiAuthed> {
let key = (
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
token.to_string(),
);
let s = self.cache.get(&key).map(|c| c.to_owned());
match s {
Some(ExpiringAuthCache { authed, expiry }) if expiry > chrono::Utc::now() => {
Some(authed)
}
#[cfg(feature = "enterprise")]
_ if token.starts_with("jwt_ext_") => {
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(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc.timestamp_nanos(exp as i64 * 1_000_000_000),
},
);
Some(authed)
} else {
None
}
}
_ if token.starts_with("jwt_") => {
let jwt_secret = JWT_SECRET.read().await;
if !jwt_secret.is_empty() {
let jwt_token = token.trim_start_matches("jwt_");
let jwt_result = jsonwebtoken::decode::<JWTAuthClaims>(
jwt_token,
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
&jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
);
match jwt_result {
Ok(payload) => {
if w_id.is_some_and(|w_id| w_id != payload.claims.workspace_id) {
tracing::error!("JWT auth error: workspace_id mismatch");
return None;
}
let username_override =
username_override_from_label(payload.claims.label);
let authed = crate::db::ApiAuthed {
email: payload.claims.email,
username: payload.claims.username,
is_admin: payload.claims.is_admin,
is_operator: payload.claims.is_operator,
groups: payload.claims.groups,
folders: payload.claims.folders,
scopes: None,
username_override,
};
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc
.timestamp_nanos(payload.claims.exp as i64 * 1_000_000_000),
},
);
Some(authed)
}
Err(err) => {
tracing::error!("JWT auth error: {:?}", err);
None
}
}
} else {
tracing::error!("JWT auth error: no jwt secret set");
None
}
}
_ => {
let user_o = sqlx::query_as::<_, (Option<String>, Option<String>, bool, Option<Vec<String>>, Option<String>)>(
"UPDATE token SET last_used_at = now() WHERE token = $1 AND (expiration > NOW() \
OR expiration IS NULL) AND (workspace_id IS NULL OR workspace_id = $2) RETURNING owner, email, super_admin, scopes, label",
)
.bind(token)
.bind(w_id.as_ref())
.fetch_optional(&self.db)
.await
.ok()
.flatten();
if let Some(user) = user_o {
let authed_o = {
match user {
(Some(owner), Some(email), super_admin, _, label) if w_id.is_some() => {
let username_override = username_override_from_label(label);
if let Some((prefix, name)) = owner.split_once('/') {
if prefix == "u" {
let (is_admin, is_operator) = if super_admin {
(true, false)
} else {
let r = sqlx::query!(
"SELECT is_admin, operator FROM usr where username = $1 AND \
workspace_id = $2 AND disabled = false",
name,
&w_id.as_ref().unwrap()
)
.fetch_one(&self.db)
.await
.ok();
if let Some(r) = r {
(r.is_admin, r.operator)
} else {
(false, true)
}
};
let w_id = &w_id.unwrap();
let groups =
get_groups_for_user(w_id, &name, &email, &self.db)
.await
.ok()
.unwrap_or_default();
let folders =
get_folders_for_user(w_id, &name, &groups, &self.db)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: name.to_string(),
is_admin,
is_operator,
groups,
folders,
scopes: None,
username_override,
})
} else {
let groups = vec![name.to_string()];
let folders = get_folders_for_user(
&w_id.unwrap(),
"",
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: format!("group-{name}"),
is_admin: false,
groups,
is_operator: false,
folders,
scopes: None,
username_override,
})
}
} else {
let groups = vec![];
let folders = vec![];
Some(ApiAuthed {
email: email,
username: owner,
is_admin: super_admin,
is_operator: true,
groups,
folders,
scopes: None,
username_override,
})
}
}
(_, Some(email), super_admin, scopes, label) => {
let username_override = username_override_from_label(label);
if w_id.is_some() {
let row_o = sqlx::query_as::<_, (String, bool, bool)>(
"SELECT username, is_admin, operator FROM usr where email = $1 AND \
workspace_id = $2 AND disabled = false",
)
.bind(&email)
.bind(&w_id.as_ref().unwrap())
.fetch_optional(&self.db)
.await
.unwrap_or(Some(("error".to_string(), false, false)));
match row_o {
Some((username, is_admin, is_operator)) => {
let groups = get_groups_for_user(
&w_id.as_ref().unwrap(),
&username,
&email,
&self.db,
)
.await
.ok()
.unwrap_or_default();
let folders = get_folders_for_user(
&w_id.unwrap(),
&username,
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email,
username,
is_admin: is_admin || super_admin,
is_operator,
groups,
folders,
scopes,
username_override,
})
}
None if super_admin => Some(ApiAuthed {
email: email.clone(),
username: email,
is_admin: super_admin,
is_operator: false,
groups: vec![],
folders: vec![],
scopes,
username_override,
}),
None => None,
}
} else {
Some(ApiAuthed {
email: email.to_string(),
username: email,
is_admin: super_admin,
is_operator: true,
groups: Vec::new(),
folders: Vec::new(),
scopes,
username_override,
})
}
}
_ => None,
}
};
if let Some(authed) = authed_o.as_ref() {
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc::now()
+ chrono::Duration::try_seconds(120).unwrap(),
},
);
}
authed_o
} else if self
.superadmin_secret
.as_ref()
.map(|x| x == token)
.unwrap_or(false)
{
Some(ApiAuthed {
email: SUPERADMIN_SECRET_EMAIL.to_string(),
username: "superadmin_secret".to_string(),
is_admin: true,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
})
} else {
None
}
}
}
}
}
async fn extract_token<S: Send + Sync>(parts: &mut Parts, state: &S) -> Option<String> {
let auth_header = parts
.headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "));
let from_cookie = match auth_header {
Some(x) => Some(x.to_owned()),
None => Extension::<Cookies>::from_request_parts(parts, state)
.await
.ok()
.and_then(|cookies| cookies.get(COOKIE_NAME).map(|c| c.value().to_owned())),
};
#[derive(Deserialize)]
struct Token {
token: Option<String>,
}
match from_cookie {
Some(token) => Some(token),
None => Query::<Token>::from_request_parts(parts, state)
.await
.ok()
.and_then(|token| token.token.clone()),
}
}
#[derive(Clone, Debug)]
pub struct Tokened {
pub token: String,
}
pub struct OptTokened {
#[allow(dead_code)]
pub token: Option<String>,
}
struct BruteForceCounter {
counter: AtomicU64,
last_reset: AtomicI64,
}
lazy_static::lazy_static! {
static ref BRUTE_FORCE_COUNTER: BruteForceCounter =
BruteForceCounter { last_reset: AtomicI64::new(0), counter: AtomicU64::new(0) };
}
impl BruteForceCounter {
async fn increment(&self) {
let now = time::OffsetDateTime::now_utc().unix_timestamp();
if self.counter.fetch_add(1, Ordering::Relaxed) > 10000 {
tracing::error!(
"Brute force attack to find valid token detected, sleeping unauthorized response for 2 seconds"
);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
if now - self.last_reset.load(Ordering::Relaxed) > 60 {
self.counter.store(0, Ordering::Relaxed);
self.last_reset.store(now, Ordering::Relaxed);
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Tokened
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(Tokened { token: "".to_string() });
};
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(tokened.clone())
} else {
let token_o = extract_token(parts, state).await;
if let Some(token) = token_o {
let tokened = Self { token };
parts.extensions.insert(tokened.clone());
Ok(tokened)
} else {
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for OptTokened
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(OptTokened { token: None });
};
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(OptTokened { token: Some(tokened.token.clone()) })
} else {
let token_o = extract_token(parts, state).await;
Ok(OptTokened { token: token_o })
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for ApiAuthed
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(ApiAuthed {
email: "".to_owned(),
username: "".to_owned(),
is_admin: false,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
});
};
let already_authed = parts.extensions.get::<ApiAuthed>();
if let Some(authed) = already_authed {
Ok(authed.clone())
} else {
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(parts, state).await
};
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else {
if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[2] == "srch"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_string())
} else {
None
}
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
parts.extensions.insert(authed.clone());
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
}
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
}
}
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}
}
fn username_override_from_label(label: Option<String>) -> Option<String> {
match label {
Some(label)
if label.starts_with("webhook-")
|| label.starts_with("http-")
|| label.starts_with("email-")
|| label.starts_with("ws-") =>
{
Some(label)
}
Some(label) if label.starts_with("ephemeral-script-end-user-") => Some(
label
.trim_start_matches("ephemeral-script-end-user-")
.to_string(),
),
Some(label) if label == "Ephemeral lsp token" => Some("lsp".to_string()),
Some(label) if label != "ephemeral-script" && label != "session" && !label.is_empty() => {
Some(format!("label-{label}"))
}
_ => None,
}
}

View File

@@ -18,9 +18,12 @@ use windmill_common::{
error::{JsonResult, Result},
utils::{not_found_if_none, StripPath},
};
use windmill_queue::{PushArgs, PushArgsOwned};
use windmill_queue::PushArgs;
use crate::db::{ApiAuthed, DB};
use crate::{
args::WebhookArgs,
db::{ApiAuthed, DB},
};
const KEEP_LAST: i64 = 8;
@@ -86,8 +89,10 @@ pub async fn new_payload(
pub async fn update_payload(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
args: PushArgsOwned,
args: WebhookArgs,
) -> Result<StatusCode> {
let args = args.args;
let mut tx = db.begin().await?;
sqlx::query!(

View File

@@ -11,8 +11,9 @@ use std::sync::Arc;
use crate::db::ApiAuthed;
use crate::{
auth::AuthCache,
db::DB,
users::{AuthCache, Tokened},
users::Tokened,
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{

View File

@@ -1,12 +1,14 @@
#[cfg(feature = "parquet")]
use crate::job_helpers_ee::get_workspace_s3_resource;
use crate::{
args::WebhookArgs,
auth::{AuthCache, OptTokened},
db::{ApiAuthed, DB},
jobs::{
run_flow_by_path_inner, run_script_by_path_inner, run_wait_result_flow_by_path_internal,
run_wait_result_script_by_path_internal, RunJobQuery,
},
users::{fetch_api_authed, AuthCache, OptTokened},
users::fetch_api_authed,
};
use axum::{
extract::{Path, Query},
@@ -32,7 +34,6 @@ use windmill_common::{
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};
use windmill_queue::PushArgsOwned;
lazy_static::lazy_static! {
static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/:\w+").unwrap();
@@ -531,7 +532,7 @@ async fn route_job(
Query(query): Query<HashMap<String, String>>,
method: http::Method,
headers: HeaderMap,
mut args: PushArgsOwned,
args: WebhookArgs,
) -> impl IntoResponse {
let route_path = route_path.to_path();
let (trigger, called_path, params, authed) = match get_http_route_trigger(
@@ -547,6 +548,14 @@ async fn route_job(
Err(e) => return e.into_response(),
};
let mut args = match args
.to_push_args_owned(&authed, &db, &trigger.workspace_id)
.await
{
Ok(args) => args,
Err(e) => return e.into_response(),
};
#[cfg(not(feature = "parquet"))]
if trigger.static_asset_config.is_some() {
return error::Error::InternalErr(

View File

@@ -8,6 +8,7 @@
use axum::body::Body;
use axum::http::HeaderValue;
use futures::TryFutureExt;
use itertools::Itertools;
use quick_cache::sync::Cache;
use serde_json::value::RawValue;
@@ -36,6 +37,7 @@ use crate::db::ApiAuthed;
use crate::users::get_scope_tags;
use crate::utils::content_plain;
use crate::{
args::{DecodeQueries, WebhookArgs},
db::DB,
users::{check_scopes, require_owner_of_path, OptAuthed},
utils::require_super_admin,
@@ -61,6 +63,7 @@ use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE};
use windmill_common::{
cache,
db::UserDB,
error::{self, to_anyhow, Error},
flow_status::{Approval, FlowStatus, FlowStatusModule},
@@ -83,7 +86,7 @@ use windmill_common::{METRICS_DEBUG_ENABLED, METRICS_ENABLED};
use windmill_common::{get_latest_deployed_hash_for_path, BASE_URL};
use windmill_queue::{
cancel_job, get_queued_job, get_result_by_id_from_running_flow, job_is_complete, push,
DecodeQueries, PushArgs, PushArgsOwned, PushIsolationLevel,
PushArgs, PushArgsOwned, PushIsolationLevel,
};
#[cfg(feature = "prometheus")]
@@ -631,7 +634,10 @@ async fn get_flow_job_debug_info(
}
}
for job_id in job_ids {
let job = GetQuery::new().with_auth(&opt_authed).fetch(&db, job_id, &w_id).await;
let job = GetQuery::new()
.with_auth(&opt_authed)
.fetch(&db, job_id, &w_id)
.await;
if let Ok(job) = job {
jobs.insert(job.id().to_string(), job);
}
@@ -772,7 +778,7 @@ impl<'a> GetQuery<'a> {
Self { with_in_tags: in_tags, ..self }
}
fn check_auth(self, email: Option<&str>) -> error::Result<()> {
fn check_auth(&self, email: Option<&str>) -> error::Result<()> {
if let Some(email) = email {
if self.with_auth.is_some_and(|x| x.is_none()) && email != "anonymous" {
return Err(Error::BadRequest(
@@ -784,6 +790,53 @@ impl<'a> GetQuery<'a> {
Ok(())
}
/// Resolve job raw values.
/// This fetch the raw values from the cache and update the job accordingly.
///
/// # Details
/// Most of the raw values (code, lock and flow) had been removed from the `job`, `queue` and
/// `completed_job` tables. Only remains ones for "preview" jobs (i.e. [`JobKind::Preview`],
/// [`JobKind::FlowPreview`] and [`JobKind::Dependencies`]). [`JobKind::Flow`] as well but only
/// when pushed from an un-updated workers.
/// This function is used to make the above change transparent for the API, as the returned jobs
/// will have the raw values as if they were still in the tables.
async fn resolve_raw_values<T>(
&self,
db: &DB,
id: Uuid,
kind: JobKind,
hash: Option<ScriptHash>,
job: &mut JobExtended<T>,
) {
let (raw_code, raw_lock, raw_flow) = (
job.raw_code.take(),
job.raw_lock.take(),
job.raw_flow.take(),
);
if self.with_flow {
// Try to fetch the flow from the cache, fallback to the preview flow.
// NOTE: This could check for the job kinds instead of the `or_else` but it's not
// necessary as `fetch_flow` return early if the job kind is not a preview one.
cache::job::fetch_flow(db, kind, hash)
.or_else(|_| cache::job::fetch_preview_flow(db, &id, raw_flow))
.await
.ok()
.inspect(|data| job.raw_flow = Some(sqlx::types::Json(data.raw_flow.clone())));
}
if self.with_code {
// Try to fetch the code from the cache, fallback to the preview code.
// NOTE: This could check for the job kinds instead of the `or_else` but it's not
// necessary as `fetch_script` return early if the job kind is not a preview one.
cache::job::fetch_script(db, kind, hash)
.or_else(|_| cache::job::fetch_preview_script(db, &id, raw_lock, raw_code))
.await
.ok()
.inspect(|data| {
(job.raw_lock, job.raw_code) = (data.lock.clone(), Some(data.code.clone()))
});
}
}
async fn fetch_queued(
self,
db: &DB,
@@ -803,6 +856,10 @@ impl<'a> GetQuery<'a> {
let mut job = query.fetch_optional(db).await?;
self.check_auth(job.as_ref().map(|job| job.created_by.as_str()))?;
if let Some(job) = job.as_mut() {
self.resolve_raw_values(db, job.id, job.job_kind, job.script_hash, job)
.await;
}
if self.with_flow {
job = resolve_maybe_value(db, workspace_id, self.with_code, job, |job| {
job.raw_flow.as_mut()
@@ -831,8 +888,15 @@ impl<'a> GetQuery<'a> {
let mut cjob = query.fetch_optional(db).await?;
self.check_auth(cjob.as_ref().map(|job| job.created_by.as_str()))?;
if let Some(job) = cjob.as_mut() {
self.resolve_raw_values(db, job.id, job.job_kind, job.script_hash, job)
.await;
}
if self.with_flow {
cjob = resolve_maybe_value(db, workspace_id, self.with_code, cjob, |job| job.raw_flow.as_mut()).await?;
cjob = resolve_maybe_value(db, workspace_id, self.with_code, cjob, |job| {
job.raw_flow.as_mut()
})
.await?;
}
if let Some(mut cjob) = cjob {
cjob.inner = format_completed_job_result(cjob.inner);
@@ -842,14 +906,16 @@ impl<'a> GetQuery<'a> {
}
async fn fetch(self, db: &DB, job_id: Uuid, workspace_id: &str) -> error::Result<Job> {
let cjob = self.fetch_completed(db, job_id, workspace_id)
let cjob = self
.fetch_completed(db, job_id, workspace_id)
.await?
.map(Job::CompletedJob);
match cjob {
Some(cjob) => Ok(cjob),
None => {
let job_maybe = self.fetch_queued(db, job_id, workspace_id)
let job_maybe = self
.fetch_queued(db, job_id, workspace_id)
.await?
.map(Job::QueuedJob);
not_found_if_none(job_maybe, "Job", job_id.to_string())
@@ -2478,10 +2544,7 @@ impl Job {
}
pub fn is_flow(&self) -> bool {
matches!(
self.job_kind(),
JobKind::Flow | JobKind::FlowPreview | JobKind::SingleScriptFlow | JobKind::FlowNode
)
self.job_kind().is_flow()
}
pub fn job_kind(&self) -> &JobKind {
@@ -2940,8 +3003,10 @@ pub async fn run_flow_by_path(
Extension(user_db): Extension<UserDB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
args: PushArgsOwned,
args: WebhookArgs,
) -> error::Result<(StatusCode, String)> {
let args = args.to_push_args_owned(&authed, &db, &w_id).await?;
run_flow_by_path_inner(authed, db, user_db, w_id, flow_path, run_query, args, None).await
}
@@ -3116,8 +3181,9 @@ pub async fn run_script_by_path(
Extension(user_db): Extension<UserDB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
args: PushArgsOwned,
args: WebhookArgs,
) -> error::Result<(StatusCode, String)> {
let args = args.to_push_args_owned(&authed, &db, &w_id).await?;
run_script_by_path_inner(
authed,
db,
@@ -3218,7 +3284,10 @@ pub async fn run_workflow_as_code(
i += 1;
}
let job = GetQuery::new().without_logs().fetch_queued(&db, job_id, &w_id).await?;
let job = GetQuery::new()
.without_logs()
.fetch_queued(&db, job_id, &w_id)
.await?;
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
@@ -3779,11 +3848,13 @@ pub async fn run_wait_result_script_by_path(
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
args: PushArgsOwned,
args: WebhookArgs,
) -> error::Result<Response> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let args = args.to_push_args_owned(&authed, &db, &w_id).await?;
run_wait_result_script_by_path_internal(
db,
run_query,
@@ -3861,11 +3932,13 @@ pub async fn run_wait_result_script_by_hash(
Extension(db): Extension<DB>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>,
args: PushArgsOwned,
args: WebhookArgs,
) -> error::Result<Response> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let args = args.to_push_args_owned(&authed, &db, &w_id).await?;
check_queue_too_long(&db, run_query.queue_limit).await?;
let hash = script_hash.0;
@@ -3945,11 +4018,13 @@ pub async fn run_wait_result_flow_by_path(
Extension(db): Extension<DB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
args: PushArgsOwned,
args: WebhookArgs,
) -> error::Result<Response> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let args = args.to_push_args_owned(&authed, &db, &w_id).await?;
run_wait_result_flow_by_path_internal(
db, run_query, flow_path, authed, user_db, args, w_id, None,
)
@@ -4692,12 +4767,12 @@ async fn run_preview_flow_job(
pub async fn run_job_by_hash(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>,
args: PushArgsOwned,
args: WebhookArgs,
) -> error::Result<(StatusCode, String)> {
let args = args.to_push_args_owned(&authed, &db, &w_id).await?;
run_job_by_hash_inner(
authed,
db,
@@ -4747,6 +4822,7 @@ pub async fn run_job_by_hash_inner(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag, &authed).await?;
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
let (uuid, tx) = push(

View File

@@ -11,15 +11,20 @@ use crate::db::ApiAuthed;
use crate::ee::ExternalJwks;
#[cfg(feature = "embedding")]
use crate::embeddings::load_embeddings_db;
#[cfg(feature = "oauth2")]
use crate::oauth2_ee::AllClients;
#[cfg(feature = "oauth2")]
use crate::oauth2_ee::SlackVerifier;
#[cfg(feature = "smtp")]
use crate::smtp_server_ee::SmtpServer;
use crate::tracing_init::MyOnFailure;
use crate::{
oauth2_ee::SlackVerifier,
tracing_init::{MyMakeSpan, MyOnResponse},
users::OptAuthed,
webhook_util::WebhookShared,
};
use anyhow::Context;
use argon2::Argon2;
use axum::extract::DefaultBodyLimit;
@@ -27,7 +32,9 @@ use axum::{middleware::from_extractor, routing::get, Extension, Router};
use db::DB;
use http::HeaderValue;
use reqwest::Client;
#[cfg(feature = "oauth2")]
use std::collections::HashMap;
use std::time::Duration;
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::RwLock;
@@ -46,7 +53,9 @@ use windmill_common::error::AppError;
mod ai;
mod apps;
mod args;
mod audit;
mod auth;
mod capture;
mod concurrency_groups;
mod configs;
@@ -59,6 +68,7 @@ mod flows;
mod folders;
mod granular_acls;
mod groups;
#[cfg(feature = "http_trigger")]
mod http_triggers;
mod indexer_ee;
mod inputs;
@@ -72,6 +82,7 @@ pub mod job_metrics;
pub mod jobs;
#[cfg(all(feature = "enterprise", feature = "kafka"))]
mod kafka_triggers_ee;
#[cfg(feature = "oauth2")]
pub mod oauth2_ee;
mod oidc_ee;
mod raw_apps;
@@ -82,7 +93,8 @@ mod scim_ee;
mod scripts;
mod service_logs;
mod settings;
pub mod smtp_server_ee;
#[cfg(feature = "smtp")]
mod smtp_server_ee;
mod static_assets;
mod stripe_ee;
mod tracing_init;
@@ -92,10 +104,13 @@ mod users_ee;
mod utils;
mod variables;
mod webhook_util;
#[cfg(feature = "websocket")]
mod websocket_triggers;
mod workers;
mod workspaces;
mod workspaces_ee;
mod workspaces_export;
mod workspaces_extra;
pub const DEFAULT_BODY_LIMIT: usize = 2097152 * 100; // 200MB
@@ -109,10 +124,6 @@ lazy_static::lazy_static! {
pub static ref COOKIE_DOMAIN: Option<String> = std::env::var("COOKIE_DOMAIN").ok();
pub static ref SLACK_SIGNING_SECRET: Option<SlackVerifier> = std::env::var("SLACK_SIGNING_SECRET")
.ok()
.map(|x| SlackVerifier::new(x).unwrap());
pub static ref IS_SECURE: Arc<RwLock<bool>> = Arc::new(RwLock::new(false));
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
@@ -122,11 +133,22 @@ lazy_static::lazy_static! {
.danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())
.build().unwrap();
}
#[cfg(feature = "oauth2")]
lazy_static::lazy_static! {
pub static ref OAUTH_CLIENTS: Arc<RwLock<AllClients>> = Arc::new(RwLock::new(AllClients {
logins: HashMap::new(),
connects: HashMap::new(),
slack: None
}));
pub static ref SLACK_SIGNING_SECRET: Option<SlackVerifier> = std::env::var("SLACK_SIGNING_SECRET")
.ok()
.map(|x| SlackVerifier::new(x).unwrap());
}
// Compliance with cloud events spec.
@@ -170,13 +192,13 @@ pub async fn run_server(
mut rx: tokio::sync::broadcast::Receiver<()>,
port_tx: tokio::sync::oneshot::Sender<String>,
server_mode: bool,
base_internal_url: String,
#[cfg(feature = "smtp")] base_internal_url: String,
) -> anyhow::Result<()> {
let user_db = UserDB::new(db.clone());
#[cfg(feature = "enterprise")]
let ext_jwks = ExternalJwks::load().await;
let auth_cache = Arc::new(users::AuthCache::new(
let auth_cache = Arc::new(crate::auth::AuthCache::new(
db.clone(),
std::env::var("SUPERADMIN_SECRET").ok(),
#[cfg(feature = "enterprise")]
@@ -213,14 +235,17 @@ pub async fn run_server(
#[cfg(feature = "embedding")]
load_embeddings_db(&db);
let smtp_server = Arc::new(SmtpServer {
db: db.clone(),
user_db: user_db,
auth_cache: auth_cache.clone(),
base_internal_url: base_internal_url.clone(),
});
if let Err(err) = smtp_server.start_listener_thread(addr).await {
tracing::error!("Error starting SMTP server: {err:#}");
#[cfg(feature = "smtp")]
{
let smtp_server = Arc::new(SmtpServer {
db: db.clone(),
user_db: user_db,
auth_cache: auth_cache.clone(),
base_internal_url: base_internal_url.clone(),
});
if let Err(err) = smtp_server.start_listener_thread(addr).await {
tracing::error!("Error starting SMTP server: {err:#}");
}
}
}
@@ -252,8 +277,11 @@ pub async fn run_server(
};
if !*CLOUD_HOSTED {
let ws_killpill_rx = rx.resubscribe();
websocket_triggers::start_websockets(db.clone(), ws_killpill_rx).await;
#[cfg(feature = "websocket")]
{
let ws_killpill_rx = rx.resubscribe();
websocket_triggers::start_websockets(db.clone(), ws_killpill_rx).await;
}
#[cfg(all(feature = "enterprise", feature = "kafka"))]
{
@@ -289,7 +317,15 @@ pub async fn run_server(
.nest("/job_metrics", job_metrics::workspaced_service())
.nest("/job_helpers", job_helpers_service)
.nest("/jobs", jobs::workspaced_service())
.nest("/oauth", oauth2_ee::workspaced_service())
.nest("/oauth", {
#[cfg(feature = "oauth2")]
{
oauth2_ee::workspaced_service()
}
#[cfg(not(feature = "oauth2"))]
Router::new()
})
.nest("/ai", ai::workspaced_service())
.nest("/raw_apps", raw_apps::workspaced_service())
.nest("/resources", resources::workspaced_service())
@@ -302,11 +338,24 @@ pub async fn run_server(
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc_ee::workspaced_service())
.nest("/http_triggers", http_triggers::workspaced_service())
.nest(
"/websocket_triggers",
websocket_triggers::workspaced_service(),
)
.nest("/http_triggers", {
#[cfg(feature = "http_trigger")]
{
http_triggers::workspaced_service()
}
#[cfg(not(feature = "http_trigger"))]
Router::new()
})
.nest("/websocket_triggers", {
#[cfg(feature = "websocket")]
{
websocket_triggers::workspaced_service()
}
#[cfg(not(feature = "websocket"))]
Router::new()
})
.nest("/kafka_triggers", kafka_triggers_service),
)
.nest("/workspaces", workspaces::global_service())
@@ -378,13 +427,29 @@ pub async fn run_server(
"/auth",
users::make_unauthed_service().layer(Extension(argon2)),
)
.nest(
"/oauth",
oauth2_ee::global_service().layer(Extension(Arc::clone(&sp_extension))),
)
.nest("/oauth", {
#[cfg(feature = "oauth2")]
{
oauth2_ee::global_service().layer(Extension(Arc::clone(&sp_extension)))
}
#[cfg(not(feature = "oauth2"))]
Router::new()
})
.nest(
"/r",
http_triggers::routes_global_service().layer(from_extractor::<OptAuthed>()),
{
#[cfg(feature = "http_trigger")]
{
http_triggers::routes_global_service()
}
#[cfg(not(feature = "http_trigger"))]
{
Router::new()
}
}
.layer(from_extractor::<OptAuthed>()),
)
.route("/version", get(git_v))
.route("/uptodate", get(is_up_to_date))

View File

@@ -10,14 +10,17 @@ use std::{collections::HashMap, fmt::Debug};
use axum::{routing::get, Json, Router};
use hmac::Mac;
use hyper::HeaderMap;
#[cfg(feature = "oauth2")]
use itertools::Itertools;
#[cfg(feature = "oauth2")]
use oauth2::{Client as OClient, *};
use serde::{Deserialize, Serialize};
use sqlx::{Postgres, Transaction};
#[cfg(feature = "oauth2")]
use windmill_common::more_serde::maybe_number_opt;
#[cfg(feature = "oauth2")]
use crate::OAUTH_CLIENTS;
use windmill_common::error;
use windmill_common::oauth2::*;
@@ -27,7 +30,6 @@ use std::str;
pub fn global_service() -> Router {
Router::new()
.route("/list_supabase", get(list_supabase))
.route("/list_logins", get(list_logins))
.route("/list_connects", get(list_connects))
}
@@ -36,17 +38,7 @@ pub fn workspaced_service() -> Router {
Router::new()
}
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum InstanceEvent {
UserAdded { email: String },
// UserDeleted { email: String },
// UserDeletedWorkspace { workspace: String, email: String },
UserAddedWorkspace { workspace: String, email: String },
UserInvitedWorkspace { workspace: String, email: String },
UserJoinedWorkspace { workspace: String, email: String, username: String },
}
#[cfg(feature = "oauth2")]
#[derive(Debug, Clone)]
pub struct ClientWithScopes {
_client: OClient,
@@ -56,7 +48,7 @@ pub struct ClientWithScopes {
_allowed_domains: Option<Vec<String>>,
_userinfo_url: Option<String>,
}
#[cfg(feature = "oauth2")]
pub type BasicClientsMap = HashMap<String, ClientWithScopes>;
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -79,6 +71,7 @@ pub struct OAuthClient {
login_config: Option<OAuthConfig>,
}
#[cfg(feature = "oauth2")]
#[derive(Debug)]
pub struct AllClients {
pub logins: BasicClientsMap,
@@ -86,6 +79,7 @@ pub struct AllClients {
pub slack: Option<OClient>,
}
#[cfg(feature = "oauth2")]
pub fn build_oauth_clients(
_base_url: &str,
_oauths_from_config: Option<HashMap<String, OAuthClient>>,
@@ -98,6 +92,7 @@ pub fn build_oauth_clients(
});
}
#[cfg(feature = "oauth2")]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TokenResponse {
access_token: AccessToken,
@@ -121,6 +116,7 @@ async fn list_logins() -> error::JsonResult<Logins> {
return Ok(Json(Logins { oauth: vec![], saml: None }));
}
#[cfg(feature = "oauth2")]
async fn list_connects() -> error::JsonResult<Vec<String>> {
Ok(Json(
(&OAUTH_CLIENTS.read().await.connects)
@@ -130,6 +126,12 @@ async fn list_connects() -> error::JsonResult<Vec<String>> {
))
}
#[cfg(not(feature = "oauth2"))]
async fn list_connects() -> error::JsonResult<Vec<String>> {
// Implementation is not open source
return Ok(Json(vec![]));
}
pub async fn _refresh_token<'c>(
_tx: Transaction<'c, Postgres>,
_path: &str,
@@ -143,13 +145,6 @@ pub async fn _refresh_token<'c>(
))
}
async fn list_supabase(_headers: HeaderMap) -> error::Result<String> {
// Implementation is not open source
Err(error::Error::BadRequest(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
pub async fn check_nb_of_user(db: &DB) -> error::Result<()> {
let nb_users_sso =
sqlx::query_scalar!("SELECT COUNT(*) FROM password WHERE login_type != 'password'",)

View File

@@ -12,7 +12,8 @@ use crate::{
triggers::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
},
users::{maybe_refresh_folders, require_owner_of_path, AuthCache},
users::{maybe_refresh_folders, require_owner_of_path},
auth::AuthCache,
utils::WithStarredInfoQuery,
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
@@ -583,6 +584,7 @@ async fn create_script_internal<'c>(
|| ns.language == ScriptLang::Deno
|| ns.language == ScriptLang::Rust
|| ns.language == ScriptLang::Ansible
|| ns.language == ScriptLang::CSharp
|| ns.language == ScriptLang::Php)
{
Some(String::new())

View File

@@ -6,15 +6,15 @@
* LICENSE-AGPL for a copy of the license.
*/
use axum::{
body::Body,
extract::OriginalUri,
http::{header, Response},
response::IntoResponse,
};
use axum::{body::Body, extract::OriginalUri, http::Response, response::IntoResponse};
#[cfg(feature = "static_frontend")]
use axum::http::header;
use hyper::Uri;
#[cfg(feature = "static_frontend")]
use mime_guess::mime;
#[cfg(feature = "static_frontend")]
use rust_embed::RustEmbed;
// static_handler is a handler that serves static files from the
@@ -22,6 +22,7 @@ pub async fn static_handler(OriginalUri(original_uri): OriginalUri) -> StaticFil
StaticFile(original_uri)
}
#[cfg(feature = "static_frontend")]
#[derive(RustEmbed)]
#[folder = "${FRONTEND_BUILD_DIR:-../../frontend/build/}"]
struct Asset;
@@ -34,6 +35,7 @@ impl IntoResponse for StaticFile {
}
}
#[cfg(feature = "static_frontend")]
const TWO_HUNDRED: &str = "200.html";
fn serve_path(path: &str) -> Response<Body> {
@@ -41,6 +43,7 @@ fn serve_path(path: &str) -> Response<Body> {
return Response::builder().status(404).body(Body::empty()).unwrap();
}
#[cfg(feature = "static_frontend")]
match Asset::get(path) {
Some(content) => {
let body = Body::from(content.data);
@@ -68,4 +71,9 @@ fn serve_path(path: &str) -> Response<Body> {
}
None => serve_path(TWO_HUNDRED),
}
#[cfg(not(feature = "static_frontend"))]
{
Response::builder().status(404).body(Body::empty()).unwrap()
}
}

View File

@@ -8,14 +8,13 @@
#![allow(non_snake_case)]
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use crate::db::ApiAuthed;
#[cfg(feature = "enterprise")]
use crate::ee::ExternalJwks;
use crate::oauth2_ee::InstanceEvent;
pub use crate::auth::Tokened;
use crate::utils::{
generate_instance_wide_unique_username, get_instance_username_or_create_pending,
};
@@ -25,43 +24,38 @@ use crate::{
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use axum::{
async_trait,
extract::{Extension, FromRequestParts, OriginalUri, Path, Query},
extract::{Extension, FromRequestParts, Path, Query},
http::request::Parts,
response::{IntoResponse, Response},
routing::{delete, get, post},
Json, Router,
};
use chrono::TimeZone;
use hyper::{header::LOCATION, StatusCode};
use lazy_static::lazy_static;
use quick_cache::sync::Cache;
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 tracing::Instrument;
use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::auth::fetch_authed_from_permissioned_as;
use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING;
use windmill_common::oauth2::InstanceEvent;
use windmill_common::users::COOKIE_NAME;
use windmill_common::users::{truncate_token, username_to_permissioned_as};
use windmill_common::utils::paginate;
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, JWT_SECRET},
auth::{get_folders_for_user, get_groups_for_user},
db::UserDB,
error::{self, Error, JsonResult, Result},
users::SUPERADMIN_SECRET_EMAIL,
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
};
use windmill_git_sync::handle_deployment_metadata;
pub const TTL_TOKEN_DB_H: u32 = 72;
const COOKIE_NAME: &str = "token";
const COOKIE_PATH: &str = "/";
pub fn workspaced_service() -> Router {
@@ -126,478 +120,6 @@ pub fn make_unauthed_service() -> Router {
.route("/is_first_time_setup", get(is_first_time_setup))
}
fn username_override_from_label(label: Option<String>) -> Option<String> {
match label {
Some(label)
if label.starts_with("webhook-")
|| label.starts_with("http-")
|| label.starts_with("email-")
|| label.starts_with("ws-") =>
{
Some(label)
}
Some(label) if label.starts_with("ephemeral-script-end-user-") => Some(
label
.trim_start_matches("ephemeral-script-end-user-")
.to_string(),
),
Some(label) if label == "Ephemeral lsp token" => Some("lsp".to_string()),
Some(label) if label != "ephemeral-script" && label != "session" && !label.is_empty() => {
Some(format!("label-{label}"))
}
_ => None,
}
}
#[derive(Clone)]
pub struct ExpiringAuthCache {
pub authed: ApiAuthed,
pub expiry: chrono::DateTime<chrono::Utc>,
}
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>,
#[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) {
self.cache.remove(&(w_id.to_string(), token));
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<ApiAuthed> {
let key = (
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
token.to_string(),
);
let s = self.cache.get(&key).map(|c| c.to_owned());
match s {
Some(ExpiringAuthCache { authed, expiry }) if expiry > chrono::Utc::now() => {
Some(authed)
}
#[cfg(feature = "enterprise")]
_ if token.starts_with("jwt_ext_") => {
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(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc.timestamp_nanos(exp as i64 * 1_000_000_000),
},
);
Some(authed)
} else {
None
}
}
_ if token.starts_with("jwt_") => {
let jwt_secret = JWT_SECRET.read().await;
if !jwt_secret.is_empty() {
let jwt_token = token.trim_start_matches("jwt_");
let jwt_result = jsonwebtoken::decode::<JWTAuthClaims>(
jwt_token,
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
&jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
);
match jwt_result {
Ok(payload) => {
if w_id.is_some_and(|w_id| w_id != payload.claims.workspace_id) {
tracing::error!("JWT auth error: workspace_id mismatch");
return None;
}
let username_override =
username_override_from_label(payload.claims.label);
let authed = crate::db::ApiAuthed {
email: payload.claims.email,
username: payload.claims.username,
is_admin: payload.claims.is_admin,
is_operator: payload.claims.is_operator,
groups: payload.claims.groups,
folders: payload.claims.folders,
scopes: None,
username_override,
};
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc
.timestamp_nanos(payload.claims.exp as i64 * 1_000_000_000),
},
);
Some(authed)
}
Err(err) => {
tracing::error!("JWT auth error: {:?}", err);
None
}
}
} else {
tracing::error!("JWT auth error: no jwt secret set");
None
}
}
_ => {
let user_o = sqlx::query_as::<_, (Option<String>, Option<String>, bool, Option<Vec<String>>, Option<String>)>(
"UPDATE token SET last_used_at = now() WHERE token = $1 AND (expiration > NOW() \
OR expiration IS NULL) AND (workspace_id IS NULL OR workspace_id = $2) RETURNING owner, email, super_admin, scopes, label",
)
.bind(token)
.bind(w_id.as_ref())
.fetch_optional(&self.db)
.await
.ok()
.flatten();
if let Some(user) = user_o {
let authed_o = {
match user {
(Some(owner), Some(email), super_admin, _, label) if w_id.is_some() => {
let username_override = username_override_from_label(label);
if let Some((prefix, name)) = owner.split_once('/') {
if prefix == "u" {
let (is_admin, is_operator) = if super_admin {
(true, false)
} else {
let r = sqlx::query!(
"SELECT is_admin, operator FROM usr where username = $1 AND \
workspace_id = $2 AND disabled = false",
name,
&w_id.as_ref().unwrap()
)
.fetch_one(&self.db)
.await
.ok();
if let Some(r) = r {
(r.is_admin, r.operator)
} else {
(false, true)
}
};
let w_id = &w_id.unwrap();
let groups =
get_groups_for_user(w_id, &name, &email, &self.db)
.await
.ok()
.unwrap_or_default();
let folders =
get_folders_for_user(w_id, &name, &groups, &self.db)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: name.to_string(),
is_admin,
is_operator,
groups,
folders,
scopes: None,
username_override,
})
} else {
let groups = vec![name.to_string()];
let folders = get_folders_for_user(
&w_id.unwrap(),
"",
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: format!("group-{name}"),
is_admin: false,
groups,
is_operator: false,
folders,
scopes: None,
username_override,
})
}
} else {
let groups = vec![];
let folders = vec![];
Some(ApiAuthed {
email: email,
username: owner,
is_admin: super_admin,
is_operator: true,
groups,
folders,
scopes: None,
username_override,
})
}
}
(_, Some(email), super_admin, scopes, label) => {
let username_override = username_override_from_label(label);
if w_id.is_some() {
let row_o = sqlx::query_as::<_, (String, bool, bool)>(
"SELECT username, is_admin, operator FROM usr where email = $1 AND \
workspace_id = $2 AND disabled = false",
)
.bind(&email)
.bind(&w_id.as_ref().unwrap())
.fetch_optional(&self.db)
.await
.unwrap_or(Some(("error".to_string(), false, false)));
match row_o {
Some((username, is_admin, is_operator)) => {
let groups = get_groups_for_user(
&w_id.as_ref().unwrap(),
&username,
&email,
&self.db,
)
.await
.ok()
.unwrap_or_default();
let folders = get_folders_for_user(
&w_id.unwrap(),
&username,
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email,
username,
is_admin: is_admin || super_admin,
is_operator,
groups,
folders,
scopes,
username_override,
})
}
None if super_admin => Some(ApiAuthed {
email: email.clone(),
username: email,
is_admin: super_admin,
is_operator: false,
groups: vec![],
folders: vec![],
scopes,
username_override,
}),
None => None,
}
} else {
Some(ApiAuthed {
email: email.to_string(),
username: email,
is_admin: super_admin,
is_operator: true,
groups: Vec::new(),
folders: Vec::new(),
scopes,
username_override,
})
}
}
_ => None,
}
};
if let Some(authed) = authed_o.as_ref() {
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc::now()
+ chrono::Duration::try_seconds(120).unwrap(),
},
);
}
authed_o
} else if self
.superadmin_secret
.as_ref()
.map(|x| x == token)
.unwrap_or(false)
{
Some(ApiAuthed {
email: SUPERADMIN_SECRET_EMAIL.to_string(),
username: "superadmin_secret".to_string(),
is_admin: true,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
})
} else {
None
}
}
}
}
}
async fn extract_token<S: Send + Sync>(parts: &mut Parts, state: &S) -> Option<String> {
let auth_header = parts
.headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "));
let from_cookie = match auth_header {
Some(x) => Some(x.to_owned()),
None => Extension::<Cookies>::from_request_parts(parts, state)
.await
.ok()
.and_then(|cookies| cookies.get(COOKIE_NAME).map(|c| c.value().to_owned())),
};
#[derive(Deserialize)]
struct Token {
token: Option<String>,
}
match from_cookie {
Some(token) => Some(token),
None => Query::<Token>::from_request_parts(parts, state)
.await
.ok()
.and_then(|token| token.token.clone()),
}
}
#[derive(Clone, Debug)]
pub struct Tokened {
pub token: String,
}
pub struct OptTokened {
pub token: Option<String>,
}
struct BruteForceCounter {
counter: AtomicU64,
last_reset: AtomicI64,
}
lazy_static! {
static ref BRUTE_FORCE_COUNTER: BruteForceCounter =
BruteForceCounter { last_reset: AtomicI64::new(0), counter: AtomicU64::new(0) };
}
impl BruteForceCounter {
async fn increment(&self) {
let now = time::OffsetDateTime::now_utc().unix_timestamp();
if self.counter.fetch_add(1, Ordering::Relaxed) > 10000 {
tracing::error!(
"Brute force attack to find valid token detected, sleeping unauthorized response for 2 seconds"
);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
if now - self.last_reset.load(Ordering::Relaxed) > 60 {
self.counter.store(0, Ordering::Relaxed);
self.last_reset.store(now, Ordering::Relaxed);
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Tokened
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(Tokened { token: "".to_string() });
};
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(tokened.clone())
} else {
let token_o = extract_token(parts, state).await;
if let Some(token) = token_o {
let tokened = Self { token };
parts.extensions.insert(tokened.clone());
Ok(tokened)
} else {
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for OptTokened
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(OptTokened { token: None });
};
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(OptTokened { token: Some(tokened.token.clone()) })
} else {
let token_o = extract_token(parts, state).await;
Ok(OptTokened { token: token_o })
}
}
}
pub async fn maybe_refresh_folders(
path: &str,
w_id: &str,
@@ -628,94 +150,6 @@ pub async fn maybe_refresh_folders(
}
}
#[async_trait]
impl<S> FromRequestParts<S> for ApiAuthed
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(ApiAuthed {
email: "".to_owned(),
username: "".to_owned(),
is_admin: false,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
});
};
let already_authed = parts.extensions.get::<ApiAuthed>();
if let Some(authed) = already_authed {
Ok(authed.clone())
} else {
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(parts, state).await
};
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else {
if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[2] == "srch"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_string())
} else {
None
}
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
parts.extensions.insert(authed.clone());
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
}
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
}
}
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}
}
pub fn check_scopes<F>(authed: &ApiAuthed, required: F) -> error::Result<()>
where
F: FnOnce() -> String,
@@ -768,6 +202,7 @@ where
}
}
#[allow(unused)]
pub async fn fetch_api_authed(
username: String,
email: String,
@@ -779,6 +214,7 @@ pub async fn fetch_api_authed(
fetch_api_authed_from_permissioned_as(permissioned_as, email, w_id, db, username_override).await
}
#[allow(unused)]
pub async fn fetch_api_authed_from_permissioned_as(
permissioned_as: String,
email: String,
@@ -847,6 +283,7 @@ pub struct UserInfo {
pub folders_read: Vec<String>,
pub folders: Vec<String>,
pub folders_owners: Vec<String>,
pub name: Option<String>,
}
#[derive(FromRow, Serialize)]
@@ -1218,6 +655,7 @@ async fn whoami(
workspace_id: w_id,
email: email.clone(),
username: email,
name: None,
is_admin,
is_super_admin: is_admin,
created_at: chrono::Utc::now(),
@@ -1306,22 +744,30 @@ async fn get_usage(
Ok(usage.to_string())
}
#[derive(FromRow, Serialize)]
pub struct User2 {
pub workspace_id: String,
pub email: String,
pub username: String,
pub is_admin: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub operator: bool,
pub disabled: bool,
pub role: Option<String>,
pub super_admin: bool,
pub name: Option<String>,
}
async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo>> {
let user = sqlx::query_as!(
User,
"SELECT * FROM usr where username = $1 AND workspace_id = $2",
User2,
"SELECT usr.*, password.super_admin, password.name FROM usr LEFT JOIN password ON usr.email = password.email Where usr.username = $1 AND workspace_id = $2
",
username,
w_id
)
.fetch_optional(db)
.await?;
let is_super_admin = sqlx::query_scalar!(
"SELECT super_admin FROM password WHERE email = $1",
user.as_ref().map(|x| &x.email)
)
.fetch_optional(db)
.await?
.unwrap_or(false);
let groups = get_groups_for_user(
&w_id,
username,
@@ -1339,8 +785,9 @@ async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo
workspace_id: usr.workspace_id,
email: usr.email,
username: usr.username,
name: usr.name,
is_admin: usr.is_admin,
is_super_admin,
is_super_admin: usr.super_admin,
created_at: usr.created_at,
operator: usr.operator,
disabled: usr.disabled,
@@ -2206,13 +1653,30 @@ async fn login(
}
}
#[derive(Deserialize)]
struct RefreshTokenQuery {
if_expiring_in_less_than_s: Option<i32>,
}
async fn refresh_token(
Extension(db): Extension<DB>,
Query(query): Query<RefreshTokenQuery>,
Tokened { token }: Tokened,
authed: ApiAuthed,
cookies: Cookies,
) -> Result<String> {
let mut tx = db.begin().await?;
if let Some(thresh_s) = query.if_expiring_in_less_than_s {
let not_expired = sqlx::query_scalar!("SELECT true FROM token WHERE token = $1 and expiration IS NOT NULL and expiration > now() + $2::int * '1 sec'::interval", &token, thresh_s)
.fetch_optional(&db)
.await?
.flatten()
.unwrap_or(false);
if not_expired {
return Ok("token expiry is far enough".to_string());
}
}
let super_admin = sqlx::query_scalar!(
"SELECT super_admin FROM password WHERE email = $1",
&authed.email
@@ -2483,7 +1947,7 @@ async fn get_all_runnables(
Extension(db): Extension<UserDB>,
authed: ApiAuthed,
Tokened { token }: Tokened,
Extension(cache): Extension<Arc<AuthCache>>,
Extension(cache): Extension<Arc<crate::auth::AuthCache>>,
) -> JsonResult<Vec<Runnable>> {
let mut tx = db.clone().begin(&authed).await?;
let mut runnables = Vec::new();

View File

@@ -10,6 +10,7 @@ use axum::{body::Body, response::Response};
use regex::Regex;
use serde::Deserialize;
use sqlx::{Postgres, Transaction};
#[cfg(feature = "enterprise")]
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
auth::{is_devops_email, is_super_admin_email},

View File

@@ -8,7 +8,6 @@
use crate::{
db::{ApiAuthed, DB},
oauth2_ee::_refresh_token,
users::{maybe_refresh_folders, require_owner_of_path},
webhook_util::{WebhookMessage, WebhookShared},
};
@@ -183,10 +182,21 @@ async fn get_variable(
let value = variable.value.unwrap_or_else(|| "".to_string());
ListableVariable {
value: if variable.is_expired.unwrap_or(false) && variable.account.is_some() {
Some(
_refresh_token(tx, &variable.path, &w_id, variable.account.unwrap(), &db)
#[cfg(feature = "oauth2")]
{
Some(
crate::oauth2_ee::_refresh_token(
tx,
&variable.path,
&w_id,
variable.account.unwrap(),
&db,
)
.await?,
)
)
}
#[cfg(not(feature = "oauth2"))]
return Err(Error::InternalErr("Require oauth2 feature".to_string()));
} else if !value.is_empty() && decrypt_secret {
let _ = tx.commit().await;
let mc = build_crypt(&db, &w_id).await?;
@@ -640,7 +650,19 @@ pub async fn get_value_internal<'c>(
.await?;
let value = variable.value;
if variable.is_expired.unwrap_or(false) && variable.account.is_some() {
_refresh_token(tx, &variable.path, &w_id, variable.account.unwrap(), db).await?
#[cfg(feature = "oauth2")]
{
crate::oauth2_ee::_refresh_token(
tx,
&variable.path,
&w_id,
variable.account.unwrap(),
db,
)
.await?
}
#[cfg(not(feature = "oauth2"))]
return Err(Error::InternalErr("Require oauth2 feature".to_string()));
} else if !value.is_empty() {
tx.commit().await?;
let mc = build_crypt(&db, &w_id).await?;

View File

@@ -8,7 +8,7 @@ use tokio::{select, sync::mpsc};
use windmill_common::METRICS_ENABLED;
use crate::db::DB;
use crate::oauth2_ee::InstanceEvent;
use windmill_common::oauth2::InstanceEvent;
#[cfg(feature = "prometheus")]
lazy_static::lazy_static! {

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