Compare commits

...

76 Commits

Author SHA1 Message Date
Alexander Petric
13e0ec2cee noopener noreferrer 2024-11-24 20:38:29 -05:00
Alexander Petric
ec6e4bfb87 croner link target _blank / underline on hover 2024-11-24 15:22:08 -05:00
Ruben Fiszel
5ed0ae697b improve backoff for schedules 2024-11-24 18:48:01 +01:00
Ruben Fiszel
89456f502f remove rsmq 2024-11-24 11:04:54 +01:00
Ruben Fiszel
278f593358 fix: retry on pushing next scheduled job of schedule 2024-11-24 08:52:51 +01:00
Ruben Fiszel
e4f1ac1f7c chore(main): release 1.430.1 (#4780)
* chore(main): release 1.430.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-23 08:52:59 +01:00
Alexander Petric
d7a08c7ba9 add documentation link for cron input field (#4779) 2024-11-23 08:52:42 +01:00
Ruben Fiszel
495d4487bc fix: expose DISABLE_DENO_LOCK 2024-11-23 08:39:57 +01:00
Ruben Fiszel
c194e124da chore(main): release 1.430.0 (#4764)
* chore(main): release 1.430.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-22 16:41:12 +01:00
wendrul
4facf3ca3e feat: Add a devops role to act as a "readonly admin" (#4775)
* Prepare sqlx

* Add devops role as bool

* Add devops reloe and use it for certain endpoints

* Critical alerts: complete devops role visiblity

* Update ee repo ref

* Prepare sqlx
2024-11-22 14:55:48 +01:00
Patrick Dobbs
e854318f29 Update lib.rs with python-taiga import alias (#4776)
* Update lib.rs with python-taiga import alias

* Update lib.rs

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2024-11-21 23:18:38 +01:00
Alexander Petric
8909bea935 feat(schedule): support for extended cron syntax (#4754)
* init

* croner with fall back on scheduling failure

* adding packages

* fallback mechanism v1

* removing debug console log

* improving cargo organization

* queue cargo.toml

* use workspace cargo-tz

* test next occurrence when creating new croner schedule

* toggle label

* treat legacy edit requests where no cron_version is provided as v1

* fix edit schedule
2024-11-21 19:24:38 +01:00
Ruben Fiszel
ecc6203386 ts nits 2024-11-21 14:45:50 +01:00
HugoCasa
b49ba59da7 fix: infer python list inner type from default if unknown inner (#4771) 2024-11-21 14:44:19 +01:00
wendrul
99ab5471fc indexer: Add early exit on receiving of handoff signal (#4773) 2024-11-21 14:44:08 +01:00
Lucas Abel
e6c7fe7027 backend: rework raw values fetching logic (#4770) 2024-11-21 13:59:42 +01:00
Ruben Fiszel
f298c79fa6 handle better non required inputs with strings 2024-11-21 12:29:06 +01:00
Lucas Abel
d0884198f1 nix: update lock (#4769) 2024-11-21 10:57:15 +01:00
Ruben Fiszel
d5da75c031 feat: allow labeled values in app multiselect 2024-11-21 10:46:15 +01:00
Lucas Abel
c99d360c3c fix: garbage collect job table + delete leaked ones (#4767) 2024-11-21 09:02:24 +01:00
Ruben Fiszel
b7a9cec289 fix: allow non already existing resources in audit logs 2024-11-21 00:43:29 +01:00
Ruben Fiszel
8c021f941b fix main 2024-11-20 23:36:31 +01:00
HugoCasa
3734d5c0c0 list input z-index nit (#4765) 2024-11-20 23:17:50 +01:00
wendrul
8f198ba68c feat: Indexer improvements: s3 backup logic reworked, settings on the frontend (#4763)
* Add settings page + common code for indexer

* Prepare sqlx
2024-11-20 22:40:36 +01:00
Ruben Fiszel
3f74eeb684 chore(main): release 1.429.0 (#4761)
* chore(main): release 1.429.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-20 20:46:56 +01:00
Guilhem
37318861ac feat: app editor ctrl nav (#4757)
* Add panzoom

* Add panzoom

* scroll top when reseting view

* fix drop in component

* Enable panzoom in connection mode

* nit view fix

* clean

* clean

* prevent component to change when using zoom menu

* Add button to enter panzoom

* Polish grid outline

* darker outline on darkmode

* change hand icon when holding cmd

* remove unused code

* reset isModifierKeyPressed on tab change

* fix resizing component issue
2024-11-20 19:44:14 +01:00
wendrul
68f781ea6f feat: svix integration (#3814)
* Add create_webhook page

* Make small fixes

* Add token and change layout

* Rename to create-webhook

* Change query param to domain
2024-11-20 19:17:35 +01:00
HugoCasa
a23cd4f9e7 fix(frontend): pdf viewer fullscreen z-index (#4762)
* fix: pdf viewer fullscreen z-index

* increase zindex of multiselect
2024-11-20 17:32:02 +01:00
Ruben Fiszel
45ce5ff8c6 add FORCE_HOSTNAME env var 2024-11-20 16:07:24 +01:00
HugoCasa
6924370f11 refactor sql result formatting (#4760) 2024-11-20 14:53:39 +01:00
Ruben Fiszel
2c325ef852 fix: add cancellable icons refresh in apps 2024-11-20 14:40:28 +01:00
Ruben Fiszel
a8700a3181 improve flow status viewer 2024-11-20 14:03:49 +01:00
Ruben Fiszel
4c002faea2 readme nits 2024-11-20 12:17:17 +01:00
Ruben Fiszel
aa8c77aaeb app nits 2024-11-20 12:12:59 +01:00
Ruben Fiszel
83ac5f032e chore(main): release 1.428.1 (#4759)
* chore(main): release 1.428.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-20 10:12:08 +01:00
Guilhem
960b69fc40 nit app editor (#4752)
* Prevent resize when connecting

* simplify cursor selection in connection mode

* remove useless log

* Add connection popover to hover
2024-11-20 09:46:37 +01:00
Lucas Abel
ff7c94c5a7 fix: grant all to new job table (#4758) 2024-11-20 09:44:34 +01:00
Ruben Fiszel
799d581e70 iteration picker nits 2024-11-20 09:08:21 +01:00
Ruben Fiszel
b4be86c706 chore(main): release 1.428.0 (#4749)
* chore(main): release 1.428.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-19 20:19:21 +01:00
Ruben Fiszel
f59c0c0076 fix: prevent groups to be ill-defined with non writer owners 2024-11-19 20:13:54 +01:00
HugoCasa
279739ddd9 app pdf nits (#4756) 2024-11-19 19:58:55 +01:00
HugoCasa
35bad085a2 use unpkg cdn for pdfjs worker (#4755) 2024-11-19 19:45:58 +01:00
Henri Courdent
5c44939dc7 Typos worker group configs (#4751) 2024-11-19 19:21:03 +01:00
HugoCasa
b3a7cb0583 feat: pdf file preview (#4753)
also fixes s3 missing content-length header
2024-11-19 19:09:20 +01:00
Guilhem
2fd80f7cdd feat: improve app connection UX #4687
* Add animation on connection plugs

* Remove connection pannel

* Modify click outside

* fix minor issue

* remove unused component

* Prevent keyboard component navigation when connecting

* fix left right panel inversion

* fix pannel logic

* close secondary menu on connection

* Adjust plug position according to id badge size

* Change components colors

* Change plug color

* Fix escape connection

* Change cursor when connecting

* Add user toast on connecting

* Add component to exclusion area in connection mode

* exit connection on click connection button

* create connection button component

* remove debug logs

* fix pen color

* polish

* Add alert message in connection mode

* fix minor issue

* fix unwanted cursor override
2024-11-19 16:34:42 +01:00
Ruben Fiszel
aae1d763b0 nit autoscaling 2024-11-19 14:28:12 +01:00
dependabot[bot]
6833180177 chore(deps): bump denoland/setup-deno from 1 to 2 (#4640)
Bumps [denoland/setup-deno](https://github.com/denoland/setup-deno) from 1 to 2.
- [Release notes](https://github.com/denoland/setup-deno/releases)
- [Commits](https://github.com/denoland/setup-deno/compare/v1...v2)

---
updated-dependencies:
- dependency-name: denoland/setup-deno
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 13:50:56 +01:00
dependabot[bot]
63b937cf6a chore(deps): bump stefanzweifel/git-auto-commit-action from 4 to 5 (#4639)
Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 4 to 5.
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 13:50:32 +01:00
dependabot[bot]
ba77af0705 chore(deps): bump cla-assistant/github-action from 2.3.1 to 2.6.1 (#4641)
Bumps [cla-assistant/github-action](https://github.com/cla-assistant/github-action) from 2.3.1 to 2.6.1.
- [Release notes](https://github.com/cla-assistant/github-action/releases)
- [Commits](https://github.com/cla-assistant/github-action/compare/v2.3.1...v2.6.1)

---
updated-dependencies:
- dependency-name: cla-assistant/github-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 13:50:14 +01:00
dependabot[bot]
673189cdf4 chore(deps): bump chart.js from 4.4.0 to 4.4.6 in /frontend (#4735)
Bumps [chart.js](https://github.com/chartjs/Chart.js) from 4.4.0 to 4.4.6.
- [Release notes](https://github.com/chartjs/Chart.js/releases)
- [Commits](https://github.com/chartjs/Chart.js/compare/v4.4.0...v4.4.6)

---
updated-dependencies:
- dependency-name: chart.js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 13:49:44 +01:00
dependabot[bot]
3eb7545bf3 chore(deps-dev): bump postcss from 8.4.41 to 8.4.49 in /frontend (#4736)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.41 to 8.4.49.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.41...8.4.49)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 13:49:30 +01:00
dependabot[bot]
8b38b57638 chore(deps): bump thiserror from 1.0.69 to 2.0.3 in /backend (#4738)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.69 to 2.0.3.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.69...2.0.3)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 13:49:14 +01:00
dependabot[bot]
825143db28 chore(deps): bump tower-http from 0.5.2 to 0.6.2 in /backend (#4743)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.5.2 to 0.6.2.
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.5.2...tower-http-0.6.2)

---
updated-dependencies:
- dependency-name: tower-http
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 13:45:27 +01:00
Ruben Fiszel
8e8608869c chore(main): release 1.427.0 (#4747)
* chore(main): release 1.427.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-19 11:32:00 +01:00
Lucas Abel
97457a5679 feat(backend): move some static fields out of job tables (#4689)
* feat(backend): move some static fields out of job tables

* backend: re-use `flow_value` if any and avoid clone

* update minimal version to 1.427.0
2024-11-19 11:27:55 +01:00
Ruben Fiszel
1c398486e6 fix: improve flow status viewer for iterations (#4744)
* fix: improve flow status viewer iteration picker

* check

* check

* nit rm console log

* progress

* progress

* progress
2024-11-19 10:27:33 +01:00
Ruben Fiszel
5e975260df chore(main): release 1.426.1 (#4741)
* chore(main): release 1.426.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-19 00:30:25 +01:00
wendrul
4d9ec909d1 fix: playbook files permission mode incompatible with Windows (#4740) 2024-11-18 22:14:19 +01:00
HugoCasa
ee9d3e0f3e kafka frontend nits (#4734)
* kafka frontend nits

* don't make ws triggers admin only

* update ee ref
2024-11-18 19:23:29 +01:00
Ruben Fiszel
46fae2a6c2 chore(main): release 1.426.0 (#4732)
* chore(main): release 1.426.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-18 17:14:55 +01:00
wendrul
6b34e316ce Add mode to playbook init for discoverability (#4733) 2024-11-18 17:09:40 +01:00
HugoCasa
88b8ffab90 feat: kafka triggers (#4713)
* feat: kafka triggers

* sqlx

* fix build

* improve error messages on windows

* nit

* fix build

* missing action

* nit

* maybe fix ssl on windows

* nit

* update ee ref

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2024-11-18 17:03:50 +01:00
wendrul
5e10782779 feat: Add mode (permissions) option to files in ansible (#4724)
* Add chmod option to files in ansible

* Add 0 prefix if string

* Rename chmod to mode
2024-11-18 17:02:38 +01:00
Alexander Petric
180809d346 fix: do not mount critical alerts modal if user is neither superadmin nor workspace admin (#4731) 2024-11-18 15:34:22 +01:00
Ruben Fiszel
e4583e9b23 fix cli handling of ai resource generalization 2024-11-16 11:27:01 +01:00
Ruben Fiszel
93063b1c50 chore(main): release 1.425.1 (#4728)
* chore(main): release 1.425.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-16 10:04:30 +01:00
HugoCasa
17d893315b fix: revert bool to text support in pg (#4727) 2024-11-16 10:01:16 +01:00
Ruben Fiszel
0acd1b4a99 fix error logs on empty get_logs 2024-11-16 09:30:36 +01:00
Ruben Fiszel
4b04cc4d1a chore(main): release 1.425.0 (#4715)
* chore(main): release 1.425.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-11-15 22:31:49 +01:00
HugoCasa
d999e91583 unbreak nullable inputs in pg (#4726) 2024-11-15 22:27:28 +01:00
pyranota
9065716020 Fix ignore confirmation modal if no changes (#4725) 2024-11-15 19:47:09 +01:00
pyranota
56f70fd7de Ignore fields with false value in DiffViewer (#4712)
* Convert false to undefined

* Remove comments

* Make it consistent

* Formatting

* Apply at downstream
2024-11-15 19:24:57 +01:00
HugoCasa
9ad1afc8f2 unbreak ::text for json in pg (#4722)
* unbreak ::text for json in pg

* better error handling
2024-11-15 18:35:21 +01:00
Alexander Petric
c069969732 cloud_hosted non ee scope (#4723) 2024-11-15 18:14:11 +01:00
Henri Courdent
9ce1d46459 Result node placeholder (#4721) 2024-11-15 16:55:53 +01:00
Alexander Petric
ff5fcc59d4 if CLOUD_HOSTED and workspace_id set, autoacknowledge for superadmin (#4719) 2024-11-15 16:31:42 +01:00
242 changed files with 7663 additions and 3439 deletions

View File

@@ -31,7 +31,7 @@ jobs:
ports:
- 8000:8000
steps:
- uses: denoland/setup-deno@v1
- uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- name: benchmark
@@ -72,7 +72,7 @@ jobs:
ports:
- 8000:8000
steps:
- uses: denoland/setup-deno@v1
- uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- name: benchmark
@@ -146,7 +146,7 @@ jobs:
--pull always
steps:
- uses: denoland/setup-deno@v1
- uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- name: benchmark
@@ -264,7 +264,7 @@ jobs:
options: >-
--pull always
steps:
- uses: denoland/setup-deno@v1
- uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- name: benchmark
@@ -289,7 +289,7 @@ jobs:
- benchmark_4workers
- benchmark_8workers
steps:
- uses: denoland/setup-deno@v1
- uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- uses: actions/checkout@v4

View File

@@ -3,8 +3,7 @@ env:
IMAGE_NAME: ${{ github.repository }}
name: Build and publish windmill for RHEL9
on:
workflow_dispatch
on: workflow_dispatch
permissions: write-all
@@ -65,7 +64,7 @@ jobs:
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -74,7 +73,7 @@ jobs:
labels: |
${{ steps.meta-ee-public.outputs.labels }}-amd64
org.opencontainers.image.licenses=Windmill-Enterprise-License
- name: Build and push publicly ee arm64
uses: depot/build-push-action@v1
with:
@@ -82,7 +81,7 @@ jobs:
platforms: linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -108,7 +107,7 @@ jobs:
run: |
mv "${{ steps.extract-ee-amd64.outputs.destination }}/windmill" "${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9"
mv "${{ steps.extract-ee-arm64.outputs.destination }}/windmill" "${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel9"
- uses: actions/upload-artifact@v4
with:
name: RHEL9-amd64 build

View File

@@ -62,7 +62,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka
tags: |
${{ steps.meta-ee-public.outputs.tags }}
labels: |

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
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka
- name: Rename binary with corresponding architecture
run: |

View File

@@ -20,4 +20,4 @@ jobs:
run: |
cd backend
cargo generate-lockfile
- uses: stefanzweifel/git-auto-commit-action@v4
- uses: stefanzweifel/git-auto-commit-action@v5

View File

@@ -1,10 +1,8 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME:
${{ github.event_name != 'pull_request' && github.repository ||
IMAGE_NAME: ${{ github.event_name != 'pull_request' && github.repository ||
'windmill-labs/windmill-test' }}
DEV_SHA:
${{ github.event_name != 'pull_request' && 'dev' || format('pr-{0}',
DEV_SHA: ${{ github.event_name != 'pull_request' && 'dev' || format('pr-{0}',
github.event.number) }}
name: Build windmill:main
@@ -140,7 +138,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}
${{ steps.meta-ee-public.outputs.tags }}
@@ -202,7 +200,7 @@ jobs:
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka
PYTHON_IMAGE=python:3.12.2-slim-bookworm
tags: |
${{ steps.meta-ee-public-py312.outputs.tags }}

View File

@@ -25,7 +25,7 @@ jobs:
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- uses: denoland/setup-deno@v1
- uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- run: cd cli && ./build.sh && cd npm && npm publish

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
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka
- name: Rename binary with corresponding architecture
run: |

View File

@@ -15,7 +15,7 @@ jobs:
== 'I have read the CLA Document and I hereby sign the CLA') ||
github.event_name == 'pull_request_target'
# Beta Release
uses: cla-assistant/github-action@v2.3.1
uses: cla-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}

3
.gitignore vendored
View File

@@ -6,4 +6,5 @@ frontend/src/routes/test.svelte
CaddyfileRemoteMalo
*.swp
**/.idea/
.direnv
.direnv
.vscode

View File

@@ -1,3 +0,0 @@
{
"python.analysis.typeCheckingMode": "basic"
}

View File

@@ -1,5 +1,110 @@
# Changelog
## [1.430.1](https://github.com/windmill-labs/windmill/compare/v1.430.0...v1.430.1) (2024-11-23)
### Bug Fixes
* expose DISABLE_DENO_LOCK ([495d448](https://github.com/windmill-labs/windmill/commit/495d4487bc2109a85ea3941c104f736b7f79c355))
## [1.430.0](https://github.com/windmill-labs/windmill/compare/v1.429.0...v1.430.0) (2024-11-22)
### Features
* Add a devops role to act as a "readonly admin" ([#4775](https://github.com/windmill-labs/windmill/issues/4775)) ([4facf3c](https://github.com/windmill-labs/windmill/commit/4facf3ca3ee10e07a82668f8594372b9bd6d8b63))
* allow labeled values in app multiselect ([d5da75c](https://github.com/windmill-labs/windmill/commit/d5da75c031b7ac560d4e6d2801937462ddb8eb9d))
* Indexer improvements: s3 backup logic reworked, settings on the frontend ([#4763](https://github.com/windmill-labs/windmill/issues/4763)) ([8f198ba](https://github.com/windmill-labs/windmill/commit/8f198ba68ceaa1f503cddfc246ecbbfc7f90f11d))
* **schedule:** support for extended cron syntax ([#4754](https://github.com/windmill-labs/windmill/issues/4754)) ([8909bea](https://github.com/windmill-labs/windmill/commit/8909bea935088918c5eab5b4508fa7014cfac253))
### Bug Fixes
* allow non already existing resources in audit logs ([b7a9cec](https://github.com/windmill-labs/windmill/commit/b7a9cec289c4e671b8deb3f39d60503135bed06d))
* garbage collect `job` table + delete leaked ones ([#4767](https://github.com/windmill-labs/windmill/issues/4767)) ([c99d360](https://github.com/windmill-labs/windmill/commit/c99d360c3c8ce2b95f086ebbdc1ff235286c46b9))
* infer python list inner type from default if unknown inner ([#4771](https://github.com/windmill-labs/windmill/issues/4771)) ([b49ba59](https://github.com/windmill-labs/windmill/commit/b49ba59da7bf0f419638d3f506c5f67b975e4e85))
## [1.429.0](https://github.com/windmill-labs/windmill/compare/v1.428.1...v1.429.0) (2024-11-20)
### Features
* app editor ctrl nav ([#4757](https://github.com/windmill-labs/windmill/issues/4757)) ([3731886](https://github.com/windmill-labs/windmill/commit/37318861ac356b73f6da652c0ef8b4fc23436e3f))
* svix integration ([#3814](https://github.com/windmill-labs/windmill/issues/3814)) ([68f781e](https://github.com/windmill-labs/windmill/commit/68f781ea6f5f53a3aafbc2c4b5dd71496254c68e))
### Bug Fixes
* add cancellable icons refresh in apps ([2c325ef](https://github.com/windmill-labs/windmill/commit/2c325ef8524e3c74844e566ecf42c245f87a2918))
* **frontend:** pdf viewer fullscreen z-index ([#4762](https://github.com/windmill-labs/windmill/issues/4762)) ([a23cd4f](https://github.com/windmill-labs/windmill/commit/a23cd4f9e7c3e327eea099aade8c3fcd30cc2b87))
## [1.428.1](https://github.com/windmill-labs/windmill/compare/v1.428.0...v1.428.1) (2024-11-20)
### Bug Fixes
* grant all to new job table ([#4758](https://github.com/windmill-labs/windmill/issues/4758)) ([ff7c94c](https://github.com/windmill-labs/windmill/commit/ff7c94c5a7caf94d571a2654f5dc48c494c7bc2e))
## [1.428.0](https://github.com/windmill-labs/windmill/compare/v1.427.0...v1.428.0) (2024-11-19)
### Features
* improve app connection UX [#4687](https://github.com/windmill-labs/windmill/issues/4687) ([2fd80f7](https://github.com/windmill-labs/windmill/commit/2fd80f7cdd21e3938311914ec733d769e13993d6))
* pdf file preview ([#4753](https://github.com/windmill-labs/windmill/issues/4753)) ([b3a7cb0](https://github.com/windmill-labs/windmill/commit/b3a7cb058384b61328e2956f8ccd782ebd7b5900))
### Bug Fixes
* prevent groups to be ill-defined with non writer owners ([f59c0c0](https://github.com/windmill-labs/windmill/commit/f59c0c007675886d41b57647dc7e93b63444e3f9))
## [1.427.0](https://github.com/windmill-labs/windmill/compare/v1.426.1...v1.427.0) (2024-11-19)
### Features
* **backend:** move some static fields out of job tables ([#4689](https://github.com/windmill-labs/windmill/issues/4689)) ([97457a5](https://github.com/windmill-labs/windmill/commit/97457a5679dd6f17b7835d4adfa66ac99c0f5404))
### Bug Fixes
* improve flow status viewer for iterations ([#4744](https://github.com/windmill-labs/windmill/issues/4744)) ([1c39848](https://github.com/windmill-labs/windmill/commit/1c398486e6d5bc5d5bc454b61b41da9acfd50f39))
## [1.426.1](https://github.com/windmill-labs/windmill/compare/v1.426.0...v1.426.1) (2024-11-18)
### Bug Fixes
* playbook files permission mode incompatible with Windows ([#4740](https://github.com/windmill-labs/windmill/issues/4740)) ([4d9ec90](https://github.com/windmill-labs/windmill/commit/4d9ec909d1af7d123fec1a64143615e6cd0bba7e))
## [1.426.0](https://github.com/windmill-labs/windmill/compare/v1.425.1...v1.426.0) (2024-11-18)
### Features
* Add mode (permissions) option to files in ansible ([#4724](https://github.com/windmill-labs/windmill/issues/4724)) ([5e10782](https://github.com/windmill-labs/windmill/commit/5e107827790292d89e51d81138eeef2b5b23a9c2))
* kafka triggers ([#4713](https://github.com/windmill-labs/windmill/issues/4713)) ([88b8ffa](https://github.com/windmill-labs/windmill/commit/88b8ffab907e662ccca612bf01e6a228566522d2))
### Bug Fixes
* do not mount critical alerts modal if user is neither superadmin nor workspace admin ([#4731](https://github.com/windmill-labs/windmill/issues/4731)) ([180809d](https://github.com/windmill-labs/windmill/commit/180809d3466fb62d6692c45cbb012f158f83528f))
## [1.425.1](https://github.com/windmill-labs/windmill/compare/v1.425.0...v1.425.1) (2024-11-16)
### Bug Fixes
* revert bool to text support in pg ([#4727](https://github.com/windmill-labs/windmill/issues/4727)) ([17d8933](https://github.com/windmill-labs/windmill/commit/17d893315bd86942c7895cdb3e9c3ab34977b258))
## [1.425.0](https://github.com/windmill-labs/windmill/compare/v1.424.0...v1.425.0) (2024-11-15)
### Features
* Handle `pip install` by `uv` ([#4517](https://github.com/windmill-labs/windmill/issues/4517)) ([f240d13](https://github.com/windmill-labs/windmill/commit/f240d1322a3c1669dfb69ebda6d05effcca26ae3))
* **monitoring:** workspace critical alerts ([#4684](https://github.com/windmill-labs/windmill/issues/4684)) ([c32038a](https://github.com/windmill-labs/windmill/commit/c32038a76d9d00703d8b865af72f083ac43414e3))
## [1.424.0](https://github.com/windmill-labs/windmill/compare/v1.423.2...v1.424.0) (2024-11-14)

View File

@@ -318,19 +318,15 @@ you to have it being synced automatically everyday.
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| WORKER_GROUP | default | The worker group the worker belongs to and get its configuration pulled from | Worker |
| MODE | standalone | The mode if the binary. Possible values: standalone, worker, server | All |
| MODE | standalone | The mode if the binary. Possible values: standalone, worker, server, agent | All |
| METRICS_ADDR | None | (ee only) The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
| JSON_FMT | false | Output the logs in json format instead of logfmt | All |
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance. Is overriden by the instance settings if any. | Server |
| SCRIPT_TOKEN_EXPIRY | 900 | The default duration period of the ephemeral-token generated at the beginning of a script | Worker |
| ZOMBIE_JOB_TIMEOUT | 30 | The timeout after which a job is considered to be zombie if the worker did not send pings about processing the job (every server check for zombie jobs every 30s) | Server |
| RESTART_ZOMBIE_JOBS | true | If true then a zombie job is restarted (in-place with the same uuid and some logs), if false the zombie job is failed | Server |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker |
| DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | Worker |
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
@@ -339,9 +335,6 @@ you to have it being synced automatically everyday.
| GOPRIVATE | | The GOPRIVATE env variable to use private go modules | Worker |
| GOPROXY | | The GOPROXY env variable to use | Worker |
| NETRC | | The netrc content to use a private go registry | Worker |
| PIP_INDEX_URL | None | The index url to pass for pip. | Worker |
| PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker |
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
| PATH | None | The path environment variable, usually inherited | Worker |
| HOME | None | The home directory to use for Go and Bash , usually inherited | Worker |
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
@@ -350,6 +343,7 @@ you to have it being synced automatically everyday.
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DISABLE_RESPONSE_LOGS | false | Disable response logs | Server |
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmins can create new workspaces | Server |
## Run a local dev setup
See the [./frontend/README_DEV.md](./frontend/README_DEV.md) file for all

View File

@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "?column?",
"name": "bool",
"type_info": "Bool"
}
],

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_flow AS \"raw_flow!: Json<Box<JsonRawValue>>\"\n FROM job WHERE id = $1 AND workspace_id = $2 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "raw_flow!: Json<Box<JsonRawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
true
]
},
"hash": "0722f461da9edb169c3c73423440eaa3e1b9f2ca3b7033bd7ed978a3b4ddf3fc"
}

View File

@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM kafka_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Bool",
"Text"
]
},
"nullable": [
null
]
},
"hash": "2139f1fb1877294bbf55d786000c5c32f582a3911efcf88e437aa90d7d5a49b5"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_code, raw_lock, raw_flow AS \"raw_flow: Json<Box<JsonRawValue>>\"\n FROM queue WHERE id = $1 AND workspace_id = $2 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<JsonRawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "217a5291438d23597b2c7f05d2c481f406364d56a129089a268f6423c548bca6"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT devops FROM password WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "devops",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "2c615128e28a3fb1ecb4b7a40f45801b58221898175c5d00dbdaaa085f769ac8"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH uuid_table as (\n select gen_random_uuid() as uuid from generate_series(1, $11)\n )\n INSERT INTO queue \n (id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout, raw_code, raw_lock, raw_flow, flow_status)\n (SELECT uuid, $1, $2, $3, $4, ('{ \"uuid\": \"' || uuid || '\" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14, $15, $16, $17, $18 FROM uuid_table) \n RETURNING id",
"query": "WITH uuid_table as (\n select unnest($11::uuid[]) as uuid\n )\n INSERT INTO queue \n (id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout, flow_status)\n (SELECT uuid, $1, $2, $3, $4, ('{ \"uuid\": \"' || uuid || '\" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14, $15 FROM uuid_table) \n RETURNING id",
"describe": {
"columns": [
{
@@ -69,13 +69,10 @@
"Varchar",
"Timestamptz",
"Varchar",
"UuidArray",
"Int4",
"Int4",
"Int4",
"Int4",
"Text",
"Text",
"Jsonb",
"Jsonb"
]
},
@@ -83,5 +80,5 @@
false
]
},
"hash": "a69ba7471d1a5faa145bebbd17d43e6dbe02e9e92ebbc31a50c99c3a04719284"
"hash": "337f31c2172194cd594042c561998a03f751b246c40daf056fced0fd91f6dd73"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC\n LIMIT $1 OFFSET $2",
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -30,16 +30,21 @@
},
{
"ordinal": 5,
"name": "devops",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "company",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "username",
"type_info": "Varchar"
}
@@ -56,10 +61,11 @@
null,
false,
false,
false,
true,
true,
true
]
},
"hash": "2eec077cc9e27d7ccd160cbaac118c321c422705f79e69550bb60f377083bcef"
"hash": "3895cee539a24b4c6ea89fa7a835fc62bc93b0530efba09fc3c32a8f93eaabb1"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM kafka_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "3997dcf2c11817e59bf50fd896381d870b847a8ba07e6197212fdf85ec901b09"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET enabled = FALSE, error = $1, server_id = NULL, last_server_ping = NULL WHERE workspace_id = $2 AND path = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "3e64c894c89ef82c4527180b82c4e82c1b7060ba0ca288dc6a7c7afcd76212e8"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE password SET devops = $1 WHERE email = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "411788111afccd826ce78b266153600939c65c75be8894322b90d9da18dcb824"
}

View File

@@ -145,6 +145,11 @@
},
{
"ordinal": 28,
"name": "cron_version",
"type_info": "Text"
},
{
"ordinal": 29,
"name": "jobs",
"type_info": "JsonArray"
}
@@ -185,6 +190,7 @@
true,
true,
true,
true,
null
]
},

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb\n FROM queue\n WHERE id = $1",
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb FROM completed_job WHERE id = $1",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
null
]
},
"hash": "aa6907b7266ee437dfbd77a9bf6c047fda88f3e72a0d10323a5e4020cf857c42"
"hash": "45950064cce9f53f73a01ddcd6911ec677297009b71041d39019c4700a571c0f"
}

View File

@@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO job (id, workspace_id, raw_code, raw_lock, raw_flow, tag)\n VALUES ($1, $2, $3, $4, $5, $6)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Text",
"Text",
"Jsonb",
"Varchar"
]
},
"nullable": []
},
"hash": "4b923c94f6adcc7a76e8073de5e46b116dba3211487c8408ce2777aafdf94a44"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET kafka_resource_path = $1, group_id = $2, topics = $3, script_path = $4, path = $5, is_flow = $6, edited_by = $7, email = $8, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL\n WHERE workspace_id = $9 AND path = $10",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "5962733746c81480abe9ab3a6ccf5664130ae7500279055c08db42d67b2822f6"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "WITH uuid_table as (\n select gen_random_uuid() as uuid from generate_series(1, $5)\n )\n INSERT INTO job\n (id, workspace_id, raw_code, raw_lock, raw_flow)\n (SELECT uuid, $1, $2, $3, $4 FROM uuid_table)\n RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text",
"Jsonb",
"Int4"
]
},
"nullable": [
false
]
},
"hash": "63e54fe57ec439b68eead00a02209f81076c5317d590e1441b557555b4d7ad96"
}

View File

@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "?column?",
"name": "bool",
"type_info": "Bool"
}
],

View File

@@ -1,23 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_flow->'modules'->($1)::text->'value'->>'type' = 'flow' FROM queue WHERE id = $2 LIMIT 1",
"query": "SELECT EXISTS(SELECT 1 FROM kafka_trigger WHERE path = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Uuid"
"Text"
]
},
"nullable": [
null
]
},
"hash": "de1abe57b6aa61155f747a3bcb98359f70eade6654b5a884915f07f3ef3fe15e"
"hash": "6d418b5cd7a4df54cfe3ec06e2a957c87f2307a218115ca749ea0e71eca2002e"
}

View File

@@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET last_server_ping = now(), error = $1 WHERE workspace_id = $2 AND path = $3 AND server_id = $4 AND enabled IS TRUE RETURNING 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "6e70ebf078ac04a2933d2f83791973e8fc108d9f32be8a8501391052d76e191e"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET enabled = $1, email = $2, edited_by = $3, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL\n WHERE path = $4 AND workspace_id = $5 RETURNING 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Bool",
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "6eb076afcf11dba0dfe35ee108c64814f95dbe5376f0539195d24a8ca96ca3e2"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job WHERE id = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "803b9c1373541cf52f416cde9e9e99ab79072e21dfaafb498aac25a059bd2f30"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT owners FROM folder WHERE name = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "owners",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "a37e9efe197609226af13a7e4f940cadce758f4d3c54f3a6b26756a3df38bab0"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_by, CONCAT(coalesce(queue.logs, ''), coalesce(job_logs.logs, '')) as logs, job_logs.log_offset, job_logs.log_file_index\n FROM queue \n LEFT JOIN job_logs ON job_logs.job_id = queue.id \n WHERE queue.id = $1 AND queue.workspace_id = $2",
"query": "SELECT created_by, CONCAT(coalesce(queue.logs, ''), coalesce(job_logs.logs, '')) as logs, coalesce(job_logs.log_offset, 0) as log_offset, job_logs.log_file_index\n FROM queue \n LEFT JOIN job_logs ON job_logs.job_id = queue.id \n WHERE queue.id = $1 AND queue.workspace_id = $2",
"describe": {
"columns": [
{
@@ -33,9 +33,9 @@
"nullable": [
false,
null,
false,
null,
true
]
},
"hash": "57111a97ff906fd79ecd8a571212cc4fea9cb9c16fe950f082ad5469bfeefc82"
"hash": "b2c30137b3b64f8c3f6aea062e8687e119599641beee0a0d6c89c5d90e82d0db"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n flow_status AS \"flow_status!: Json<Box<RawValue>>\",\n raw_flow->'modules'->(flow_status->'step')::int AS \"module: Json<Box<RawValue>>\"\n FROM queue WHERE id = $1 AND workspace_id = $2 LIMIT 1",
"query": "SELECT\n flow_status AS \"flow_status!: Json<Box<RawValue>>\",\n coalesce(job.raw_flow, queue.raw_flow)->'modules'->(flow_status->'step')::int AS \"module: Json<Box<RawValue>>\"\n FROM queue LEFT JOIN job USING(id, workspace_id) WHERE id = $1 AND workspace_id = $2 LIMIT 1",
"describe": {
"columns": [
{
@@ -25,5 +25,5 @@
null
]
},
"hash": "1e7ce0c140410ae799f9c0c5772e4be4506bfd238c88f1b1c3ddf39b29071446"
"hash": "b3a9ab85a78edf292ddb0af33f7435bbcc84f63c9079e29d84043f258eba208e"
}

View File

@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as \"websocket_used!\", \n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\"",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "websocket_used!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "http_routes_used!",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "kafka_used!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null,
null
]
},
"hash": "cc8a71abdeccf0787695af32aa21fe73aba65cfdc8329c02b4d257de4d2d168a"
}

View File

@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET server_id = $1, last_server_ping = now() WHERE enabled IS TRUE AND workspace_id = $2 AND path = $3 AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') RETURNING true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "d6615719bf8db4b333ed55c9a3c160ad1962668fc3305d8e80ae91ef73614a80"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_code, raw_lock, raw_flow AS \"raw_flow: Json<Box<RawValue>>\"\n FROM job WHERE id = $1 AND workspace_id = $2 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",
"Text"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "da861c06f6448f68069ba3be9b677641c5c6b2c1e4960c74db27dbaffcbd4c40"
}

View File

@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as \"websocket_used!\", EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as \"http_routes_used!\"",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "websocket_used!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "http_routes_used!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null
]
},
"hash": "db24f1f6ef1e26b7de7926bd3f32d1fa673c4970c25ff9ad98f0fdf199801b53"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company, username, NULL::bool as operator_only FROM password WHERE email = $1",
"query": "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only FROM password WHERE email = $1",
"describe": {
"columns": [
{
@@ -20,26 +20,31 @@
},
{
"ordinal": 3,
"name": "verified",
"name": "devops",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "verified",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "company",
"type_info": "Varchar"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "operator_only",
"type_info": "Bool"
}
@@ -54,11 +59,12 @@
null,
false,
false,
false,
true,
true,
true,
null
]
},
"hash": "2c14d3a88193f16ad3b8cd590749cb5537995f2499f6cb8f0f316fb62902d542"
"hash": "df5b933f81ca7e3bbb3fb522baedf749fa3bbbf2c0e43d5d4ea148b5bc990067"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb\n FROM completed_job\n WHERE id = $1",
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb FROM job WHERE id = $1",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
null
]
},
"hash": "31dd23f6768052e486668172e15eba1a3f9b6a8e5678a7ee8e5456ff4405d6f9"
"hash": "e1923bc755bd6b8cc871ae9381a97d3c63a0fce3dd57c93f48a2136f7395fa3a"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value FROM global_settings WHERE name = 'indexer_settings'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "e2d8028163c014f4cdff0d85550371fe28ec8233fa80413324ce0194ce909e3c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, login_type::text, verified, super_admin, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, email LIMIT $1 OFFSET $2",
"query": "SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -25,21 +25,26 @@
},
{
"ordinal": 4,
"name": "devops",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "company",
"type_info": "Varchar"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "operator_only",
"type_info": "Bool"
}
@@ -55,11 +60,12 @@
null,
false,
false,
false,
true,
true,
true,
null
]
},
"hash": "1a4d291c2f239f7b50c116594cebb031862e1a18ad9204e02a0194817db26d6a"
"hash": "f07a705df1a988827e099d146f5308b763293a27adf30d02df605317791d8126"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_flow AS \"raw_flow!: Json<Box<sqlx::types::JsonRawValue>>\"\n FROM job WHERE id = $1 AND workspace_id = $2 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "raw_flow!: Json<Box<sqlx::types::JsonRawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
true
]
},
"hash": "f24303a2386575a750a48a21d1b6cdecd6e64a0415c60ad1495f100c5d85bbe1"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_code, raw_lock, raw_flow AS \"raw_flow: Json<Box<JsonRawValue>>\"\n FROM job WHERE id = $1 AND workspace_id = $2 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<JsonRawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "fb1a32318b35ec5c8129eb3660b79eb5e6d1e01fcf01cc05d3c7ddf47295c2f5"
}

818
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.424.0"
version = "1.430.1"
authors.workspace = true
edition.workspace = true
@@ -29,7 +29,7 @@ members = [
]
[workspace.package]
version = "1.424.0"
version = "1.430.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -62,6 +62,7 @@ jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemal
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy"]
sqlx = ["windmill-worker/sqlx"]
deno_core = ["windmill-worker/deno_core", "dep:deno_core"]
kafka = ["windmill-api/kafka"]
[dependencies]
anyhow.workspace = true
@@ -82,7 +83,6 @@ chrono.workspace = true
git-version.workspace = true
base64.workspace = true
sha2.workspace = true
rsmq_async.workspace = true
url.workspace = true
lazy_static.workspace = true
once_cell.workspace = true
@@ -139,12 +139,12 @@ headers = "^0"
hyper = { version = "^1", features = ["full"] }
tokio = { version = "^1", features = ["full", "tracing"] }
tower = "^0"
tower-http = { version = "^0.5", features = ["trace", "cors"] }
tower-http = { version = "^0.6", features = ["trace", "cors"] }
tower-cookies = "^0.10"
serde = "^1"
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
uuid = { version = "^1", features = ["serde", "v4"] }
thiserror = "^1"
thiserror = "^2"
anyhow = "^1"
chrono = { version = "0.4.35", features = ["serde"] }
chrono-tz = "^0"
@@ -230,7 +230,6 @@ async-stripe = { version = "0.39.1", features = [
] }
async_zip = { version = "0.0.11", features = ["full"] }
once_cell = "1.17.1"
rsmq_async = { version = "5.1.5" }
gosyn = "0.2.6"
bytes = "1.4.0"
gethostname = "0.4.3"
@@ -266,6 +265,7 @@ tokio-native-tls = "^0"
openssl = "=0.10"
mail-parser = "^0"
matchit = "=0.7.3"
rdkafka = { version = "0.36.2", features = ["cmake-build", "ssl-vendored"] }
datafusion = "39.0.0"
object_store = { version = "0.10.0", features = ["aws", "azure"] }
@@ -287,6 +287,8 @@ triomphe = "^0"
tantivy = "0.22.0"
backon = "1.3.0"
# Macro-related
proc-macro2 = "1.0"
pulldown-cmark = "0.9"
@@ -295,4 +297,4 @@ syn = { version = "2.0.74", features = ["full"] }
quote = "1.0.36"
regex-lite = "0.1.6"
yaml-rust = "0.4.5"
tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] }
tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] }

View File

@@ -1 +1 @@
51dcbf93b0d127af9f33fa346cc63fcd2475d4fa
446e4fbc59048bb11c18648ac094e57c0bfa4a28

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TABLE kafka_trigger;

View File

@@ -0,0 +1,69 @@
-- Add up migration script here
-- Add up migration script here
CREATE TABLE kafka_trigger (
path VARCHAR(255) NOT NULL,
kafka_resource_path VARCHAR(255) NOT NULL,
topics VARCHAR(255)[] NOT NULL,
group_id VARCHAR(255) NOT NULL,
script_path VARCHAR(255) NOT NULL,
is_flow BOOLEAN NOT NULL,
workspace_id VARCHAR(50) NOT NULL,
edited_by VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
edited_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
extra_perms JSONB NOT NULL DEFAULT '{}',
server_id VARCHAR(50) NULL,
last_server_ping TIMESTAMPTZ NULL,
error TEXT NULL,
enabled BOOLEAN NOT NULL,
PRIMARY KEY (path, workspace_id)
);
GRANT ALL ON kafka_trigger TO windmill_user;
GRANT ALL ON kafka_trigger TO windmill_admin;
ALTER TABLE kafka_trigger ENABLE ROW LEVEL SECURITY;
CREATE POLICY admin_policy ON kafka_trigger FOR ALL TO windmill_admin USING (true);
CREATE POLICY see_folder_extra_perms_user_select ON kafka_trigger FOR SELECT TO windmill_user
USING (SPLIT_PART(kafka_trigger.path, '/', 1) = 'f' AND SPLIT_PART(kafka_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_insert ON kafka_trigger FOR INSERT TO windmill_user
WITH CHECK (SPLIT_PART(kafka_trigger.path, '/', 1) = 'f' AND SPLIT_PART(kafka_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_update ON kafka_trigger FOR UPDATE TO windmill_user
USING (SPLIT_PART(kafka_trigger.path, '/', 1) = 'f' AND SPLIT_PART(kafka_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_delete ON kafka_trigger FOR DELETE TO windmill_user
USING (SPLIT_PART(kafka_trigger.path, '/', 1) = 'f' AND SPLIT_PART(kafka_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_own ON kafka_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(kafka_trigger.path, '/', 1) = 'u' AND SPLIT_PART(kafka_trigger.path, '/', 2) = current_setting('session.user'));
CREATE POLICY see_member ON kafka_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(kafka_trigger.path, '/', 1) = 'g' AND SPLIT_PART(kafka_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
CREATE POLICY see_extra_perms_user_select ON kafka_trigger FOR SELECT TO windmill_user
USING (extra_perms ? CONCAT('u/', current_setting('session.user')));
CREATE POLICY see_extra_perms_user_insert ON kafka_trigger FOR INSERT TO windmill_user
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_update ON kafka_trigger FOR UPDATE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_delete ON kafka_trigger FOR DELETE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_groups_select ON kafka_trigger FOR SELECT TO windmill_user
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]);
CREATE POLICY see_extra_perms_groups_insert ON kafka_trigger FOR INSERT TO windmill_user
WITH CHECK (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
CREATE POLICY see_extra_perms_groups_update ON kafka_trigger FOR UPDATE TO windmill_user
USING (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
CREATE POLICY see_extra_perms_groups_delete ON kafka_trigger FOR DELETE TO windmill_user
USING (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));

View File

@@ -0,0 +1 @@
ALTER TABLE schedule DROP COLUMN cron_version;

View File

@@ -0,0 +1,2 @@
-- Add existing rows to have 'v1' as the cron_version
ALTER TABLE schedule ADD COLUMN cron_version TEXT DEFAULT 'v1';

View File

@@ -0,0 +1,4 @@
-- Add down migration script here
DROP TABLE job CASCADE;
DROP VIEW queue_view CASCADE;
DROP VIEW completed_job_view CASCADE;

View File

@@ -0,0 +1,38 @@
-- Add up migration script here
CREATE TABLE job (
id UUID PRIMARY KEY,
raw_code TEXT,
raw_lock TEXT,
raw_flow jsonb NULL,
tag VARCHAR(50),
workspace_id VARCHAR(50)
);
-- Create `queue_view` and `completed_job_view` views.
DO $$
DECLARE
t TEXT;
BEGIN
FOR t IN VALUES ('queue'), ('completed_job') LOOP
EXECUTE format(
'CREATE OR REPLACE VIEW '||t||'_view AS
SELECT %s, job_logs.log_offset, job_logs.log_file_index FROM '||t||'
LEFT JOIN job ON '||t||'.id = job.id AND '||t||'.workspace_id = job.workspace_id
LEFT JOIN job_logs ON '||t||'.id = job_logs.job_id', (
SELECT string_agg(
CASE
WHEN column_name = 'logs' THEN -- Concatenate logs from base and job_logs.
'concat(coalesce('||t||'.logs, ''''), coalesce(job_logs.logs, '''')) as logs'
WHEN column_name IN ('raw_code', 'raw_lock', 'raw_flow') THEN -- Coalesce column from base and job.
format('coalesce('||t||'.%s, job.%s) as %s', column_name, column_name, column_name)
ELSE
format('%s.%s', t, column_name)
END,
', '
)
FROM information_schema.columns
WHERE table_name = t
)
);
END LOOP;
END $$;

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
GRANT ALL ON job TO windmill_user;
GRANT ALL ON job TO windmill_admin;

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
-- Nothing to do here

View File

@@ -0,0 +1,4 @@
-- Add up migration script here
DELETE FROM job
WHERE NOT EXISTS (SELECT 1 FROM completed_job WHERE completed_job.id = job.id)
AND NOT EXISTS (SELECT 1 FROM queue WHERE queue.id = job.id);

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE password DROP COLUMN devops;

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE password ADD COLUMN devops BOOLEAN NOT NULL DEFAULT false;

View File

@@ -63,6 +63,7 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
"msgraph" => "msgraph-sdk",
"pythonjsonlogger" => "python-json-logger",
"socks" => "PySocks",
"taiga" => "python-taiga",
};
fn replace_import(x: String) -> String {

View File

@@ -93,6 +93,12 @@ pub fn parse_python_signature(
.iter()
.enumerate()
.map(|(i, x)| {
let mut typ = x
.as_arg()
.annotation
.as_ref()
.map_or(Typ::Unknown, |e| parse_expr(e));
let default = if i >= def_arg_start {
params
.defaults()
@@ -103,19 +109,28 @@ pub fn parse_python_signature(
None
};
let mut typ = x
.as_arg()
.annotation
.as_ref()
.map_or(Typ::Unknown, |e| parse_expr(e));
let should_get_type_from_default = match &typ {
Typ::Unknown => true,
// if the type is a list of unknowns, we should get the type from the default
Typ::List(inner) => matches!(inner.as_ref(), Typ::Unknown),
_ => false,
};
if typ == Typ::Unknown
if should_get_type_from_default
&& default.is_some()
&& default != Some(json!(FUNCTION_CALL))
{
typ = json_to_typ(default.as_ref().unwrap());
}
// if the type is still a list of unknowns after checking the default, we set it to a list of strings to not break past behavior
match typ {
Typ::List(inner) if matches!(inner.as_ref(), Typ::Unknown) => {
typ = Typ::List(Box::new(Typ::Str(None)));
}
_ => {}
}
Arg {
otyp: None,
name: x.as_arg().arg.to_string(),
@@ -193,7 +208,7 @@ fn parse_typ(id: &str) -> Typ {
"int" => Typ::Int,
"bool" => Typ::Bool,
"dict" => Typ::Object(vec![]),
"list" => Typ::List(Box::new(Typ::Str(None))),
"list" => Typ::List(Box::new(Typ::Unknown)),
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
@@ -593,4 +608,69 @@ def main(): return
Ok(())
}
#[test]
fn test_parse_python_sig_8() -> anyhow::Result<()> {
let code = r#"
from typing import List
def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b"]): return
"#;
println!(
"{}",
serde_json::to_string(&parse_python_signature(code, None)?)?
);
assert_eq!(
parse_python_signature(code, None)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: None,
name: "a".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "e".to_string(),
typ: Typ::List(Box::new(Typ::Int)),
default: None,
has_default: false,
oidx: None
},
Arg {
otyp: None,
name: "b".to_string(),
typ: Typ::List(Box::new(Typ::Int)),
default: Some(json!([1, 2, 3, 4])),
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "c".to_string(),
typ: Typ::List(Box::new(Typ::Int)),
default: Some(json!([1, 2, 3, 4])),
has_default: true,
oidx: None
},
Arg {
otyp: None,
name: "d".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: Some(json!(["a", "b"])),
has_default: true,
oidx: None
}
],
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
Ok(())
}
}

View File

@@ -198,6 +198,7 @@ pub enum ResourceOrVariablePath {
pub struct FileResource {
pub resource_path: ResourceOrVariablePath,
pub target_path: String,
pub mode: Option<u32>,
}
#[derive(Debug, Clone)]
@@ -437,36 +438,65 @@ fn count_consecutive_vs(s: &str) -> usize {
fn parse_file_resource(yaml: &Yaml) -> anyhow::Result<FileResource> {
if let Yaml::Hash(f) = yaml {
let target_path = f
.get(&Yaml::String("target".to_string()))
.and_then(|x| x.as_str())
.map(|x| x.to_string())
.ok_or(anyhow!(
"No `target` provided for the file. Please input a target path (only relative paths are allowed) where the ansible playbook can read this file.",
))?;
let mut mode = None;
if let Some(u) = f.get(&Yaml::String("mode".to_string())) {
let mode_val: u32 = match u {
Yaml::Integer(u) => {
u.clone().try_into().map_err(|e| {
anyhow!(
"Invalid value for `mode` permissions property on targeted file to: {}, err: {e}",
target_path
)
})?
}
Yaml::String(s) => {
let val = if s.starts_with("0b") {
u32::from_str_radix(&s[2..], 2)
} else if s.starts_with("0o") {
u32::from_str_radix(&s[2..], 8)
} else if s.starts_with("0") {
u32::from_str_radix(&s[1..], 8)
} else {
u32::from_str_radix(s, 8)
};
val.map_err(|e| anyhow!("Error parsing permission mode value {s}: {e}"))?
}
_ => {
return Err(anyhow!("Invalid field in `mode`, expected integer like 0o644"));
}
};
if mode_val <= 0o777 {
mode = Some(mode_val);
} else {
return Err(anyhow!("The provided value for `mode` is too big. Make sure that you are using the octal prefix (0o), e.g. `mode: 0o644`"));
}
}
if let Some(Yaml::String(resource_path)) = f.get(&Yaml::String("resource".to_string())) {
let target_path = f
.get(&Yaml::String("target".to_string()))
.and_then(|x| x.as_str())
.map(|x| x.to_string())
.ok_or(anyhow!(
"No `target` provided for file resource {}. Please input a target relative path for the ansible playbook to see this file.",
resource_path
))?;
return Ok(FileResource {
resource_path: ResourceOrVariablePath::Resource(resource_path.clone()),
target_path,
mode,
});
}
if let Some(Yaml::String(resource_path)) = f.get(&Yaml::String("variable".to_string())) {
let target_path = f
.get(&Yaml::String("target".to_string()))
.and_then(|x| x.as_str())
.map(|x| x.to_string())
.ok_or(anyhow!(
"No `target` provided for file resource {}. Please input a target relative path for the ansible playbook to see this file.",
resource_path
))?;
return Ok(FileResource {
resource_path: ResourceOrVariablePath::Variable(resource_path.clone()),
target_path,
mode,
});
}
return Err(anyhow!(
"File resource should have a `resource` field, linking to a text file resource"
"Files should have a `resource` or `variable` field that will be the contents of the local text file"
));
}
return Err(anyhow!("Invalid file resource: Should be a dictionary."));

View File

@@ -8,8 +8,8 @@
use anyhow::Context;
use monitor::{
reload_timeout_wait_result_setting, send_current_log_file_to_object_store,
send_logs_to_object_store,
reload_indexer_config, 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};
@@ -33,7 +33,7 @@ use windmill_common::{
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
DEFAULT_TAGS_WORKSPACES_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING,
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING,
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, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
@@ -341,27 +341,6 @@ async fn windmill_main() -> anyhow::Result<()> {
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
};
let rsmq_config = std::env::var("REDIS_URL").ok().map(|x| {
let url = x.parse::<url::Url>().unwrap();
let mut config = rsmq_async::RsmqOptions { ..Default::default() };
config.host = url.host_str().expect("redis host required").to_owned();
config.password = url.password().map(|s| s.to_owned());
config.db = url
.path_segments()
.and_then(|mut segments| segments.next())
.and_then(|segment| segment.parse().ok())
.unwrap_or(0);
config.ns = url
.query_pairs()
.find(|s| s.0 == "rsmq_namespace")
.map(|s| s.1)
.unwrap_or(std::borrow::Cow::Borrowed("rsmq"))
.into_owned();
config.port = url.port().unwrap_or(6379).to_string();
config
});
tracing::info!("Connecting to database...");
let db = windmill_common::connect_db(server_mode, indexer_mode).await?;
tracing::info!("Database connected");
@@ -376,13 +355,6 @@ async fn windmill_main() -> anyhow::Result<()> {
.unwrap_or_else(|| "UNKNOWN".to_string())
);
let rsmq = if let Some(config) = rsmq_config {
tracing::info!("Redis config set: {:?}", config);
Some(rsmq_async::MultiplexedRsmq::new(config).await.unwrap())
} else {
None
};
let is_agent = mode == Mode::Agent;
if !is_agent {
@@ -497,7 +469,6 @@ Windmill Community Edition {GIT_VERSION}
monitor_db(
&db,
&base_internal_url,
rsmq.clone(),
server_mode,
worker_mode,
true,
@@ -516,7 +487,6 @@ Windmill Community Edition {GIT_VERSION}
let addr = SocketAddr::from((server_bind_address, port));
let rsmq2 = rsmq.clone();
let (base_internal_tx, base_internal_rx) = tokio::sync::oneshot::channel::<String>();
DirBuilder::new()
@@ -529,6 +499,8 @@ Windmill Community Edition {GIT_VERSION}
let should_index_jobs =
mode == Mode::Indexer || (enable_standalone_indexer && mode == Mode::Standalone);
reload_indexer_config(&db).await;
#[cfg(feature = "tantivy")]
let (index_reader, index_writer) = if should_index_jobs {
let (r, w) = windmill_indexer::completed_runs_ee::init_index(&db).await?;
@@ -548,7 +520,7 @@ Windmill Community Edition {GIT_VERSION}
index_writer,
indexer_rx,
)
.await;
.await?;
}
Ok(())
}
@@ -573,7 +545,7 @@ Windmill Community Edition {GIT_VERSION}
log_index_writer,
log_indexer_rx,
)
.await;
.await?;
}
Ok(())
}
@@ -595,7 +567,6 @@ Windmill Community Edition {GIT_VERSION}
if !is_agent {
windmill_api::run_server(
db.clone(),
rsmq2,
index_reader,
log_index_reader,
addr,
@@ -627,7 +598,6 @@ Windmill Community Edition {GIT_VERSION}
killpill_tx.clone(),
num_workers,
base_internal_url.clone(),
rsmq.clone(),
mode.clone() == Mode::Agent,
hostname.clone(),
)
@@ -643,13 +613,12 @@ Windmill Community Edition {GIT_VERSION}
killpill_phase2_tx.send(())?;
tracing::info!("Phase 2 of shutdown completed");
}
Ok(()) as anyhow::Result<()>
Ok(())
};
let monitor_f = async {
let db = db.clone();
let tx = killpill_tx.clone();
let rsmq = rsmq.clone();
let base_internal_url = base_internal_url.to_string();
let h = tokio::spawn(async move {
@@ -666,7 +635,6 @@ Windmill Community Edition {GIT_VERSION}
monitor_db(
&db,
&base_internal_url,
rsmq.clone(),
server_mode,
worker_mode,
false,
@@ -737,6 +705,9 @@ Windmill Community Edition {GIT_VERSION}
SMTP_SETTING => {
reload_smtp_config(&db).await;
},
INDEXER_SETTING => {
reload_indexer_config(&db).await;
},
TIMEOUT_WAIT_RESULT_SETTING => {
reload_timeout_wait_result_setting(&db).await
},
@@ -954,13 +925,12 @@ fn display_config(envs: &[&str]) {
)
}
pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
pub async fn run_workers(
db: Pool<Postgres>,
mut rx: tokio::sync::broadcast::Receiver<()>,
tx: tokio::sync::broadcast::Sender<()>,
num_workers: i32,
base_internal_url: String,
rsmq: Option<R>,
agent_mode: bool,
hostname: String,
) -> anyhow::Result<()> {
@@ -1032,7 +1002,6 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
let rx = killpill_rxs.pop().unwrap();
let tx = tx.clone();
let base_internal_url = base_internal_url.clone();
let rsmq2 = rsmq.clone();
let hostname = hostname.clone();
handles.push(tokio::spawn(async move {
@@ -1050,7 +1019,6 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
rx,
tx,
&base_internal_url,
rsmq2,
agent_mode,
);

View File

@@ -11,7 +11,6 @@ use std::{
};
use chrono::{NaiveDateTime, Utc};
use rsmq_async::MultiplexedRsmq;
use serde::de::DeserializeOwned;
use sqlx::{Pool, Postgres};
use tokio::{
@@ -43,6 +42,7 @@ use windmill_common::{
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
},
indexer::load_indexer_config,
jobs::QueuedJob,
oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH,
server::load_smtp_config,
@@ -51,8 +51,8 @@ use windmill_common::{
utils::{now_from_db, rd_string, report_critical_error, Mode},
worker::{
load_worker_config, make_pull_query, make_suspended_pull_query, reload_custom_tags_setting,
update_min_version, DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SMTP_CONFIG,
WORKER_CONFIG, WORKER_GROUP,
update_min_version, DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG,
SMTP_CONFIG, WORKER_CONFIG, WORKER_GROUP,
},
BASE_URL, CRITICAL_ALERT_MUTE_UI_ENABLED, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL,
HUB_BASE_URL, JOB_RETENTION_SECS, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
@@ -685,6 +685,13 @@ pub async fn delete_expired_items(db: &DB) -> () {
{
tracing::error!("Error deleting log file: {:?}", e);
}
if let Err(e) =
sqlx::query!("DELETE FROM job WHERE id = ANY($1)", &deleted_jobs)
.execute(&mut *tx)
.await
{
tracing::error!("Error deleting job: {:?}", e);
}
}
}
Err(e) => {
@@ -1021,7 +1028,6 @@ pub async fn monitor_pool(db: &DB) {
pub async fn monitor_db(
db: &Pool<Postgres>,
base_internal_url: &str,
rsmq: Option<MultiplexedRsmq>,
server_mode: bool,
_worker_mode: bool,
initial_load: bool,
@@ -1029,8 +1035,8 @@ pub async fn monitor_db(
) {
let zombie_jobs_f = async {
if server_mode && !initial_load {
handle_zombie_jobs(db, base_internal_url, rsmq.clone(), "server").await;
match handle_zombie_flows(db, rsmq.clone()).await {
handle_zombie_jobs(db, base_internal_url, "server").await;
match handle_zombie_flows(db).await {
Err(err) => {
tracing::error!("Error handling zombie flows: {:?}", err);
}
@@ -1186,6 +1192,17 @@ pub async fn reload_smtp_config(db: &Pool<Postgres>) {
}
}
pub async fn reload_indexer_config(db: &Pool<Postgres>) {
let indexer_config = load_indexer_config(&db).await;
if let Err(e) = indexer_config {
tracing::error!("Error reloading indexer config: {:?}", e)
} else {
let mut wc = INDEXER_CONFIG.write().await;
tracing::info!("Reloading smtp config...");
*wc = indexer_config.unwrap()
}
}
pub async fn reload_worker_config(
db: &DB,
tx: tokio::sync::broadcast::Sender<()>,
@@ -1291,12 +1308,7 @@ pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
Ok(())
}
async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
db: &Pool<Postgres>,
base_internal_url: &str,
rsmq: Option<R>,
worker_name: &str,
) {
async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker_name: &str) {
if *RESTART_ZOMBIE_JOBS {
let restarted = sqlx::query!(
"UPDATE queue SET running = false, started_at = null
@@ -1405,7 +1417,6 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
true,
same_worker_tx_never_used,
"",
rsmq.clone(),
worker_name,
send_result_never_used,
#[cfg(feature = "benchmark")]
@@ -1415,10 +1426,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
}
}
async fn handle_zombie_flows(
db: &DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
) -> error::Result<()> {
async fn handle_zombie_flows(db: &DB) -> error::Result<()> {
let flows = sqlx::query_as::<_, QueuedJob>(
r#"
SELECT *
@@ -1465,7 +1473,7 @@ async fn handle_zombie_flows(
}
);
report_critical_error(reason.clone(), db.clone(), Some(&flow.workspace_id), None).await;
cancel_zombie_flow_job(db, flow, &rsmq, reason).await?;
cancel_zombie_flow_job(db, flow, reason).await?;
}
}
@@ -1495,7 +1503,7 @@ async fn handle_zombie_flows(
job.workspace_id,
flow.last_ping
);
cancel_zombie_flow_job(db, job, &rsmq,
cancel_zombie_flow_job(db, job,
format!("Flow {} cancelled as one of the parallel branch {} was unable to make the last transition ", flow.parent_flow_id, flow.job_id))
.await?;
} else {
@@ -1508,7 +1516,6 @@ async fn handle_zombie_flows(
async fn cancel_zombie_flow_job(
db: &Pool<Postgres>,
flow: QueuedJob,
rsmq: &Option<MultiplexedRsmq>,
message: String,
) -> Result<(), error::Error> {
let tx = db.begin().await.unwrap();
@@ -1524,7 +1531,6 @@ async fn cancel_zombie_flow_job(
flow.workspace_id.as_str(),
tx,
db,
rsmq.clone(),
true,
false,
)

View File

@@ -0,0 +1,5 @@
INSERT INTO public.completed_job (
id, workspace_id, created_by, created_at, duration_ms, success, flow_status, result, job_kind, language
) VALUES (
'1eecb96a-c8b0-4a3d-b1b6-087878c55e41', 'test-workspace', 'test-user', '2023-01-01 00:00:00', 1000, true, '{"_metadata": {"column_order": ["b", "a"]}}', '[{"a": "second", "b": "first"}]', 'script', 'postgresql'
)

View File

@@ -1,3 +1,4 @@
use serde::de::DeserializeOwned;
use std::str::FromStr;
use windmill_api_client::types::{NewScript, NewScriptLanguage};
@@ -125,7 +126,6 @@ impl ApiServer {
db.clone(),
None,
None,
None,
addr,
rx,
port_tx,
@@ -898,8 +898,8 @@ impl RunJob {
hm_args.insert(k, windmill_common::worker::to_raw_value(&v));
}
let tx = PushIsolationLevel::IsolatedRoot(db.clone(), None);
let (uuid, tx) = windmill_queue::push::<rsmq_async::MultiplexedRsmq>(
let tx = PushIsolationLevel::IsolatedRoot(db.clone());
let (uuid, tx) = windmill_queue::push(
&db,
tx,
"test-workspace",
@@ -1017,7 +1017,7 @@ fn spawn_test_worker(
windmill_common::worker::make_suspended_pull_query(&wc).await;
windmill_common::worker::make_pull_query(&wc).await;
}
windmill_worker::run_worker::<rsmq_async::MultiplexedRsmq>(
windmill_worker::run_worker(
&db,
worker_instance,
worker_name,
@@ -1027,7 +1027,6 @@ fn spawn_test_worker(
rx,
tx2,
&base_internal_url,
None,
false,
)
.await
@@ -3112,6 +3111,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
summary: None,
tag: None,
paused_until: None,
cron_version: None,
};
let _ = client.create_schedule("test-workspace", &schedule).await;
@@ -3179,6 +3179,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
no_flow_overlap: None,
tag: None,
paused_until: None,
cron_version: None,
},
)
.await
@@ -3261,6 +3262,7 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
summary: None,
tag: None,
paused_until: None,
cron_version: None,
};
let _ = client.create_schedule("test-workspace", &schedule).await;
@@ -3329,6 +3331,7 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
no_flow_overlap: None,
tag: None,
paused_until: None,
cron_version: None,
},
)
.await
@@ -3612,3 +3615,71 @@ def main():
run_deployed_relative_imports(&db, content.clone(), ScriptLang::Python3).await;
run_preview_relative_imports(&db, content, ScriptLang::Python3).await;
}
#[sqlx::test(fixtures("base", "result_format"))]
async fn test_result_format(db: Pool<Postgres>) {
let ordered_result_job_id = "1eecb96a-c8b0-4a3d-b1b6-087878c55e41";
set_jwt_secret().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let token = windmill_worker::create_token_for_owner(
&db,
"test-workspace",
"u/test-user",
"",
100,
"",
&Uuid::nil(),
)
.await
.unwrap();
#[derive(Debug, Deserialize)]
struct JobResponse {
result: Option<Box<serde_json::value::RawValue>>,
}
async fn get_result<T: DeserializeOwned>(url: String) -> T {
reqwest::get(url)
.await
.unwrap()
.error_for_status()
.unwrap()
.json()
.await
.unwrap()
}
let correct_result = r#"[{"b":"first","a":"second"}]"#;
let job_response: JobResponse = get_result(format!("http://localhost:{port}/api/w/test-workspace/jobs_u/get/{ordered_result_job_id}?token={token}&no_logs=true")).await;
assert_eq!(job_response.result.unwrap().get(), correct_result);
let job_response: JobResponse = get_result(format!("http://localhost:{port}/api/w/test-workspace/jobs_u/completed/get_result_maybe/{ordered_result_job_id}?token={token}&no_logs=true")).await;
assert_eq!(job_response.result.unwrap().get(), correct_result);
let job_result: Box<serde_json::value::RawValue> = get_result(format!("http://localhost:{port}/api/w/test-workspace/jobs_u/completed/get_result/{ordered_result_job_id}?token={token}&no_logs=true")).await;
assert_eq!(job_result.get(), correct_result);
let response = windmill_api::jobs::run_wait_result(
&db,
Uuid::parse_str(ordered_result_job_id).unwrap(),
"test-workspace".to_string(),
None,
"test-user",
)
.await
.unwrap();
let result: Box<serde_json::value::RawValue> = serde_json::from_slice(
&axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap()
.to_vec(),
)
.unwrap();
assert_eq!(result.get(), correct_result);
}

View File

@@ -19,6 +19,7 @@ parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/par
prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus"]
openidconnect = ["dep:openidconnect"]
tantivy = ["dep:windmill-indexer"]
kafka = ["dep:rdkafka"]
[dependencies]
windmill-queue.workspace = true
@@ -76,7 +77,6 @@ async-stripe = { workspace = true, optional = true }
lazy_static.workspace = true
prometheus = { workspace = true, optional = true }
async_zip.workspace = true
rsmq_async.workspace = true
regex.workspace = true
bytes.workspace = true
samael = { workspace = true, optional = true }
@@ -96,6 +96,7 @@ url = { workspace = true, optional = true}
jsonwebtoken = { workspace = true }
matchit.workspace = true
tokio-tungstenite.workspace = true
rdkafka = { workspace = true, optional = true }
pin-project.workspace = true
http.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.424.0
version: 1.430.1
title: Windmill API
contact:
@@ -407,6 +407,8 @@ paths:
properties:
is_super_admin:
type: boolean
is_devops:
type: boolean
name:
type: string
responses:
@@ -2332,9 +2334,12 @@ paths:
type: boolean
websocket_used:
type: boolean
kafka_used:
type: boolean
required:
- http_routes_used
- websocket_used
- kafka_used
/w/{workspace}/users/list:
get:
@@ -7120,6 +7125,8 @@ paths:
type: string
timezone:
type: string
cron_version:
type: string
required:
- schedule
- timezone
@@ -7685,6 +7692,169 @@ paths:
schema:
type: string
/w/{workspace}/kafka_triggers/create:
post:
summary: create kafka trigger
operationId: createKafkaTrigger
tags:
- kafka_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: new kafka trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewKafkaTrigger"
responses:
"201":
description: kafka trigger created
content:
text/plain:
schema:
type: string
/w/{workspace}/kafka_triggers/update/{path}:
post:
summary: update kafka trigger
operationId: updateKafkaTrigger
tags:
- kafka_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: updated trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EditKafkaTrigger"
responses:
"200":
description: kafka trigger updated
content:
text/plain:
schema:
type: string
/w/{workspace}/kafka_triggers/delete/{path}:
delete:
summary: delete kafka trigger
operationId: deleteKafkaTrigger
tags:
- kafka_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: kafka trigger deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/kafka_triggers/get/{path}:
get:
summary: get kafka trigger
operationId: getKafkaTrigger
tags:
- kafka_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: kafka trigger deleted
content:
application/json:
schema:
$ref: "#/components/schemas/KafkaTrigger"
/w/{workspace}/kafka_triggers/list:
get:
summary: list kafka triggers
operationId: listKafkaTriggers
tags:
- kafka_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
required: true
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: path
description: filter by path
in: query
schema:
type: string
- name: is_flow
in: query
schema:
type: boolean
- name: path_start
in: query
schema:
type: string
responses:
"200":
description: kafka trigger list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/KafkaTrigger"
/w/{workspace}/kafka_triggers/exists/{path}:
get:
summary: does kafka trigger exists
operationId: existsKafkaTrigger
tags:
- kafka_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: kafka trigger exists
content:
application/json:
schema:
type: boolean
/w/{workspace}/kafka_triggers/setenabled/{path}:
post:
summary: set enabled kafka trigger
operationId: setKafkaTriggerEnabled
tags:
- kafka_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: updated kafka trigger enable
required: true
content:
application/json:
schema:
type: object
properties:
enabled:
type: boolean
required:
- enabled
responses:
"200":
description: kafka trigger enabled set
content:
text/plain:
schema:
type: string
/groups/list:
get:
@@ -8554,6 +8724,7 @@ paths:
raw_app,
http_trigger,
websocket_trigger,
kafka_trigger,
]
responses:
"200":
@@ -8592,6 +8763,7 @@ paths:
raw_app,
http_trigger,
websocket_trigger,
kafka_trigger,
]
requestBody:
description: acl to add
@@ -8641,6 +8813,7 @@ paths:
raw_app,
http_trigger,
websocket_trigger,
kafka_trigger,
]
requestBody:
description: acl to add
@@ -11627,6 +11800,8 @@ components:
paused_until:
type: string
format: date-time
cron_version:
type: string
required:
- path
- edited_by
@@ -11709,6 +11884,8 @@ components:
paused_until:
type: string
format: date-time
cron_version:
type: string
required:
- path
- schedule
@@ -11758,6 +11935,8 @@ components:
paused_until:
type: string
format: date-time
cron_version:
type: string
required:
- schedule
- timezone
@@ -11932,6 +12111,8 @@ components:
type: number
websocket_count:
type: number
kafka_count:
type: number
WebsocketTrigger:
type: object
@@ -12103,6 +12284,114 @@ components:
required:
- runnable_result
KafkaTrigger:
type: object
properties:
path:
type: string
edited_by:
type: string
edited_at:
type: string
format: date-time
script_path:
type: string
kafka_resource_path:
type: string
group_id:
type: string
topics:
type: array
items:
type: string
is_flow:
type: boolean
extra_perms:
type: object
additionalProperties:
type: boolean
email:
type: string
workspace_id:
type: string
server_id:
type: string
last_server_ping:
type: string
format: date-time
error:
type: string
enabled:
type: boolean
required:
- path
- edited_by
- edited_at
- script_path
- kafka_resource_path
- group_id
- topics
- extra_perms
- is_flow
- email
- workspace_id
- enabled
NewKafkaTrigger:
type: object
properties:
path:
type: string
script_path:
type: string
is_flow:
type: boolean
kafka_resource_path:
type: string
group_id:
type: string
topics:
type: array
items:
type: string
enabled:
type: boolean
required:
- path
- script_path
- is_flow
- kafka_resource_path
- group_id
- topics
EditKafkaTrigger:
type: object
properties:
kafka_resource_path:
type: string
group_id:
type: string
topics:
type: array
items:
type: string
path:
type: string
script_path:
type: string
is_flow:
type: boolean
required:
- path
- script_path
- kafka_resource_path
- group_id
- topics
- is_flow
Group:
type: object
properties:
@@ -12293,6 +12582,8 @@ components:
enum: ["password", "github"]
super_admin:
type: boolean
devops:
type: boolean
verified:
type: boolean
name:

View File

@@ -8,11 +8,6 @@ use std::collections::HashMap;
* LICENSE-AGPL for a copy of the license.
*/
#[cfg(feature = "parquet")]
use crate::{job_helpers_ee::{
get_random_file_name, get_s3_resource, get_workspace_s3_resource, upload_file_internal,
UploadFileResponse,
}, users::fetch_api_authed_from_permissioned_as};
use crate::{
db::{ApiAuthed, DB},
resources::get_resource_value_interpolated_internal,
@@ -22,6 +17,14 @@ use crate::{
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
#[cfg(feature = "parquet")]
use crate::{
job_helpers_ee::{
get_random_file_name, get_s3_resource, get_workspace_s3_resource, upload_file_internal,
UploadFileResponse,
},
users::fetch_api_authed_from_permissioned_as,
};
use axum::{
extract::{Extension, Json, Path, Query},
response::IntoResponse,
@@ -61,7 +64,7 @@ use windmill_common::{
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel, QueueTransaction};
use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel};
pub fn workspaced_service() -> Router {
Router::new()
@@ -648,12 +651,11 @@ async fn create_app(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(mut app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let mut tx = user_db.clone().begin(&authed).await?;
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
app.policy.on_behalf_of_email = Some(authed.email.clone());
@@ -667,7 +669,7 @@ async fn create_app(
&app.path,
w_id
)
.fetch_one(&mut tx)
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
@@ -683,7 +685,7 @@ async fn create_app(
&app.path,
&w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
let id = sqlx::query_scalar!(
@@ -696,7 +698,7 @@ async fn create_app(
json!(app.policy),
app.draft_only,
)
.fetch_one(&mut tx)
.fetch_one(&mut *tx)
.await?;
let v_id = sqlx::query_scalar!(
@@ -708,7 +710,7 @@ async fn create_app(
serde_json::to_string(&app.value).unwrap(),
authed.username,
)
.fetch_one(&mut tx)
.fetch_one(&mut *tx)
.await?;
sqlx::query!(
@@ -716,11 +718,11 @@ async fn create_app(
v_id,
id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
audit_log(
&mut tx,
&mut *tx,
&authed,
"apps.create",
ActionKind::Create,
@@ -806,7 +808,6 @@ async fn delete_app(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
@@ -859,7 +860,6 @@ async fn delete_app(
version: 0, // dummy version as it will not get inserted in db
},
Some(format!("App '{}' deleted", path)),
rsmq,
true,
)
.await?;
@@ -890,7 +890,6 @@ async fn update_app(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
) -> Result<String> {
@@ -898,7 +897,7 @@ async fn update_app(
let path = path.to_path();
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let mut tx = user_db.clone().begin(&authed).await?;
let npath = if ns.policy.is_some() || ns.path.is_some() || ns.summary.is_some() {
let mut sqlb = SqlBuilder::update_table("app");
@@ -915,7 +914,7 @@ async fn update_app(
npath,
w_id
)
.fetch_one(&mut tx)
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
@@ -947,7 +946,7 @@ async fn update_app(
sqlb.returning("path");
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?;
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut *tx).await?;
not_found_if_none(npath_o, "App", path)?
} else {
path.to_owned()
@@ -958,7 +957,7 @@ async fn update_app(
npath,
w_id
)
.fetch_one(&mut tx)
.fetch_one(&mut *tx)
.await?;
let v_id = sqlx::query_scalar!(
@@ -970,7 +969,7 @@ async fn update_app(
serde_json::to_string(&nvalue).unwrap(),
authed.username,
)
.fetch_one(&mut tx)
.fetch_one(&mut *tx)
.await?;
sqlx::query!(
@@ -979,7 +978,7 @@ async fn update_app(
npath,
w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
v_id
} else {
@@ -988,7 +987,7 @@ async fn update_app(
npath,
w_id
)
.fetch_one(&mut tx)
.fetch_one(&mut *tx)
.await?;
if let Some(v_id) = v_id {
v_id
@@ -1005,11 +1004,11 @@ async fn update_app(
path,
&w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
audit_log(
&mut tx,
&mut *tx,
&authed,
"apps.update",
ActionKind::Update,
@@ -1019,8 +1018,7 @@ async fn update_app(
)
.await?;
let tx: PushIsolationLevel<'_, rsmq_async::MultiplexedRsmq> =
PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Transaction(tx);
let mut args: HashMap<String, Box<serde_json::value::RawValue>> = HashMap::new();
if let Some(dm) = ns.deployment_message {
args.insert("deployment_message".to_string(), to_raw_value(&dm));
@@ -1135,7 +1133,6 @@ async fn execute_component(
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<ExecuteApp>,
) -> Result<String> {
@@ -1249,7 +1246,7 @@ async fn execute_component(
}
_ => unreachable!(),
};
let tx = windmill_queue::PushIsolationLevel::IsolatedRoot(db.clone(), rsmq);
let tx = windmill_queue::PushIsolationLevel::IsolatedRoot(db.clone());
let (uuid, tx) = push(
&db,
@@ -1288,7 +1285,6 @@ async fn upload_s3_file_from_app() -> Result<()> {
));
}
#[cfg(feature = "parquet")]
#[derive(Debug, Deserialize, Clone)]
struct UploadFileToS3Query {
@@ -1353,9 +1349,14 @@ async fn upload_s3_file_from_app(
let (username, permissioned_as, email) =
get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?;
let on_behalf_authed =
fetch_api_authed_from_permissioned_as(permissioned_as, email, &w_id, &db, username)
.await?;
let on_behalf_authed = fetch_api_authed_from_permissioned_as(
permissioned_as,
email,
&w_id,
&db,
Some(username),
)
.await?;
if let Some(file_key) = query.file_key {
// file key is provided => requires workspace, user or list policy and must match the regex

View File

@@ -128,7 +128,7 @@ async fn delete_draft(
// path,
// w_id
// )
// .fetch_optional(&mut tx)
// .fetch_optional(&mut *tx)
// .await?;
// tx.commit().await?;

View File

@@ -46,7 +46,7 @@ use windmill_common::{
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, StripPath},
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel, QueueTransaction};
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel};
pub fn workspaced_service() -> Router {
Router::new()
@@ -327,7 +327,6 @@ async fn create_flow(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
@@ -348,10 +347,10 @@ async fn create_flow(
// cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
let authed = maybe_refresh_folders(&nf.path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let mut tx = user_db.clone().begin(&authed).await?;
check_path_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
check_schedule_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
check_path_conflict(&mut tx, &w_id, &nf.path).await?;
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
let schema_str = nf.schema.and_then(|x| serde_json::to_string(&x.0).ok());
@@ -371,7 +370,7 @@ async fn create_flow(
schema_str,
&authed.username,
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
let version = sqlx::query_scalar!(
@@ -384,7 +383,7 @@ async fn create_flow(
schema_str,
&authed.username,
)
.fetch_one(&mut tx)
.fetch_one(&mut *tx)
.await?;
sqlx::query!(
@@ -392,18 +391,18 @@ async fn create_flow(
version,
nf.path,
w_id
).execute(&mut tx).await?;
).execute(&mut *tx).await?;
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'",
nf.path,
&w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
audit_log(
&mut tx,
&mut *tx,
&authed,
"flows.create",
ActionKind::Create,
@@ -460,7 +459,7 @@ async fn create_flow(
nf.path,
w_id
)
.execute(&mut new_tx)
.execute(&mut *new_tx)
.await?;
new_tx.commit().await?;
@@ -638,7 +637,6 @@ async fn update_flow_history(
async fn update_flow(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
@@ -660,9 +658,9 @@ async fn update_flow(
let flow_path = flow_path.to_path();
let authed = maybe_refresh_folders(&flow_path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let mut tx = user_db.clone().begin(&authed).await?;
check_schedule_conflict(tx.transaction_mut(), &w_id, flow_path).await?;
check_schedule_conflict(&mut tx, &w_id, flow_path).await?;
let schema = nf.schema.map(|x| x.0);
let old_dep_job = sqlx::query_scalar!(
@@ -670,7 +668,7 @@ async fn update_flow(
flow_path,
w_id
)
.fetch_optional(&mut tx)
.fetch_optional(&mut *tx)
.await?;
let old_dep_job = not_found_if_none(old_dep_job, "Flow", flow_path)?;
@@ -695,7 +693,7 @@ async fn update_flow(
flow_path,
w_id,
)
.execute(&mut tx)
.execute(&mut *tx)
.await.map_err(|e| error::Error::InternalErr(format!("Error updating flow due to flow update: {e:#}")))?;
if is_new_path {
@@ -710,7 +708,7 @@ async fn update_flow(
flow_path,
w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await
.map_err(|e| {
error::Error::InternalErr(format!("Error updating flow due to create new flow: {e:#}"))
@@ -722,7 +720,7 @@ async fn update_flow(
flow_path,
w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await
.map_err(|e| {
error::Error::InternalErr(format!(
@@ -735,7 +733,7 @@ async fn update_flow(
flow_path,
w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await
.map_err(|e| {
error::Error::InternalErr(format!(
@@ -752,7 +750,7 @@ async fn update_flow(
schema_str,
&authed.username,
)
.fetch_one(&mut tx)
.fetch_one(&mut *tx)
.await
.map_err(|e| {
error::Error::InternalErr(format!(
@@ -763,10 +761,10 @@ async fn update_flow(
sqlx::query!(
"UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3",
version, nf.path, w_id
).execute(&mut tx).await?;
).execute(&mut *tx).await?;
if is_new_path {
check_schedule_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
if !authed.is_admin {
require_owner_of_path(&authed, flow_path)?;
@@ -778,7 +776,7 @@ async fn update_flow(
.bind(&nf.path)
.bind(&flow_path)
.bind(&w_id)
.fetch_all(&mut tx)
.fetch_all(&mut *tx)
.await.map_err(|e| error::Error::InternalErr(format!("Error updating flow due to related schedules update: {e:#}")))?;
let schedule = sqlx::query_as::<_, Schedule>(
@@ -786,16 +784,16 @@ async fn update_flow(
.bind(&nf.path)
.bind(&flow_path)
.bind(&w_id)
.fetch_optional(&mut tx)
.fetch_optional(&mut *tx)
.await.map_err(|e| error::Error::InternalErr(format!("Error updating flow due to related schedule update: {e:#}")))?;
if let Some(schedule) = schedule {
clear_schedule(tx.transaction_mut(), &flow_path, &w_id).await?;
clear_schedule(&mut tx, &flow_path, &w_id).await?;
schedulables.push(schedule);
}
for schedule in schedulables.into_iter() {
clear_schedule(tx.transaction_mut(), &schedule.path, &w_id).await?;
clear_schedule(&mut tx, &schedule.path, &w_id).await?;
if schedule.enabled {
tx = push_scheduled_job(&db, tx, &schedule, None).await?;
@@ -807,11 +805,11 @@ async fn update_flow(
flow_path,
&w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
audit_log(
&mut tx,
&mut *tx,
&authed,
"flows.update",
ActionKind::Create,
@@ -878,7 +876,7 @@ async fn update_flow(
nf.path,
w_id
)
.execute(&mut new_tx)
.execute(&mut *new_tx)
.await
.map_err(|e| {
error::Error::InternalErr(format!(
@@ -890,7 +888,7 @@ async fn update_flow(
"UPDATE queue SET canceled = true WHERE id = $1",
old_dep_job
)
.execute(&mut new_tx)
.execute(&mut *new_tx)
.await
.map_err(|e| {
error::Error::InternalErr(format!(
@@ -1041,7 +1039,6 @@ async fn archive_flow_by_path(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(archived): Json<Archived>,
) -> Result<String> {
@@ -1088,7 +1085,6 @@ async fn archive_flow_by_path(
"unarchived"
}
)),
rsmq,
true,
)
.await?;
@@ -1105,7 +1101,6 @@ async fn delete_flow_by_path(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
@@ -1151,7 +1146,6 @@ async fn delete_flow_by_path(
version: 0, // dummy version as it will not get inserted in db
},
Some(format!("Flow '{}' deleted", path)),
rsmq,
true,
)
.await?;

View File

@@ -161,7 +161,6 @@ async fn create_folder(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(cache): Extension<Arc<AuthCache>>,
Path(w_id): Path<String>,
Json(ng): Json<NewFolder>,
@@ -223,7 +222,6 @@ async fn create_folder(
&w_id,
DeployedObject::Folder { path: format!("f/{}", ng.name) },
Some(format!("Folder '{}' created", ng.name)),
rsmq,
true,
)
.await?;
@@ -278,7 +276,6 @@ async fn update_folder(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
Json(mut ng): Json<UpdateFolder>,
) -> Result<String> {
@@ -376,7 +373,6 @@ async fn update_folder(
&w_id,
DeployedObject::Folder { path: format!("f/{}", name) },
Some(format!("Folder '{}' updated", name)),
rsmq,
true,
)
.await?;
@@ -523,7 +519,6 @@ async fn delete_folder(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
@@ -566,7 +561,6 @@ async fn delete_folder(
&w_id,
DeployedObject::Folder { path: format!("f/{}", name) },
Some(format!("Folder '{}' deleted", name)),
rsmq,
true,
)
.await?;

View File

@@ -23,7 +23,7 @@ use windmill_common::{
utils::{not_found_if_none, StripPath},
};
const KINDS: [&str; 10] = [
const KINDS: [&str; 12] = [
"script",
"group_",
"resource",
@@ -34,6 +34,8 @@ const KINDS: [&str; 10] = [
"app",
"raw_app",
"http_trigger",
"websocket_trigger",
"kafka_trigger",
];
pub fn workspaced_service() -> Router {
@@ -53,7 +55,6 @@ async fn add_granular_acl(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(GranularAcl { owner, write }): Json<GranularAcl>,
) -> Result<String> {
@@ -86,6 +87,25 @@ async fn add_granular_acl(
}
}
if kind == "folder" {
if let Some(obj) = sqlx::query_scalar!(
"SELECT owners FROM folder WHERE name = $1 AND workspace_id = $2",
path,
w_id
)
.fetch_optional(&mut *tx)
.await?
{
if obj.contains(&owner) {
if write != Some(true) {
return Err(Error::BadRequest(
"Cannot remove write permission for folder owner".to_string(),
));
}
}
}
}
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, $1, to_jsonb($2), \
true) WHERE {identifier} = $3 AND workspace_id = $4 RETURNING extra_perms"
@@ -109,8 +129,7 @@ async fn add_granular_acl(
&w_id,
DeployedObject::Folder { path: format!("f/{}", path) },
Some(format!("Folder '{}' changed permissions", path)),
rsmq,
true,
true,
)
.await?
}
@@ -122,8 +141,7 @@ async fn add_granular_acl(
// &w_id,
// DeployedObject::App { path: path.to_string(), parent_path: None, version: 0 },
// Some(format!("App '{}' changed permissions", path)),
// rsmq,
// true,
// // true,
// )
// .await?
// }
@@ -139,8 +157,7 @@ async fn add_granular_acl(
// hash: ScriptHash(0),
// },
// Some(format!("Script '{}' changed permissions", path)),
// rsmq,
// true,
// // true,
// )
// .await?
// }
@@ -152,8 +169,7 @@ async fn add_granular_acl(
// &w_id,
// DeployedObject::Flow { path: path.to_string(), parent_path: None },
// Some(format!("Flow '{}' changed permissions", path)),
// rsmq,
// true,
// // true,
// )
// .await?
// }
@@ -167,7 +183,6 @@ async fn remove_granular_acl(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(GranularAcl { owner, write: _ }): Json<GranularAcl>,
) -> Result<String> {
@@ -226,8 +241,7 @@ async fn remove_granular_acl(
&w_id,
DeployedObject::Folder { path: format!("f/{}", path) },
Some(format!("Folder '{}' changed permissions", path)),
rsmq,
true,
true,
)
.await?
}
@@ -239,8 +253,7 @@ async fn remove_granular_acl(
// &w_id,
// DeployedObject::App { path: path.to_string(), parent_path: None, version: 0 },
// Some(format!("App '{}' changed permissions", path)),
// rsmq,
// true,
// // true,
// )
// .await?
// }
@@ -256,8 +269,7 @@ async fn remove_granular_acl(
// hash: ScriptHash(0),
// },
// Some(format!("Script '{}' changed permissions", path)),
// rsmq,
// true,
// // true,
// )
// .await?
// }
@@ -269,8 +281,7 @@ async fn remove_granular_acl(
// &w_id,
// DeployedObject::Flow { path: path.to_string(), parent_path: None },
// Some(format!("Flow '{}' changed permissions", path)),
// rsmq,
// true,
// // true,
// )
// .await?
// }

View File

@@ -223,7 +223,6 @@ async fn create_group(
authed: ApiAuthed,
Extension(_db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(ng): Json<NewGroup>,
) -> Result<String> {
@@ -273,7 +272,6 @@ async fn create_group(
&w_id,
windmill_git_sync::DeployedObject::Group { name: ng.name.clone() },
Some(format!("Created group '{}'", &ng.name)),
rsmq,
true,
)
.await?;
@@ -442,7 +440,6 @@ async fn delete_group(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
@@ -492,7 +489,6 @@ async fn delete_group(
&w_id,
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Deleted group '{}'", &name)),
rsmq,
true,
)
.await?;
@@ -504,7 +500,6 @@ async fn update_group(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
Json(eg): Json<EditGroup>,
) -> Result<String> {
@@ -542,7 +537,6 @@ async fn update_group(
&w_id,
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Updated group '{}'", &name)),
rsmq,
true,
)
.await?;
@@ -554,7 +548,6 @@ async fn add_user(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
Json(Username { username: user_username }): Json<Username>,
) -> Result<String> {
@@ -593,7 +586,6 @@ async fn add_user(
&w_id,
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Added user to group '{}'", &name)),
rsmq,
true,
)
.await?;
@@ -712,7 +704,6 @@ async fn remove_user(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
Json(Username { username: user_username }): Json<Username>,
) -> Result<String> {
@@ -754,7 +745,6 @@ async fn remove_user(
&w_id,
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Removed user from group '{}'", &name)),
rsmq,
true,
)
.await?;

View File

@@ -507,7 +507,7 @@ async fn get_http_route_trigger(
trigger.email.clone(),
&trigger.workspace_id,
&db,
username_override.unwrap_or("anonymous".to_string()),
Some(username_override.unwrap_or("anonymous".to_string())),
)
.await?;
@@ -517,7 +517,6 @@ async fn get_http_route_trigger(
async fn route_job(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(route_path): Path<StripPath>,
OptAuthed(opt_authed): OptAuthed,
Query(query): Query<HashMap<String, String>>,
@@ -654,8 +653,7 @@ async fn route_job(
authed,
db,
user_db,
rsmq,
trigger.workspace_id.clone(),
trigger.workspace_id.clone(),
StripPath(trigger.script_path.to_owned()),
run_query,
args,
@@ -669,8 +667,7 @@ async fn route_job(
run_query,
StripPath(trigger.script_path.to_owned()),
authed,
rsmq,
user_db,
user_db,
args,
trigger.workspace_id.clone(),
label_prefix,
@@ -684,8 +681,7 @@ async fn route_job(
authed,
db,
user_db,
rsmq,
trigger.workspace_id.clone(),
trigger.workspace_id.clone(),
StripPath(trigger.script_path.to_owned()),
run_query,
args,
@@ -699,8 +695,7 @@ async fn route_job(
run_query,
StripPath(trigger.script_path.to_owned()),
authed,
rsmq,
user_db,
user_db,
trigger.workspace_id.clone(),
args,
label_prefix,

View File

@@ -21,10 +21,7 @@ use tokio::time::Instant;
use tower::ServiceBuilder;
use windmill_common::error::JsonResult;
use windmill_common::flow_status::{JobResult, RestartedFrom};
use windmill_common::jobs::{
format_completed_job_result, format_result, CompletedJobWithFormattedResult, FormattedResult,
ENTRYPOINT_OVERRIDE,
};
use windmill_common::jobs::{format_completed_job_result, format_result, ENTRYPOINT_OVERRIDE};
use windmill_common::worker::{CLOUD_HOSTED, TMP_DIR};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
@@ -350,7 +347,6 @@ async fn get_db_clock(Extension(db): Extension<DB>) -> windmill_common::error::J
}
async fn cancel_job_api(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
@@ -376,7 +372,6 @@ async fn cancel_job_api(
&w_id,
tx,
&db,
rsmq,
false,
opt_authed.is_none(),
),
@@ -415,7 +410,6 @@ async fn cancel_job_api(
}
async fn cancel_persistent_script_api(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
@@ -436,7 +430,6 @@ async fn cancel_persistent_script_api(
script_path.to_path(),
&w_id,
&db,
rsmq,
)
.await?;
@@ -465,7 +458,6 @@ async fn cancel_persistent_script_api(
}
async fn force_cancel(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
@@ -491,7 +483,6 @@ async fn force_cancel(
&w_id,
tx,
&db,
rsmq,
true,
opt_authed.is_none(),
),
@@ -683,23 +674,18 @@ async fn get_job(
}
lazy_static::lazy_static! {
static ref GET_COMPLETED_JOB_QUERY_NO_LOGS: String = generate_get_job_query(true, "completed_job");
static ref GET_COMPLETED_JOB_QUERY: String = generate_get_job_query(false, "completed_job");
static ref GET_QUEUED_JOB_QUERY_NO_LOGS: String = generate_get_job_query(true, "queue");
static ref GET_QUEUED_JOB_QUERY: String = generate_get_job_query(false, "queue");
static ref GET_COMPLETED_JOB_QUERY_NO_LOGS: String = generate_get_job_query(true, "completed_job_view");
static ref GET_COMPLETED_JOB_QUERY: String = generate_get_job_query(false, "completed_job_view");
static ref GET_QUEUED_JOB_QUERY_NO_LOGS: String = generate_get_job_query(true, "queue_view");
static ref GET_QUEUED_JOB_QUERY: String = generate_get_job_query(false, "queue_view");
}
fn generate_get_job_query(no_logs: bool, table: &str) -> String {
let log_expr = if no_logs {
"null".to_string()
} else {
format!("right(concat(coalesce({table}.logs, ''), job_logs.logs), 20000)")
format!("right({table}.logs, 20000)")
};
let join = if no_logs {
"".to_string()
} else {
format!("LEFT JOIN job_logs ON {table}.id = job_logs.job_id")
};
let additional_fields = if table == "completed_job" {
let additional_fields = if table == "completed_job_view" {
"duration_ms,
success,
result,
@@ -733,7 +719,6 @@ fn generate_get_job_query(no_logs: bool, table: &str) -> String {
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language,
raw_lock, email, visible_to_owner, mem_peak, tag, priority, {additional_fields}
FROM {table}
{join}
WHERE id = $1 AND {table}.workspace_id = $2");
}
pub async fn get_queued_job_ex(
@@ -744,7 +729,11 @@ pub async fn get_queued_job_ex(
// first optional is if authed need to be checked, second is the opt_authed itself
opt_authed: Option<&Option<ApiAuthed>>,
) -> error::Result<Option<JobExtended<QueuedJob>>> {
let query = if no_logs { &*GET_QUEUED_JOB_QUERY_NO_LOGS } else { &*GET_QUEUED_JOB_QUERY };
let query = if no_logs {
&*GET_QUEUED_JOB_QUERY_NO_LOGS
} else {
&*GET_QUEUED_JOB_QUERY
};
let job = sqlx::query_as::<_, JobExtended<QueuedJob>>(query)
.bind(job_id)
.bind(workspace_id)
@@ -769,7 +758,11 @@ pub async fn get_completed_job_ex(
// first optional is if authed need to be checked, second is the opt_authed itself
opt_authed: Option<&Option<ApiAuthed>>,
) -> error::Result<Option<JobExtended<CompletedJob>>> {
let query = if no_logs { &*GET_COMPLETED_JOB_QUERY_NO_LOGS } else { &*GET_COMPLETED_JOB_QUERY };
let query = if no_logs {
&*GET_COMPLETED_JOB_QUERY_NO_LOGS
} else {
&*GET_COMPLETED_JOB_QUERY
};
let cjob = sqlx::query_as::<_, JobExtended<CompletedJob>>(query)
.bind(job_id)
.bind(workspace_id)
@@ -785,13 +778,7 @@ pub async fn get_completed_job_ex(
}
if let Some(mut cjob) = cjob {
let CompletedJobWithFormattedResult { mut cj, result } = format_completed_job_result(cjob.inner);
cj.result = match result {
Some(FormattedResult::RawValue(rv)) => rv,
Some(FormattedResult::Vec(v)) => Some(to_raw_value(&v)),
None => None,
}.map(sqlx::types::Json);
cjob.inner = cj;
cjob.inner = format_completed_job_result(cjob.inner);
return Ok(Some(cjob));
}
@@ -950,7 +937,7 @@ async fn get_job_logs(
Ok(content_plain(Body::from(logs)))
} else {
let text = sqlx::query!(
"SELECT created_by, CONCAT(coalesce(queue.logs, ''), coalesce(job_logs.logs, '')) as logs, job_logs.log_offset, job_logs.log_file_index
"SELECT created_by, CONCAT(coalesce(queue.logs, ''), coalesce(job_logs.logs, '')) as logs, coalesce(job_logs.log_offset, 0) as log_offset, job_logs.log_file_index
FROM queue
LEFT JOIN job_logs ON job_logs.job_id = queue.id
WHERE queue.id = $1 AND queue.workspace_id = $2",
@@ -971,10 +958,14 @@ async fn get_job_logs(
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(r) = get_logs_from_store(text.log_offset, &logs, &text.log_file_index).await {
if let Some(r) =
get_logs_from_store(text.log_offset.unwrap_or(0), &logs, &text.log_file_index).await
{
return r.map(content_plain);
}
if let Some(r) = get_logs_from_disk(text.log_offset, &logs, &text.log_file_index).await {
if let Some(r) =
get_logs_from_disk(text.log_offset.unwrap_or(0), &logs, &text.log_file_index).await
{
return r.map(content_plain);
}
@@ -1378,7 +1369,6 @@ async fn cancel_jobs(
db: &DB,
username: &str,
w_id: &str,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
) -> error::JsonResult<Vec<Uuid>> {
let mut uuids = vec![];
let mut tx = db.begin().await?;
@@ -1468,7 +1458,6 @@ async fn cancel_jobs(
if trivial_jobs.contains(&job_id) {
continue;
}
let rsmq = rsmq.clone();
match tokio::time::timeout(tokio::time::Duration::from_secs(5), async move {
let tx = db.begin().await?;
let (tx, _) = windmill_queue::cancel_job(
@@ -1478,7 +1467,6 @@ async fn cancel_jobs(
w_id,
tx,
db,
rsmq,
false,
false,
)
@@ -1514,7 +1502,6 @@ async fn cancel_selection(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(jobs): Json<Vec<Uuid>>,
@@ -1528,14 +1515,7 @@ async fn cancel_selection(
.await?;
tx.commit().await?;
cancel_jobs(
jobs_to_cancel,
&db,
authed.username.as_str(),
w_id.as_str(),
rsmq,
)
.await
cancel_jobs(jobs_to_cancel, &db, authed.username.as_str(), w_id.as_str()).await
}
async fn list_filtered_uuids(
@@ -1607,8 +1587,7 @@ async fn count_completed_jobs_detail(
Query(query): Query<CountCompletedJobsQuery>,
) -> error::JsonResult<i64> {
let mut sqlb = SqlBuilder::select_from("completed_job");
sqlb
.field("COUNT(*) as count");
sqlb.field("COUNT(*) as count");
if !query.all_workspaces.unwrap_or(false) {
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
@@ -1616,7 +1595,10 @@ async fn count_completed_jobs_detail(
if let Some(after_s_ago) = query.completed_after_s_ago {
let after = Utc::now() - chrono::Duration::seconds(after_s_ago);
sqlb.and_where_gt("started_at + duration_ms / 1000 * interval '1 second'", "?".bind(&after.to_rfc3339()));
sqlb.and_where_gt(
"started_at + duration_ms / 1000 * interval '1 second'",
"?".bind(&after.to_rfc3339()),
);
}
if let Some(success) = query.success {
@@ -1624,19 +1606,21 @@ async fn count_completed_jobs_detail(
}
if let Some(tags) = query.tags {
sqlb.and_where_in("tag", &tags.split(",").map(|t| format!("'{}'", t)).collect::<Vec<_>>());
sqlb.and_where_in(
"tag",
&tags
.split(",")
.map(|t| format!("'{}'", t))
.collect::<Vec<_>>(),
);
}
let sql = sqlb.sql()?;
let stats = sqlx::query_scalar::<_, i64>(&sql)
.fetch_one(&db)
.await?;
let stats = sqlx::query_scalar::<_, i64>(&sql).fetch_one(&db).await?;
Ok(Json(stats))
}
async fn count_completed_jobs(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
@@ -2306,6 +2290,13 @@ pub struct JobExtended<T> {
#[serde(flatten)]
inner: T,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_lock: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_flow: Option<sqlx::types::Json<Box<RawValue>>>,
#[sqlx(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub self_wait_time_ms: Option<i64>,
@@ -2315,8 +2306,19 @@ pub struct JobExtended<T> {
}
impl<T> JobExtended<T> {
pub fn new(self_wait_time_ms: Option<i64>, aggregate_wait_time_ms: Option<i64>, inner: T) -> Self {
Self { inner, self_wait_time_ms, aggregate_wait_time_ms }
pub fn new(
self_wait_time_ms: Option<i64>,
aggregate_wait_time_ms: Option<i64>,
inner: T,
) -> Self {
Self {
inner,
raw_code: None,
raw_lock: None,
raw_flow: None,
self_wait_time_ms,
aggregate_wait_time_ms,
}
}
}
@@ -2618,7 +2620,10 @@ impl UnifiedJob {
impl<'a> From<UnifiedJob> for Job {
fn from(uj: UnifiedJob) -> Self {
match uj.typ.as_ref() {
"CompletedJob" => Job::CompletedJob(JobExtended::new(uj.self_wait_time_ms, uj.aggregate_wait_time_ms, CompletedJob {
"CompletedJob" => Job::CompletedJob(JobExtended::new(
uj.self_wait_time_ms,
uj.aggregate_wait_time_ms,
CompletedJob {
workspace_id: uj.workspace_id,
id: uj.id,
parent_job: uj.parent_job,
@@ -2636,12 +2641,10 @@ impl<'a> From<UnifiedJob> for Job {
deleted: uj.deleted,
canceled: uj.canceled,
canceled_by: uj.canceled_by,
raw_code: None,
canceled_reason: None,
job_kind: uj.job_kind,
schedule_path: uj.schedule_path,
permissioned_as: uj.permissioned_as,
raw_flow: None,
is_flow_step: uj.is_flow_step,
language: uj.language,
is_skipped: uj.is_skipped,
@@ -2651,8 +2654,12 @@ impl<'a> From<UnifiedJob> for Job {
tag: uj.tag,
priority: uj.priority,
labels: uj.labels,
})),
"QueuedJob" => Job::QueuedJob(JobExtended::new(uj.self_wait_time_ms, uj.aggregate_wait_time_ms, QueuedJob {
},
)),
"QueuedJob" => Job::QueuedJob(JobExtended::new(
uj.self_wait_time_ms,
uj.aggregate_wait_time_ms,
QueuedJob {
workspace_id: uj.workspace_id,
id: uj.id,
parent_job: uj.parent_job,
@@ -2666,8 +2673,6 @@ impl<'a> From<UnifiedJob> for Job {
scheduled_for: uj.scheduled_for.unwrap(),
logs: None,
flow_status: None,
raw_code: None,
raw_lock: None,
canceled: uj.canceled,
canceled_by: uj.canceled_by,
canceled_reason: None,
@@ -2675,7 +2680,6 @@ impl<'a> From<UnifiedJob> for Job {
job_kind: uj.job_kind,
schedule_path: uj.schedule_path,
permissioned_as: uj.permissioned_as,
raw_flow: None,
is_flow_step: uj.is_flow_step,
language: uj.language,
same_worker: false,
@@ -2693,7 +2697,8 @@ impl<'a> From<UnifiedJob> for Job {
flow_step_id: None,
cache_ttl: None,
priority: uj.priority,
})),
},
)),
t => panic!("job type {} not valid", t),
}
}
@@ -2845,22 +2850,17 @@ pub async fn run_flow_by_path(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
args: PushArgsOwned,
) -> error::Result<(StatusCode, String)> {
run_flow_by_path_inner(
authed, db, user_db, rsmq, w_id, flow_path, run_query, args, None,
)
.await
run_flow_by_path_inner(authed, db, user_db, w_id, flow_path, run_query, args, None).await
}
pub async fn run_flow_by_path_inner(
authed: ApiAuthed,
db: DB,
user_db: UserDB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
w_id: String,
flow_path: StripPath,
run_query: RunJobQuery,
@@ -2894,7 +2894,7 @@ pub async fn run_flow_by_path_inner(
check_tag_available_for_workspace(&w_id, &tag).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
let (uuid, tx) = push(
&db,
tx,
@@ -2936,7 +2936,6 @@ pub async fn restart_flow(
_authed: ApiAuthed,
Extension(_db): Extension<DB>,
Extension(_user_db): Extension<UserDB>,
Extension(_rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((_w_id, _job_id, _step_id, _branch_or_iteration_n)): Path<(
String,
Uuid,
@@ -2955,7 +2954,6 @@ pub async fn restart_flow(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, job_id, step_id, branch_or_iteration_n)): Path<(
String,
Uuid,
@@ -2989,7 +2987,7 @@ pub async fn restart_flow(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
let (uuid, tx) = push(
&db,
@@ -3028,7 +3026,6 @@ pub async fn run_script_by_path(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
args: PushArgsOwned,
@@ -3037,7 +3034,6 @@ pub async fn run_script_by_path(
authed,
db,
user_db,
rsmq,
w_id,
script_path,
run_query,
@@ -3051,7 +3047,6 @@ pub async fn run_script_by_path_inner(
authed: ApiAuthed,
db: DB,
user_db: UserDB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
w_id: String,
script_path: StripPath,
run_query: RunJobQuery,
@@ -3072,7 +3067,7 @@ pub async fn run_script_by_path_inner(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
let (uuid, tx) = push(
&db,
@@ -3114,7 +3109,6 @@ pub async fn run_workflow_as_code(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, job_id, entrypoint)): Path<(String, Uuid, String)>,
Query(run_query): Query<RunJobQuery>,
Query(wkflow_query): Query<WorkflowAsCodeQuery>,
@@ -3144,15 +3138,15 @@ pub async fn run_workflow_as_code(
}
let job = not_found_if_none(job, "Queued Job", &job_id.to_string())?;
let JobExtended { inner: job, .. } = job;
let JobExtended { inner: job, raw_code, raw_lock, .. } = job;
let (job_payload, tag, _delete_after_use, timeout) = match job.job_kind {
JobKind::Preview => (
JobPayload::Code(RawCode {
hash: None,
content: job.raw_code.unwrap_or_default(),
content: raw_code.unwrap_or_default(),
path: job.script_path,
language: job.language.unwrap_or_else(|| ScriptLang::Deno),
lock: job.raw_lock,
lock: raw_lock,
custom_concurrency_key: windmill_queue::custom_concurrency_key(&db, job.id)
.await
.map_err(to_anyhow)?,
@@ -3190,7 +3184,7 @@ pub async fn run_workflow_as_code(
i += 1;
}
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
@@ -3235,7 +3229,7 @@ pub async fn run_workflow_as_code(
job_id,
w_id,
entrypoint
).execute(&mut tx).await?;
).execute(&mut *tx).await?;
} else {
tracing::info!("Skipping update of flow status for job {job_id} in workspace {w_id}");
}
@@ -3281,7 +3275,6 @@ impl Drop for Guard {
&w_id,
tx,
&db,
None,
false,
false,
)
@@ -3317,7 +3310,7 @@ pub struct WindmillCompositeResult {
windmill_content_type: Option<String>,
result: Option<Box<RawValue>>,
}
async fn run_wait_result(
pub async fn run_wait_result(
db: &DB,
uuid: Uuid,
w_id: String,
@@ -3364,15 +3357,13 @@ async fn run_wait_result(
.bind(&w_id)
.fetch_optional(db)
.await?;
if let Some(raw_result) = row {
result = match format_result(
if let Some(mut raw_result) = row {
format_result(
raw_result.language.as_ref(),
raw_result.flow_status.map(|x| x.0),
raw_result.result.map(|x| x.0),
) {
FormattedResult::RawValue(rv) => rv,
FormattedResult::Vec(v) => Some(to_raw_value(&v)),
};
raw_result.flow_status.as_ref(),
raw_result.result.as_mut(),
);
result = raw_result.result.map(|x| x.0);
}
}
@@ -3581,7 +3572,6 @@ async fn log_job_view(
pub async fn run_wait_result_job_by_path_get(
method: hyper::http::Method,
authed: ApiAuthed,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
@@ -3620,7 +3610,7 @@ pub async fn run_wait_result_job_by_path_get(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
let (uuid, tx) = push(
&db,
@@ -3659,7 +3649,6 @@ pub async fn run_wait_result_job_by_path_get(
pub async fn run_wait_result_flow_by_path_get(
method: hyper::http::Method,
authed: ApiAuthed,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
@@ -3691,7 +3680,7 @@ pub async fn run_wait_result_flow_by_path_get(
let args = PushArgsOwned { extra: Some(payload_args), args: HashMap::new() };
run_wait_result_flow_by_path_internal(
db, run_query, flow_path, authed, rsmq, user_db, args, w_id, None,
db, run_query, flow_path, authed, user_db, args, w_id, None,
)
.await
}
@@ -3699,7 +3688,6 @@ pub async fn run_wait_result_flow_by_path_get(
pub async fn run_wait_result_script_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
@@ -3713,7 +3701,6 @@ pub async fn run_wait_result_script_by_path(
run_query,
script_path,
authed,
rsmq,
user_db,
w_id,
args,
@@ -3727,7 +3714,6 @@ pub async fn run_wait_result_script_by_path_internal(
run_query: RunJobQuery,
script_path: StripPath,
authed: ApiAuthed,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
user_db: UserDB,
w_id: String,
args: PushArgsOwned,
@@ -3743,7 +3729,7 @@ pub async fn run_wait_result_script_by_path_internal(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
let (uuid, tx) = push(
&db,
@@ -3784,7 +3770,6 @@ pub async fn run_wait_result_script_by_path_internal(
pub async fn run_wait_result_script_by_hash(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>,
@@ -3818,7 +3803,7 @@ pub async fn run_wait_result_script_by_hash(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
let (uuid, tx) = push(
&db,
@@ -3869,7 +3854,6 @@ pub async fn run_wait_result_script_by_hash(
pub async fn run_wait_result_flow_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
@@ -3879,7 +3863,7 @@ pub async fn run_wait_result_flow_by_path(
check_license_key_valid().await?;
run_wait_result_flow_by_path_internal(
db, run_query, flow_path, authed, rsmq, user_db, args, w_id, None,
db, run_query, flow_path, authed, user_db, args, w_id, None,
)
.await
}
@@ -3889,7 +3873,6 @@ pub async fn run_wait_result_flow_by_path_internal(
run_query: RunJobQuery,
flow_path: StripPath,
authed: ApiAuthed,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
user_db: UserDB,
args: PushArgsOwned,
w_id: String,
@@ -3923,7 +3906,7 @@ pub async fn run_wait_result_flow_by_path_internal(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
let (uuid, tx) = push(
&db,
@@ -3966,7 +3949,6 @@ async fn run_preview_script(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
Json(preview): Json<Preview>,
@@ -3983,7 +3965,7 @@ async fn run_preview_script(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(preview.tag.clone());
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into());
let (uuid, tx) = push(
&db,
@@ -4035,7 +4017,6 @@ async fn run_bundle_preview_script(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
mut multipart: axum::extract::Multipart,
@@ -4066,8 +4047,7 @@ async fn run_bundle_preview_script(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(preview.tag.clone());
check_tag_available_for_workspace(&w_id, &tag).await?;
let ltx =
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq.clone());
let ltx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into());
let args = preview.args.unwrap_or_default();
@@ -4201,7 +4181,6 @@ pub struct RunDependenciesResponse {
async fn run_dependencies_job(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(req): Json<RunDependenciesRequest>,
) -> error::Result<Response> {
@@ -4242,7 +4221,7 @@ async fn run_dependencies_job(
let (uuid, tx) = push(
&db,
PushIsolationLevel::IsolatedRoot(db.clone(), rsmq),
PushIsolationLevel::IsolatedRoot(db.clone()),
&w_id,
JobPayload::RawScriptDependencies {
script_path: script_path,
@@ -4289,7 +4268,6 @@ pub struct RunFlowDependenciesResponse {
async fn run_flow_dependencies_job(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(req): Json<RunFlowDependenciesRequest>,
) -> error::Result<Response> {
@@ -4301,7 +4279,7 @@ async fn run_flow_dependencies_job(
let (uuid, tx) = push(
&db,
PushIsolationLevel::IsolatedRoot(db.clone(), rsmq),
PushIsolationLevel::IsolatedRoot(db.clone()),
&w_id,
JobPayload::RawFlowDependencies { path: req.path, flow_value: req.flow_value },
PushArgs::from(&HashMap::from([(
@@ -4352,7 +4330,6 @@ struct BatchInfo {
async fn add_batch_jobs(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(_rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, n)): Path<(String, i32)>,
Json(batch_info): Json<BatchInfo>,
) -> error::JsonResult<Vec<Uuid>> {
@@ -4371,7 +4348,7 @@ async fn add_batch_jobs(
raw_code,
raw_lock,
raw_flow,
flow_status
flow_status,
) = match batch_info.kind.as_str() {
"script" => {
if let Some(path) = batch_info.path {
@@ -4462,19 +4439,19 @@ async fn add_batch_jobs(
add_virtual_items_if_necessary(&mut value.modules);
let flow_status = FlowStatus::new(&value);
(
None, // script_hash
path, // script_path
job_kind, // job_kind
None, // language
None, // dedicated_worker
value.concurrency_key.clone(), // custom_concurrency_key
value.concurrent_limit.clone(), // concurrent_limit
value.concurrency_time_window_s, // concurrency_time_window_s
None, // timeout
None, // raw_code
None, // raw_lock
Some(value), // raw_flow
Some(flow_status), // flow_status
None, // script_hash
path, // script_path
job_kind, // job_kind
None, // language
None, // dedicated_worker
value.concurrency_key.clone(), // custom_concurrency_key
value.concurrent_limit.clone(), // concurrent_limit
value.concurrency_time_window_s, // concurrency_time_window_s
None, // timeout
None, // raw_code
None, // raw_lock
Some(value), // raw_flow
Some(flow_status), // flow_status
)
}
"noop" => (
@@ -4514,11 +4491,28 @@ async fn add_batch_jobs(
let uuids = sqlx::query_scalar!(
r#"WITH uuid_table as (
select gen_random_uuid() as uuid from generate_series(1, $11)
select gen_random_uuid() as uuid from generate_series(1, $5)
)
INSERT INTO job
(id, workspace_id, raw_code, raw_lock, raw_flow)
(SELECT uuid, $1, $2, $3, $4 FROM uuid_table)
RETURNING id"#,
w_id,
raw_code,
raw_lock,
raw_flow.map(sqlx::types::Json) as Option<sqlx::types::Json<FlowValue>>,
n
)
.fetch_all(&db)
.await?;
let uuids = sqlx::query_scalar!(
r#"WITH uuid_table as (
select unnest($11::uuid[]) as uuid
)
INSERT INTO queue
(id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout, raw_code, raw_lock, raw_flow, flow_status)
(SELECT uuid, $1, $2, $3, $4, ('{ "uuid": "' || uuid || '" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14, $15, $16, $17, $18 FROM uuid_table)
(id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout, flow_status)
(SELECT uuid, $1, $2, $3, $4, ('{ "uuid": "' || uuid || '" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14, $15 FROM uuid_table)
RETURNING id"#,
hash.map(|h| h.0),
path,
@@ -4530,13 +4524,10 @@ async fn add_batch_jobs(
authed.email,
Utc::now(),
w_id,
n,
&uuids,
concurrent_limit,
concurrent_time_window_s,
timeout,
raw_code,
raw_lock,
raw_flow.map(sqlx::types::Json) as Option<sqlx::types::Json<FlowValue>>,
flow_status.map(sqlx::types::Json) as Option<sqlx::types::Json<FlowStatus>>
)
.fetch_all(&db)
@@ -4559,7 +4550,6 @@ async fn run_preview_flow_job(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
Json(raw_flow): Json<PreviewFlow>,
@@ -4573,7 +4563,7 @@ async fn run_preview_flow_job(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(raw_flow.tag.clone());
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into());
let (uuid, tx) = push(
&db,
@@ -4614,7 +4604,6 @@ pub async fn run_job_by_hash(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>,
args: PushArgsOwned,
@@ -4623,7 +4612,6 @@ pub async fn run_job_by_hash(
authed,
db,
user_db,
rsmq,
w_id,
script_hash,
run_query,
@@ -4637,7 +4625,6 @@ pub async fn run_job_by_hash_inner(
authed: ApiAuthed,
db: DB,
user_db: UserDB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
w_id: String,
script_hash: ScriptHash,
run_query: RunJobQuery,
@@ -4670,7 +4657,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).await?;
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into());
let (uuid, tx) = push(
&db,
@@ -5116,8 +5103,7 @@ async fn get_completed_job<'a>(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
let job_o = get_completed_job_ex(&db, &w_id, id, false, Some(&opt_authed))
.await?;
let job_o = get_completed_job_ex(&db, &w_id, id, false, Some(&opt_authed)).await?;
let cj = not_found_if_none(job_o, "Completed Job", id.to_string())?;
let response = Json(cj).into_response();
@@ -5180,7 +5166,7 @@ async fn get_completed_job_result(
.await?
};
let raw_result = not_found_if_none(result_o, "Completed Job", id.to_string())?;
let mut raw_result = not_found_if_none(result_o, "Completed Job", id.to_string())?;
if opt_authed.is_none() && raw_result.created_by.unwrap_or_default() != "anonymous" {
match (suspended_job, resume_id, approver, secret) {
@@ -5220,15 +5206,15 @@ async fn get_completed_job_result(
}
}
let result = format_result(
format_result(
raw_result.language.as_ref(),
raw_result.flow_status.map(|x| x.0),
raw_result.result.map(|x| x.0),
raw_result.flow_status.as_ref(),
raw_result.result.as_mut(),
);
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(result).into_response())
Ok(Json(raw_result.result).into_response())
}
#[derive(Deserialize)]
@@ -5274,7 +5260,7 @@ struct CompletedJobResult {
started: Option<bool>,
success: Option<bool>,
completed: bool,
result: Option<FormattedResult>,
result: Option<sqlx::types::Json<Box<RawValue>>>,
}
#[derive(Deserialize)]
@@ -5296,11 +5282,11 @@ async fn get_completed_job_result_maybe(
.fetch_optional(&db)
.await?;
if let Some(res) = result_o {
let result = format_result(
if let Some(mut res) = result_o {
format_result(
res.language.as_ref(),
res.flow_status.map(|x| x.0),
res.result.map(|x| x.0),
res.flow_status.as_ref(),
res.result.as_mut(),
);
if opt_authed.is_none() && res.created_by != "anonymous" {
return Err(Error::BadRequest(
@@ -5314,7 +5300,7 @@ async fn get_completed_job_result_maybe(
started: Some(true),
success: Some(res.success),
completed: true,
result: Some(result),
result: res.result,
})
.into_response())
} else if get_started.is_some_and(|x| x) {

View File

@@ -0,0 +1,13 @@
use crate::db::DB;
use axum::Router;
pub fn workspaced_service() -> Router {
Router::new()
}
pub async fn start_kafka_consumers(
_db: DB,
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
// implementation is not open source
}

View File

@@ -38,12 +38,13 @@ use tower_http::{
trace::TraceLayer,
};
use windmill_common::db::UserDB;
use windmill_common::worker::{ALL_TAGS, CLOUD_HOSTED};
use windmill_common::{BASE_URL, INSTANCE_NAME, utils::GIT_VERSION};
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{utils::GIT_VERSION, BASE_URL, INSTANCE_NAME};
use crate::scim_ee::has_scim_token;
use windmill_common::error::AppError;
mod ai;
mod apps;
mod audit;
mod capture;
@@ -62,12 +63,13 @@ mod http_triggers;
mod indexer_ee;
mod inputs;
mod integration;
mod ai;
#[cfg(feature = "parquet")]
mod job_helpers_ee;
pub mod job_metrics;
pub mod jobs;
#[cfg(all(feature = "enterprise", feature = "kafka"))]
mod kafka_triggers_ee;
pub mod oauth2_ee;
mod oidc_ee;
mod raw_apps;
@@ -95,7 +97,6 @@ mod workspaces_ee;
pub const DEFAULT_BODY_LIMIT: usize = 2097152 * 100; // 200MB
lazy_static::lazy_static! {
pub static ref REQUEST_SIZE_LIMIT: Arc<RwLock<usize>> = Arc::new(RwLock::new(DEFAULT_BODY_LIMIT));
@@ -161,7 +162,6 @@ type ServiceLogIndexReader = windmill_indexer::service_logs_ee::ServiceLogIndexR
pub async fn run_server(
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
job_index_reader: Option<IndexReader>,
log_index_reader: Option<ServiceLogIndexReader>,
addr: SocketAddr,
@@ -170,17 +170,6 @@ pub async fn run_server(
server_mode: bool,
base_internal_url: String,
) -> anyhow::Result<()> {
if let Some(mut rsmq) = rsmq.clone() {
for tag in ALL_TAGS.read().await.iter() {
let r =
rsmq_async::RsmqConnection::create_queue(&mut rsmq, &tag, None, None, None).await;
if let Err(e) = r {
tracing::info!("Redis queue {tag} could not be created: {e:#}");
} else {
tracing::info!("Redis queue {tag} created");
}
}
}
let user_db = UserDB::new(db.clone());
#[cfg(feature = "enterprise")]
@@ -200,7 +189,6 @@ pub async fn run_server(
let middleware_stack = ServiceBuilder::new()
.layer(Extension(db.clone()))
.layer(Extension(rsmq.clone()))
.layer(Extension(user_db.clone()))
.layer(Extension(auth_cache.clone()))
.layer(Extension(job_index_reader))
@@ -227,7 +215,6 @@ pub async fn run_server(
db: db.clone(),
user_db: user_db,
auth_cache: auth_cache.clone(),
rsmq: rsmq.clone(),
base_internal_url: base_internal_url.clone(),
});
if let Err(err) = smtp_server.start_listener_thread(addr).await {
@@ -235,6 +222,9 @@ pub async fn run_server(
}
}
// #[cfg(feature = "kafka")]
// start_listening().await;
let job_helpers_service = {
#[cfg(feature = "parquet")]
{
@@ -247,9 +237,27 @@ pub async fn run_server(
}
};
let kafka_triggers_service = {
#[cfg(all(feature = "enterprise", feature = "kafka"))]
{
kafka_triggers_ee::workspaced_service()
}
#[cfg(not(all(feature = "enterprise", feature = "kafka")))]
{
Router::new()
}
};
if !*CLOUD_HOSTED {
let ws_killpill_rx = rx.resubscribe();
websocket_triggers::start_websockets(db.clone(), rsmq, ws_killpill_rx).await;
websocket_triggers::start_websockets(db.clone(), ws_killpill_rx).await;
#[cfg(all(feature = "enterprise", feature = "kafka"))]
{
let kafka_killpill_rx = rx.resubscribe();
kafka_triggers_ee::start_kafka_consumers(db.clone(), kafka_killpill_rx).await;
}
}
// build our application with a route
@@ -296,7 +304,8 @@ pub async fn run_server(
.nest(
"/websocket_triggers",
websocket_triggers::workspaced_service(),
),
)
.nest("/kafka_triggers", kafka_triggers_service),
)
.nest("/workspaces", workspaces::global_service())
.nest(
@@ -321,10 +330,7 @@ pub async fn run_server(
"/srch/w/:workspace_id/index",
indexer_ee::workspaced_service(),
)
.nest(
"/srch/index",
indexer_ee::global_service(),
)
.nest("/srch/index", indexer_ee::global_service())
.nest("/oidc", oidc_ee::global_service())
.nest(
"/saml",

View File

@@ -646,7 +646,6 @@ async fn create_resource(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Query(q): Query<CreateResourceQuery>,
Json(resource): Json<CreateResource>,
@@ -696,7 +695,6 @@ async fn create_resource(
&w_id,
DeployedObject::Resource { path: resource.path.clone(), parent_path: None },
Some(format!("Resource '{}' created", resource.path.clone())),
rsmq.clone(),
true,
)
.await?;
@@ -717,7 +715,6 @@ async fn delete_resource(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -756,7 +753,6 @@ async fn delete_resource(
&w_id,
DeployedObject::Resource { path: path.to_string(), parent_path: Some(path.to_string()) },
Some(format!("Resource '{}' deleted", path)),
rsmq.clone(),
true,
)
.await?;
@@ -774,7 +770,6 @@ async fn update_resource(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditResource>,
) -> Result<String> {
@@ -844,7 +839,6 @@ async fn update_resource(
&w_id,
DeployedObject::Resource { path: npath.to_string(), parent_path: Some(path.to_string()) },
Some(format!("Resource '{}' updated", npath)),
rsmq.clone(),
true,
)
.await?;
@@ -871,7 +865,6 @@ async fn update_resource_value(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(nv): Json<UpdateResource>,
) -> Result<String> {
@@ -905,7 +898,6 @@ async fn update_resource_value(
&w_id,
DeployedObject::Resource { path: path.to_string(), parent_path: Some(path.to_string()) },
None,
rsmq.clone(),
true,
)
.await?;
@@ -1024,7 +1016,6 @@ async fn create_resource_type(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(resource_type): Json<CreateResourceType>,
) -> Result<(StatusCode, String)> {
@@ -1056,7 +1047,6 @@ async fn create_resource_type(
"Resource Type '{}' created",
resource_type.name.clone()
)),
rsmq.clone(),
true,
)
.await?;
@@ -1111,7 +1101,6 @@ async fn delete_resource_type(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
@@ -1144,7 +1133,6 @@ async fn delete_resource_type(
&w_id,
DeployedObject::ResourceType { path: name.clone() },
None,
rsmq.clone(),
true,
)
.await?;
@@ -1162,7 +1150,6 @@ async fn update_resource_type(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
Json(ns): Json<EditResourceType>,
) -> Result<String> {
@@ -1201,7 +1188,6 @@ async fn update_resource_type(
&w_id,
DeployedObject::ResourceType { path: name.clone() },
None,
rsmq.clone(),
true,
)
.await?;

View File

@@ -28,10 +28,10 @@ use windmill_common::{
db::UserDB,
error::{Error, JsonResult, Result},
schedule::Schedule,
utils::{not_found_if_none, paginate, Pagination, StripPath},
utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath},
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::{schedule::push_scheduled_job, QueueTransaction};
use windmill_queue::schedule::push_scheduled_job;
pub fn workspaced_service() -> Router {
Router::new()
@@ -75,6 +75,7 @@ pub struct NewSchedule {
pub retry: Option<serde_json::Value>,
pub tag: Option<String>,
pub paused_until: Option<DateTime<Utc>>,
pub cron_version: Option<String>,
}
#[derive(Serialize, Deserialize)]
@@ -123,7 +124,6 @@ async fn create_schedule(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(ns): Json<NewSchedule>,
) -> Result<String> {
@@ -151,27 +151,22 @@ async fn create_schedule(
));
}
let mut tx: QueueTransaction<'_, _> = (rsmq.clone(), user_db.begin(&authed).await?).into();
let mut tx: Transaction<'_, Postgres> = user_db.begin(&authed).await?;
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
check_path_conflict(tx.transaction_mut(), &w_id, &ns.path).await?;
check_flow_conflict(
tx.transaction_mut(),
&w_id,
&ns.path,
ns.is_flow,
&ns.script_path,
)
.await?;
// Check schedule for error
ScheduleType::from_str(&ns.schedule, ns.cron_version.as_deref())?;
check_path_conflict(&mut tx, &w_id, &ns.path).await?;
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
let schedule = sqlx::query_as::<_, Schedule>(
"INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \
is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, \
on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, \
on_success, on_success_extra_args, \
ws_error_handler_muted, retry, summary, no_flow_overlap, tag, paused_until \
ws_error_handler_muted, retry, summary, no_flow_overlap, tag, paused_until, cron_version \
) VALUES ( \
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25 \
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26 \
) RETURNING *")
.bind(&w_id)
.bind(&ns.path)
@@ -198,7 +193,8 @@ async fn create_schedule(
.bind(&ns.no_flow_overlap.unwrap_or(false))
.bind(&ns.tag)
.bind(&ns.paused_until)
.fetch_one(&mut tx)
.bind(&ns.cron_version.unwrap_or("v2".to_string()))
.fetch_one(&mut *tx)
.await
.map_err(|e| Error::InternalErr(format!("inserting schedule in {w_id}: {e:#}")))?;
@@ -209,13 +205,12 @@ async fn create_schedule(
&w_id,
DeployedObject::Schedule { path: ns.path.clone() },
Some(format!("Schedule '{}' created", ns.path.clone())),
rsmq.clone(),
true,
)
.await?;
audit_log(
&mut tx,
&mut *tx,
&authed,
"schedule.create",
ActionKind::Create,
@@ -245,25 +240,24 @@ async fn edit_schedule(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(es): Json<EditSchedule>,
) -> Result<String> {
let path = path.to_path();
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
(rsmq.clone(), user_db.begin(&authed).await?).into();
let mut tx = user_db.begin(&authed).await?;
cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
// Check schedule for error
ScheduleType::from_str(&es.schedule, es.cron_version.as_deref())?;
clear_schedule(tx.transaction_mut(), path, &w_id).await?;
clear_schedule(&mut tx, path, &w_id).await?;
let schedule = sqlx::query_as::<_, Schedule>(
"UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, \
on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, \
on_recovery_extra_args = $10, on_success = $11, on_success_extra_args = $12, \
ws_error_handler_muted = $13, retry = $14, summary = $15, \
no_flow_overlap = $16, tag = $17, paused_until = $18
no_flow_overlap = $16, tag = $17, paused_until = $18, cron_version = COALESCE($21, cron_version) \
WHERE path = $19 AND workspace_id = $20 RETURNING *")
.bind(&es.schedule)
.bind(&es.timezone)
@@ -285,7 +279,8 @@ async fn edit_schedule(
.bind(&es.paused_until)
.bind(&path)
.bind(&w_id)
.fetch_one(&mut tx)
.bind(&es.cron_version)
.fetch_one(&mut *tx)
.await
.map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e:#}")))?;
@@ -296,13 +291,12 @@ async fn edit_schedule(
&w_id,
DeployedObject::Schedule { path: path.to_string() },
None,
rsmq.clone(),
true,
)
.await?;
audit_log(
&mut tx,
&mut *tx,
&authed,
"schedule.edit",
ActionKind::Update,
@@ -401,6 +395,7 @@ pub struct ScheduleWJobs {
pub no_flow_overlap: bool,
pub tag: Option<String>,
pub paused_until: Option<DateTime<Utc>>,
pub cron_version: Option<String>,
}
async fn list_schedule_with_jobs(
@@ -463,23 +458,18 @@ async fn exists_schedule(
pub struct PreviewPayload {
pub schedule: String,
pub timezone: String,
pub cron_version: Option<String>,
}
pub async fn preview_schedule(
Json(payload): Json<PreviewPayload>,
) -> JsonResult<Vec<DateTime<Utc>>> {
let schedule = cron::Schedule::from_str(&payload.schedule)
.map_err(|e| Error::BadRequest(e.to_string()))?;
let schedule = ScheduleType::from_str(&payload.schedule, payload.cron_version.as_deref())?;
let tz =
chrono_tz::Tz::from_str(&payload.timezone).map_err(|e| Error::BadRequest(e.to_string()))?;
let upcoming: Vec<DateTime<Utc>> = schedule
.upcoming(tz)
.take(5)
// Convert back to UTC for a standardised API response. The client will convert to the local timezone.
.map(|x| x.with_timezone(&Utc))
.collect();
let upcoming: Vec<DateTime<Utc>> = schedule.upcoming(tz, 5)?;
Ok(Json(upcoming))
}
@@ -488,12 +478,10 @@ pub async fn set_enabled(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<SetEnabled>,
) -> Result<String> {
let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
(rsmq.clone(), user_db.begin(&authed).await?).into();
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
let schedule_o = sqlx::query_as::<_, Schedule>(
"UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *")
@@ -501,12 +489,12 @@ pub async fn set_enabled(
.bind(&authed.email)
.bind(&path)
.bind(&w_id)
.fetch_optional(&mut tx)
.fetch_optional(&mut *tx)
.await?;
let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
clear_schedule(tx.transaction_mut(), path, &w_id).await?;
clear_schedule(&mut tx, path, &w_id).await?;
handle_deployment_metadata(
&authed.email,
@@ -515,13 +503,12 @@ pub async fn set_enabled(
&w_id,
DeployedObject::Schedule { path: path.to_string() },
None,
rsmq.clone(),
true,
)
.await?;
audit_log(
&mut tx,
&mut *tx,
&authed,
"schedule.setenabled",
ActionKind::Update,
@@ -546,12 +533,11 @@ pub async fn set_enabled(
// authed: ApiAuthed,
// Extension(db): Extension<DB>,
// Extension(user_db): Extension<UserDB>,
// Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
// Path((w_id, path)): Path<(String, StripPath)>,
// // Path((w_id, path)): Path<(String, StripPath)>,
// Json(payload): Json<SetEnabled>,
// ) -> Result<String> {
// let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
// (rsmq, user_db.begin(&authed).await?).into();
// (user_db.begin(&authed).await?).into();
// let path = path.to_path();
// let schedule_o = sqlx::query_as!(
// Schedule,
@@ -561,15 +547,15 @@ pub async fn set_enabled(
// path,
// w_id
// )
// .fetch_optional(&mut tx)
// .fetch_optional(&mut *tx)
// .await?;
// let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
// clear_schedule(tx.transaction_mut(), path, &w_id).await?;
// clear_schedule(&mut tx, path, &w_id).await?;
// audit_log(
// &mut tx,
// &mut *tx,
// &authed,
// "schedule.setenabled",
// ActionKind::Update,
@@ -594,7 +580,6 @@ async fn delete_schedule(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
@@ -640,7 +625,6 @@ async fn delete_schedule(
&w_id,
DeployedObject::Schedule { path: path.to_string() },
Some(format!("Schedule '{}' deleted", path)),
rsmq.clone(),
true,
)
.await?;
@@ -664,7 +648,6 @@ async fn delete_schedule(
async fn set_default_error_handler(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(payload): Json<ErrorOrRecoveryHandler>,
) -> Result<()> {
@@ -791,7 +774,6 @@ async fn set_default_error_handler(
&w_id,
DeployedObject::Schedule { path: updated_schedule_path },
None,
rsmq.clone(),
true,
)
.await?;
@@ -846,6 +828,7 @@ pub struct EditSchedule {
pub no_flow_overlap: Option<bool>,
pub tag: Option<String>,
pub paused_until: Option<DateTime<Utc>>,
pub cron_version: Option<String>,
}
pub async fn clear_schedule<'c>(

View File

@@ -61,7 +61,7 @@ use windmill_common::{
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_parser_ts::remove_pinned_imports;
use windmill_queue::{schedule::push_scheduled_job, PushIsolationLevel, QueueTransaction};
use windmill_queue::{schedule::push_scheduled_job, PushIsolationLevel};
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
@@ -352,7 +352,6 @@ async fn create_snapshot_script() -> Result<(StatusCode, String)> {
async fn create_snapshot_script(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
@@ -374,7 +373,6 @@ async fn create_snapshot_script(
w_id.clone(),
authed.clone(),
db.clone(),
rsmq.clone(),
user_db.clone(),
webhook.clone(),
)
@@ -426,13 +424,12 @@ async fn create_snapshot_script(
async fn create_script(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
) -> Result<(StatusCode, String)> {
let (hash, tx) = create_script_internal(ns, w_id, authed, db, rsmq, user_db, webhook).await?;
let (hash, tx) = create_script_internal(ns, w_id, authed, db, user_db, webhook).await?;
tx.commit().await?;
Ok((StatusCode::CREATED, format!("{}", hash)))
}
@@ -442,13 +439,9 @@ async fn create_script_internal<'c>(
w_id: String,
authed: ApiAuthed,
db: sqlx::Pool<Postgres>,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
user_db: UserDB,
webhook: WebhookShared,
) -> Result<(
ScriptHash,
QueueTransaction<'c, rsmq_async::MultiplexedRsmq>,
)> {
) -> Result<(ScriptHash, Transaction<'c, Postgres>)> {
let codebase = ns.codebase.as_ref();
#[cfg(not(feature = "enterprise"))]
if ns.ws_error_handler_muted.is_some_and(|val| val) {
@@ -460,13 +453,13 @@ async fn create_script_internal<'c>(
let script_path = ns.path.clone();
let hash = ScriptHash(hash_script(&ns));
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq.clone(), user_db.begin(&authed).await?).into();
let mut tx: Transaction<'_, Postgres> = user_db.begin(&authed).await?;
if sqlx::query_scalar!(
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
hash.0,
&w_id
)
.fetch_optional(&mut tx)
.fetch_optional(&mut *tx)
.await?
.is_some()
{
@@ -481,7 +474,7 @@ async fn create_script_internal<'c>(
)
.bind(&ns.path)
.bind(&w_id)
.fetch_optional(&mut tx)
.fetch_optional(&mut *tx)
.await?;
struct ParentInfo {
p_hashes: Vec<i64>,
@@ -500,7 +493,7 @@ async fn create_script_internal<'c>(
s.hash.0,
&w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
Ok(None)
}
@@ -510,7 +503,7 @@ async fn create_script_internal<'c>(
p_hash.0,
&w_id
)
.fetch_optional(&mut tx)
.fetch_optional(&mut *tx)
.await?
.is_none()
{
@@ -524,7 +517,7 @@ async fn create_script_internal<'c>(
p_hash.0,
&w_id
)
.fetch_optional(&mut tx)
.fetch_optional(&mut *tx)
.await?;
if let Some(clashing_hash) = clashing_hash_o {
@@ -536,7 +529,7 @@ async fn create_script_internal<'c>(
};
let ScriptWithStarred { script: ps, .. } =
get_script_by_hash_internal(tx.transaction_mut(), &w_id, p_hash, None).await?;
get_script_by_hash_internal(&mut tx, &w_id, p_hash, None).await?;
if ps.path != ns.path {
require_owner_of_path(&authed, &ps.path)?;
@@ -571,7 +564,7 @@ async fn create_script_internal<'c>(
p_hash.0,
&w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
r
}
@@ -655,7 +648,7 @@ async fn create_script_internal<'c>(
codebase,
ns.has_preprocessor,
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
let p_path_opt = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone());
if let Some(ref p_path) = p_path_opt {
@@ -664,7 +657,7 @@ async fn create_script_internal<'c>(
p_path,
&w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
let mut schedulables = sqlx::query_as::<_, Schedule>(
@@ -672,7 +665,7 @@ async fn create_script_internal<'c>(
.bind(&ns.path)
.bind(&p_path)
.bind(&w_id)
.fetch_all(&mut tx)
.fetch_all(&mut *tx)
.await?;
let schedule = sqlx::query_as::<_, Schedule>(
@@ -680,7 +673,7 @@ async fn create_script_internal<'c>(
.bind(&ns.path)
.bind(&p_path)
.bind(&w_id)
.fetch_optional(&mut tx)
.fetch_optional(&mut *tx)
.await?;
if let Some(schedule) = schedule {
@@ -688,7 +681,7 @@ async fn create_script_internal<'c>(
}
for schedule in schedulables {
clear_schedule(tx.transaction_mut(), &schedule.path, &w_id).await?;
clear_schedule(&mut tx, &schedule.path, &w_id).await?;
if schedule.enabled {
tx = push_scheduled_job(&db, tx, &schedule, None).await?;
@@ -700,12 +693,12 @@ async fn create_script_internal<'c>(
ns.path,
&w_id
)
.execute(&mut tx)
.execute(&mut *tx)
.await?;
}
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {
audit_log(
&mut tx,
&mut *tx,
&authed,
"scripts.update",
ActionKind::Update,
@@ -724,7 +717,7 @@ async fn create_script_internal<'c>(
);
} else {
audit_log(
&mut tx,
&mut *tx,
&authed,
"scripts.create",
ActionKind::Create,
@@ -811,7 +804,6 @@ async fn create_script_internal<'c>(
parent_path: p_path_opt,
},
ns.deployment_message,
rsmq,
false,
)
.await?;
@@ -956,7 +948,6 @@ async fn get_latest_version(
) -> JsonResult<Option<ScriptHistory>> {
let mut tx = user_db.begin(&authed).await?;
let row_o = sqlx::query!(
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg
FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash
WHERE s.workspace_id = $1 AND s.path = $2
@@ -964,7 +955,6 @@ async fn get_latest_version(
w_id,
path.to_path(),
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
@@ -978,7 +968,6 @@ async fn get_latest_version(
} else {
return Ok(Json(None));
}
}
async fn update_script_history(
@@ -1295,7 +1284,6 @@ async fn archive_script_by_path(
Extension(webhook): Extension<WebhookShared>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<()> {
let path = path.to_path();
@@ -1334,7 +1322,6 @@ async fn archive_script_by_path(
parent_path: Some(path.to_string()),
},
Some(format!("Script '{}' archived", path)),
rsmq,
true,
)
.await?;
@@ -1428,7 +1415,6 @@ async fn delete_script_by_path(
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<String> {
let path = path.to_path();
@@ -1493,7 +1479,6 @@ async fn delete_script_by_path(
parent_path: Some(path.to_string()),
},
Some(format!("Script '{}' deleted", path)),
rsmq,
true,
)
.await?;

View File

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::utils::content_plain;
use crate::utils::{content_plain, require_devops_role};
use axum::{body::Body, extract::Query, response::Response, routing::get, Extension, Json, Router};
use serde::Serialize;
@@ -15,10 +15,7 @@ use windmill_common::{
utils::Pagination,
};
use crate::{
db::{ApiAuthed, DB},
utils::require_super_admin,
};
use crate::db::{ApiAuthed, DB};
pub fn global_service() -> Router {
Router::new()
@@ -51,7 +48,7 @@ async fn list_files(
Query(pagination): Query<Pagination>,
Query(lq): Query<LogFileQuery>,
) -> JsonResult<Vec<LogFile>> {
require_super_admin(&db, &email).await?;
require_devops_role(&db, &email).await?;
let (per_page, offset) = windmill_common::utils::paginate(pagination);
let mut sqlb = sql_builder::SqlBuilder::select_from("log_file")
@@ -98,7 +95,7 @@ async fn get_log_file(
) -> windmill_common::error::Result<Response> {
use windmill_common::tracing_init::TMP_WINDMILL_LOGS_SERVICE;
require_super_admin(&db, &email).await?;
require_devops_role(&db, &email).await?;
let path = path.to_path();
#[cfg(feature = "parquet")]
let s3_client = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS

View File

@@ -24,6 +24,9 @@ use axum::{
#[cfg(feature = "enterprise")]
use axum::extract::Query;
#[cfg(feature = "enterprise")]
use crate::utils::require_devops_role;
use serde::Deserialize;
#[cfg(feature = "enterprise")]
use windmill_common::ee::{send_critical_alert, CriticalAlertKind, CriticalErrorChannel};
@@ -60,8 +63,14 @@ pub fn global_service() -> Router {
.route("/customer_portal", post(create_customer_portal_session))
.route("/test_critical_channels", post(test_critical_channels))
.route("/critical_alerts", get(get_critical_alerts))
.route("/critical_alerts/:id/acknowledge", post(acknowledge_critical_alert))
.route("/critical_alerts/acknowledge_all", post(acknowledge_all_critical_alerts));
.route(
"/critical_alerts/:id/acknowledge",
post(acknowledge_critical_alert),
)
.route(
"/critical_alerts/acknowledge_all",
post(acknowledge_all_critical_alerts),
);
#[cfg(feature = "parquet")]
{
@@ -440,7 +449,7 @@ pub async fn get_critical_alerts(
authed: ApiAuthed,
Query(params): Query<crate::utils::AlertQueryParams>,
) -> JsonResult<Vec<crate::utils::CriticalAlert>> {
require_super_admin(&db, &authed.email).await?;
require_devops_role(&db, &authed.email).await?;
crate::utils::get_critical_alerts(db, params, None).await
}
@@ -456,8 +465,7 @@ pub async fn acknowledge_critical_alert(
authed: ApiAuthed,
Path(id): Path<i32>,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
require_devops_role(&db, &authed.email).await?;
crate::utils::acknowledge_critical_alert(db, None, id).await
}

View File

@@ -6,7 +6,6 @@ pub struct SmtpServer {
pub auth_cache: Arc<AuthCache>,
pub db: DB,
pub user_db: UserDB,
pub rsmq: Option<rsmq_async::MultiplexedRsmq>,
pub base_internal_url: String,
}

View File

@@ -18,6 +18,7 @@ pub struct TriggersCount {
webhook_count: i64,
email_count: i64,
websocket_count: i64,
kafka_count: i64,
}
pub(crate) async fn get_triggers_count_internal(
db: &DB,
@@ -64,6 +65,16 @@ pub(crate) async fn get_triggers_count_internal(
.await?
.unwrap_or(0);
let kafka_count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM kafka_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
path,
is_flow,
w_id
)
.fetch_one(db)
.await?
.unwrap_or(0);
let webhook_count = (if is_flow {
sqlx::query_scalar!(
"SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
@@ -105,6 +116,7 @@ pub(crate) async fn get_triggers_count_internal(
webhook_count,
email_count,
websocket_count,
kafka_count,
}))
}

View File

@@ -92,7 +92,6 @@ pub fn global_service() -> Router {
.route("/setpassword", post(set_password))
.route("/set_password_of/:user", post(set_password_of_user))
.route("/set_login_type/:user", post(set_login_type))
.route("/create", post(create_user))
.route("/update/:user", post(update_user))
.route("/delete/:user", delete(delete_user))
@@ -746,7 +745,7 @@ pub async fn fetch_api_authed(
email: String,
w_id: &str,
db: &DB,
username_override: String,
username_override: Option<String>,
) -> error::Result<ApiAuthed> {
let permissioned_as = username_to_permissioned_as(username.as_str());
fetch_api_authed_from_permissioned_as(permissioned_as, email, w_id, db, username_override).await
@@ -757,7 +756,7 @@ pub async fn fetch_api_authed_from_permissioned_as(
email: String,
w_id: &str,
db: &DB,
username_override: String,
username_override: Option<String>,
) -> error::Result<ApiAuthed> {
let authed =
fetch_authed_from_permissioned_as(permissioned_as, email.clone(), w_id, db).await?;
@@ -769,7 +768,7 @@ pub async fn fetch_api_authed_from_permissioned_as(
groups: authed.groups,
folders: authed.folders,
scopes: authed.scopes,
username_override: Some(username_override),
username_override: username_override,
})
}
@@ -796,6 +795,7 @@ pub struct GlobalUserInfo {
email: String,
login_type: Option<String>,
super_admin: bool,
devops: bool,
verified: bool,
name: Option<String>,
company: Option<String>,
@@ -853,6 +853,7 @@ pub struct DeclineInvite {
#[derive(Deserialize)]
pub struct EditUser {
pub is_super_admin: Option<bool>,
pub is_devops: Option<bool>,
pub name: Option<String>,
}
@@ -874,7 +875,6 @@ pub struct EditLoginType {
pub login_type: String,
}
#[derive(FromRow, Serialize)]
pub struct TruncatedToken {
pub label: Option<String>,
@@ -1042,10 +1042,10 @@ async fn list_users_as_super_admin(
GlobalUserInfo,
"WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),
authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)
SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, name, company, username
SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username
FROM password
WHERE email IN (SELECT email FROM active_users)
ORDER BY super_admin DESC
ORDER BY super_admin DESC, devops DESC
LIMIT $1 OFFSET $2",
per_page as i32,
offset as i32
@@ -1055,7 +1055,7 @@ async fn list_users_as_super_admin(
} else {
sqlx::query_as!(
GlobalUserInfo,
"SELECT email, login_type::text, verified, super_admin, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, email LIMIT \
"SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT \
$1 OFFSET $2",
per_page as i32,
offset as i32
@@ -1218,7 +1218,7 @@ async fn global_whoami(
) -> JsonResult<GlobalUserInfo> {
let user = sqlx::query_as!(
GlobalUserInfo,
"SELECT email, login_type::TEXT, super_admin, verified, name, company, username, NULL::bool as operator_only FROM password WHERE \
"SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only FROM password WHERE \
email = $1",
email
)
@@ -1233,6 +1233,7 @@ async fn global_whoami(
email: email.clone(),
login_type: Some("superadmin_secret".to_string()),
super_admin: true,
devops: false,
verified: true,
name: None,
company: None,
@@ -1476,7 +1477,7 @@ async fn whois(
// ) -> Result<(StatusCode, String)> {
// let mut tx = db.begin().await?;
// require_super_admin(&mut tx, email).await?;
// require_super_admin(&mut *tx, email).await?;
// sqlx::query!(
// "INSERT INTO invite_code
@@ -1485,7 +1486,7 @@ async fn whois(
// nu.code,
// nu.seats
// )
// .execute(&mut tx)
// .execute(&mut *tx)
// .await?;
// tx.commit().await?;
@@ -1547,7 +1548,6 @@ async fn accept_invite(
authed: ApiAuthed,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Json(nu): Json<AcceptInvite>,
) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?;
@@ -1610,7 +1610,6 @@ async fn accept_invite(
&nu.workspace_id,
windmill_git_sync::DeployedObject::User { email: authed.email.clone() },
Some(format!("User '{}' accepted invite", &authed.email)),
rsmq,
true,
)
.await?;
@@ -1780,7 +1779,6 @@ async fn get_workspace_user(
async fn update_workspace_user(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, username_to_update)): Path<(String, String)>,
Json(eu): Json<EditWorkspaceUser>,
) -> Result<String> {
@@ -1849,7 +1847,6 @@ async fn update_workspace_user(
&w_id,
windmill_git_sync::DeployedObject::User { email: user_email.clone() },
Some(format!("Updated user '{}'", &user_email)),
rsmq,
true,
)
.await?;
@@ -1876,6 +1873,16 @@ async fn update_user(
.await?;
}
if let Some(dv) = eu.is_devops {
sqlx::query_scalar!(
"UPDATE password SET devops = $1 WHERE email = $2",
dv,
&email_to_update
)
.execute(&mut *tx)
.await?;
}
if let Some(n) = eu.name {
sqlx::query_scalar!(
"UPDATE password SET name = $1 WHERE email = $2",
@@ -1959,16 +1966,14 @@ async fn create_user(
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Extension(argon2): Extension<Arc<Argon2<'_>>>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Json(nu): Json<NewUser>,
) -> Result<(StatusCode, String)> {
crate::users_ee::create_user(authed, db, webhook, argon2, rsmq, nu).await
crate::users_ee::create_user(authed, db, webhook, argon2, nu).await
}
async fn delete_workspace_user(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, username_to_delete)): Path<(String, String)>,
) -> Result<String> {
let mut tx = db.begin().await?;
@@ -2023,7 +2028,6 @@ async fn delete_workspace_user(
"Removed user '{}' from workspace",
&email_to_delete
)),
rsmq,
true,
)
.await?;
@@ -2081,8 +2085,10 @@ async fn set_login_type(
.await?;
tx.commit().await?;
Ok(format!("login type of {} updated to {}", email, et.login_type))
Ok(format!(
"login type of {} updated to {}",
email, et.login_type
))
}
async fn login(

View File

@@ -15,7 +15,6 @@ pub async fn create_user(
_db: DB,
_webhook: WebhookShared,
_argon2: Arc<Argon2<'_>>,
_rsmq: Option<rsmq_async::MultiplexedRsmq>,
mut _nu: NewUser,
) -> Result<(StatusCode, String)> {
Err(Error::InternalErr(

View File

@@ -11,7 +11,7 @@ use regex::Regex;
use serde::Deserialize;
use sqlx::{Postgres, Transaction};
use windmill_common::{
auth::is_super_admin_email,
auth::{is_devops_email, is_super_admin_email},
error::{self, Error},
DB,
};
@@ -32,13 +32,25 @@ pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
if !is_admin {
Err(Error::NotAuthorized(
"This endpoint require caller to be a super admin".to_owned(),
"This endpoint requires the caller to be a super admin".to_owned(),
))
} else {
Ok(())
}
}
pub async fn require_devops_role(db: &DB, email: &str) -> error::Result<()> {
let is_devops = is_devops_email(db, email).await?;
if is_devops {
Ok(())
} else {
Err(Error::NotAuthorized(
"This endpoint requires the caller to have the `devops` role".to_string(),
))
}
}
lazy_static::lazy_static! {
pub static ref INVALID_USERNAME_CHARS: Regex = Regex::new(r"[^A-Za-z0-9_]").unwrap();
}

View File

@@ -299,7 +299,6 @@ async fn create_variable(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
Json(variable): Json<CreateVariable>,
@@ -352,7 +351,6 @@ async fn create_variable(
&w_id,
DeployedObject::Variable { path: variable.path.clone(), parent_path: None },
Some(format!("Variable '{}' created", variable.path.clone())),
rsmq.clone(),
true,
)
.await?;
@@ -384,7 +382,6 @@ async fn delete_variable(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -424,7 +421,6 @@ async fn delete_variable(
&w_id,
DeployedObject::Variable { path: path.to_string(), parent_path: Some(path.to_string()) },
Some(format!("Variable '{}' deleted", path)),
rsmq.clone(),
true,
)
.await?;
@@ -455,7 +451,6 @@ async fn update_variable(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>,
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
Json(ns): Json<EditVariable>,
@@ -578,7 +573,6 @@ async fn update_variable(
&w_id,
DeployedObject::Variable { path: npath.clone(), parent_path: Some(path.to_string()) },
None,
rsmq.clone(),
true,
)
.await?;

View File

@@ -23,9 +23,7 @@ use windmill_audit::{audit_ee::audit_log, ActionKind};
use windmill_common::{
db::UserDB,
error::{self, to_anyhow, JsonResult},
utils::{
not_found_if_none, paginate, report_critical_error, require_admin, Pagination, StripPath,
},
utils::{not_found_if_none, paginate, report_critical_error, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
INSTANCE_NAME,
};
@@ -189,7 +187,6 @@ async fn create_websocket_trigger(
"Websocket triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
));
}
require_admin(authed.is_admin, &authed.username)?;
let mut tx = user_db.begin(&authed).await?;
@@ -332,7 +329,6 @@ async fn delete_websocket_trigger(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
@@ -377,7 +373,6 @@ async fn exists_websocket_trigger(
async fn listen_to_unlistened_websockets(
db: &DB,
rsmq: &Option<rsmq_async::MultiplexedRsmq>,
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
) -> () {
match sqlx::query_as::<_, WebsocketTrigger>(
@@ -391,7 +386,7 @@ async fn listen_to_unlistened_websockets(
Ok(mut triggers) => {
triggers.shuffle(&mut rand::thread_rng());
for trigger in triggers {
maybe_listen_to_websocket(trigger, db.clone(), rsmq.clone(), killpill_rx.resubscribe()).await;
maybe_listen_to_websocket(trigger, db.clone(), killpill_rx.resubscribe()).await;
}
}
Err(err) => {
@@ -400,13 +395,9 @@ async fn listen_to_unlistened_websockets(
};
}
pub async fn start_websockets(
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
pub async fn start_websockets(db: DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>) -> () {
tokio::spawn(async move {
listen_to_unlistened_websockets(&db, &rsmq, &killpill_rx).await;
listen_to_unlistened_websockets(&db, &&killpill_rx).await;
loop {
tokio::select! {
biased;
@@ -414,7 +405,7 @@ pub async fn start_websockets(
return;
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(15)) => {
listen_to_unlistened_websockets(&db, &rsmq, &killpill_rx).await;
listen_to_unlistened_websockets(&db, &&killpill_rx).await;
}
}
}
@@ -424,7 +415,6 @@ pub async fn start_websockets(
async fn maybe_listen_to_websocket(
ws_trigger: WebsocketTrigger,
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
match sqlx::query_scalar!(
@@ -435,7 +425,7 @@ async fn maybe_listen_to_websocket(
).fetch_optional(&db).await {
Ok(has_lock) => {
if has_lock.flatten().unwrap_or(false) {
tokio::spawn(listen_to_websocket(ws_trigger, db, rsmq, killpill_rx));
tokio::spawn(listen_to_websocket(ws_trigger, db, killpill_rx));
} else {
tracing::info!("Websocket {} already being listened to", ws_trigger.url);
}
@@ -519,7 +509,6 @@ async fn wait_runnable_result(
ws_trigger: &WebsocketTrigger,
username_override: String,
db: &DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
) -> error::Result<String> {
let user_db = UserDB::new(db.clone());
let authed = fetch_api_authed(
@@ -527,7 +516,7 @@ async fn wait_runnable_result(
ws_trigger.email.clone(),
&ws_trigger.workspace_id,
&db,
username_override,
Some(username_override),
)
.await?;
@@ -541,7 +530,6 @@ async fn wait_runnable_result(
authed,
db.clone(),
user_db,
rsmq.clone(),
ws_trigger.workspace_id.clone(),
StripPath(path.clone()),
RunJobQuery::default(),
@@ -554,7 +542,6 @@ async fn wait_runnable_result(
authed,
db.clone(),
user_db,
rsmq.clone(),
ws_trigger.workspace_id.clone(),
StripPath(path.clone()),
RunJobQuery::default(),
@@ -618,7 +605,6 @@ async fn send_initial_messages(
ws_trigger: &WebsocketTrigger,
mut writer: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
db: &DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
) -> error::Result<()> {
let initial_messages: Vec<InitialMessage> = ws_trigger
.initial_messages
@@ -658,7 +644,6 @@ async fn send_initial_messages(
ws_trigger,
"init".to_string(),
db,
rsmq.clone(),
)
.await?;
@@ -692,7 +677,6 @@ async fn get_url_from_runnable(
is_flow: bool,
ws_trigger: &WebsocketTrigger,
db: &DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
) -> error::Result<String> {
tracing::info!("Running runnable {path} (is_flow: {is_flow}) to get websocket URL",);
@@ -703,7 +687,6 @@ async fn get_url_from_runnable(
ws_trigger,
"url".to_string(),
db,
rsmq,
)
.await?;
@@ -770,10 +753,11 @@ async fn disable_with_error(db: &DB, ws_trigger: &WebsocketTrigger, error: Strin
async fn listen_to_websocket(
ws_trigger: WebsocketTrigger,
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
update_ping(&db, &ws_trigger, Some("Connecting...")).await;
if let None = update_ping(&db, &ws_trigger, Some("Connecting...")).await {
return;
}
let url = ws_trigger.url.as_str();
@@ -797,7 +781,7 @@ async fn listen_to_websocket(
)) => {
return;
},
url_result = get_url_from_runnable(path, url.starts_with("$flow:"), &ws_trigger, &db, rsmq.clone()) => match url_result {
url_result = get_url_from_runnable(path, url.starts_with("$flow:"), &ws_trigger, &db) => match url_result {
Ok(url) => url,
Err(err) => {
disable_with_error(
@@ -850,7 +834,7 @@ async fn listen_to_websocket(
return;
}
_ = async {
if let Err(err) = send_initial_messages(&ws_trigger, writer, &db, rsmq.clone()).await {
if let Err(err) = send_initial_messages(&ws_trigger, writer, &db).await {
disable_with_error(&db, &ws_trigger, format!("Error sending initial messages: {:?}", err)).await;
} else {
// if initial messages sent successfully, wait forever
@@ -897,7 +881,7 @@ async fn listen_to_websocket(
}
}
if should_handle {
if let Err(err) = run_job(&db, rsmq.clone(), &ws_trigger, text).await {
if let Err(err) = run_job(&db, &ws_trigger, text).await {
report_critical_error(format!("Failed to trigger job from websocket {}: {:?}", ws_trigger.url, err), db.clone(), Some(&ws_trigger.workspace_id), None).await;
};
}
@@ -948,12 +932,7 @@ async fn listen_to_websocket(
}
}
async fn run_job(
db: &DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
trigger: &WebsocketTrigger,
msg: String,
) -> anyhow::Result<()> {
async fn run_job(db: &DB, trigger: &WebsocketTrigger, msg: String) -> anyhow::Result<()> {
let args = PushArgsOwned {
args: HashMap::from([("msg".to_string(), to_raw_value(&msg))]),
extra: Some(HashMap::from([(
@@ -970,7 +949,7 @@ async fn run_job(
trigger.email.clone(),
&trigger.workspace_id,
db,
"anonymous".to_string(),
Some("anonymous".to_string()),
)
.await?;
@@ -983,7 +962,6 @@ async fn run_job(
authed,
db.clone(),
user_db,
rsmq,
trigger.workspace_id.clone(),
StripPath(trigger.script_path.to_owned()),
run_query,
@@ -996,7 +974,6 @@ async fn run_job(
authed,
db.clone(),
user_db,
rsmq,
trigger.workspace_id.clone(),
StripPath(trigger.script_path.to_owned()),
run_query,

View File

@@ -59,6 +59,9 @@ use windmill_common::{
};
use windmill_git_sync::handle_deployment_metadata;
#[cfg(feature = "enterprise")]
use windmill_common::utils::require_admin_or_devops;
use crate::oauth2_ee::InstanceEvent;
use crate::variables::{decrypt, encrypt};
use hyper::{header, StatusCode};
@@ -121,8 +124,14 @@ pub fn workspaced_service() -> Router {
.route("/usage", get(get_usage))
.route("/used_triggers", get(get_used_triggers))
.route("/critical_alerts", get(get_critical_alerts))
.route("/critical_alerts/:id/acknowledge", post(acknowledge_critical_alert))
.route("/critical_alerts/acknowledge_all", post(acknowledge_all_critical_alerts))
.route(
"/critical_alerts/:id/acknowledge",
post(acknowledge_critical_alert),
)
.route(
"/critical_alerts/acknowledge_all",
post(acknowledge_all_critical_alerts),
)
.route("/critical_alerts/mute", post(mute_critical_alerts));
#[cfg(feature = "stripe")]
@@ -494,7 +503,6 @@ async fn edit_slack_command(
async fn run_slack_message_test_job(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(req): Json<RunSlackMessageTestJobRequest>,
) -> JsonResult<RunSlackMessageTestJobResponse> {
@@ -511,7 +519,6 @@ async fn run_slack_message_test_job(
let uuid = windmill_queue::push_error_handler(
&db,
rsmq,
Uuid::parse_str("00000000-0000-0000-0000-000000000000")?,
None,
Some("slack_message_test".to_string()),
@@ -591,11 +598,10 @@ async fn is_allowed_auto_domain(ApiAuthed { email, .. }: ApiAuthed) -> JsonResul
async fn edit_auto_invite(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(ea): Json<EditAutoInvite>,
) -> Result<String> {
crate::workspaces_ee::edit_auto_invite(authed, db, rsmq, w_id, ea).await
crate::workspaces_ee::edit_auto_invite(authed, db, w_id, ea).await
}
async fn edit_webhook(
@@ -1296,6 +1302,7 @@ async fn set_encryption_key(
struct UsedTriggers {
pub websocket_used: bool,
pub http_routes_used: bool,
pub kafka_used: bool,
}
async fn get_used_triggers(
@@ -1306,7 +1313,10 @@ async fn get_used_triggers(
let mut tx = user_db.begin(&authed).await?;
let websocket_used = sqlx::query_as!(
UsedTriggers,
r#"SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as "websocket_used!", EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as "http_routes_used!""#,
r#"SELECT
EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as "websocket_used!",
EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as "http_routes_used!",
EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as "kafka_used!""#,
w_id,
)
.fetch_one(&mut *tx)
@@ -1456,7 +1466,7 @@ async fn create_workspace(
// nw.id,
// "finland does not actually exist",
// )
// .execute(&mut tx)
// .execute(&mut *tx)
// .await?;
let automate_username_creation = sqlx::query_scalar!(
@@ -1874,7 +1884,6 @@ async fn add_user(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(mut nu): Json<NewWorkspaceUser>,
) -> Result<(StatusCode, String)> {
@@ -1981,7 +1990,6 @@ async fn add_user(
&w_id,
windmill_git_sync::DeployedObject::User { email: nu.email.clone() },
Some(format!("Added user '{}' to workspace", &nu.email)),
rsmq,
true,
)
.await?;
@@ -3088,7 +3096,7 @@ pub async fn get_critical_alerts(
authed: ApiAuthed,
Query(params): Query<crate::utils::AlertQueryParams>,
) -> JsonResult<Vec<crate::utils::CriticalAlert>> {
require_admin(authed.is_admin, &authed.username)?;
require_admin_or_devops(authed.is_admin, &authed.username, &authed.email, &db).await?;
crate::utils::get_critical_alerts(db, params, Some(w_id)).await
}
@@ -3104,8 +3112,8 @@ pub async fn acknowledge_critical_alert(
Path((w_id, id)): Path<(String, i32)>,
authed: ApiAuthed,
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
crate::utils::acknowledge_critical_alert(db, Some(w_id), id).await
require_admin_or_devops(authed.is_admin, &authed.username, &authed.email, &db).await?;
crate::utils::acknowledge_critical_alert(db, Some(w_id), id).await
}
#[cfg(not(feature = "enterprise"))]
@@ -3128,7 +3136,6 @@ pub async fn acknowledge_all_critical_alerts() -> Error {
Error::NotFound("Critical Alerts require EE".to_string())
}
#[cfg(feature = "enterprise")]
#[derive(Deserialize)]
pub struct MuteCriticalAlertRequest {
@@ -3163,10 +3170,13 @@ async fn mute_critical_alerts(
.execute(&db)
.await?;
Ok(format!("Updated mute criticital alert ui settings for workspace: {}", &w_id))
Ok(format!(
"Updated mute criticital alert ui settings for workspace: {}",
&w_id
))
}
#[cfg(not(feature = "enterprise"))]
pub async fn mute_critical_alerts() -> Error {
Error::NotFound("Critical Alerts require EE".to_string())
}
}

View File

@@ -6,7 +6,6 @@ use crate::{
pub async fn edit_auto_invite(
_authed: ApiAuthed,
_db: DB,
_rsmq: Option<rsmq_async::MultiplexedRsmq>,
_w_id: String,
_ea: EditAutoInvite,
) -> windmill_common::error::Result<String> {

View File

@@ -20,5 +20,4 @@ serde_json.workspace = true
tracing.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-queue.workspace = true
rsmq_async.workspace = true
anyhow.workspace = true

View File

@@ -26,6 +26,7 @@ anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
hex.workspace = true
rand.workspace = true
sqlx = { workspace = true, features = ["postgres"] }
@@ -59,6 +60,7 @@ const_format.workspace = true
crc.workspace = true
windmill-macros.workspace = true
semver.workspace = true
croner = "2.0.6"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemalloc-ctl = { optional = true, workspace = true }

View File

@@ -73,6 +73,20 @@ pub async fn is_super_admin_email(db: &DB, email: &str) -> Result<bool> {
Ok(is_admin)
}
pub async fn is_devops_email(db: &DB, email: &str) -> Result<bool> {
if is_super_admin_email(db, email).await? {
return Ok(true)
}
let is_devops = sqlx::query_scalar!("SELECT devops FROM password WHERE email = $1", email)
.fetch_optional(db)
.await
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e:#}")))?
.unwrap_or(false);
Ok(is_devops)
}
pub fn permissioned_as_to_username(permissioned_as: &str) -> String {
if let Some((prefix, name)) = permissioned_as.split_once('/') {
if prefix == "u" {

View File

@@ -16,7 +16,6 @@ use rand::Rng;
use serde::{Deserialize, Serialize, Serializer};
use crate::{
error::Error,
more_serde::{default_empty_string, default_id, default_null, default_true, is_default},
scripts::{Schema, ScriptHash, ScriptLang},
};
@@ -652,29 +651,3 @@ pub fn add_virtual_items_if_necessary(modules: &mut Vec<FlowModule>) {
});
}
}
pub async fn has_failure_module<'c>(flow: sqlx::types::Uuid, db: &sqlx::Pool<sqlx::Postgres>, completed: bool) -> Result<bool, Error> {
if completed {
sqlx::query_scalar!(
"SELECT raw_flow->'failure_module' != 'null'::jsonb
FROM completed_job
WHERE id = $1",
flow
)
} else {
sqlx::query_scalar!(
"SELECT raw_flow->'failure_module' != 'null'::jsonb
FROM queue
WHERE id = $1",
flow
)
}
.fetch_one(db)
.await
.map_err(|e| {
Error::InternalErr(format!(
"error during retrieval of has_failure_module: {e:#}"
))
})
.map(|v| v.unwrap_or(false))
}

View File

@@ -15,6 +15,7 @@ pub const PIP_INDEX_URL_SETTING: &str = "pip_index_url";
pub const SCIM_TOKEN_SETTING: &str = "scim_token";
pub const SAML_METADATA_SETTING: &str = "saml_metadata";
pub const SMTP_SETTING: &str = "smtp_settings";
pub const INDEXER_SETTING: &str = "indexer_settings";
pub const TIMEOUT_WAIT_RESULT_SETTING: &str = "timeout_wait_result";
pub const UNIQUE_ID_SETTING: &str = "uid";

View File

@@ -0,0 +1,114 @@
use serde::Deserialize;
use crate::{error, DB};
pub const BYTES_PER_MB: u64 = 1_048_576;
pub const BYTES_PER_KB: u64 = 1024;
#[derive(Clone, Debug)]
pub struct TantivyIndexerSettings {
pub writer_memory_budget: u64,
pub commit_job_max_batch_size: u64,
pub commit_log_max_batch_size: u64,
pub refresh_index_period: u64,
pub refresh_log_index_period: u64,
pub max_indexed_job_log_size: usize,
}
impl Default for TantivyIndexerSettings {
fn default() -> Self {
TantivyIndexerSettings {
writer_memory_budget: 300_000_000,
commit_job_max_batch_size: 100_000,
commit_log_max_batch_size: 10_000,
refresh_index_period: 300,
refresh_log_index_period: 300,
max_indexed_job_log_size: 1_000_000,
}
}
}
#[derive(Deserialize, Default)]
pub struct TantivyIndexerSettingsOpt {
pub writer_memory_budget: Option<u64>,
pub commit_job_max_batch_size: Option<u64>,
pub commit_log_max_batch_size: Option<u64>,
pub refresh_index_period: Option<u64>,
pub refresh_log_index_period: Option<u64>,
pub max_indexed_job_log_size: Option<usize>,
}
pub async fn load_indexer_config(db: &DB) -> error::Result<TantivyIndexerSettings> {
let config: TantivyIndexerSettingsOpt =
sqlx::query_scalar!("SELECT value FROM global_settings WHERE name = 'indexer_settings'",)
.fetch_optional(db)
.await?
.map(|x| serde_json::from_value(x).ok())
.flatten()
.unwrap_or_default();
let TantivyIndexerSettings {
commit_job_max_batch_size,
commit_log_max_batch_size,
refresh_index_period,
refresh_log_index_period,
max_indexed_job_log_size,
writer_memory_budget,
} = get_indexer_rates_from_env();
Ok(TantivyIndexerSettings {
writer_memory_budget: config.writer_memory_budget.unwrap_or(writer_memory_budget),
commit_job_max_batch_size: config
.commit_job_max_batch_size
.unwrap_or(commit_job_max_batch_size),
commit_log_max_batch_size: config
.commit_log_max_batch_size
.unwrap_or(commit_log_max_batch_size),
refresh_index_period: config.refresh_index_period.unwrap_or(refresh_index_period),
refresh_log_index_period: config
.refresh_log_index_period
.unwrap_or(refresh_log_index_period),
max_indexed_job_log_size: config
.max_indexed_job_log_size
.unwrap_or(max_indexed_job_log_size),
})
}
pub fn get_env_var(env_var: &str) -> Option<u64> {
match std::env::var(env_var).map(|x| x.parse()) {
Ok(Ok(i)) => Some(i),
Err(_) => None,
Ok(Err(e)) => {
tracing::error!("Failed to parse env var {}: {}", env_var, e);
None
}
}
}
pub fn get_indexer_rates_from_env() -> TantivyIndexerSettings {
let mut settings = TantivyIndexerSettings::default();
if let Some(b) = get_env_var("TANTIVY_INDEX_WRITER_MEMORY_BUDGET__MB") {
settings.writer_memory_budget = b * BYTES_PER_MB as u64;
}
if let Some(b) = get_env_var("TANTIVY_DOC_COMMIT_MAX_BATCH_SIZE") {
settings.commit_job_max_batch_size = b;
}
if let Some(b) = get_env_var("TANTIVY_SERVICE_LOG_COMMIT_MAX_BATCH_SIZE") {
settings.commit_log_max_batch_size = b;
}
if let Some(b) = get_env_var("TANTIVY_REFRESH_INDEX_PERIOD__S") {
settings.refresh_index_period = b;
}
if let Some(b) = get_env_var("TANTIVY_REFRESH_LOG_INDEX_PERIOD__S") {
settings.refresh_log_index_period = b;
}
if let Some(b) = get_env_var("TANTIVY_MAX_INDEXED_JOB_LOG_SIZE__MB") {
settings.max_indexed_job_log_size = (b * BYTES_PER_MB) as usize;
}
if let Some(b) = get_env_var("TANTIVY_MAX_INDEXED_JOB_LOG_SIZE__KB") {
settings.max_indexed_job_log_size = (b * BYTES_PER_KB) as usize;
}
settings
}

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