Compare commits

..

14 Commits

Author SHA1 Message Date
fatonramadani
76266f3efb Apply automatic changes 2024-01-08 10:37:39 +00:00
Faton Ramadani
2cdda619de Merge branch 'main' into operator-menu 2024-01-08 11:35:59 +01:00
Faton Ramadani
37744ff583 Merge branch 'main' into operator-menu 2024-01-02 06:38:21 +01:00
Faton Ramadani
dbe14757d4 Merge branch 'main' of https://github.com/windmill-labs/windmill 2024-01-02 06:38:12 +01:00
Faton Ramadani
12bd4af3ee Merge branch 'main' into operator-menu 2023-12-29 16:54:12 +01:00
Faton Ramadani
9cdaf9c201 feat(frontend): wip operator menu 2023-12-29 16:52:28 +01:00
Faton Ramadani
ec094e8897 feat(frontend): wip operator menu 2023-12-29 16:48:09 +01:00
Faton Ramadani
cf61e09b90 feat(frontend): wip operator menu 2023-12-29 16:22:02 +01:00
Faton Ramadani
6a0ae83b49 feat(frontend): wip operator menu 2023-12-29 15:44:08 +01:00
Faton Ramadani
2f4be74e94 feat(frontend): wip operator menu 2023-12-29 15:42:03 +01:00
Guillaume Bouvignies
28811023a4 feat: PIP_LOCAL_DEPENDENCIES now accepts regex and is configurable from the UI (#2931)
* feat: PIP_LOCAL_DEPENDENCIES now accepts regex

* Add worker config FE
2023-12-29 22:02:46 +09:00
Faton Ramadani
e80534ca65 fix(frontend): fix wording (#2930)
* fix(frontend): fix wording

* fix(frontend): fix wording
2023-12-29 21:58:06 +09:00
Henri Courdent
95be0155dc Carousel link and trigger frontend (#2929)
Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-12-29 12:24:00 +01:00
Faton Ramadani
004781cd3d fix(frontend): Fix decision tree (#2928)
* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): decision tree history

* fix(frontend): fix wording
2023-12-29 03:33:23 +01:00
508 changed files with 89776 additions and 18904 deletions

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim-buster as nsjail
FROM python:3.10-slim-buster as nsjail
WORKDIR /nsjail
@@ -58,8 +58,8 @@ RUN /usr/local/bin/python3 -m pip install nltk
RUN mkdir -p /nsjail_data/python && HOME=/nsjail_data/python /usr/local/bin/python3 -m nltk.downloader vader_lexicon
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
COPY --from=oven/bun:1.0 /usr/local/bin/bun /usr/bin/bun
COPY --from=denoland/deno:1.40.2 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.0.0 /usr/local/bin/bun /usr/bin/bun
COPY --from=denoland/deno:latest /usr/bin/deno /usr/bin/deno
RUN apt-get update \
&& apt-get install -y postgresql-client --allow-unauthenticated

View File

@@ -101,7 +101,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe
features=enterprise,enterprise_saml
nsjail=true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
@@ -233,49 +233,49 @@ jobs:
${{ steps.extract.outputs.destination }}/*
${{ steps.extract-ee.outputs.destination }}/*
# attach_arm64_binary_to_release:
# needs: [build, build_ee]
# runs-on: ubicoud
# if: ${{ startsWith(github.ref, 'refs/tags/') }}
# env:
# ARCH: arm64
# steps:
# - uses: actions/checkout@v3
attach_arm64_binary_to_release:
needs: [build, build_ee]
runs-on: ubicoud
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
ARCH: arm64
steps:
- uses: actions/checkout@v3
# - run: |
# # pulling docker image with desired arch so that actions-docker-extract doesn't do it
# docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
# docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
- run: |
# pulling docker image with desired arch so that actions-docker-extract doesn't do it
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
# - run: |
# # Checks the image is in docker prior to running actions-docker-extract. It fails if not
# # Also useful to visually check that the arch is the right opencontainers
# docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
# docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
- run: |
# Checks the image is in docker prior to running actions-docker-extract. It fails if not
# Also useful to visually check that the arch is the right opencontainers
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
# - uses: shrink/actions-docker-extract@v3
# id: extract
# with:
# image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
# path: "/usr/src/app/windmill"
- uses: shrink/actions-docker-extract@v3
id: extract
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
path: "/usr/src/app/windmill"
# - uses: shrink/actions-docker-extract@v3
# id: extract-ee
# with:
# image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
# path: "/usr/src/app/windmill"
- uses: shrink/actions-docker-extract@v3
id: extract-ee
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
path: "/usr/src/app/windmill"
# - name: Rename binary with corresponding architecture
# run: |
# mv "${{ steps.extract.outputs.destination }}/windmill" "${{ steps.extract.outputs.destination }}/windmill-${ARCH}"
# mv "${{ steps.extract-ee.outputs.destination }}/windmill" "${{ steps.extract-ee.outputs.destination }}/windmill-ee-${ARCH}"
- name: Rename binary with corresponding architecture
run: |
mv "${{ steps.extract.outputs.destination }}/windmill" "${{ steps.extract.outputs.destination }}/windmill-${ARCH}"
mv "${{ steps.extract-ee.outputs.destination }}/windmill" "${{ steps.extract-ee.outputs.destination }}/windmill-ee-${ARCH}"
# - name: Attach binary to release
# uses: softprops/action-gh-release@v1
# with:
# files: |
# ${{ steps.extract.outputs.destination }}/*
# ${{ steps.extract-ee.outputs.destination }}/*
- name: Attach binary to release
uses: softprops/action-gh-release@v1
with:
files: |
${{ steps.extract.outputs.destination }}/*
${{ steps.extract-ee.outputs.destination }}/*
publish_ecr_s3:
needs: [build_ee]
@@ -384,48 +384,3 @@ jobs:
run: |
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:main
build_ee_cuda:
if: ${{ startsWith(github.ref, 'refs/tags/') }}
needs: [build_ee]
runs-on: ubicloud
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-cuda
flavor: |
latest=false
tags: |
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License

1
.gitignore vendored
View File

@@ -5,4 +5,3 @@ local/
frontend/src/routes/test.svelte
CaddyfileRemoteMalo
*.swp
**/.idea/

View File

@@ -1,568 +1,6 @@
# Changelog
## [1.258.4](https://github.com/windmill-labs/windmill/compare/v1.258.3...v1.258.4) (2024-01-31)
### Bug Fixes
* improve git sync ([23f06d1](https://github.com/windmill-labs/windmill/commit/23f06d1a0424862afd6f733ed103f7a66f59dd12))
* improve git sync ([cc0aec8](https://github.com/windmill-labs/windmill/commit/cc0aec87438ebe4aef6dae1464b9dc267da3ad97))
## [1.258.3](https://github.com/windmill-labs/windmill/compare/v1.258.2...v1.258.3) (2024-01-31)
### Bug Fixes
* **cli:** restore other files support ([7494e7e](https://github.com/windmill-labs/windmill/commit/7494e7ee2d7f56a95b6f976b9646c2bce3bdc22b))
* **frontend:** Fix adding nodes to decision tree ([#3107](https://github.com/windmill-labs/windmill/issues/3107)) ([740801f](https://github.com/windmill-labs/windmill/commit/740801f4a73849919419ee96f4fc090eef855a17))
* improve git sync ([b063164](https://github.com/windmill-labs/windmill/commit/b0631648c3899134e668c30b890efc40aa2c0c49))
* improve git sync ([1c4129c](https://github.com/windmill-labs/windmill/commit/1c4129c4f68f690eac6bffe9016bc8506bb9cf37))
## [1.258.2](https://github.com/windmill-labs/windmill/compare/v1.258.1...v1.258.2) (2024-01-31)
### Bug Fixes
* **cli:** push folders first + on_behalf_of stripped from metadata ([d2cbc7a](https://github.com/windmill-labs/windmill/commit/d2cbc7a41671e91e07fc1e81e966c51133abcc72))
## [1.258.1](https://github.com/windmill-labs/windmill/compare/v1.258.0...v1.258.1) (2024-01-31)
### Bug Fixes
* **cli:** add support for restart_unless_cancelled in sync ([b4d0a3c](https://github.com/windmill-labs/windmill/commit/b4d0a3c4239973fa32e198b117c25e0d13a53b4c))
* fix RETENTION period setting UI ([5cec2ed](https://github.com/windmill-labs/windmill/commit/5cec2edaefd908a5bf3c9853035e6e2d2466656d))
## [1.258.0](https://github.com/windmill-labs/windmill/compare/v1.257.0...v1.258.0) (2024-01-30)
### Features
* Support sending SIGINT to jobs ([#3094](https://github.com/windmill-labs/windmill/issues/3094)) ([a719170](https://github.com/windmill-labs/windmill/commit/a719170a6ae125f03312fc1c12a73fd2f01a09c6))
### Bug Fixes
* improve array static editor ([ef17fd0](https://github.com/windmill-labs/windmill/commit/ef17fd0f5ea02df218200709f742de2c5ae4cc76))
## [1.257.0](https://github.com/windmill-labs/windmill/compare/v1.256.0...v1.257.0) (2024-01-30)
### Features
* **frontend:** Correctly set the licence key on the approval page ([#3112](https://github.com/windmill-labs/windmill/issues/3112)) ([8ebc90a](https://github.com/windmill-labs/windmill/commit/8ebc90abb2e9d99c4f20cb2e14c0a5487d5a11a0))
### Bug Fixes
* **cli:** avoid flows in script generate-metadata ([565e166](https://github.com/windmill-labs/windmill/commit/565e1668b372e7ac4482fc13962b32b1dbb084b3))
* **frontend:** expose Filters + Displayed row count in the outputs ([#3101](https://github.com/windmill-labs/windmill/issues/3101)) ([b55c0bd](https://github.com/windmill-labs/windmill/commit/b55c0bd2c5a1646709328d8ee9b191a34a2c976c))
* **frontend:** fix logpanel ([#3111](https://github.com/windmill-labs/windmill/issues/3111)) ([64441b3](https://github.com/windmill-labs/windmill/commit/64441b34522ba99ce44e74a2c81dcc4999acdfa3))
* simplify folder creation by non admins ([34253fd](https://github.com/windmill-labs/windmill/commit/34253fd43b01fbe15dafe233d008fc9b98c29828))
## [1.256.0](https://github.com/windmill-labs/windmill/compare/v1.255.0...v1.256.0) (2024-01-30)
### Features
* list jobs metrics ([#3104](https://github.com/windmill-labs/windmill/issues/3104)) ([26d5c6c](https://github.com/windmill-labs/windmill/commit/26d5c6c1ba2dfa7edb0217bf7dc89f45279781a7))
### Bug Fixes
* add support for NPM_CONFIG_REGISTRY ([47fcfbb](https://github.com/windmill-labs/windmill/commit/47fcfbbbdf179d241f1deb3d5527f8faa0a8132e))
* add support for NPM_CONFIG_REGISTRY ([27f4624](https://github.com/windmill-labs/windmill/commit/27f4624b30fcb04c98d093e49c614baa5a938528))
* **frontend:** fix chartjs when resolvedDatasets is not defined ([#3106](https://github.com/windmill-labs/windmill/issues/3106)) ([0102dce](https://github.com/windmill-labs/windmill/commit/0102dcef8ecbc1d8a9528bac8fdfd95b93689155))
## [1.255.0](https://github.com/windmill-labs/windmill/compare/v1.254.1...v1.255.0) (2024-01-29)
### Features
* **cli:** allow all sync options to be passable from wmill.yaml directly ([2a80df4](https://github.com/windmill-labs/windmill/commit/2a80df4a80cbd4ccecf2066e55efe092b070fefa))
* **cli:** global generate-metadata + inherit deps from closest package.json/requirements.txt + bun settable as default ([#3102](https://github.com/windmill-labs/windmill/issues/3102)) ([49c1bc5](https://github.com/windmill-labs/windmill/commit/49c1bc50f3ae6aba42317c2a0ef181927e915d26))
* **cli:** make --raw the default for cli sync ([28a1966](https://github.com/windmill-labs/windmill/commit/28a196657fa60d4db7c6528367e578b1202c5353))
* **cli:** make default typescript configurable ([1f46bcb](https://github.com/windmill-labs/windmill/commit/1f46bcba72592e39820074d160381fe0ff42ac70))
* Download s3 file as stream in Python and TS ([#3099](https://github.com/windmill-labs/windmill/issues/3099)) ([6160889](https://github.com/windmill-labs/windmill/commit/616088979378712eab5b3abbb646d3e688d2cece))
* **frontend:** handle file default value ([#3095](https://github.com/windmill-labs/windmill/issues/3095)) ([94ddf80](https://github.com/windmill-labs/windmill/commit/94ddf803566e94c443f989fc0c7bde578b704786))
* Passing HOME env var through to python workers ([#3092](https://github.com/windmill-labs/windmill/issues/3092)) ([ec911f6](https://github.com/windmill-labs/windmill/commit/ec911f6a5a68b9d4b2703bef8ebc3342c755a35a))
* update openai models + increase length + improve code completion ([#3097](https://github.com/windmill-labs/windmill/issues/3097)) ([6d77578](https://github.com/windmill-labs/windmill/commit/6d77578590cc666272bb364d21b7f2a3fe4494ac))
### Bug Fixes
* **frontend:** various UI fix ([#3098](https://github.com/windmill-labs/windmill/issues/3098)) ([cbfa5ff](https://github.com/windmill-labs/windmill/commit/cbfa5ff8871097c4f292e32b850a6c10a0809575))
* improve display result ([ff559ec](https://github.com/windmill-labs/windmill/commit/ff559ecdbdfedc09e17a1ccfc94d17d5b587c453))
* improve schedule args clearing on script change ([59e0be7](https://github.com/windmill-labs/windmill/commit/59e0be77ad215e13dbd1e071766152c3fa676fde))
* update deno to 1.38 -&gt; 1.40.2 ([a5d2536](https://github.com/windmill-labs/windmill/commit/a5d25362dbbe09c0c544ddddf0749936cdb8e317))
* use extra headers when urlencoded ([#3103](https://github.com/windmill-labs/windmill/issues/3103)) ([8fcf119](https://github.com/windmill-labs/windmill/commit/8fcf119798c576a57e6dee3ed5119b7b5d1130f6))
## [1.254.1](https://github.com/windmill-labs/windmill/compare/v1.254.0...v1.254.1) (2024-01-27)
### Bug Fixes
* render all responsiveness fix ([18d832c](https://github.com/windmill-labs/windmill/commit/18d832c6347c31fb2c2ddc1268cfd066d01352ae))
* render all responsiveness fix ([dfabb37](https://github.com/windmill-labs/windmill/commit/dfabb371003e955a05e4d8672fb95a637f236a72))
## [1.254.0](https://github.com/windmill-labs/windmill/compare/v1.253.8...v1.254.0) (2024-01-26)
### Features
* **frontend:** add support for render all ([#3084](https://github.com/windmill-labs/windmill/issues/3084)) ([4607939](https://github.com/windmill-labs/windmill/commit/460793954944f695d6c8fed88424d48571348135))
* **frontend:** S3 resource schema ([#3083](https://github.com/windmill-labs/windmill/issues/3083)) ([fa8a6e8](https://github.com/windmill-labs/windmill/commit/fa8a6e8c3dcfcca6f17c2fdd4910ab7980192e11))
### Bug Fixes
* **frontend:** add missing InitializeComponent ([#3088](https://github.com/windmill-labs/windmill/issues/3088)) ([6a73ccf](https://github.com/windmill-labs/windmill/commit/6a73ccf6261d58f019bbb01e10c4f81be62f66e6))
* **frontend:** Fix currency input dark mode ([#3085](https://github.com/windmill-labs/windmill/issues/3085)) ([bcc341c](https://github.com/windmill-labs/windmill/commit/bcc341c255ee04dfbf28ed678b025310f57e501c))
* load input history correctly on past versions ([80eeba5](https://github.com/windmill-labs/windmill/commit/80eeba5ee34204876a4db4e3fda7539247cd0322))
## [1.253.8](https://github.com/windmill-labs/windmill/compare/v1.253.7...v1.253.8) (2024-01-26)
### Bug Fixes
* add support for instance name for mssql ([91289a0](https://github.com/windmill-labs/windmill/commit/91289a0d5a96d6f7b11e60d512d0d3a9ceb341e7))
* git sync include changing permissions on apps, scripts, flows ([ee965a1](https://github.com/windmill-labs/windmill/commit/ee965a1a4c1e122c5b0adb93c548b41d587bd0a2))
* git sync include changing permissions on folders ([0f6c127](https://github.com/windmill-labs/windmill/commit/0f6c127002cee35479231140ec96eef2176bba42))
* improve git sync on rename/deletion ([a025146](https://github.com/windmill-labs/windmill/commit/a0251463f803db7e35572ba68862345d2b53a399))
## [1.253.7](https://github.com/windmill-labs/windmill/compare/v1.253.6...v1.253.7) (2024-01-25)
### Bug Fixes
* Improvement for Python write_s3_file ([#3079](https://github.com/windmill-labs/windmill/issues/3079)) ([082aa6a](https://github.com/windmill-labs/windmill/commit/082aa6a61d860354929efcccfcff695c5ccc8c1d))
## [1.253.6](https://github.com/windmill-labs/windmill/compare/v1.253.5...v1.253.6) (2024-01-25)
### Bug Fixes
* merge conflicts ([7d6039f](https://github.com/windmill-labs/windmill/commit/7d6039f9694bfef672c508dbc2699d33bedeae23))
## [1.253.5](https://github.com/windmill-labs/windmill/compare/v1.253.4...v1.253.5) (2024-01-25)
### Bug Fixes
* openapi definition ([09e08c3](https://github.com/windmill-labs/windmill/commit/09e08c3c94dfff12ac8df9750bea3fadfa455f40))
## [1.253.4](https://github.com/windmill-labs/windmill/compare/v1.253.3...v1.253.4) (2024-01-25)
### Bug Fixes
* add s3 parquet file renderer ([4a6710e](https://github.com/windmill-labs/windmill/commit/4a6710ea913da64ce2caf93499c9ec69d89b3cf3))
* add s3 parquet file renderer ([2f8243b](https://github.com/windmill-labs/windmill/commit/2f8243b39085222a3bde8c31fce6fc36a7b8f453))
* improve parquet renderer error ([4b348a0](https://github.com/windmill-labs/windmill/commit/4b348a032e3cb7ea3c945635ed8d3cb1bd91b9b4))
## [1.253.3](https://github.com/windmill-labs/windmill/compare/v1.253.2...v1.253.3) (2024-01-25)
### Bug Fixes
* Better UI for S3 download and S3 TS SDK endpoints ([#3065](https://github.com/windmill-labs/windmill/issues/3065)) ([da6edee](https://github.com/windmill-labs/windmill/commit/da6edee4505318deef4557a82eb61356a5f1bfb4))
## [1.253.2](https://github.com/windmill-labs/windmill/compare/v1.253.1...v1.253.2) (2024-01-24)
### Bug Fixes
* **app:** improve app reactivity ([737c4fb](https://github.com/windmill-labs/windmill/commit/737c4fb497ce4d04c8404b745dd00868724cfc51))
## [1.253.1](https://github.com/windmill-labs/windmill/compare/v1.253.0...v1.253.1) (2024-01-24)
### Bug Fixes
* **scim:** improve get_user for scim ([a66208f](https://github.com/windmill-labs/windmill/commit/a66208f326cb1be9ede6408beb4b6357d7a8bd57))
## [1.253.0](https://github.com/windmill-labs/windmill/compare/v1.252.0...v1.253.0) (2024-01-24)
### Features
* **frontend:** app editor right click menu ([#3050](https://github.com/windmill-labs/windmill/issues/3050)) ([2b8c0bb](https://github.com/windmill-labs/windmill/commit/2b8c0bbaeae22e2231edb7b8560f58003312353f))
### Bug Fixes
* bun default registry and performance improvements ([801106e](https://github.com/windmill-labs/windmill/commit/801106e9b8878e5fd1acd805497ed81f0bf5c99c))
## [1.252.0](https://github.com/windmill-labs/windmill/compare/v1.251.1...v1.252.0) (2024-01-24)
### Features
* **cli:** add support for excludes in yaml.conf ([21bf011](https://github.com/windmill-labs/windmill/commit/21bf0115fc3e9af96262594eb76d664a3c531498))
* Custom concurrency key for scripts ([#3046](https://github.com/windmill-labs/windmill/issues/3046)) ([f189224](https://github.com/windmill-labs/windmill/commit/f189224b8d0b17fe7a5d78795ac4129f31919538))
* exporting tarball/sync doesn't require admin perms anymore ([c2fb24d](https://github.com/windmill-labs/windmill/commit/c2fb24d4803b9654c248f84861e86b99b559b6eb))
* **frontend:** add support for range area ([#3068](https://github.com/windmill-labs/windmill/issues/3068)) ([0dd54f9](https://github.com/windmill-labs/windmill/commit/0dd54f93bb652af17173697682a6080a513b7f5f))
* **frontend:** Ag Grid compactness ([#3052](https://github.com/windmill-labs/windmill/issues/3052)) ([1ffb4c5](https://github.com/windmill-labs/windmill/commit/1ffb4c5abebc418f41a969a983a03a2ebc521ea6))
* **frontend:** Changelog updates ([#3067](https://github.com/windmill-labs/windmill/issues/3067)) ([c14b880](https://github.com/windmill-labs/windmill/commit/c14b880a7c93424ff13cf0fe8d6e9499af6e56ac))
* **frontend:** display a warning with a documentation link on how to… ([#3012](https://github.com/windmill-labs/windmill/issues/3012)) ([c73bdad](https://github.com/windmill-labs/windmill/commit/c73bdad08c556b656c7fdcd509f097de675126c4))
* Set a default app for each workspace ([#3014](https://github.com/windmill-labs/windmill/issues/3014)) ([3225420](https://github.com/windmill-labs/windmill/commit/32254203d81d4353fbfb7f6cd7b72f1e237dc45c))
### Bug Fixes
* fix add resource with keys as objects ([0c88abc](https://github.com/windmill-labs/windmill/commit/0c88abccf0ebbeccd3bfb0b6a066feb78cc8ecf3))
* improve decision tree ([e6b8f73](https://github.com/windmill-labs/windmill/commit/e6b8f73c9cc8d94b58388726c5a31832871c398e))
* no-emit-index-url on lockfiles ([450267a](https://github.com/windmill-labs/windmill/commit/450267a84e428a7b5b107aa50ddb51a0f0a7f187))
* remove first part of account_identifier for snowflake ([e365693](https://github.com/windmill-labs/windmill/commit/e365693497e7055ee891748621ee3472efc6047a))
## [1.251.1](https://github.com/windmill-labs/windmill/compare/v1.251.0...v1.251.1) (2024-01-23)
### Bug Fixes
* improve npm typescript client ([5b98b00](https://github.com/windmill-labs/windmill/commit/5b98b005cda2615b332d904dc3d4a6a7898ae46e))
## [1.251.0](https://github.com/windmill-labs/windmill/compare/v1.250.0...v1.251.0) (2024-01-22)
### Features
* Download button for s3 files ([#3059](https://github.com/windmill-labs/windmill/issues/3059)) ([376038d](https://github.com/windmill-labs/windmill/commit/376038d70b04fe012566872bee6a4cc4b46db0c7))
## [1.250.0](https://github.com/windmill-labs/windmill/compare/v1.249.0...v1.250.0) (2024-01-22)
### Features
* deprecate .wmillignore in favor of wmill.yaml/includes ([b8defbc](https://github.com/windmill-labs/windmill/commit/b8defbcfc68181397ae50daba6616df26e4383fd))
### Bug Fixes
* fix initialization callback of AppDbexplorer ([1fcdad7](https://github.com/windmill-labs/windmill/commit/1fcdad7f7b284d0a66db57df0582c89a146ef653))
## [1.249.0](https://github.com/windmill-labs/windmill/compare/v1.248.0...v1.249.0) (2024-01-21)
### Features
* File path is option when uploading a file to S3 ([#3029](https://github.com/windmill-labs/windmill/issues/3029)) ([bbf897a](https://github.com/windmill-labs/windmill/commit/bbf897a718f403de0e9809914acd18b4b79fd605))
* improve cli to generate proper metadata and schema by default ([d1eed4e](https://github.com/windmill-labs/windmill/commit/d1eed4e09d09b4ec2a3aa27608dc2a70fc0a4d0a))
* improve handling of pinned versions for bun ([ab010ce](https://github.com/windmill-labs/windmill/commit/ab010ce4f3a0628d4699f558bd463e657a8f1a97))
* non owner can resume flows if resume url is in message ([ac87e2f](https://github.com/windmill-labs/windmill/commit/ac87e2f85b55742fd1472b3ba18cc1e523b56b98))
### Bug Fixes
* **frontend:** Fix display for array of objects ([#3051](https://github.com/windmill-labs/windmill/issues/3051)) ([773e2d3](https://github.com/windmill-labs/windmill/commit/773e2d3103d23b5eb31a47565d577461adf239df))
* improve approval/prompt helpers ([0d7d2ef](https://github.com/windmill-labs/windmill/commit/0d7d2efde8b6bb00ba8d14ce341fe230d79fe9f2))
* more explicit CLI error ([#3049](https://github.com/windmill-labs/windmill/issues/3049)) ([c4f0b67](https://github.com/windmill-labs/windmill/commit/c4f0b67fb65b705f7fecae93991880925f7b3c80))
* use hash on objects instead of shallow equal comparison to improve trigger reliability of apps ([d1cfe7c](https://github.com/windmill-labs/windmill/commit/d1cfe7c202cfa34bdc46997a50c278c9f8ff4fb0))
## [1.248.0](https://github.com/windmill-labs/windmill/compare/v1.247.0...v1.248.0) (2024-01-19)
### Features
* improve SCIM support for groups ([77f7fb2](https://github.com/windmill-labs/windmill/commit/77f7fb2dd35c337a3a79fcf9038c73cf3939f85f))
### Bug Fixes
* fix rename apps from home menu ([91ea031](https://github.com/windmill-labs/windmill/commit/91ea031f281af8776e94890e80101197f9c71e46))
## [1.247.0](https://github.com/windmill-labs/windmill/compare/v1.246.15...v1.247.0) (2024-01-19)
### Features
* **frontend:** Rich table display ([#3028](https://github.com/windmill-labs/windmill/issues/3028)) ([54cad28](https://github.com/windmill-labs/windmill/commit/54cad2886b7e5ca26c6da9547376b43daac9e881))
### Bug Fixes
* add ability to rename group from scim ([9fefdcc](https://github.com/windmill-labs/windmill/commit/9fefdccc132c4a61ab0a72a86c4dc9bbb23a811e))
* **frontend:** fix hidden wizards ([#3045](https://github.com/windmill-labs/windmill/issues/3045)) ([b64eb3d](https://github.com/windmill-labs/windmill/commit/b64eb3d8b10ba6ca453715dbf8d18b8ad7e3db17))
* improve onDemandOnly runnables ([a0d7ea2](https://github.com/windmill-labs/windmill/commit/a0d7ea22b4cd29f47ec308c9c6e2c2d8ab0ed38f))
## [1.246.15](https://github.com/windmill-labs/windmill/compare/v1.246.14...v1.246.15) (2024-01-18)
### Bug Fixes
* improve error message format for logs ([582339c](https://github.com/windmill-labs/windmill/commit/582339c83ef32a81c3b02d88a47fab3fd18ce2fe))
## [1.246.14](https://github.com/windmill-labs/windmill/compare/v1.246.13...v1.246.14) (2024-01-18)
### Bug Fixes
* fix OIDC issuer ([8b302b4](https://github.com/windmill-labs/windmill/commit/8b302b4dc564c6336deca26d36c270c70a0efd98))
## [1.246.13](https://github.com/windmill-labs/windmill/compare/v1.246.12...v1.246.13) (2024-01-18)
### Bug Fixes
* cli sync improvement ([58cad70](https://github.com/windmill-labs/windmill/commit/58cad70363310eb107a3e4c8c7b9428630e5deb3))
## [1.246.12](https://github.com/windmill-labs/windmill/compare/v1.246.11...v1.246.12) (2024-01-18)
### Bug Fixes
* cli sync improvement ([c8f269b](https://github.com/windmill-labs/windmill/commit/c8f269b870adac9628b197f4ab3516ccdbd590d3))
## [1.246.11](https://github.com/windmill-labs/windmill/compare/v1.246.10...v1.246.11) (2024-01-18)
### Bug Fixes
* improve cli script deletion ([20c422c](https://github.com/windmill-labs/windmill/commit/20c422c5467b4188a7618bc4c7c303c70346a5a9))
## [1.246.10](https://github.com/windmill-labs/windmill/compare/v1.246.9...v1.246.10) (2024-01-18)
### Bug Fixes
* make cli backcompatible with respect to lockfile ([eb000f1](https://github.com/windmill-labs/windmill/commit/eb000f1fbc697869aab46b3be430c8d56b7a7e8c))
* make some eval onDemandOnly ([36905da](https://github.com/windmill-labs/windmill/commit/36905daef60d78725d5b78d70e314281ed297565))
## [1.246.9](https://github.com/windmill-labs/windmill/compare/v1.246.8...v1.246.9) (2024-01-17)
### Bug Fixes
* avoid too long diffs in cli ([828cdd4](https://github.com/windmill-labs/windmill/commit/828cdd45f08a12071a028052b19503a441f047b1))
## [1.246.8](https://github.com/windmill-labs/windmill/compare/v1.246.7...v1.246.8) (2024-01-17)
### Bug Fixes
* improve lockfile handling for cli ([d00de26](https://github.com/windmill-labs/windmill/commit/d00de2640abfe6265f12ee4f237c9ee5ba3e00db))
## [1.246.7](https://github.com/windmill-labs/windmill/compare/v1.246.6...v1.246.7) (2024-01-17)
### Bug Fixes
* improve lockfile handling for cli ([8a6ea49](https://github.com/windmill-labs/windmill/commit/8a6ea496022ad70c658121e9694b634177dcc578))
## [1.246.6](https://github.com/windmill-labs/windmill/compare/v1.246.5...v1.246.6) (2024-01-17)
### Bug Fixes
* improve lockfile handling for cli ([0e9b649](https://github.com/windmill-labs/windmill/commit/0e9b649d03f79cdf6d5bece5dc0ae8072e22f5b0))
## [1.246.5](https://github.com/windmill-labs/windmill/compare/v1.246.4...v1.246.5) (2024-01-17)
### Bug Fixes
* improve app push for cli ([e6de809](https://github.com/windmill-labs/windmill/commit/e6de809ff29a9df54af5fef3f425072e24de49ec))
* lock file in metadata is now a string ([#3027](https://github.com/windmill-labs/windmill/issues/3027)) ([8752dcb](https://github.com/windmill-labs/windmill/commit/8752dcbb191279b44a2b86ee0ed45ab040465b96))
## [1.246.4](https://github.com/windmill-labs/windmill/compare/v1.246.3...v1.246.4) (2024-01-17)
### Bug Fixes
* **frontend:** Fix deno logo + add missing onDestroy ([#3025](https://github.com/windmill-labs/windmill/issues/3025)) ([b9de44c](https://github.com/windmill-labs/windmill/commit/b9de44c2b7daf964834af1d3598dbf40971e4a4f))
* make DisplayResult more resilient ([b42c84d](https://github.com/windmill-labs/windmill/commit/b42c84df40e234436e7fcf30a64317e654bed3ed))
* wmill app push &lt;path_to_app_file&gt; ([#3024](https://github.com/windmill-labs/windmill/issues/3024)) ([803962a](https://github.com/windmill-labs/windmill/commit/803962a943001ff5f2a58bf36f955cc16e92d2b1))
## [1.246.3](https://github.com/windmill-labs/windmill/compare/v1.246.2...v1.246.3) (2024-01-17)
### Bug Fixes
* oidc token generation endpoint GET -&gt; POST ([1f3e374](https://github.com/windmill-labs/windmill/commit/1f3e374b85581da463fda8727d9379d6711b7da8))
## [1.246.2](https://github.com/windmill-labs/windmill/compare/v1.246.1...v1.246.2) (2024-01-17)
### Bug Fixes
* oidc token generation endpoint GET -&gt; POST ([3119830](https://github.com/windmill-labs/windmill/commit/3119830062e9d4e30438950e208a2dde4eb12759))
## [1.246.1](https://github.com/windmill-labs/windmill/compare/v1.246.0...v1.246.1) (2024-01-17)
### Bug Fixes
* expose getIdToken in python-client ([4604ccd](https://github.com/windmill-labs/windmill/commit/4604ccde7dd656627605b8c55256cc3628235cab))
* expose getIdToken in typescript-client ([6568c9f](https://github.com/windmill-labs/windmill/commit/6568c9f93aa477c142bde6d989e4eec3a2440687))
## [1.246.0](https://github.com/windmill-labs/windmill/compare/v1.245.1...v1.246.0) (2024-01-17)
### Features
* OIDC support ([#3017](https://github.com/windmill-labs/windmill/issues/3017)) ([640ebcb](https://github.com/windmill-labs/windmill/commit/640ebcb146eae371abfa637a4f55fe1919aab013))
## [1.245.1](https://github.com/windmill-labs/windmill/compare/v1.245.0...v1.245.1) (2024-01-16)
### Bug Fixes
* CLI script generate-metadata creates a default file if none exist ([#3015](https://github.com/windmill-labs/windmill/issues/3015)) ([eb48e0a](https://github.com/windmill-labs/windmill/commit/eb48e0a1071d33e19425f2228e029453b3484458))
## [1.245.0](https://github.com/windmill-labs/windmill/compare/v1.244.4...v1.245.0) (2024-01-16)
### Features
* add script bootstrap and script generate-metadata CLI commands ([#3007](https://github.com/windmill-labs/windmill/issues/3007)) ([b9bee40](https://github.com/windmill-labs/windmill/commit/b9bee403f1ee922c776cf7a82aef0cdfc04c4c10))
* Browse s3 bucket content from workspace settings page ([#3013](https://github.com/windmill-labs/windmill/issues/3013)) ([1053979](https://github.com/windmill-labs/windmill/commit/10539790d20e01faf7aa992f44c89ab623a794c0))
* **frontend:** Add running runs on the script detail page ([#3005](https://github.com/windmill-labs/windmill/issues/3005)) ([c93932a](https://github.com/windmill-labs/windmill/commit/c93932a5b3b17cf3ddc7c86bf047343dc5da114d))
* **frontend:** add specific bun and deno icons ([#3006](https://github.com/windmill-labs/windmill/issues/3006)) ([e9ab3ce](https://github.com/windmill-labs/windmill/commit/e9ab3ceac901503c6d6e0af0954516af80e1f4a0))
### Bug Fixes
* s3 resource is accessed by backend with admin permissions ([#3011](https://github.com/windmill-labs/windmill/issues/3011)) ([9fffe4f](https://github.com/windmill-labs/windmill/commit/9fffe4f6f578665242612a596300e93b6cf2e6b6))
## [1.244.4](https://github.com/windmill-labs/windmill/compare/v1.244.2...v1.244.4) (2024-01-15)
### Features
* experimental nodejs support ([047ee10](https://github.com/windmill-labs/windmill/commit/047ee10246f8e1bb952d3b8cdf21612948ac9843))
* git sync branch name no contains the workspace ID and the type ([#3004](https://github.com/windmill-labs/windmill/issues/3004))
### Bug Fixes
* git sync branch name no contains the workspace ID and the type ([#3004](https://github.com/windmill-labs/windmill/issues/3004)) ([d845864](https://github.com/windmill-labs/windmill/commit/d845864872aff0057d6a3f5d9df2cc4a8c642be5))
* improve bun imports resolutions ([2b28854](https://github.com/windmill-labs/windmill/commit/2b288542bdfdba62b9182002db6c8c23cdd9869f))
* improve bun imports resolutions ([64e592b](https://github.com/windmill-labs/windmill/commit/64e592b874ef0414dcea2a3dd113d323cff0661e))
## [1.244.2](https://github.com/windmill-labs/windmill/compare/v1.244.1...v1.244.2) (2024-01-13)
### Bug Fixes
* improve favorite menu ([a1f93a4](https://github.com/windmill-labs/windmill/commit/a1f93a495e36e04b40c58bb8e33ab2336628ba8b))
## [1.244.1](https://github.com/windmill-labs/windmill/compare/v1.244.0...v1.244.1) (2024-01-13)
### Bug Fixes
* menu colors ([b84cd6d](https://github.com/windmill-labs/windmill/commit/b84cd6d52d723fea55a407d347afec1c669da03a))
* menu colors ([5201dcd](https://github.com/windmill-labs/windmill/commit/5201dcdd679690bf82b2606d85829ae9333e09ac))
## [1.244.0](https://github.com/windmill-labs/windmill/compare/v1.243.0...v1.244.0) (2024-01-13)
### Features
* **frontend:** Operator mode ([#2973](https://github.com/windmill-labs/windmill/issues/2973)) ([aaff17f](https://github.com/windmill-labs/windmill/commit/aaff17f813ae3f31cae58bb40b9c8118e772a2d8))
### Bug Fixes
* add ability to set secret variable from python ([0733dd1](https://github.com/windmill-labs/windmill/commit/0733dd118e463f73caa9155b97fc8d9b02759e06))
* improve oauth accounts permissions ([1621975](https://github.com/windmill-labs/windmill/commit/16219755a7fa8b9ff5a901c18842a1eceb68086a))
* improve table behavior when searching ([f0c4901](https://github.com/windmill-labs/windmill/commit/f0c4901c218cfc4564f9f2be0cacaf3d9997d822))
## [1.243.0](https://github.com/windmill-labs/windmill/compare/v1.242.0...v1.243.0) (2024-01-13)
### Features
* add support for bun install scopes ([d785def](https://github.com/windmill-labs/windmill/commit/d785deff3312b076e3ed9043924f514f8823e041))
* **frontend:** s3 file upload ([#2976](https://github.com/windmill-labs/windmill/issues/2976)) ([3c59fb8](https://github.com/windmill-labs/windmill/commit/3c59fb8b4d8a80077c2f352ccf1314fd32ff442c))
* GIt sync to multiple repo ([#2996](https://github.com/windmill-labs/windmill/issues/2996)) ([fda0e28](https://github.com/windmill-labs/windmill/commit/fda0e28db31fba9f71be6db7280696c3949fd7be))
* support dynamic args in tags ([aafd7d9](https://github.com/windmill-labs/windmill/commit/aafd7d90037bae20808e836d34c352ca6b357155))
* Tag override for scheduled scripts ([#2998](https://github.com/windmill-labs/windmill/issues/2998)) ([99484bd](https://github.com/windmill-labs/windmill/commit/99484bdc20bac966ca1d9d45ca4278133ea91b87))
### Bug Fixes
* allow for any extra bunfig config ([e200889](https://github.com/windmill-labs/windmill/commit/e200889cff6c5166ce6564d7f9d606c1613fe03b))
## [1.242.0](https://github.com/windmill-labs/windmill/compare/v1.241.0...v1.242.0) (2024-01-12)
### Features
* Instance group management page ([#2994](https://github.com/windmill-labs/windmill/issues/2994)) ([5f54f55](https://github.com/windmill-labs/windmill/commit/5f54f557903792ea6307b17f966c216cbd402709))
### Bug Fixes
* catch more agGrid errors ([01dbf54](https://github.com/windmill-labs/windmill/commit/01dbf548f712c2e669038e4eb704c470d2cb0426))
## [1.241.0](https://github.com/windmill-labs/windmill/compare/v1.240.0...v1.241.0) (2024-01-12)
### Features
* Instance group CRUD API ([#2992](https://github.com/windmill-labs/windmill/issues/2992)) ([5a157c4](https://github.com/windmill-labs/windmill/commit/5a157c415e7f075d3f57872a9adf7f5130632bb6))
* make dedicated workers for flows able to share runtime for the same scripts ([d59b89e](https://github.com/windmill-labs/windmill/commit/d59b89ec3cdf2285c0eac56d7678fc0b9a2c2a32))
### Bug Fixes
* add cache for flow as flow step ([28ac763](https://github.com/windmill-labs/windmill/commit/28ac7632cf767b1c0df30d93c841ad32bf891202))
* git sync now works for delete and rename ([#2988](https://github.com/windmill-labs/windmill/issues/2988)) ([cde574b](https://github.com/windmill-labs/windmill/commit/cde574b8910bbaf737e0d7e515c42fc796911c5e))
## [1.240.0](https://github.com/windmill-labs/windmill/compare/v1.239.0...v1.240.0) (2024-01-11)
### Features
* **cli:** introduce --stateful for CLI, in preparation for --raw to become the default ([39ecf80](https://github.com/windmill-labs/windmill/commit/39ecf8017ee88c4311cb2dd580b20cd59066612e))
## [1.239.0](https://github.com/windmill-labs/windmill/compare/v1.238.0...v1.239.0) (2024-01-11)
### Features
* add configurable ordering for script's generated UI ([717ccc9](https://github.com/windmill-labs/windmill/commit/717ccc94a06ee65a5676c7d9091faf892396657e))
* **frontend:** AG chart ([#2972](https://github.com/windmill-labs/windmill/issues/2972)) ([e3c1661](https://github.com/windmill-labs/windmill/commit/e3c166154da4251e21a5f6a6fcf5b2e101b558e1))
### Bug Fixes
* **cli:** improve .wmillignore handling of folders ([6996c90](https://github.com/windmill-labs/windmill/commit/6996c9083d9ae93922caf68e5658f18b49678630))
* handle Etc/Unknown timezone better ([8c97ef0](https://github.com/windmill-labs/windmill/commit/8c97ef0394925b49a04c253259783be301e8f4ee))
* only cache flow if it's a success ([589e683](https://github.com/windmill-labs/windmill/commit/589e683c27f15fcb48993f2f6e91523abef1794a))
* spelling error dtails -&gt; details ([#2986](https://github.com/windmill-labs/windmill/issues/2986)) ([308c4ce](https://github.com/windmill-labs/windmill/commit/308c4ceb475fef033ea87af5f772e1f35b4fb16d))
## [1.238.0](https://github.com/windmill-labs/windmill/compare/v1.237.0...v1.238.0) (2024-01-10)
### Features
* add ability to use secrets in pip requirements ([3517c7f](https://github.com/windmill-labs/windmill/commit/3517c7f28389a4ddf46a6bef4f2044aa94174050))
* add support for multiselect in python ([c8a793d](https://github.com/windmill-labs/windmill/commit/c8a793d35ea843fb75428182edc06cfe7105af3b))
* **frontend:** add shortcuts to audit logs ([#2975](https://github.com/windmill-labs/windmill/issues/2975)) ([4147d16](https://github.com/windmill-labs/windmill/commit/4147d1604a4c50ad0c667b413d64b44b357fb7e4))
* **frontend:** Display the index by which a node can be selected with the setTab function + add tooltip ([#2974](https://github.com/windmill-labs/windmill/issues/2974)) ([cffae36](https://github.com/windmill-labs/windmill/commit/cffae3633107d9359e04354ba01876ccaf98193a))
* S3 multipart upload accepts a custom S3 resource ([#2982](https://github.com/windmill-labs/windmill/issues/2982)) ([eea0d92](https://github.com/windmill-labs/windmill/commit/eea0d92dd11a3607fbb156e2bf3a3a001ab5e6a0))
### Bug Fixes
* main compile ([#2983](https://github.com/windmill-labs/windmill/issues/2983)) ([6d5c3f0](https://github.com/windmill-labs/windmill/commit/6d5c3f0f2252be15bf0169a42f5baa06d592911c))
## [1.237.0](https://github.com/windmill-labs/windmill/compare/v1.236.2...v1.237.0) (2024-01-08)
### Features
* make okta SSO configurable using custom domain ([4f01ee8](https://github.com/windmill-labs/windmill/commit/4f01ee89de743eab6ae323dea1ec4190ee23e0bf))
### Bug Fixes
* delete is captured in more fields in app ([d340fbc](https://github.com/windmill-labs/windmill/commit/d340fbc02fd9500249c1d23799c1bdbfe9602cb8))
## [1.236.2](https://github.com/windmill-labs/windmill/compare/v1.236.1...v1.236.2) (2024-01-08)

View File

@@ -100,8 +100,8 @@ SHELL ["/bin/bash", "-c"]
RUN apt update -y
RUN apt install -y unzip curl
RUN [ "$TARGETPLATFORM" == "linux/arm64" ] && curl -Lsf https://github.com/LukeChannings/deno-arm64/releases/download/v1.40.2/deno-linux-arm64.zip -o deno.zip || true
RUN [ "$TARGETPLATFORM" == "linux/amd64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v1.40.2/deno-x86_64-unknown-linux-gnu.zip -o deno.zip || true
RUN [ "$TARGETPLATFORM" == "linux/arm64" ] && curl -Lsf https://github.com/LukeChannings/deno-arm64/releases/download/v1.38.0/deno-linux-arm64.zip -o deno.zip || true
RUN [ "$TARGETPLATFORM" == "linux/amd64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v1.38.0/deno-x86_64-unknown-linux-gnu.zip -o deno.zip || true
RUN unzip deno.zip && rm deno.zip
@@ -193,12 +193,6 @@ RUN set -eux; \
ENV PATH="${PATH}:/usr/local/go/bin"
ENV GO_PATH=/usr/local/go/bin/go
ARG nsjail=""
RUN if [ "$nsjail" = "true" ]; then apt-get -y update \
&& apt-get install -y \
curl nodejs npm; fi
# go build is slower the first time it is ran, so we prewarm it in the build
RUN mkdir -p /tmp/gobuildwarm && cd /tmp/gobuildwarm && go mod init gobuildwarm && printf "package foo\nimport (\"fmt\")\nfunc main() { fmt.Println(42) }" > warm.go && go build -x && rm -rf /tmp/gobuildwarm
@@ -215,7 +209,7 @@ RUN chmod 755 /usr/bin/deno
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
COPY --from=oven/bun:1.0.25 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.0.18 /usr/local/bin/bun /usr/bin/bun
# add the docker client to call docker from a worker if enabled
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/

View File

@@ -357,6 +357,8 @@ 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 |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| DENO_EXTRA_IMPORT_MAP | None | extra import map to use to run deno scripts (format: `key=value,key=value`) | Worker |
| NPM_CONFIG_REGISTRY | None | Registry to use for NPM dependencies, set if you have a private repository you need to use instead of the default public NPM registry | Worker |
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
@@ -365,7 +367,8 @@ you to have it being synced automatically everyday.
| GLOBAL_CACHE_INTERVAL | 10\*60 | (Enterprise Edition only) Interval in seconds in between bucket sync of the cache. This interval \* 2 is the time at which you're guaranteed all the worker's caches are synced together. | Worker |
| WORKER_TAGS | 'deno,go,python3,bash,flow,hub,dependency' | The worker groups assigned to that workers | Worker |
| DEDICATED_WORKER | None | Unique script to run on that worker. Has to be in the form of `<workspace>:<script_path>` | Worker |
| CUSTOM_TAGS | None | The custom tags assignable to scripts. | Server | |
| CUSTOM_TAGS | None | The custom tags assignable to scripts. | Server |
| JOB_RETENTION_SECS | 60*60*24\*60 //60 days | **Overriden by the instance settings UI** The time in seconds after which jobs get deleted. Set to 0 or -1 to never delete |
| WAIT_RESULT_FAST_POLL_INTERVAL_MS | 50 | The time in between polling for the run_wait_result endpoints in fast poll mode | Server |
| WAIT_RESULT_SLOW_POLL_INTERVAL_MS | 200 | The time in between polling for the run_wait_result endpoints in fast poll mode | Server |
| WAIT_RESULT_FAST_POLL_DURATION_SECS | 2 | The duration of fast poll mode before switching to slow poll | Server |
@@ -392,6 +395,42 @@ you to have it being synced automatically everyday.
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| DISABLE_SERVER | false | Disable the external API, operate as a worker only instance | Worker |
## Run a local dev setup
### only Frontend
This will use the backend of <https://app.windmill.dev> but your own frontend
with hot-code reloading.
1. Go to `frontend/`:
1. `npm install`
2. `npm run generate-backend-client`
3. `npm run dev`
2. Et voilà, windmill should be available at `http://localhost:3000/`
### Backend + Frontend
See the [./frontend/README_DEV.md](./frontend/README_DEV.md) file for all
running options.
1. Create a Postgres Database for Windmill and create an admin role inside your
Postgres setup. The easiest way to get a working postgres is running
`cargo install --version ^0.7 sqlx-cli && sqlx migrate run`. This will also
avoid compile time issue with sqlx's `query!` macro
2. Install [nsjail](https://github.com/google/nsjail) and have it accessible in
your PATH
3. Install deno and python3, have the bins at `/usr/bin/deno` and
`/usr/local/bin/python3`
4. Install [caddy](https://caddyserver.com)
5. Go to `frontend/`:
1. `npm install`, `npm run generate-backend-client` then `npm run dev`
2. In another shell `npm run build` otherwise the backend will not find the
`frontend/build` folder and will crash
3. In another shell `sudo caddy run --config Caddyfile`
6. Go to `backend/`:
`DATABASE_URL=<DATABASE_URL_TO_YOUR_WINDMILL_DB> RUST_LOG=info cargo run`
7. Et voilà, windmill should be available at `http://localhost/`
## Contributors
<a href="https://github.com/windmill-labs/windmill/graphs/contributors">

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name FROM instance_group WHERE external_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "1498f1920ae2d47fd7c369285569344890a8fc50e503b15182ab3d2f90c75c2e"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value, is_secret, path from variable WHERE variable.path = $1 AND variable.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "is_secret",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "1587639f13a1a3ad97bd324df38661f3e846f1992704c86ddad74dea151b0ec4"
}

View File

@@ -92,11 +92,6 @@
"ordinal": 17,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 18,
"name": "default_app",
"type_info": "Varchar"
}
],
"parameters": {
@@ -122,7 +117,6 @@
true,
false,
true,
true,
true
]
},

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO instance_group (name, scim_display_name, external_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "2368de71006526e662b39692f42226288b76cb1be7d1326a2cd03532eb405fc8"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT concurrency_key FROM script WHERE hash = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "concurrency_key",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true
]
},
"hash": "2719f910142b32476a16025bb9836b0cab019ba0a436b330ea3a53fba4725f73"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET default_app = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "36918406736022663b0d33467e2140e86fe8fc8d1f20dc76b8ae70b9a3b5833e"
}

View File

@@ -125,11 +125,6 @@
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 25,
"name": "jobs",
"type_info": "JsonArray"
}
@@ -166,7 +161,6 @@
true,
true,
false,
true,
null
]
},

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27)",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)",
"describe": {
"columns": [],
"parameters": {
@@ -65,11 +65,10 @@
"Int2",
"Bool",
"Bool",
"Int4",
"Varchar"
"Int4"
]
},
"nullable": []
},
"hash": "260feb784bb0b223bd9276d6a82bd26be90efb17a4323b9673e93ff88513942a"
"hash": "4a393e61e33c1204c01044254692629e6ee2c053c7b1fa10f84462a327c739b3"
}

View File

@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name, external_id, scim_display_name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE external_id = $1 GROUP BY name",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "external_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "scim_display_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "emails",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
true,
null
]
},
"hash": "4c1f35d1375e900bfa956c574e1d0430ae53c739b3ea603f1a9cef1bd64004e1"
}

View File

@@ -92,11 +92,6 @@
"ordinal": 17,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 18,
"name": "default_app",
"type_info": "Varchar"
}
],
"parameters": {
@@ -122,7 +117,6 @@
true,
false,
true,
true,
true
]
},

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND script_hash IS NOT NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "54bb09f68de5615a75b2174032e5fcebc314eeeccbcc609ba154fd062fe0f2fd"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT job FROM token WHERE token = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "55cf43cb9219b43f8e9f94b23b62846cd0b1ef5f64d20b0d975d0058730f427b"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "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, ws_error_handler_muted, retry, summary, no_flow_overlap, tag ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22 ) RETURNING *",
"query": "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, ws_error_handler_muted, retry, summary, no_flow_overlap ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 ) RETURNING *",
"describe": {
"columns": [
{
@@ -122,11 +122,6 @@
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
@@ -151,8 +146,7 @@
"Bool",
"Jsonb",
"Varchar",
"Bool",
"Varchar"
"Bool"
]
},
"nullable": [
@@ -179,9 +173,8 @@
false,
true,
true,
false,
true
false
]
},
"hash": "597335a4a1eda7799303e0d930b5468af2364f5d2075aab48787b93e5774336d"
"hash": "6bb9e6cab7034bf28ef2597445dc617b60c49b42568fdb975cf2d2dc8702167e"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET default_app = $1 WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "72b2bc804e1926e1a938ba839a319fd1ca9a8145cc33fa6f912c84fec5eb226e"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM concurrency_counter WHERE concurrency_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "79fe8688b10d3805ec082b7ec581dc2b17e527d177c736a7adb50af954cc7013"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT app.versions[array_upper(app.versions, 1)] FROM app WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "versions",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "7bb9808f744590c0a962053e28564a37b980d3e9b1ceb23f69b9729b73e0bd33"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO global_settings (name, value) VALUES ('rsa_keys', $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "85c116da4a43a5ade37bb9ab2660d206b722ec4192368599c3a5027f50a89c80"
}

View File

@@ -122,11 +122,6 @@
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
@@ -160,8 +155,7 @@
false,
true,
true,
false,
true
false
]
},
"hash": "911b1e1f2a5ba6d5159916e5598020e680c45043b0736ad0153ee261a151dd90"

View File

@@ -122,11 +122,6 @@
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
@@ -161,8 +156,7 @@
false,
true,
true,
false,
true
false
]
},
"hash": "96dc1cd308f3c21cfb50b88048054dc03f93e261d25969d66aa48e9d0502960f"

View File

@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name, external_id, scim_display_name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE external_id = $1 group by name, external_id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "external_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "scim_display_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "emails",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
true,
null
]
},
"hash": "a002b2f47928f0f235c7d87ebe5f9606387d17454c7ef14a628e3bed107a242b"
}

View File

@@ -1,32 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "emails",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true,
null
]
},
"hash": "a00f3f18087326432c9114998e47cff4f78d1b28cdb8adc6b18b937e1cf142d1"
}

View File

@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE name = $1 GROUP BY name",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "emails",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
null
]
},
"hash": "a355b3e13faa52b12c7d01fd8c78a34a13579ebf69a73ac6897edaa9e87ab9f1"
}

View File

@@ -122,11 +122,6 @@
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
@@ -159,8 +154,7 @@
false,
true,
true,
false,
true
false
]
},
"hash": "aa2800113a8a8805f47cdc1dd0f29d94c546fe531e7edd3e91da4978af5442fb"

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND app_version IS NOT NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "aee9f7f1d99910c669ef7224a6cc237ac7a0d7f5a24849bb62a6e1a8babd8ef9"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "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, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14, tag = $15\n WHERE path = $16 AND workspace_id = $17 RETURNING *",
"query": "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, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14 WHERE path = $15 AND workspace_id = $16 RETURNING *",
"describe": {
"columns": [
{
@@ -122,11 +122,6 @@
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
@@ -145,7 +140,6 @@
"Jsonb",
"Varchar",
"Bool",
"Varchar",
"Text",
"Text"
]
@@ -174,9 +168,8 @@
false,
true,
true,
false,
true
false
]
},
"hash": "90435687152ccd37db7e0a6a5baeb558db50f5f96b1e8eff0b10e4398566f325"
"hash": "b7ed211ec7611c994a49565e46e5893788bcf49b2c9357fbc45a8353a7c84f48"
}

View File

@@ -122,11 +122,6 @@
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
@@ -160,8 +155,7 @@
false,
true,
true,
false,
true
false
]
},
"hash": "c10348d26e3e3dc9e345d9044db35db9906617eb9c98aaf58a55d9681ffb85e3"

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value->>'private_key' FROM global_settings WHERE name = 'rsa_keys'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "c87db5bfb559c912e35415c9945009041a5f5a03eaf03fe6bf27eed406640b6a"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO instance_group (name) VALUES ($1) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": []
},
"hash": "cac375edf290d68d487de4273c3696b824685978760b308d8b578df95cf8db45"
}

View File

@@ -122,11 +122,6 @@
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
@@ -160,8 +155,7 @@
false,
true,
true,
false,
true
false
]
},
"hash": "dd74fa9468b5fe8c8ad657ded06076c11a78d0206af2af20685c691d4d9520bb"

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "emails",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
null
]
},
"hash": "df8ebebb61194d9427ee922d037fc0f224dcb80d7097f4af0ebc8effd5e2205b"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE instance_group SET scim_display_name = $1, name = $2 where external_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "e45efe23065e74bcfacc0b52a2995a78175ac18986d8bc68110d796fb282743c"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND script_hash IS NULL and app_version IS NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "e517ebdbb5c81ea0cb7355aaea0c17ad9a45ddc77f236399b453f9ddb93acecd"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin, email, job)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7, $8)",
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin, email)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7)",
"describe": {
"columns": [],
"parameters": {
@@ -11,11 +11,10 @@
"Varchar",
"Text",
"Bool",
"Varchar",
"Uuid"
"Varchar"
]
},
"nullable": []
},
"hash": "42cb4bc3abc8cba2193a7fbbae8275c1bc830081a43e3a4a897c41b6b3099b1d"
"hash": "e9c0e331c16312bf086b17c91466c5389d41454fd3f18d73c2e9554845ee9a72"
}

View File

@@ -122,11 +122,6 @@
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
@@ -160,8 +155,7 @@
false,
true,
true,
false,
true
false
]
},
"hash": "eced0a09ba547ce1dccb54a5419b22373603c9d01f77047b3553bde125bf71e8"

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT default_app FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "default_app",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "ed1a053c7b22d9cb69767be40d33f3be67b6160cd258c86b8e8f22a6d601afd0"
}

View File

@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE name = $1 GROUP BY name",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "emails",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
null
]
},
"hash": "ef0f4447498a117e4495ed9335d803403ad0055efb6da4bdd467b4ac9bf4e478"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4) RETURNING id",
"query": "INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, $2, $3, now() + ($4 || ' seconds')::interval, $5) RETURNING id",
"describe": {
"columns": [
{
@@ -11,6 +11,7 @@
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Text",
@@ -21,5 +22,5 @@
false
]
},
"hash": "dc649111c4c862c36c26ad8a9eff8c31c2533a4a73184d0276f9a388ff2fa574"
"hash": "fa567c205929f41799a64aefac97c4504c7993b22478530e9345b9bc117e92e9"
}

View File

@@ -122,11 +122,6 @@
"ordinal": 23,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 24,
"name": "tag",
"type_info": "Varchar"
}
],
"parameters": {
@@ -158,8 +153,7 @@
false,
true,
true,
false,
true
false
]
},
"hash": "fdd3710a381dac33ef5ee5ec5564a4874e6d7807d43fa6ea7b8408cad9e97480"

1208
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.258.4"
version = "1.236.2"
authors.workspace = true
edition.workspace = true
@@ -22,7 +22,7 @@ members = [
]
[workspace.package]
version = "1.258.4"
version = "1.236.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -37,7 +37,6 @@ incremental = true
[features]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-git-sync/enterprise"]
enterprise_saml = ["windmill-api/enterprise_saml"]
stripe = ["windmill-api/stripe"]
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark"]
flamegraph = ["windmill-common/flamegraph", "windmill-worker/flamegraph"]
loki = ["windmill-common/loki"]
@@ -69,7 +68,7 @@ uuid.workspace = true
gethostname.workspace = true
serde_json.workspace = true
serde.workspace = true
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true, default-features = false, features = ['rt_tokio']}
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true, default-features = false}
[dev-dependencies]
serde_json.workspace = true
@@ -189,7 +188,7 @@ rsmq_async = { version = "5.1.5" }
gosyn = "0.2.6"
bytes = "1.4.0"
gethostname = "0.4.3"
wasm-bindgen = "=0.2.89"
wasm-bindgen = "0.2"
serde-wasm-bindgen = "0.4"
wasm-bindgen-test = "0.3.0"
convert_case = "0.6.0"
@@ -210,11 +209,9 @@ tokenizers = "0.14.1"
candle-core = "0.3.0"
candle-transformers = "0.3.0"
candle-nn = "0.3.0"
tiberius = { version = "0.12.2", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"] }
tiberius = { version = "0.12.2", default-features = false, features = ["rustls", "tds73", "chrono"] }
aws-sdk-s3 = "1.5.0"
aws-config = "1.0.3"
polars = { version = "0.35.4", features = ["lazy", "parquet", "aws", "csv", "dtype-full", "serde", "strings", "extract_groups"] }
polars-io = { version = "0.35.4", features = ["csv"] }
polars = { version = "0.35.2", features = ["lazy", "parquet", "aws", "csv", "dtype-full"] }
polars-io = { version = "0.35.2", features = ["csv"] }
object_store = { version = "0.8.0", features = ["aws"] }
openidconnect = { version = "3.4.0" }

View File

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

View File

@@ -1,2 +0,0 @@
-- Add up migration script here
ALTER TABLE token ADD COLUMN IF NOT EXISTS job UUID;

View File

@@ -1,2 +0,0 @@
-- Add down migration script here
UPDATE workspace_settings SET git_sync = git_sync->0;

View File

@@ -1,2 +0,0 @@
-- Add up migration script here
UPDATE workspace_settings SET git_sync = '[]'::jsonb || git_sync

View File

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

View File

@@ -1,2 +0,0 @@
-- Add up migration script here
ALTER TABLE schedule ADD COLUMN tag VARCHAR(50);

View File

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

View File

@@ -1,11 +0,0 @@
-- Add up migration script here
DROP POLICY IF EXISTS see_own ON account;
DROP POLICY IF EXISTS see_member ON account;
DROP POLICY IF EXISTS see_folder_extra_perms_user on account;
ALTER TABLE account DISABLE ROW LEVEL SECURITY;
ALTER TABLE account DROP COLUMN IF EXISTS owner;
GRANT ALL ON account TO windmill_admin;
GRANT ALL ON account TO windmill_user;

View File

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

View File

@@ -1,3 +0,0 @@
-- Add up migration script here
ALTER TABLE instance_group ADD COLUMN IF NOT EXISTS scim_display_name VARCHAR(255);
UPDATE instance_group SET external_id = name, scim_display_name = name;

View File

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

View File

@@ -1,2 +0,0 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN concurrency_key VARCHAR(255);

View File

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

View File

@@ -1,2 +0,0 @@
-- Add up migration script here
ALTER TABLE workspace_settings ADD COLUMN default_app VARCHAR(255);

View File

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

View File

@@ -1,3 +0,0 @@
-- Add up migration script here
ALTER TABLE resume_job
DROP CONSTRAINT resume_job_value_check;

View File

@@ -13,4 +13,4 @@ windmill-parser.workspace = true
rustpython-parser.workspace = true
itertools.workspace = true
serde_json.workspace = true
anyhow.workspace = true
anyhow.workspace = true

View File

@@ -93,7 +93,22 @@ pub fn parse_python_signature(code: &str) -> anyhow::Result<MainArgSignature> {
.as_arg()
.annotation
.as_ref()
.map_or(Typ::Unknown, |e| parse_expr(e));
.map_or(Typ::Unknown, |e| match e.as_ref() {
Expr::Name(ExprName { id, .. }) => match id.as_ref() {
"str" => Typ::Str(None),
"float" => Typ::Float,
"int" => Typ::Int,
"bool" => Typ::Bool,
"dict" => Typ::Object(vec![]),
"list" => Typ::List(Box::new(Typ::Str(None))),
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
"Sql" | "sql" => Typ::Sql,
_ => Typ::Resource(id.to_string()),
},
_ => Typ::Unknown,
});
if typ == Typ::Unknown
&& default.is_some()
@@ -119,58 +134,6 @@ pub fn parse_python_signature(code: &str) -> anyhow::Result<MainArgSignature> {
}
}
fn parse_expr(e: &Box<Expr>) -> Typ {
match e.as_ref() {
Expr::Name(ExprName { id, .. }) => parse_typ(id.as_ref()),
Expr::Subscript(x) => match x.value.as_ref() {
Expr::Name(ExprName { id, .. }) => match id.as_str() {
"Literal" => {
let values = match x.slice.as_ref() {
Expr::Tuple(elts) => {
let v: Vec<String> = elts
.elts
.iter()
.map(|x| match x {
Expr::Constant(c) => c.value.as_str().map(|x| x.to_string()),
_ => None,
})
.filter_map(|x| x)
.collect();
if v.is_empty() {
None
} else {
Some(v)
}
}
_ => None,
};
Typ::Str(values)
}
"List" => Typ::List(Box::new(parse_expr(&x.slice))),
_ => Typ::Unknown,
},
_ => Typ::Unknown,
},
_ => Typ::Unknown,
}
}
fn parse_typ(id: &str) -> Typ {
match id {
"str" => Typ::Str(None),
"float" => Typ::Float,
"int" => Typ::Int,
"bool" => Typ::Bool,
"dict" => Typ::Object(vec![]),
"list" => Typ::List(Box::new(Typ::Str(None))),
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
"Sql" | "sql" => Typ::Sql,
_ => Typ::Resource(id.to_string()),
}
}
fn to_value<R>(et: &Expr<R>) -> Option<serde_json::Value> {
match et {
Expr::Constant(ExprConstant { value, .. }) => Some(constant_to_value(value)),
@@ -403,44 +366,4 @@ def main(test1: str,
Ok(())
}
#[test]
fn test_parse_python_sig_4() -> anyhow::Result<()> {
let code = r#"
import os
def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): return
"#;
//println!("{}", serde_json::to_string()?);
assert_eq!(
parse_python_signature(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: None,
name: "test1".to_string(),
typ: Typ::Str(Some(vec!["foo".to_string(), "bar".to_string()])),
default: None,
has_default: false
},
Arg {
otyp: None,
name: "test2".to_string(),
typ: Typ::List(Box::new(Typ::Str(Some(vec![
"foo".to_string(),
"bar".to_string()
])))),
default: None,
has_default: false
}
]
}
);
Ok(())
}
}

View File

@@ -22,5 +22,4 @@ swc_ecma_visit.workspace = true
serde_json.workspace = true
anyhow.workspace = true
convert_case.workspace = true
regex.workspace = true
lazy_static.workspace = true
regex.workspace = true

View File

@@ -284,38 +284,12 @@ fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String
(id.sym.to_string(), typ, nullable)
}
lazy_static::lazy_static! {
static ref RE_SNK_CASE: Regex = Regex::new(r"_(\d)").unwrap();
static ref IMPORTS_VERSION: Regex = Regex::new(r"^((?:\@[^\/\@]+\/[^\/\@]+)|(?:[^\/\@]+))(?:\@(?:[^\/]+))?(.*)$").unwrap();
}
pub fn remove_pinned_imports(code: &str) -> anyhow::Result<String> {
let mut imports = parse_expr_for_imports(code)?;
imports.sort_by_key(|f| 0 - (f.len() as i32));
let mut content = code.to_string();
for import in imports {
let to_c = IMPORTS_VERSION.captures(&import);
if let Some(to) = to_c.and_then(|x| {
x.get(1).map(|y| {
format!(
"{}{}",
y.as_str(),
x.get(2).map(|z| z.as_str()).unwrap_or("")
)
})
}) {
content = content.replace(&import, &to);
}
}
Ok(content)
}
fn to_snake_case(s: &str) -> String {
let r = s.to_case(Case::Snake);
// s_3 => s3
RE_SNK_CASE.replace_all(&r, "$1").to_string()
let re = Regex::new(r"_(\d)").unwrap();
re.replace_all(&r, "$1").to_string()
}
fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -eou pipefail
deno task wasmbuild --out ../../../cli/wasm/

View File

@@ -1,5 +0,0 @@
{
"tasks": {
"wasmbuild": "deno run -A https://deno.land/x/wasmbuild@0.15.4/main.ts"
}
}

View File

@@ -3,7 +3,7 @@
"collaborators": [
"Ruben Fiszel <ruben@windmill.dev>"
],
"version": "1.237.0",
"version": "1.226.9",
"files": [
"windmill_parser_wasm_bg.wasm",
"windmill_parser_wasm.js",
@@ -14,4 +14,4 @@
"sideEffects": [
"./snippets/*"
]
}
}

View File

@@ -538,10 +538,6 @@ async function __wbg_load(module, imports) {
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_eval_ff4183ac1495b791 = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
@@ -593,6 +589,10 @@ function __wbg_get_imports() {
const ret = getObject(arg0) in getObject(arg1);
return ret;
};
imports.wbg.__wbg_eval_596393dc5ae50a1b = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
const ret = getObject(arg0) == getObject(arg1);
return ret;

View File

@@ -22,12 +22,11 @@ use tokio::{
use windmill_api::HTTP_CLIENT;
use windmill_common::{
global_settings::{
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CUSTOM_TAGS_SETTING,
DISABLE_STATS_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
EXTRA_PIP_INDEX_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
RETENTION_PERIOD_SECS_SETTING,
BASE_URL_SETTING, CUSTOM_TAGS_SETTING, DISABLE_STATS_SETTING, ENV_SETTINGS,
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
},
stats::schedule_stats,
utils::{rd_string, Mode},
@@ -43,10 +42,9 @@ use windmill_worker::{
use crate::monitor::{
initial_load, load_keep_job_dir, load_require_preexisting_user, monitor_db, monitor_pool,
reload_base_url_setting, reload_bunfig_install_scopes_setting,
reload_extra_pip_index_url_setting, reload_job_default_timeout_setting, reload_license_key,
reload_npm_config_registry_setting, reload_retention_period_setting, reload_server_config,
reload_worker_config,
reload_base_url_setting, reload_extra_pip_index_url_setting,
reload_job_default_timeout_setting, reload_license_key, reload_npm_config_registry_setting,
reload_retention_period_setting, reload_server_config, reload_worker_config,
};
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
@@ -428,9 +426,6 @@ Windmill Community Edition {GIT_VERSION}
NPM_CONFIG_REGISTRY_SETTING => {
reload_npm_config_registry_setting(&db).await
},
BUNFIG_INSTALL_SCOPES_SETTING => {
reload_bunfig_install_scopes_setting(&db).await
},
KEEP_JOB_DIR_SETTING => {
load_keep_job_dir(&db).await;
},

View File

@@ -21,9 +21,9 @@ use windmill_api::{
use windmill_common::{
error,
global_settings::{
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, EXPOSE_DEBUG_METRICS_SETTING,
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
BASE_URL_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
EXTRA_PIP_INDEX_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
RETENTION_PERIOD_SECS_SETTING,
},
@@ -35,9 +35,8 @@ use windmill_common::{
BASE_URL, DB, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
};
use windmill_worker::{
create_token_for_owner, handle_job_error, AuthedClient, SendResult, BUNFIG_INSTALL_SCOPES,
JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, NPM_CONFIG_REGISTRY, PIP_EXTRA_INDEX_URL,
SCRIPT_TOKEN_EXPIRY,
create_token_for_owner, handle_job_error, AuthedClient, SendResult, JOB_DEFAULT_TIMEOUT,
KEEP_JOB_DIR, NPM_CONFIG_REGISTRY, PIP_EXTRA_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
};
#[cfg(feature = "enterprise")]
@@ -139,9 +138,6 @@ pub async fn initial_load(
if worker_mode {
reload_npm_config_registry_setting(&db).await;
}
if worker_mode {
reload_bunfig_install_scopes_setting(&db).await;
}
}
pub async fn load_metrics_enabled(db: &DB) -> error::Result<()> {
@@ -306,33 +302,29 @@ pub async fn delete_expired_items(db: &DB) -> () {
}
pub async fn reload_extra_pip_index_url_setting(db: &DB) {
reload_option_setting_with_tracing(
if let Err(e) = reload_option_setting(
db,
EXTRA_PIP_INDEX_URL_SETTING,
"PIP_EXTRA_INDEX_URL",
PIP_EXTRA_INDEX_URL.clone(),
)
.await;
.await
{
tracing::error!("Error reloading extra_pip_index_url period: {:?}", e)
}
}
pub async fn reload_npm_config_registry_setting(db: &DB) {
reload_option_setting_with_tracing(
if let Err(e) = reload_option_setting(
db,
NPM_CONFIG_REGISTRY_SETTING,
"NPM_CONFIG_REGISTRY",
NPM_CONFIG_REGISTRY.clone(),
)
.await;
}
pub async fn reload_bunfig_install_scopes_setting(db: &DB) {
reload_option_setting_with_tracing(
db,
BUNFIG_INSTALL_SCOPES_SETTING,
"BUNFIG_INSTALL_SCOPES",
BUNFIG_INSTALL_SCOPES.clone(),
)
.await;
.await
{
tracing::error!("Error reloading npm_config_registry period: {:?}", e)
}
}
pub async fn reload_retention_period_setting(db: &DB) {
@@ -340,7 +332,7 @@ pub async fn reload_retention_period_setting(db: &DB) {
db,
RETENTION_PERIOD_SECS_SETTING,
"JOB_RETENTION_SECS",
60 * 60 * 24 * 30,
60 * 60 * 24 * 60,
JOB_RETENTION_SECS.clone(),
|x| x,
)
@@ -351,13 +343,16 @@ pub async fn reload_retention_period_setting(db: &DB) {
}
pub async fn reload_job_default_timeout_setting(db: &DB) {
reload_option_setting_with_tracing(
if let Err(e) = reload_option_setting(
db,
JOB_DEFAULT_TIMEOUT_SECS_SETTING,
"JOB_DEFAULT_TIMEOUT_SECS",
JOB_DEFAULT_TIMEOUT.clone(),
)
.await;
.await
{
tracing::error!("Error reloading job default timeout: {:?}", e)
}
}
pub async fn reload_request_size(db: &DB) {
@@ -405,16 +400,6 @@ pub async fn reload_license_key(db: &DB) -> error::Result<()> {
Ok(())
}
pub async fn reload_option_setting_with_tracing<T: FromStr + DeserializeOwned>(
db: &DB,
setting_name: &str,
std_env_var: &str,
lock: Arc<RwLock<Option<T>>>,
) {
if let Err(e) = reload_option_setting(db, setting_name, std_env_var, lock.clone()).await {
tracing::error!("Error reloading setting {}: {:?}", setting_name, e)
}
}
pub async fn reload_option_setting<T: FromStr + DeserializeOwned>(
db: &DB,
setting_name: &str,
@@ -781,7 +766,6 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
"ephemeral-script",
*SCRIPT_TOKEN_EXPIRY,
&job.email,
&job.id,
)
.await
.expect("could not create job token");

View File

@@ -34,12 +34,11 @@ pub async fn start() -> anyhow::Result<(String, PgEmbed)> {
pg.start_db().await.expect("pg start db");
//TODO: re-enable this to make it work
// if !pg.database_exists("windmill").await.expect("db exists") {
// pg.create_database("windmill")
// .await
// .expect("pg create database");
// }
if !pg.database_exists("windmill").await.expect("db exists") {
pg.create_database("windmill")
.await
.expect("pg create database");
}
let uri = pg.full_db_uri("windmill");
Ok((uri, pg))

View File

@@ -300,7 +300,7 @@ mod suspend_resume {
let second = completed.next().await.unwrap();
// print_job(second, &db).await;
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil()).await.unwrap();
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
))
@@ -401,7 +401,7 @@ mod suspend_resume {
/* ... and send a request resume it. */
let second = completed.next().await.unwrap();
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil()).await.unwrap();
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
))
@@ -2873,7 +2873,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
retry: None,
no_flow_overlap: None,
summary: None,
tag: None,
};
let _ = client.create_schedule("test-workspace", &schedule).await;
@@ -2936,8 +2936,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
ws_error_handler_muted: None,
retry: None,
summary: None,
no_flow_overlap: None,
tag: None,
no_flow_overlap: None
},
)
.await
@@ -3017,7 +3016,6 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
retry: None,
no_flow_overlap: None,
summary: None,
tag: None,
};
let _ = client.create_schedule("test-workspace", &schedule).await;
@@ -3081,8 +3079,7 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
ws_error_handler_muted: None,
retry: None,
summary: None,
no_flow_overlap: None,
tag: None,
no_flow_overlap: None
},
)
.await
@@ -3148,7 +3145,7 @@ async fn run_deployed_relative_imports(db: &Pool<Postgres>, script_content: Stri
is_template: None,
kind: None,
parent_hash: None,
lock: None,
lock: vec![],
summary: "".to_string(),
tag: None,
schema: std::collections::HashMap::new(),
@@ -3158,7 +3155,6 @@ async fn run_deployed_relative_imports(db: &Pool<Postgres>, script_content: Stri
timeout: None,
restart_unless_cancelled: None,
deployment_message: None,
concurrency_key: None,
},
).await.unwrap();

View File

@@ -9,8 +9,7 @@ name = "windmill_api"
path = "src/lib.rs"
[features]
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "openidconnect"]
stripe = ["async-stripe"]
enterprise = ["windmill-queue/enterprise", "async-stripe", "windmill-audit/enterprise", "windmill-git-sync/enterprise"]
enterprise_saml = ["samael"]
benchmark = []
@@ -28,7 +27,6 @@ windmill-common = { workspace = true, features = [
windmill-audit.workspace = true
windmill-parser.workspace = true
windmill-parser-py-imports.workspace = true
windmill-parser-ts.workspace = true
windmill-git-sync.workspace = true
tokio.workspace = true
anyhow.workspace = true
@@ -90,4 +88,3 @@ aws-sdk-s3.workspace = true
polars.workspace = true
polars-io.workspace = true
object_store.workspace = true
openidconnect = { workspace = true, optional = true}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.258.4
version: 1.236.2
title: Windmill API
contact:
@@ -620,7 +620,7 @@ paths:
"200":
description: status
content:
text/plain:
text/plain::
schema:
type: string
@@ -646,7 +646,7 @@ paths:
"200":
description: status
content:
text/plain:
text/plain::
schema:
type: string
@@ -660,7 +660,7 @@ paths:
"200":
description: status
content:
text/plain:
text/plain::
schema:
type: string
@@ -1193,11 +1193,7 @@ paths:
large_file_storage:
$ref: "#/components/schemas/LargeFileStorage"
git_sync:
type: array
items:
$ref: "#/components/schemas/WorkspaceGitSync"
default_app:
type: string
$ref: "#/components/schemas/WorkspaceGitSync"
required:
- code_completion_enabled
@@ -1536,9 +1532,7 @@ paths:
type: object
properties:
git_sync_settings:
type: array
items:
$ref: "#/components/schemas/WorkspaceGitSync"
$ref: "#/components/schemas/WorkspaceGitSync"
responses:
"200":
@@ -1547,52 +1541,6 @@ paths:
application/json:
schema: {}
/w/{workspace}/workspaces/edit_default_app:
post:
summary: edit default app for workspace
operationId: editWorkspaceDefaultApp
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: Workspace default app
required: true
content:
application/json:
schema:
type: object
properties:
default_app_path:
type: string
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/workspaces/default_app:
get:
summary: get default app for workspace
operationId: getWorkspaceDefaultApp
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: status
content:
application/json:
schema:
type: object
properties:
default_app_path:
type: string
/w/{workspace}/workspaces/get_large_file_storage_config:
get:
summary: get large file storage config
@@ -1729,28 +1677,6 @@ paths:
items:
$ref: "#/components/schemas/TruncatedToken"
/w/{workspace}/oidc/token/{audience}:
post:
summary: get OIDC token (ee only)
operationId: getOidcToken
tags:
- oidc
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: audience
in: path
required: true
schema:
type: string
responses:
"200":
description: new oidc token
content:
text/plain:
schema:
type: string
/w/{workspace}/variables/create:
post:
summary: create variable
@@ -2068,10 +1994,13 @@ paths:
type: string
expires_in:
type: integer
owner:
type: string
client:
type: string
required:
- expires_in
- owner
- client
responses:
"200":
@@ -4687,45 +4616,6 @@ paths:
type: string
format: uuid
/w/{workspace}/jobs/run/dependencies:
post:
summary: run a one-off dependencies job
operationId: runRawScriptDependencies
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: raw script content
required: true
content:
application/json:
schema:
type: object
properties:
raw_scripts:
type: array
items:
$ref: "#/components/schemas/RawScriptForDependencies"
entrypoint:
type: string
required:
- entrypoint
- raw_scripts
responses:
"201":
description: dependency job result
content:
application/json:
schema:
type: object
properties:
lock:
type: string
required:
- lock
/w/{workspace}/jobs/run/preview_flow:
post:
summary: run flow preview
@@ -5082,8 +4972,6 @@ paths:
completed:
type: boolean
result: {}
success:
type: boolean
started:
type: boolean
required:
@@ -5740,134 +5628,6 @@ paths:
schema:
$ref: "#/components/schemas/InstanceGroup"
/groups/create:
post:
summary: create instance group
operationId: createInstanceGroup
tags:
- group
requestBody:
description: create instance group
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
summary:
type: string
required:
- name
responses:
"200":
description: instance group created
content:
text/plain:
schema:
type: string
/groups/update/{name}:
post:
summary: update instance group
operationId: updateInstanceGroup
tags:
- group
parameters:
- $ref: "#/components/parameters/Name"
requestBody:
description: update instance group
required: true
content:
application/json:
schema:
type: object
properties:
new_summary:
type: string
required:
- new_summary
responses:
"200":
description: instance group updated
content:
text/plain:
schema:
type: string
/groups/delete/{name}:
delete:
summary: delete instance group
operationId: deleteInstanceGroup
tags:
- group
parameters:
- $ref: "#/components/parameters/Name"
responses:
"200":
description: instance group deleted
content:
text/plain:
schema:
type: string
/groups/adduser/{name}:
post:
summary: add user to instance group
operationId: addUserToInstanceGroup
tags:
- group
parameters:
- $ref: "#/components/parameters/Name"
requestBody:
description: user to add to instance group
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
required:
- email
responses:
"200":
description: user added to instance group
content:
text/plain:
schema:
type: string
/groups/removeuser/{name}:
post:
summary: remove user from instance group
operationId: removeUserFromInstanceGroup
tags:
- group
parameters:
- $ref: "#/components/parameters/Name"
requestBody:
description: user to remove from instance group
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
required:
- email
responses:
"200":
description: user removed from instance group
content:
text/plain:
schema:
type: string
/w/{workspace}/groups/list:
get:
summary: list groups
@@ -6958,7 +6718,7 @@ paths:
/w/{workspace}/job_helpers/list_stored_files:
get:
summary: List the file keys available in the workspace files storage (S3)
summary: List the file keys available in the worspace files storage (S3)
operationId: listStoredFiles
tags:
- helpers
@@ -6987,8 +6747,6 @@ paths:
type: array
items:
$ref: "#/components/schemas/WindmillLargeFile"
restricted_access:
type: boolean
required:
- windmill_large_files
@@ -7058,73 +6816,6 @@ paths:
schema:
$ref: "#/components/schemas/WindmillFilePreview"
/w/{workspace}/job_helpers/load_parquet_preview/{path}:
get:
summary: Load a preview of a parquet file
operationId: loadParquetPreview
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
- name: offset
in: query
schema:
type: number
- name: limit
in: query
schema:
type: number
- name: sort_col
in: query
schema:
type: string
- name: sort_desc
in: query
schema:
type: boolean
- name: search_col
in: query
schema:
type: string
- name: search_term
in: query
schema:
type: string
responses:
"200":
description: Parquet Preview
content:
application/json:
schema: {}
/w/{workspace}/job_helpers/generate_download_url:
get:
summary: Generate a unique URL to download the file
operationId: generateDownloadUrl
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: file_key
in: query
required: true
schema:
type: string
responses:
"200":
description: Download URL
content:
application/json:
schema:
type: object
properties:
download_url:
type: string
required:
- download_url
/w/{workspace}/job_helpers/delete_s3_file:
delete:
summary: Permanently delete file from S3
@@ -7188,8 +6879,6 @@ paths:
properties:
file_key:
type: string
file_extension:
type: string
part_content:
type: array
items:
@@ -7204,13 +6893,9 @@ paths:
type: boolean
cancel_upload:
type: boolean
s3_resource_path:
type: string
file_expiration:
type: string
format: date-time
required:
- part_content
- file_key
- part_content_base64
- parts
- is_final
- cancel_upload
@@ -7230,60 +6915,11 @@ paths:
$ref: "#/components/schemas/UploadFilePart"
is_done:
type: boolean
file_key:
type: string
required:
- file_key
- upload_id
- parts
- is_done
/w/{workspace}/job_helpers/multipart_download_s3_file:
post:
summary: Download file to S3 bucket
operationId: multipartFileDownload
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: Query args for a multipart file upload to S3
required: true
content:
application/json:
schema:
type: object
properties:
file_key:
type: string
part_number:
type: integer
file_size:
type: integer
s3_resource_path:
type: string
required:
- file_key
- part_number
responses:
"200":
description: Chunk of the downloaded file
content:
application/json:
schema:
type: object
properties:
file_size:
type: integer
part_content:
type: array
items:
type: integer
next_part_number:
type: integer
required:
- part_content
/w/{workspace}/job_metrics/get/{id}:
post:
summary: get job metrics
@@ -7331,38 +6967,6 @@ paths:
items:
$ref: "#/components/schemas/TimeseriesMetric"
/concurrency_groups/list:
get:
summary: List all concurrency groups
operationId: listConcurrencyGroups
tags:
- concurrencyGroups
responses:
"200":
description: all concurrency groups
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ConcurrencyGroup"
/concurrency_groups/{concurrency_id}:
delete:
summary: Delete concurrency group
operationId: deleteConcurrencyGroup
tags:
- concurrencyGroups
parameters:
- $ref: "#/components/parameters/ConcurrencyId"
responses:
"200":
description: concurrency group removed
content:
application/json:
schema:
type: object
properties: {}
components:
securitySchemes:
bearerAuth:
@@ -7697,12 +7301,6 @@ components:
in: query
schema:
type: boolean
ConcurrencyId:
name: concurrency_id
in: path
required: true
schema:
type: string
schemas:
$ref: "../../openflow.openapi.yaml#/components/schemas"
@@ -7834,7 +7432,9 @@ components:
is_template:
type: boolean
lock:
type: string
type: array
items:
type: string
language:
type: string
enum:
@@ -7884,8 +7484,6 @@ components:
type: boolean
deployment_message:
type: string
concurrency_key:
type: string
required:
- path
- summary
@@ -8697,8 +8295,6 @@ components:
enum: [code, identity, http]
dedicated_worker:
type: boolean
lock:
type: string
required:
- args
@@ -8857,8 +8453,6 @@ components:
type: string
no_flow_overlap:
type: boolean
tag:
type: string
required:
- path
- edited_by
@@ -8932,8 +8526,6 @@ components:
type: boolean
summary:
type: string
tag:
type: string
required:
- path
- schedule
@@ -8974,8 +8566,6 @@ components:
type: boolean
summary:
type: string
tag:
type: string
required:
- schedule
- timezone
@@ -9006,8 +8596,6 @@ components:
properties:
name:
type: string
summary:
type: string
emails:
type: array
items:
@@ -9453,8 +9041,6 @@ components:
enum: ["S3Storage"]
s3_resource_path:
type: string
public_resource:
type: boolean
WindmillLargeFile:
type: object
@@ -9490,6 +9076,8 @@ components:
content_type:
type: string
enum: ["RawText", "Csv", "Parquet", "Unknown"]
download_url:
type: string
required:
- content_type
@@ -9586,46 +9174,3 @@ components:
required:
- timestamp
- value
RawScriptForDependencies:
type: object
properties:
raw_code:
type: string
path:
type: string
language:
type: string
enum:
[
python3,
deno,
go,
bash,
powershell,
postgresql,
mysql,
bigquery,
snowflake,
mssql,
graphql,
nativets,
bun,
]
required:
- raw_code
- path
- language
ConcurrencyGroup:
type: object
properties:
concurrency_id:
type: string
job_uuids:
type: array
items:
type: string
required:
- concurrency_id
- job_uuids

View File

@@ -10,6 +10,7 @@ use std::collections::HashMap;
use crate::{
db::{ApiAuthed, DB},
users::{require_owner_of_path, OptAuthed},
variables::build_crypt,
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
@@ -38,9 +39,7 @@ use windmill_common::{
utils::{
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, Pagination, StripPath,
},
variables::build_crypt,
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::{push, PushArgs, PushIsolationLevel, QueueTransaction};
pub fn workspaced_service() -> Router {
@@ -588,18 +587,17 @@ async fn create_app(
)
.await?;
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
if let Some(dm) = app.deployment_message {
args.insert("deployment_message".to_string(), json!(dm));
}
let tx = PushIsolationLevel::Transaction(tx);
let (dependency_job_uuid, new_tx) = push(
&db,
tx,
&w_id,
JobPayload::AppDependencies { path: app.path.clone(), version: v_id },
args,
JobPayload::AppDependencies {
path: app.path.clone(),
version: v_id,
deployment_message: app.deployment_message,
},
PushArgs::empty(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
@@ -665,9 +663,7 @@ pub async fn get_hub_app_by_id(
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> {
@@ -692,11 +688,10 @@ async fn delete_app(
sqlx::query!(
"DELETE FROM app WHERE path = $1 AND workspace_id = $2",
path,
&w_id
w_id
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed.username,
@@ -708,36 +703,6 @@ async fn delete_app(
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::App {
path: path.to_string(),
parent_path: Some(path.to_string()),
version: 0, // dummy version as it will not get inserted in db
},
Some(format!("App '{}' deleted", path)),
rsmq,
true,
)
.await?;
sqlx::query!(
"DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND app_version IS NOT NULL",
path,
w_id
)
.execute(&db)
.await
.map_err(|e| {
Error::InternalErr(format!(
"error deleting deployment metadata for script with path {path} in workspace {w_id}: {e}"
))
})?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() },
@@ -772,7 +737,7 @@ async fn update_app(
require_owner_of_path(&authed, path)?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)",
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
npath,
w_id
)
@@ -811,11 +776,10 @@ async fn update_app(
sqlb.returning("path");
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
tracing::error!("update_app sql: {}", sql);
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()
"".to_string()
};
let v_id = if let Some(nvalue) = &ns.value {
let app_id = sqlx::query_scalar!(
@@ -846,23 +810,9 @@ async fn update_app(
)
.execute(&mut tx)
.await?;
v_id
Some(v_id)
} else {
let v_id = sqlx::query_scalar!(
"SELECT app.versions[array_upper(app.versions, 1)] FROM app WHERE path = $1 AND workspace_id = $2",
npath,
w_id
)
.fetch_one(&mut tx)
.await?;
if let Some(v_id) = v_id {
v_id
} else {
return Err(Error::BadRequest(format!(
"App with path {} not found",
npath
)));
}
None
};
sqlx::query!(
@@ -886,38 +836,38 @@ async fn update_app(
let tx: PushIsolationLevel<'_, rsmq_async::MultiplexedRsmq> =
PushIsolationLevel::Transaction(tx);
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
if let Some(dm) = ns.deployment_message {
args.insert("deployment_message".to_string(), json!(dm));
if let Some(v_id) = v_id {
let (dependency_job_uuid, new_tx) = push(
&db,
tx,
&w_id,
JobPayload::AppDependencies {
path: npath.clone(),
version: v_id,
deployment_message: ns.deployment_message,
},
PushArgs::empty(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
None,
None,
None,
None,
None,
false,
false,
None,
true,
None,
None,
None,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
new_tx.commit().await?;
}
args.insert("parent_path".to_string(), json!(path));
let (dependency_job_uuid, new_tx) = push(
&db,
tx,
&w_id,
JobPayload::AppDependencies { path: npath.clone(), version: v_id },
args,
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
None,
None,
None,
None,
None,
false,
false,
None,
true,
None,
None,
None,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
new_tx.commit().await?;
webhook.send_message(
w_id.clone(),

View File

@@ -1,105 +0,0 @@
use crate::db::{ApiAuthed, DB};
use std::collections::HashMap;
use axum::extract::Path;
use axum::routing::{delete, get};
use axum::{Extension, Json, Router};
use polars::prelude::IntoVec;
use serde::Serialize;
use windmill_common::error::Error::{InternalErr, PermissionDenied};
use windmill_common::error::JsonResult;
#[cfg(feature = "enterprise")]
pub fn global_service() -> Router {
Router::new()
.route("/list", get(list_concurrency_groups))
.route("/*id", delete(delete_concurrency_group))
}
#[cfg(not(feature = "enterprise"))]
pub fn global_service() -> Router {
Router::new()
}
#[derive(Serialize)]
pub struct ConcurrencyGroups {
concurrency_id: String,
job_uuids: Vec<String>,
}
async fn list_concurrency_groups(
authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> JsonResult<Vec<ConcurrencyGroups>> {
if !authed.is_admin {
return Err(PermissionDenied(
"Only administrators can see concurrency groups".to_string(),
));
}
let concurrency_groups_raw = sqlx::query_as::<_, (String, serde_json::Value)>(
"SELECT * FROM concurrency_counter ORDER BY concurrency_id ASC",
)
.fetch_all(&db)
.await?;
let mut concurrency_groups: Vec<ConcurrencyGroups> = vec![];
for (concurrency_id, job_uuids_json) in concurrency_groups_raw {
let job_uuids_map = serde_json::from_value::<HashMap<String, serde_json::Value>>(
job_uuids_json,
)
.map_err(|err| {
tracing::error!(
"Error deserializing concurrency_counter table content: {:?}",
err
);
InternalErr(format!(
"Error deserializing concurrency_counter table content: {}",
err.to_string()
))
})?;
concurrency_groups.push(ConcurrencyGroups {
concurrency_id: concurrency_id.clone(),
job_uuids: job_uuids_map.keys().into_vec(),
})
}
return Ok(Json(concurrency_groups));
}
async fn delete_concurrency_group(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(concurrency_id): Path<String>,
) -> JsonResult<()> {
if !authed.is_admin {
return Err(PermissionDenied(
"Only administrators can delete concurrency groups".to_string(),
));
}
let mut tx = db.begin().await?;
let concurrency_group = sqlx::query_as::<_, (String, i64)>(
"SELECT concurrency_id, (select COUNT(*) from jsonb_object_keys(job_uuids)) as n_job_uuids FROM concurrency_counter WHERE concurrency_id = $1 FOR UPDATE",
)
.bind(concurrency_id.clone())
.fetch_optional(&mut *tx)
.await?;
let n_job_uuids = concurrency_group.map(|cg| cg.1).unwrap_or_default();
if n_job_uuids > 0 {
tx.commit().await?;
return Err(InternalErr(
"Concurrency group is currently in use, unable to remove it. Retry later.".to_string(),
));
}
sqlx::query!(
"DELETE FROM concurrency_counter WHERE concurrency_id = $1",
concurrency_id.clone(),
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(()))
}

View File

@@ -6,8 +6,6 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::collections::HashMap;
use crate::db::ApiAuthed;
use crate::{
db::DB,
@@ -26,7 +24,6 @@ use axum::{
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sql_builder::prelude::*;
use sql_builder::SqlBuilder;
use sqlx::{FromRow, Postgres, Transaction};
@@ -41,7 +38,7 @@ use windmill_common::{
scripts::Schema,
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, StripPath},
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::PushArgs;
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel, QueueTransaction};
pub fn workspaced_service() -> Router {
@@ -357,11 +354,6 @@ async fn create_flow(
)
.await?;
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
if let Some(dm) = nf.deployment_message {
args.insert("deployment_message".to_string(), json!(dm));
}
let tx = PushIsolationLevel::Transaction(tx);
let (dependency_job_uuid, mut new_tx) = push(
&db,
@@ -370,8 +362,9 @@ async fn create_flow(
JobPayload::FlowDependencies {
path: nf.path.clone(),
dedicated_worker: nf.dedicated_worker,
deployment_message: nf.deployment_message,
},
args,
PushArgs::empty(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
@@ -575,12 +568,6 @@ async fn update_flow(
let tx = PushIsolationLevel::Transaction(tx);
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
if let Some(dm) = nf.deployment_message {
args.insert("deployment_message".to_string(), json!(dm));
}
args.insert("parent_path".to_string(), json!(flow_path));
let (dependency_job_uuid, mut new_tx) = push(
&db,
tx,
@@ -588,8 +575,9 @@ async fn update_flow(
JobPayload::FlowDependencies {
path: nf.path.clone(),
dedicated_worker: nf.dedicated_worker,
deployment_message: nf.deployment_message,
},
args,
PushArgs::empty(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
@@ -720,10 +708,8 @@ struct Archived {
async fn archive_flow_by_path(
authed: ApiAuthed,
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> {
@@ -750,27 +736,6 @@ async fn archive_flow_by_path(
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Flow { path: path.to_string(), parent_path: Some(path.to_string()) },
Some(format!(
"Flow '{}' {}",
path,
if archived.archived.unwrap_or(true) {
"archived"
} else {
"unarchived"
}
)),
rsmq,
true,
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::ArchiveFlow { workspace: w_id, path: path.to_owned() },
@@ -781,9 +746,7 @@ async fn archive_flow_by_path(
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> {
@@ -817,32 +780,6 @@ async fn delete_flow_by_path(
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Flow { path: path.to_string(), parent_path: Some(path.to_string()) },
Some(format!("Flow '{}' deleted", path)),
rsmq,
true,
)
.await?;
sqlx::query!(
"DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND script_hash IS NULL and app_version IS NULL",
path,
w_id
)
.execute(&db)
.await
.map_err(|e| {
Error::InternalErr(format!(
"error deleting deployment metadata for script with path {path} in workspace {w_id}: {e}"
))
})?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteFlow { workspace: w_id, path: path.to_owned() },
@@ -880,7 +817,6 @@ mod tests {
)]
.into(),
hash: None,
tag_override: None,
},
stop_after_if: None,
summary: None,
@@ -949,7 +885,6 @@ mod tests {
path: "test".to_string(),
input_transforms: HashMap::new(),
hash: None,
tag_override: None,
},
stop_after_if: Some(StopAfterIf {
expr: "previous.isEmpty()".to_string(),
@@ -985,8 +920,7 @@ mod tests {
}
},
"type": "script",
"path": "test",
"tag_override": Option::<String>::None,
"path": "test"
},
},
{
@@ -1029,8 +963,7 @@ mod tests {
"value": {
"input_transforms": {},
"type": "script",
"path": "test",
"tag_override": Option::<String>::None,
"path": "test"
},
"stop_after_if": {
"expr": "previous.isEmpty()",

View File

@@ -32,7 +32,6 @@ use windmill_common::{
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Postgres, Transaction};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
pub fn workspaced_service() -> Router {
Router::new()
@@ -151,10 +150,8 @@ lazy_static! {
async fn create_folder(
authed: ApiAuthed,
Tokened { token }: Tokened,
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>,
@@ -169,56 +166,43 @@ async fn create_folder(
check_name_conflict(&mut tx, &w_id, &ng.name).await?;
cache.invalidate(&w_id, token).await;
let owner = username_to_permissioned_as(&authed.username);
let owners = ng.owners.unwrap_or_else(|| vec![owner.clone()]);
let owners = if owners.contains(&owner) {
owners.clone()
} else {
owners
.iter()
.cloned()
.chain(std::iter::once(owner))
.collect()
};
let owners = &ng.owners.unwrap_or_else(|| vec![owner.clone()]);
let mut extra_perms = ng
.extra_perms
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
if extra_perms.is_object() {
let extra_mut = extra_perms.as_object_mut().unwrap();
for o in &owners {
extra_mut.insert(o.clone(), serde_json::json!(true));
if let Some(extra_perms) = ng.extra_perms.clone() {
for o in owners {
if !extra_perms
.get(&o)
.and_then(|x| x.as_bool())
.unwrap_or(false)
{
return Err(windmill_common::error::Error::BadRequest(format!(
"Owner {} would not have permission to write to folder and that is an inconsistent state",
o
)));
}
}
} else {
return Err(windmill_common::error::Error::BadRequest(format!(
"extra_perms must be an object"
)));
}
let extra_perms = ng.extra_perms.unwrap_or_else(|| {
let mut map = serde_json::Map::new();
for o in owners {
map.insert(o.clone(), serde_json::json!(true));
}
serde_json::Value::Object(map)
});
sqlx::query_as!(
Folder,
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) VALUES ($1, $2, $3, $4, $5)",
w_id,
ng.name,
ng.display_name.unwrap_or(ng.name.clone()),
&owners,
owners,
extra_perms,
)
.execute(&mut *tx)
.await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Folder { path: format!("f/{}/folder.meta.*", ng.name) },
Some(format!("Folder '{}' created", ng.name)),
rsmq,
true,
)
.await?;
audit_log(
&mut *tx,
&authed.username,
@@ -466,9 +450,7 @@ async fn get_folder_usage(
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> {
@@ -495,18 +477,6 @@ async fn delete_folder(
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Folder { path: format!("f/{}/folder.meta.*", name) },
Some(format!("Folder '{}' deleted", name)),
rsmq,
true,
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteFolder { workspace: w_id, name: name.clone() },

View File

@@ -12,7 +12,6 @@ use axum::{
routing::{get, post},
Json, Router,
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use crate::db::ApiAuthed;
@@ -40,7 +39,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> {
@@ -81,66 +79,6 @@ async fn add_granular_acl(
let _ = not_found_if_none(obj_o, &kind, &path)?;
tx.commit().await?;
match kind {
"folder" => {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Folder { path: format!("f/{}/folder.meta.*", path) },
Some(format!("Folder '{}' changed permissions", path)),
rsmq,
true,
)
.await?
}
// "app" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::App { path: path.to_string(), parent_path: None, version: 0 },
// Some(format!("App '{}' changed permissions", path)),
// rsmq,
// true,
// )
// .await?
// }
// "script" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::Script {
// path: path.to_string(),
// parent_path: None,
// hash: ScriptHash(0),
// },
// Some(format!("Script '{}' changed permissions", path)),
// rsmq,
// true,
// )
// .await?
// }
// "flow" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::Flow { path: path.to_string(), parent_path: None },
// Some(format!("Flow '{}' changed permissions", path)),
// rsmq,
// true,
// )
// .await?
// }
_ => (),
}
Ok("Successfully modified granular acl".to_string())
}
@@ -148,7 +86,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> {
@@ -187,73 +124,13 @@ async fn remove_granular_acl(
))
.bind(owner)
.bind(path)
.bind(&w_id)
.bind(w_id)
.fetch_optional(&mut *tx)
.await?;
let _ = not_found_if_none(obj_o, &kind, &path)?;
tx.commit().await?;
match kind {
"folder" => {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Folder { path: format!("f/{}/folder.meta.*", path) },
Some(format!("Folder '{}' changed permissions", path)),
rsmq,
true,
)
.await?
}
// "app" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::App { path: path.to_string(), parent_path: None, version: 0 },
// Some(format!("App '{}' changed permissions", path)),
// rsmq,
// true,
// )
// .await?
// }
// "script" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::Script {
// path: path.to_string(),
// parent_path: None,
// hash: ScriptHash(0),
// },
// Some(format!("Script '{}' changed permissions", path)),
// rsmq,
// true,
// )
// .await?
// }
// "flow" => {
// handle_deployment_metadata(
// &authed.email,
// &authed.username,
// &db,
// &w_id,
// DeployedObject::Flow { path: path.to_string(), parent_path: None },
// Some(format!("Flow '{}' changed permissions", path)),
// rsmq,
// true,
// )
// .await?
// }
_ => (),
}
Ok("Successfully removed granular acl".to_string())
}

View File

@@ -43,7 +43,6 @@ pub fn global_service() -> Router {
.route("/list", get(list_igroups))
.route("/get/:name", get(get_igroup))
.route("/create", post(create_igroup))
.route("/update/:name", post(update_igroup))
.route("/delete/:name", delete(delete_igroup))
.route("/adduser/:name", post(add_user_igroup))
.route("/removeuser/:name", post(remove_user_igroup))
@@ -293,55 +292,14 @@ async fn create_igroup(
Ok(format!("Created group {}", ng.name))
}
#[derive(Deserialize)]
struct IGroupUpdate {
new_summary: String,
}
async fn update_igroup(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(name): Path<String>,
Json(igroup_update): Json<IGroupUpdate>,
) -> Result<String> {
require_super_admin(&db, &authed.email).await?;
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
let exists_opt = sqlx::query("SELECT 1 FROM instance_group WHERE name = $1")
.bind(name.clone())
.fetch_optional(&mut *tx)
.await?;
not_found_if_none(exists_opt, "instance_group", name.clone())?;
sqlx::query("UPDATE instance_group SET summary = $1 WHERE name = $2")
.bind(igroup_update.new_summary)
.bind(&name)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed.username,
"igroup.updated",
ActionKind::Delete,
"global",
Some(&name.to_string()),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Deleted group {}", name))
}
async fn delete_igroup(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(name): Path<String>,
Json(ng): Json<NewGroup>,
) -> Result<String> {
require_super_admin(&db, &authed.email).await?;
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
sqlx::query!("DELETE FROM instance_group WHERE name = $1", name)
sqlx::query!("DELETE FROM instance_group WHERE name = $1", ng.name,)
.execute(&mut *tx)
.await?;
@@ -351,13 +309,13 @@ async fn delete_igroup(
"igroup.delete",
ActionKind::Delete,
"global",
Some(&name.to_string()),
Some(&ng.name.to_string()),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Deleted group {}", name))
Ok(format!("Created group {}", ng.name))
}
pub async fn get_group_opt<'c>(
@@ -577,7 +535,6 @@ async fn add_user_igroup(
#[derive(Serialize)]
struct IGroup {
name: String,
summary: Option<String>,
emails: Option<Vec<String>>,
}
async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
@@ -585,7 +542,7 @@ async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
let groups = sqlx::query_as!(
IGroup,
"SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name"
"SELECT name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name"
)
.fetch_all(&mut *tx)
.await?;
@@ -597,7 +554,7 @@ async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
async fn get_igroup(Path(name): Path<String>, Extension(db): Extension<DB>) -> JsonResult<IGroup> {
let group = sqlx::query_as!(
IGroup,
"SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE name = $1 GROUP BY name",
"SELECT name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE name = $1 GROUP BY name",
name
)
.fetch_optional(&db)

View File

@@ -1,9 +1,7 @@
use std::time::{SystemTime, UNIX_EPOCH};
use std::{cmp, time::Duration};
use crate::{db::DB, resources::get_resource_value_interpolated_internal, users::Tokened};
use anyhow::Context;
use aws_sdk_s3::primitives::DateTime;
use aws_sdk_s3::{
presigning::PresigningConfig,
primitives::ByteStream,
@@ -29,10 +27,7 @@ use polars::{
prelude::CsvReader,
};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use tower_http::cors::{Any, CorsLayer};
use windmill_common::error::{Error, JsonResult};
use windmill_common::worker::to_raw_value;
use windmill_common::{
db::UserDB,
error,
@@ -81,14 +76,6 @@ pub fn workspaced_service() -> Router {
"/load_file_preview",
get(load_file_preview).layer(cors.clone()),
)
.route(
"/load_parquet_preview/*path",
get(load_parquet_preview).layer(cors.clone()),
)
.route(
"/generate_download_url",
get(generate_download_url).layer(cors.clone()),
)
.route(
"/delete_s3_file",
delete(delete_s3_file).layer(cors.clone()),
@@ -98,10 +85,6 @@ pub fn workspaced_service() -> Router {
"/multipart_upload_s3_file",
post(multipart_upload_s3_file).layer(cors.clone()),
)
.route(
"/multipart_download_s3_file",
post(multipart_download_s3_file).layer(cors.clone()),
)
}
#[derive(Deserialize)]
@@ -149,8 +132,8 @@ struct DuckdbConnectionSettingsQueryV2 {
async fn duckdb_connection_settings_v2(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Json(query): Json<DuckdbConnectionSettingsQueryV2>,
@@ -159,19 +142,15 @@ async fn duckdb_connection_settings_v2(
Some(s3_resource_path) => {
get_s3_resource(
&authed,
&user_db,
&db,
Some(user_db),
&token,
&w_id,
s3_resource_path.as_str(),
)
.await?
}
None => {
let (_, s3_resource_opt) =
get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id).await?;
s3_resource_opt
}
None => get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?,
};
let s3_resource = s3_resource_opt.ok_or(error::Error::NotFound(
"No datasets storage resource defined at the workspace level".to_string(),
@@ -246,8 +225,8 @@ struct PolarsStorageOptions {
async fn polars_connection_settings_v2(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Json(query): Json<PolarsConnectionSettingsQueryV2>,
@@ -256,19 +235,15 @@ async fn polars_connection_settings_v2(
Some(s3_resource_path) => {
get_s3_resource(
&authed,
&user_db,
&db,
Some(user_db),
&token,
&w_id,
s3_resource_path.as_str(),
)
.await?
}
None => {
let (_, s3_resource_opt) =
get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id).await?;
s3_resource_opt
}
None => get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?,
};
let s3_resource = s3_resource_opt.ok_or(error::Error::NotFound(
"No datasets storage resource defined at the workspace level".to_string(),
@@ -299,8 +274,8 @@ struct S3ResourceInfoQuery {
async fn s3_resource_info(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Json(query): Json<S3ResourceInfoQuery>,
@@ -309,19 +284,15 @@ async fn s3_resource_info(
Some(s3_resource_path) => {
get_s3_resource(
&authed,
&user_db,
&db,
Some(user_db),
&token,
&w_id,
s3_resource_path.as_str(),
)
.await?
}
None => {
let (_, s3_resource_opt) =
get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id).await?;
s3_resource_opt
}
None => get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?,
};
let s3_resource = s3_resource_opt.ok_or(error::Error::NotFound(
"No datasets storage resource defined at the workspace level".to_string(),
@@ -336,11 +307,12 @@ struct WindmillLargeFile {
async fn test_connection(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
) -> error::JsonResult<()> {
let (_, s3_resource_opt) = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
if s3_resource_opt.is_none() {
return Err(error::Error::NotFound(
"No datasets storage resource defined at the workspace level".to_string(),
@@ -372,28 +344,19 @@ struct ListStoredFilesQuery {
#[derive(Serialize)]
struct ListStoredDatasetsResponse {
pub restricted_access: Option<bool>,
windmill_large_files: Vec<WindmillLargeFile>,
pub next_marker: Option<String>,
}
async fn list_stored_files(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<ListStoredFilesQuery>,
) -> error::JsonResult<ListStoredDatasetsResponse> {
let (public_resource, s3_resource_opt) =
get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id).await?;
if !public_resource.unwrap_or(false) && s3_resource_opt.is_none() {
return Ok(Json(ListStoredDatasetsResponse {
windmill_large_files: vec![],
next_marker: None,
restricted_access: Some(true),
}));
}
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
@@ -445,8 +408,7 @@ async fn list_stored_files(
return Ok(Json(ListStoredDatasetsResponse {
windmill_large_files: stored_datasets,
next_marker,
restricted_access: Some(false),
next_marker: next_marker,
}));
}
@@ -489,6 +451,7 @@ struct LoadFilePreviewResponse {
pub content: Option<String>,
pub content_type: WindmillContentType,
pub msg: Option<String>,
pub download_url: Option<String>,
}
#[derive(Serialize)]
@@ -501,13 +464,14 @@ enum WindmillContentType {
async fn load_file_metadata(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<LoadFileMetadataQuery>,
) -> error::JsonResult<LoadFileMetadataResponse> {
let file_key = query.file_key.clone();
let (_, s3_resource_opt) = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
@@ -539,47 +503,9 @@ async fn load_file_metadata(
return Ok(Json(response));
}
#[derive(Deserialize)]
struct LoadParquetQuery {
limit: Option<u32>,
offset: Option<i64>,
sort_col: Option<String>,
sort_desc: Option<bool>,
search_col: Option<String>,
search_term: Option<String>,
}
async fn load_parquet_preview(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path((w_id, file_key)): Path<(String, String)>,
Query(query): Query<LoadParquetQuery>,
) -> error::JsonResult<Box<RawValue>> {
let (_, s3_resource_opt) = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
))?;
return read_s3_parquet_chunk(
&s3_resource,
&file_key,
query.limit,
query.offset,
query
.sort_col
.map(|v| (v.to_string(), query.sort_desc.unwrap_or(false))),
query
.search_col
.map(|v| (v.to_string(), query.search_term.unwrap_or_default())),
)
.await
.map(Json);
}
async fn load_file_preview(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
@@ -593,7 +519,7 @@ async fn load_file_preview(
}
let file_key = query.file_key.clone();
let (_, s3_resource_opt) = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
@@ -620,6 +546,19 @@ async fn load_file_preview(
)
};
// URL expires 30 minutes after its generation
let presigned_config = PresigningConfig::expires_in(Duration::from_secs(60 * 30))
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
let download_url = s3_client
.get_object()
.bucket(&s3_bucket)
.key(&file_key)
.presigned(presigned_config)
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?
.uri()
.to_string();
let file_chunk_length = if s3_object_content_length.is_some() {
cmp::min(
query.read_bytes_length,
@@ -705,59 +644,24 @@ async fn load_file_preview(
};
let response: LoadFilePreviewResponse = match content_preview {
Ok(content) => LoadFilePreviewResponse { content_type, content: Some(content), msg: None },
Ok(content) => LoadFilePreviewResponse {
content_type: content_type,
content: Some(content),
msg: None,
download_url: Some(download_url),
},
Err(err) => {
LoadFilePreviewResponse { content_type, content: None, msg: Some(err.to_string()) }
}
Err(err) => LoadFilePreviewResponse {
content_type: content_type,
content: None,
msg: Some(err.to_string()),
download_url: Some(download_url),
},
};
return Ok(Json(response));
}
#[derive(Deserialize)]
struct GenerateDownloadUrlQuery {
pub file_key: String,
}
#[derive(Serialize)]
struct GenerateDownloadUrlResponse {
pub download_url: String,
}
async fn generate_download_url(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<GenerateDownloadUrlQuery>,
) -> JsonResult<GenerateDownloadUrlResponse> {
let file_key = query.file_key.clone();
let (_, s3_resource_opt) = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
))?;
let s3_client = build_s3_client(&s3_resource);
let s3_bucket = s3_resource.bucket.clone();
// URL expires 5 minutes after its generation
let presigned_config = PresigningConfig::expires_in(Duration::from_secs(60 * 5))
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
let download_url = s3_client
.get_object()
.bucket(&s3_bucket)
.key(&file_key)
.presigned(presigned_config)
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?
.uri()
.to_string();
return Ok(Json(GenerateDownloadUrlResponse { download_url }));
}
#[derive(Deserialize)]
struct DeleteS3FileQuery {
pub file_key: String,
@@ -765,13 +669,14 @@ struct DeleteS3FileQuery {
async fn delete_s3_file(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<DeleteS3FileQuery>,
) -> error::JsonResult<()> {
let file_key = query.file_key.clone();
let (_, s3_resource_opt) = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
@@ -800,12 +705,13 @@ struct MoveS3FileQuery {
async fn move_s3_file(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Query(query): Query<MoveS3FileQuery>,
) -> error::JsonResult<()> {
let (_, s3_resource_opt) = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
@@ -839,100 +745,9 @@ async fn move_s3_file(
return Ok(Json(()));
}
#[derive(Deserialize)]
struct DownloadFileQuery {
pub file_key: String,
pub part_number: i64, // part number of the file to download. A file part are approx 5MB
pub file_size: Option<i64>, // leave empty for the first call, it will be returned in the response and ideally sent back in the next call
pub s3_resource_path: Option<String>,
}
#[derive(Serialize)]
struct DownloadFileResponse {
pub file_size: Option<i64>,
pub part_content: Vec<u8>,
pub next_part_number: Option<i64>, // is None when this is the last part being returned
}
async fn multipart_download_s3_file(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Json(query): Json<DownloadFileQuery>,
) -> error::JsonResult<DownloadFileResponse> {
let s3_resource_opt = match query.s3_resource_path.clone() {
Some(s3_resource_path) => {
get_s3_resource(
&authed,
&db,
Some(user_db),
&token,
&w_id,
s3_resource_path.as_str(),
)
.await?
}
None => {
let (_, s3_resource_opt) =
get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
s3_resource_opt
}
};
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
))?;
let bucket = s3_resource.bucket.clone();
let file_key = query.file_key.clone();
let s3_client = build_s3_client(&s3_resource);
let file_size = match query.file_size {
Some(fs) => Some(fs),
None => {
let s3_object_metadata = s3_client
.head_object()
.bucket(&bucket)
.key(&file_key)
.send()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
s3_object_metadata.content_length()
}
};
let chunk_size_bytes: i64 = 5 * 1024 * 1024;
let from_byte = query.part_number * chunk_size_bytes;
let (length, next_part_number) =
if file_size.is_some() && file_size.unwrap() - from_byte <= chunk_size_bytes {
(file_size.unwrap() - from_byte, None)
} else {
(chunk_size_bytes, Some(query.part_number + 1))
};
let payload =
read_object_chunk(&s3_client, bucket.as_str(), &file_key, from_byte, length).await?;
tracing::warn!(
"Reading chunk {} with length {} - payload size: {}",
from_byte,
length,
payload.len()
);
return Ok(Json(DownloadFileResponse {
file_size,
part_content: payload,
next_part_number,
}));
}
#[derive(Deserialize)]
struct UploadFileQuery {
pub file_key: Option<String>, // if none, the file will be placed in windmill_uploads/ with a random name.
pub file_extension: Option<String>, // preferred extension for the file in case a random name has to be generated
pub file_key: String,
pub part_content: Vec<u8>,
pub upload_id: Option<String>, // should be None for the first call to initiate the upload
@@ -940,9 +755,6 @@ struct UploadFileQuery {
pub is_final: bool, // whether it's the final chunk
pub cancel_upload: bool, // whether the upload should be cancelled. upload_id should be set. subsequent calls with this upload_id will fail
pub s3_resource_path: Option<String>, // custom S3 resource to use for this upload. It None, the workspace S3 resource will be used
pub file_expiration: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Deserialize, Serialize, Clone)]
@@ -956,13 +768,12 @@ struct UploadFileResponse {
pub upload_id: String,
pub parts: Vec<UploadFilePart>, // parts already uploaded, with their part_number and the tag associated
pub is_done: bool, // whether the transfer is finished, either b/c it got cancelled or because the last chunk was uploaded
pub file_key: String,
}
async fn multipart_upload_s3_file(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
Json(query): Json<UploadFileQuery>,
@@ -972,42 +783,8 @@ async fn multipart_upload_s3_file(
query.parts.len(),
query.is_final
);
let file_key = match query.file_key.clone() {
Some(fk) => fk,
None => {
// for now, we place all files into `windmill_uploads` folder with a random name
// TODO: make the folder configurable via the workspace settings
format!(
"windmill_uploads/upload_{}_{}.{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis(),
rand::random::<u16>(),
query.file_extension.clone().unwrap_or("file".to_string())
)
.to_string()
}
};
let s3_resource_opt = match query.s3_resource_path.clone() {
Some(s3_resource_path) => {
get_s3_resource(
&authed,
&db,
Some(user_db),
&token,
&w_id,
s3_resource_path.as_str(),
)
.await?
}
None => {
let (_, s3_resource_opt) =
get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
s3_resource_opt
}
};
let file_key = query.file_key.clone();
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
@@ -1029,10 +806,9 @@ async fn multipart_upload_s3_file(
error::Error::InternalErr(err.to_string())
})?;
return Ok(Json(UploadFileResponse {
upload_id,
upload_id: upload_id,
parts: vec![], // empty parts as the transfer has been cancelled
is_done: true,
file_key,
}));
}
@@ -1041,14 +817,10 @@ async fn multipart_upload_s3_file(
(upload_id, parts.len() + 1)
}
UploadFileQuery { upload_id: None, ref parts, .. } if parts.len() == 0 => {
let mut upload_builder = s3_client
let multipart_upload_res = s3_client
.create_multipart_upload()
.bucket(&bucket)
.key(&file_key);
if let Some(file_expiration) = query.file_expiration {
upload_builder = upload_builder.expires(DateTime::from_secs(file_expiration.timestamp()));
}
let multipart_upload_res = upload_builder
.key(&file_key)
.send()
.await
.map_err(|err| {
@@ -1103,7 +875,7 @@ async fn multipart_upload_s3_file(
let _complete_multipart_upload_res = s3_client
.complete_multipart_upload()
.bucket(&s3_resource.bucket)
.key(&file_key)
.key(&query.file_key)
.upload_id(&upload_id)
.multipart_upload(
CompletedMultipartUpload::builder()
@@ -1119,10 +891,9 @@ async fn multipart_upload_s3_file(
}
return Ok(Json(UploadFileResponse {
upload_id,
upload_id: upload_id,
parts: new_parts,
is_done: query.is_final,
file_key,
}));
}
@@ -1133,21 +904,23 @@ pub struct S3Object {
async fn get_workspace_s3_resource<'c>(
authed: &ApiAuthed,
user_db: &UserDB,
db: &DB,
user_db: Option<UserDB>,
token: &str,
w_id: &str,
) -> error::Result<(Option<bool>, Option<S3Resource>)> {
) -> error::Result<Option<S3Resource>> {
let mut tx = user_db.clone().begin(authed).await?;
let raw_lfs_opt = sqlx::query_scalar!(
"SELECT large_file_storage FROM workspace_settings WHERE workspace_id = $1",
w_id
)
.fetch_optional(db)
.fetch_optional(&mut *tx)
.await?
.flatten();
tx.commit().await?;
if raw_lfs_opt.is_none() {
return Ok((None, None));
return Ok(None);
}
let large_file_storage = serde_json::from_value::<LargeFileStorage>(
@@ -1166,39 +939,17 @@ async fn get_workspace_s3_resource<'c>(
LargeFileStorage::S3Storage(s3_lfs) => s3_lfs,
};
// if the resource is declared public, we replace user_db with None such that the resource info will be
// retrieved using `db` (and ACLs will be bypassed)
let effective_user_db = if user_db.is_some() && s3_lfs.public_resource.unwrap_or(false) {
None
} else {
user_db
};
let stripped_resource_path = match s3_lfs.s3_resource_path.strip_prefix("$res:") {
Some(stripped) => stripped,
None => s3_lfs.s3_resource_path.as_str(),
};
let s3_resource = match get_s3_resource(
authed,
db,
effective_user_db,
token,
w_id,
stripped_resource_path,
)
.await
{
Ok(s3_resource) => Ok(s3_resource),
Err(Error::NotAuthorized(_)) if !s3_lfs.public_resource.unwrap_or(false) => Ok(None),
Err(err) => Err(err),
};
return s3_resource.map(|res| (s3_lfs.public_resource, res));
return get_s3_resource(authed, user_db, db, token, w_id, stripped_resource_path).await;
}
async fn get_s3_resource<'c>(
authed: &ApiAuthed,
user_db: &UserDB,
db: &DB,
user_db: Option<UserDB>,
token: &str,
w_id: &str,
s3_resource_path: &str,
@@ -1250,16 +1001,16 @@ fn build_polars_s3_config(s3_resource_ref: &S3Resource) -> CloudOptions {
return CloudOptions::default().with_aws(s3_configs);
}
async fn read_object_chunk(
async fn read_s3_text_object_head(
s3_client: &aws_sdk_s3::Client,
s3_bucket: &str,
file_key: &str,
from_byte: i64,
from_char: i64,
length: i64,
) -> error::Result<Vec<u8>> {
) -> error::Result<String> {
let s3_object = s3_client
.get_object()
.range(format!("bytes={}-{}", from_byte, from_byte + length - 1).to_string())
.range(format!("bytes={}-{}", from_char, length).to_string())
.bucket(s3_bucket)
.key(file_key)
.send()
@@ -1283,17 +1034,7 @@ async fn read_object_chunk(
})?
.into_bytes()
.to_vec();
return Ok(payload);
}
async fn read_s3_text_object_head(
s3_client: &aws_sdk_s3::Client,
s3_bucket: &str,
file_key: &str,
from_byte: i64,
length: i64,
) -> error::Result<String> {
let payload = read_object_chunk(s3_client, s3_bucket, file_key, from_byte, length).await?;
let file_header_str = String::from_utf8(payload).map_err(|err| {
tracing::warn!(
"Encoding of file {} unsupported. Error was: {:?}",
@@ -1349,72 +1090,6 @@ async fn read_s3_parquet_object_head(
return polars_df_result;
}
async fn read_s3_parquet_chunk(
s3_resource_ref: &S3Resource,
file_key: &str,
limit: Option<u32>,
offset: Option<i64>,
sort: Option<(String, bool)>,
search: Option<(String, String)>,
) -> error::Result<Box<RawValue>> {
let s3_cloud_config = build_polars_s3_config(s3_resource_ref);
let args: ScanArgsParquet = ScanArgsParquet {
n_rows: None,
cache: false,
parallel: polars::io::parquet::ParallelStrategy::Auto,
rechunk: false,
row_count: None,
low_memory: false,
use_statistics: false,
hive_partitioning: false,
cloud_options: Some(s3_cloud_config),
};
let file_key_clone = file_key.to_string();
let s3_bucket_clone = s3_resource_ref.bucket.to_string();
return tokio::task::spawn_blocking(move || {
let s3_file_key = format!("s3://{}/{}", s3_bucket_clone, file_key_clone);
let lzdf_result = LazyFrame::scan_parquet(s3_file_key, args);
match lzdf_result {
Err(err) => {
tracing::warn!("Error fetching parquet file from S3: {:?}", err);
return Err(error::Error::InternalErr(err.to_string()));
}
Ok(lzdf) => {
let df = lzdf
.select(&[col("*")])
.slice(offset.unwrap_or(0), limit.unwrap_or(100));
let df = if let Some(sort) = sort {
df.sort(
&sort.0,
SortOptions {
descending: sort.1,
nulls_last: false,
multithreaded: false,
maintain_order: false,
},
)
} else {
df
};
use polars::prelude::*;
let df = if let Some(search) = search {
df.filter(col(&search.0).str().contains_literal(lit(search.1.clone())))
} else {
df
};
let df = df
.collect()
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
return Ok(to_raw_value(&df));
}
}
})
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
}
async fn csv_file_preview_with_fallback(
s3_client: &aws_sdk_s3::Client,
s3_bucket: &str,

View File

@@ -9,10 +9,7 @@
use axum::http::HeaderValue;
use serde_json::value::RawValue;
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use tokio::time::Instant;
use windmill_common::flow_status::RestartedFrom;
use windmill_common::variables::get_workspace_key;
use crate::db::ApiAuthed;
@@ -20,6 +17,7 @@ use crate::{
db::DB,
users::{check_scopes, require_owner_of_path, OptAuthed},
utils::require_super_admin,
variables::get_workspace_key,
};
use anyhow::Context;
use axum::{
@@ -51,40 +49,18 @@ use windmill_common::{
users::username_to_permissioned_as,
utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath},
};
use windmill_common::{
get_latest_deployed_hash_for_path, BASE_URL, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
};
use windmill_common::{get_latest_deployed_hash_for_path, BASE_URL};
use windmill_queue::{
add_completed_job_error, get_queued_job, get_result_by_id_from_running_flow, job_is_complete,
push, CanceledBy, PushArgs, PushIsolationLevel,
};
fn setup_list_jobs_debug_metrics() -> Option<prometheus::Histogram> {
let api_list_jobs_query_duration = if METRICS_DEBUG_ENABLED.load(Ordering::Relaxed)
&& METRICS_ENABLED.load(Ordering::Relaxed)
{
Some(
prometheus::register_histogram!(prometheus::HistogramOpts::new(
"api_list_jobs_query_duration",
"Duration of listing jobs (query)",
))
.expect("register prometheus metric"),
)
} else {
None
};
api_list_jobs_query_duration
}
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST])
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
.allow_origin(Any);
let api_list_jobs_query_duration = setup_list_jobs_debug_metrics();
Router::new()
.route(
"/run/f/*script_path",
@@ -135,10 +111,7 @@ pub fn workspaced_service() -> Router {
.route("/run/preview", post(run_preview_job))
.route("/add_batch_jobs/:n", post(add_batch_jobs))
.route("/run/preview_flow", post(run_preview_flow_job))
.route(
"/list",
get(list_jobs).layer(Extension(api_list_jobs_query_duration)),
)
.route("/list", get(list_jobs))
.route("/queue/list", get(list_queue_jobs))
.route("/queue/count", get(count_queue_jobs))
.route("/queue/cancel_all", post(cancel_all))
@@ -179,7 +152,6 @@ pub fn workspaced_service() -> Router {
"/result_by_id/:job_id/:node_id",
get(get_result_by_id).layer(cors.clone()),
)
.route("/run/dependencies", post(run_dependencies_job))
}
pub fn global_service() -> Router {
@@ -820,7 +792,6 @@ async fn list_jobs(
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListCompletedQuery>,
Extension(api_list_jobs_query_duration): Extension<Option<prometheus::Histogram>>,
) -> error::JsonResult<Vec<Job>> {
check_scopes(&authed, || format!("listjobs"))?;
@@ -950,18 +921,8 @@ async fn list_jobs(
sqlc.unwrap().query()?
};
let mut tx = user_db.begin(&authed).await?;
let start = Instant::now();
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql).fetch_all(&mut *tx).await?;
tx.commit().await?;
if let Some(api_list_jobs_query_duration) = api_list_jobs_query_duration {
let duration = start.elapsed().as_secs_f64();
api_list_jobs_query_duration.observe(duration);
tracing::info!("list_jobs query took {}s: {}", duration, sql);
}
Ok(Json(jobs.into_iter().map(From::from).collect()))
}
@@ -1013,7 +974,7 @@ pub async fn resume_suspended_job(
let flow_status = parent_flow
.flow_status()
.ok_or_else(|| anyhow::anyhow!("unable to find the flow status in the flow job"))?;
conditionally_require_authed_user(authed.clone(), flow_status)?;
conditionally_require_authed_user(authed, flow_status)?;
let exists = sqlx::query_scalar!(
r#"
@@ -1029,22 +990,12 @@ pub async fn resume_suspended_job(
return Err(anyhow::anyhow!("resume request already sent").into());
}
let approver = if authed.as_ref().is_none()
|| (approver
.approver
.clone()
.is_some_and(|x| x != "".to_string()))
{
approver.approver
} else {
authed.map(|x| x.username)
};
insert_resume_job(
resume_id,
job_id,
&parent_flow_info,
value,
approver,
approver.approver,
&mut tx,
)
.await?;
@@ -1617,7 +1568,6 @@ struct Preview {
language: Option<ScriptLang>,
tag: Option<String>,
dedicated_worker: Option<bool>,
lock: Option<String>,
}
#[derive(Deserialize)]
@@ -1995,10 +1945,10 @@ pub struct WindmillCompositeResult {
windmill_content_type: Option<String>,
result: Option<Box<RawValue>>,
}
async fn run_wait_result(
async fn run_wait_result<T>(
db: &DB,
uuid: Uuid,
w_id: String,
Path((w_id, _)): Path<(String, T)>,
node_id_for_empty_return: Option<String>,
) -> error::Result<Response> {
let mut result;
@@ -2233,7 +2183,7 @@ pub async fn run_wait_result_job_by_path_get(
.await?;
tx.commit().await?;
let wait_result = run_wait_result(&db, uuid, w_id, None).await;
let wait_result = run_wait_result(&db, uuid, Path((w_id, script_path)), None).await;
if delete_after_use.unwrap_or(false) {
delete_job_metadata_after_use(&db, uuid).await?;
}
@@ -2352,7 +2302,7 @@ async fn run_wait_result_script_by_path_internal(
.await?;
tx.commit().await?;
let wait_result = run_wait_result(&db, uuid, w_id, None).await;
let wait_result = run_wait_result(&db, uuid, Path((w_id, script_path)), None).await;
if delete_after_use.unwrap_or(false) {
delete_job_metadata_after_use(&db, uuid).await?;
}
@@ -2427,7 +2377,7 @@ pub async fn run_wait_result_script_by_hash(
.await?;
tx.commit().await?;
let wait_result = run_wait_result(&db, uuid, w_id, None).await;
let wait_result = run_wait_result(&db, uuid, Path((w_id, script_hash)), None).await;
if delete_after_use.unwrap_or(false) {
delete_job_metadata_after_use(&db, uuid).await?;
}
@@ -2509,7 +2459,7 @@ async fn run_wait_result_flow_by_path_internal(
.await?;
tx.commit().await?;
run_wait_result(&db, uuid, w_id, early_return).await
run_wait_result(&db, uuid, Path((w_id, flow_path)), early_return).await
}
async fn run_preview_job(
@@ -2546,7 +2496,7 @@ async fn run_preview_job(
content: preview.content.unwrap_or_default(),
path: preview.path,
language: preview.language.unwrap_or(ScriptLang::Deno),
lock: preview.lock,
lock: None,
concurrent_limit: None, // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here
concurrency_time_window_s: None, // TODO(gbouv): same as above
cache_ttl: None,
@@ -2577,99 +2527,6 @@ async fn run_preview_job(
Ok((StatusCode::CREATED, uuid.to_string()))
}
#[derive(Deserialize)]
pub struct RunDependenciesRequest {
pub raw_scripts: Vec<RawScriptForDependencies>,
pub entrypoint: String,
pub raw_deps: Option<String>,
}
#[derive(Deserialize, Clone)]
pub struct RawScriptForDependencies {
pub script_path: String,
pub raw_code: Option<String>,
pub language: ScriptLang,
}
#[derive(Serialize)]
pub struct RunDependenciesResponse {
pub dependencies: String,
}
pub 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> {
check_scopes(&authed, || format!("runscript"))?;
if authed.is_operator {
return Err(error::Error::NotAuthorized(
"Operators cannot run dependencies jobs for security reasons".to_string(),
));
}
if req.raw_scripts.len() != 1 || req.raw_scripts[0].script_path != req.entrypoint {
return Err(error::Error::InternalErr(
"For now only a single raw script can be passed to this endpoint, and the entrypoint should be set to the script path".to_string(),
));
}
let raw_script = req.raw_scripts[0].clone();
let script_path = raw_script.script_path;
let (args, raw_code) = if let Some(deps) = req.raw_deps {
let mut hm = HashMap::new();
hm.insert(
"raw_deps".to_string(),
JsonRawValue::from_string("true".to_string()).unwrap(),
);
(
PushArgs { extra: hm, args: sqlx::types::Json(HashMap::new()) },
deps,
)
} else {
(
PushArgs::empty(),
raw_script.raw_code.unwrap_or_else(|| "".to_string()),
)
};
let language = raw_script.language;
let (uuid, tx) = push(
&db,
PushIsolationLevel::IsolatedRoot(db.clone(), rsmq),
&w_id,
JobPayload::RawScriptDependencies {
script_path: script_path,
content: raw_code,
language: language,
},
args,
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
None,
None,
None,
None,
None,
false,
false,
None,
true,
None,
None,
None,
None,
)
.await?;
tx.commit().await?;
let wait_result = run_wait_result(&db, uuid, w_id, None).await;
wait_result
}
#[derive(Deserialize)]
struct BatchInfo {
kind: String,
@@ -3196,12 +3053,6 @@ pub struct RawResult<'a> {
pub result: &'a JsonRawValue,
}
#[derive(FromRow)]
pub struct RawResultWithSuccess<'a> {
pub result: &'a JsonRawValue,
pub success: bool,
}
impl<'a> IntoResponse for RawResult<'a> {
fn into_response(self) -> Response {
Json(self.result).into_response()
@@ -3242,7 +3093,6 @@ async fn get_completed_job_result(
#[derive(Serialize)]
struct CompletedJobResult<'c> {
started: Option<bool>,
success: Option<bool>,
completed: bool,
result: Option<&'c JsonRawValue>,
}
@@ -3257,19 +3107,17 @@ async fn get_completed_job_result_maybe(
Path((w_id, id)): Path<(String, Uuid)>,
Query(GetCompletedJobQuery { get_started }): Query<GetCompletedJobQuery>,
) -> error::Result<Response> {
let result_o = sqlx::query(
"SELECT result, success FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(&w_id)
.fetch_optional(&db)
.await?;
let result_o =
sqlx::query("SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(&w_id)
.fetch_optional(&db)
.await?;
if let Some(result) = result_o {
let res = RawResultWithSuccess::from_row(&result)?;
let res = RawResult::from_row(&result)?;
Ok(Json(CompletedJobResult {
started: Some(true),
success: Some(res.success),
completed: true,
result: Some(res.result),
})
@@ -3283,21 +3131,15 @@ async fn get_completed_job_result_maybe(
.fetch_optional(&db)
.await?
.unwrap_or(false);
Ok(Json(CompletedJobResult {
started: Some(started),
completed: false,
success: None,
result: None,
})
.into_response())
Ok(
Json(CompletedJobResult { started: Some(started), completed: false, result: None })
.into_response(),
)
} else {
Ok(Json(CompletedJobResult {
started: None,
completed: false,
success: None,
result: None,
})
.into_response())
Ok(
Json(CompletedJobResult { started: None, completed: false, result: None })
.into_response(),
)
}
}

View File

@@ -63,9 +63,6 @@ pub mod job_helpers;
pub mod job_metrics;
pub mod jobs;
pub mod oauth2;
mod concurrency_groups;
mod oidc;
mod openai;
mod raw_apps;
mod resources;
@@ -215,8 +212,7 @@ pub async fn run_server(
users::workspaced_service().layer(Extension(argon2.clone())),
)
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc::workspaced_service()),
.nest("/workspaces", workspaces::workspaced_service()),
)
.nest("/workspaces", workspaces::global_service())
.nest(
@@ -239,7 +235,6 @@ pub async fn run_server(
.route_layer(from_extractor::<ApiAuthed>())
.route_layer(from_extractor::<users::Tokened>())
.nest("/jobs", jobs::global_root_service())
.nest("/oidc", oidc::global_service())
.nest(
"/saml",
saml::global_service().layer(Extension(Arc::new(sp_extension.0))),
@@ -248,7 +243,6 @@ pub async fn run_server(
"/scim",
scim::global_service().route_layer(axum::middleware::from_fn(has_scim_token)),
)
.nest("/concurrency_groups", concurrency_groups::global_service())
.nest("/scripts_u", scripts::global_unauthed_service())
.nest(
"/w/:workspace_id/apps_u",

View File

@@ -38,13 +38,16 @@ use windmill_common::jobs::JobPayload;
use windmill_common::more_serde::maybe_number_opt;
use windmill_common::users::username_to_permissioned_as;
use windmill_common::utils::{not_found_if_none, now_from_db};
use windmill_common::variables::build_crypt;
use crate::db::ApiAuthed;
use crate::saml::SamlSsoLogin;
use crate::users::{login_externally, LoginUserInfo};
use crate::webhook_util::{InstanceEvent, WebhookShared};
use crate::{db::DB, variables::encrypt, workspaces::WorkspaceSettings};
use crate::{
db::DB,
variables::{build_crypt, encrypt},
workspaces::WorkspaceSettings,
};
use crate::{BASE_URL, HTTP_CLIENT, IS_SECURE, OAUTH_CLIENTS, SLACK_SIGNING_SECRET};
use windmill_common::error::{self, to_anyhow, Error};
use windmill_common::oauth2::*;
@@ -382,6 +385,7 @@ async fn connect(
#[derive(Deserialize)]
struct CreateAccount {
client: String,
owner: String,
refresh_token: Option<String>,
expires_in: i64,
}
@@ -395,10 +399,11 @@ async fn create_account(
let mut tx = user_db.begin(&authed).await?;
let id = sqlx::query_scalar!(
"INSERT INTO account (workspace_id, client, expires_at, refresh_token) VALUES ($1, \
$2, now() + ($3 || ' seconds')::interval, $4) RETURNING id",
"INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, \
$2, $3, now() + ($4 || ' seconds')::interval, $5) RETURNING id",
w_id,
payload.client,
payload.owner,
payload.expires_in.to_string(),
payload.refresh_token
)
@@ -732,7 +737,7 @@ async fn connect_slack_callback(
"INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
w_id,
"slack",
"The group slack commands act on behalf of",
"The group slack commands act on belhalf of",
serde_json::json!({username_to_permissioned_as(&authed.username): true})
)
.execute(&mut *tx)

View File

@@ -1,305 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2023
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use std::process::Command;
use anyhow;
#[cfg(feature = "enterprise")]
use openidconnect::{
core::{
CoreClaimName, CoreJsonWebKeySet, CoreJwsSigningAlgorithm, CoreProviderMetadata,
CoreResponseType, CoreRsaPrivateSigningKey, CoreSubjectIdentifierType,
},
AdditionalClaims, AuthUrl, EmptyAdditionalProviderMetadata, IssuerUrl, JsonWebKeyId,
JsonWebKeySetUrl, ResponseTypes,
};
#[cfg(feature = "enterprise")]
impl AdditionalClaims for JobClaim {}
use crate::db::DB;
use axum::extract::Path;
use axum::routing::{get, post};
use axum::Extension;
use axum::{Json, Router};
use serde::{Deserialize, Serialize};
#[cfg(feature = "enterprise")]
pub fn global_service() -> Router {
Router::new()
.route(
"/.well-known/openid-configuration",
get(openid_configuration),
)
.route("/jwks", get(jwks))
}
#[cfg(not(feature = "enterprise"))]
pub fn global_service() -> Router {
Router::new()
}
#[cfg(not(feature = "enterprise"))]
pub fn workspaced_service() -> Router {
Router::new()
}
#[cfg(feature = "enterprise")]
pub fn workspaced_service() -> Router {
Router::new().route("/token/:audience", post(gen_token))
}
#[derive(Debug, Clone, serde::Serialize)]
struct Keys {
private_key: String,
}
async fn gen_pems(db: &DB) -> anyhow::Result<Keys> {
let private_key_cmd = Command::new("openssl")
.arg("genrsa")
.arg("--traditional")
.arg("2048")
.output()
.expect("failed to execute process");
let private_key = String::from_utf8(private_key_cmd.stdout).unwrap();
tracing::debug!("Generated private key: {}", private_key);
let keys = Keys { private_key };
sqlx::query!(
"INSERT INTO global_settings (name, value) VALUES ('rsa_keys', $1)",
serde_json::to_value(&keys).unwrap()
)
.execute(db)
.await?;
Ok(keys)
}
#[cfg(feature = "enterprise")]
async fn get_private_key(db: &DB) -> anyhow::Result<String> {
let key = sqlx::query_scalar!(
"SELECT value->>'private_key' FROM global_settings WHERE name = 'rsa_keys'",
)
.fetch_optional(db)
.await?
.flatten();
if let Some(key) = key {
return Ok(key);
} else {
let keys = gen_pems(db).await?;
return Ok(keys.private_key);
}
}
#[cfg(feature = "enterprise")]
pub async fn jwks(
Extension(db): Extension<DB>,
) -> windmill_common::error::JsonResult<CoreJsonWebKeySet> {
use openidconnect::PrivateSigningKey;
let private_key = get_private_key(&db).await?;
let jwks = CoreJsonWebKeySet::new(vec![CoreRsaPrivateSigningKey::from_pem(
&private_key,
Some(JsonWebKeyId::new("windmill".to_string())),
)
.map_err(|e| anyhow::anyhow!("Failed to parse PEM: {}", e))?
.as_verification_key()]);
Ok(Json(jwks))
}
#[cfg(feature = "enterprise")]
pub async fn openid_configuration() -> windmill_common::error::JsonResult<CoreProviderMetadata> {
use windmill_common::BASE_URL;
let base_url = BASE_URL.read().await.clone();
return get_provider_metadata(base_url)
.map(Json)
.map_err(|e| e.into());
}
#[cfg(feature = "enterprise")]
pub fn get_provider_metadata(base_url: String) -> anyhow::Result<CoreProviderMetadata> {
let provider_metadata = CoreProviderMetadata::new(
IssuerUrl::new(format!("{base_url}/api/oidc/"))?,
AuthUrl::new(format!("{base_url}/api/oidc/"))?,
JsonWebKeySetUrl::new(format!("{base_url}/api/oidc/jwks"))?,
vec![
// Optional: support the implicit flow.
ResponseTypes::new(vec![CoreResponseType::Token, CoreResponseType::IdToken]), // Other flows including hybrid flows may also be specified here.
],
vec![CoreSubjectIdentifierType::Public],
vec![CoreJwsSigningAlgorithm::RsaSsaPssSha256],
EmptyAdditionalProviderMetadata {},
)
// Recommended: specify the supported ID token claims.
.set_claims_supported(Some(vec![
// Providers may also define an enum instead of using CoreClaimName.
CoreClaimName::new("sub".to_string()),
CoreClaimName::new("aud".to_string()),
CoreClaimName::new("email".to_string()),
CoreClaimName::new("email_verified".to_string()),
CoreClaimName::new("exp".to_string()),
CoreClaimName::new("iat".to_string()),
CoreClaimName::new("iss".to_string()),
CoreClaimName::new("job_id".to_string()),
CoreClaimName::new("path".to_string()),
CoreClaimName::new("flow_path".to_string()),
CoreClaimName::new("groups".to_string()),
CoreClaimName::new("username".to_string()),
CoreClaimName::new("workspace".to_string()),
]));
return Ok(provider_metadata);
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
struct JobClaim {
job_id: String,
path: Option<String>,
flow_path: Option<String>,
groups: Vec<String>,
username: String,
email: String,
workspace: String,
}
use crate::db::ApiAuthed;
use crate::users::Tokened;
#[cfg(feature = "enterprise")]
pub async fn gen_token(
authed: ApiAuthed,
token: Tokened,
Extension(db): Extension<DB>,
Path((w_id, audience)): Path<(String, String)>,
) -> windmill_common::error::Result<String> {
use chrono::{Duration, Utc};
use openidconnect::{
core::{CoreGenderClaim, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm},
Audience, EndUserEmail, IdToken, IdTokenClaims, StandardClaims, SubjectIdentifier,
};
use windmill_queue::get_queued_job;
use crate::users::get_groups_for_user;
let private_key = get_private_key(&db).await?;
let username = authed.username;
let email = authed.email;
let job_id = {
let job = sqlx::query_scalar!("SELECT job FROM token WHERE token = $1", token.token)
.fetch_optional(&db)
.await?
.flatten();
if job.is_none() {
return Err(anyhow::anyhow!("Token not found").into());
} else {
job.unwrap()
}
};
let mut tx = db.begin().await?;
let job = get_queued_job(job_id, &w_id, &mut tx).await?;
tx.commit().await?;
let job = job.ok_or_else(|| anyhow::anyhow!("Queued job {} not found", job_id))?;
let issue_url = format!("{}/api/oidc/", crate::BASE_URL.read().await.clone());
let flow_path = if let Some(uuid) = job.parent_job {
sqlx::query_scalar!("SELECT script_path FROM queue WHERE id = $1", uuid)
.fetch_optional(&db)
.await?
.flatten()
} else {
None
};
let groups = get_groups_for_user(&w_id, &username, &email, &db)
.await
.ok()
.unwrap_or_default();
let id_token = IdToken::<
JobClaim,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
CoreJsonWebKeyType,
>::new(
IdTokenClaims::<JobClaim, CoreGenderClaim>::new(
// Specify the issuer URL for the OpenID Connect Provider.
IssuerUrl::new(issue_url)
.map_err(|e| anyhow::anyhow!("Failed to generate IssueUrl: {}", e))?,
// The audience is usually a single entry with the client ID of the client for whom
// the ID token is intended. This is a required claim.
vec![Audience::new(audience)],
// The ID token expiration is usually much shorter than that of the access or refresh
// tokens issued to clients.
Utc::now() + Duration::hours(48),
// The issue time is usually the current time.
Utc::now(),
// Set the standard claims defined by the OpenID Connect Core spec.
StandardClaims::new(
// Stable subject identifiers are recommended in place of e-mail addresses or other
// potentially unstable identifiers. This is the only required claim.
SubjectIdentifier::new(format!(
"{}::{}::{}::{}",
email,
job.script_path
.clone()
.unwrap_or_else(|| "no_path".to_string()),
flow_path.clone().unwrap_or_else(|| "no_flow".to_string()),
w_id
)),
)
// Optional: specify the user's e-mail address. This should only be provided if the
// client has been granted the 'profile' or 'email' scopes.
.set_email(Some(EndUserEmail::new(job.email.clone())))
// Optional: specify whether the provider has verified the user's e-mail address.
.set_email_verified(Some(true)),
// OpenID Connect Providers may supply custom claims by providing a struct that
// implements the AdditionalClaims trait. This requires manually using the
// generic IdTokenClaims struct rather than the CoreIdTokenClaims type alias,
// however.
JobClaim {
job_id: job_id.to_string(),
path: job.script_path,
flow_path,
username: job.created_by,
email: job.email,
workspace: job.workspace_id,
groups,
},
),
// The private key used for signing the ID token. For confidential clients (those able
// to maintain a client secret), a CoreHmacKey can also be used, in conjunction
// with one of the CoreJwsSigningAlgorithm::HmacSha* signing algorithms. When using an
// HMAC-based signing algorithm, the UTF-8 representation of the client secret should
// be used as the HMAC key.
&CoreRsaPrivateSigningKey::from_pem(
&private_key,
Some(JsonWebKeyId::new("windmill".to_string())),
)
.map_err(|e| anyhow::anyhow!("Invalid private key: {}", e))?,
// Uses the RS256 signature algorithm. This crate supports any RS*, PS*, or HS*
// signature algorithm.
CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,
// When returning the ID token alongside an access token (e.g., in the Authorization Code
// flow), it is recommended to pass the access token here to set the `at_hash` claim
// automatically.
None,
// When returning the ID token alongside an authorization code (e.g., in the implicit
// flow), it is recommended to pass the authorization code here to set the `c_hash` claim
// automatically.
None,
)
.map_err(|e| anyhow::anyhow!("Failed to generate token: {}", e))?;
Ok(id_token.to_string())
}

View File

@@ -2,6 +2,7 @@ use std::collections::HashMap;
use crate::{
db::{ApiAuthed, DB},
variables::build_crypt,
HTTP_CLIENT,
};
@@ -16,10 +17,7 @@ use magic_crypt::MagicCryptTrait;
use quick_cache::sync::Cache;
use serde_json::value::RawValue;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{to_anyhow, Error},
variables::build_crypt,
};
use windmill_common::error::{to_anyhow, Error};
use serde::Deserialize;

View File

@@ -401,7 +401,7 @@ async fn get_resource_value_interpolated(
) -> JsonResult<Option<serde_json::Value>> {
return get_resource_value_interpolated_internal(
&authed,
Some(user_db),
&user_db,
&db,
w_id.as_str(),
path.to_path(),
@@ -416,14 +416,14 @@ use async_recursion::async_recursion;
pub async fn get_resource_value_interpolated_internal(
authed: &ApiAuthed,
user_db: Option<UserDB>, // if none, no permission will be checked to access the resource
user_db: &UserDB,
db: &DB,
workspace: &str,
path: &str,
job_id: Option<Uuid>,
token: &str,
) -> Result<Option<serde_json::Value>> {
let mut tx = authed_transaction_or_default(authed, user_db.clone(), db).await?;
let mut tx = user_db.clone().begin(authed).await?;
let value_o = sqlx::query_scalar!(
"SELECT value from resource WHERE path = $1 AND workspace_id = $2",
@@ -440,16 +440,7 @@ pub async fn get_resource_value_interpolated_internal(
let value = not_found_if_none(value_o, "Resource", path)?;
if let Some(value) = value {
Ok(Some(
transform_json_value(
authed,
user_db.clone(),
db,
workspace,
value,
&job_id,
token,
)
.await?,
transform_json_value(authed, user_db, db, workspace, value, &job_id, token).await?,
))
} else {
Ok(None)
@@ -459,7 +450,7 @@ pub async fn get_resource_value_interpolated_internal(
#[async_recursion]
pub async fn transform_json_value<'c>(
authed: &ApiAuthed,
user_db: Option<UserDB>, // if none, no permission will be checked to access the resources/variables
user_db: &UserDB,
db: &DB,
workspace: &str,
v: Value,
@@ -469,19 +460,9 @@ pub async fn transform_json_value<'c>(
match v {
Value::String(y) if y.starts_with("$var:") => {
let path = y.strip_prefix("$var:").unwrap();
let tx: Transaction<'_, Postgres> =
authed_transaction_or_default(authed, user_db.clone(), db).await?;
let v = crate::variables::get_value_internal(
tx,
db,
workspace,
path,
user_db
.clone()
.map(|_| authed.username.as_str())
.unwrap_or("backend"),
)
.await?;
let tx: Transaction<'_, Postgres> = user_db.clone().begin(authed).await?;
let v = crate::variables::get_value_internal(tx, db, workspace, path, &authed.username)
.await?;
Ok(Value::String(v))
}
Value::String(y) if y.starts_with("$res:") => {
@@ -489,8 +470,7 @@ pub async fn transform_json_value<'c>(
if path.split("/").count() < 2 {
return Err(Error::InternalErr(format!("Invalid resource path: {path}")));
}
let mut tx: Transaction<'_, Postgres> =
authed_transaction_or_default(authed, user_db.clone(), db).await?;
let mut tx: Transaction<'_, Postgres> = user_db.clone().begin(authed).await?;
let v = sqlx::query_scalar!(
"SELECT value from resource WHERE path = $1 AND workspace_id = $2",
path,
@@ -501,13 +481,13 @@ pub async fn transform_json_value<'c>(
tx.commit().await?;
let v = not_found_if_none(v, "Resource", path)?;
if let Some(v) = v {
transform_json_value(authed, user_db.clone(), db, workspace, v, job_id, token).await
transform_json_value(authed, user_db, db, workspace, v, job_id, token).await
} else {
Ok(Value::Null)
}
}
Value::String(y) if y.starts_with("$") && job_id.is_some() => {
let mut tx = authed_transaction_or_default(authed, user_db.clone(), db).await?;
let mut tx = user_db.clone().begin(authed).await?;
let job = sqlx::query_as::<_, QueuedJob>(
"SELECT * FROM queue WHERE id = $1 AND workspace_id = $2",
)
@@ -520,8 +500,7 @@ pub async fn transform_json_value<'c>(
let job = not_found_if_none(job, "Job", job_id.unwrap().to_string())?;
let flow_path = if let Some(uuid) = job.parent_job {
let mut tx: Transaction<'_, Postgres> =
authed_transaction_or_default(authed, user_db.clone(), db).await?;
let mut tx: Transaction<'_, Postgres> = user_db.clone().begin(authed).await?;
let p = sqlx::query_scalar!("SELECT script_path FROM queue WHERE id = $1", uuid)
.fetch_optional(&mut *tx)
.await?
@@ -544,8 +523,6 @@ pub async fn transform_json_value<'c>(
flow_path,
job.schedule_path.clone(),
job.flow_step_id.clone(),
job.root_job.map(|x| x.to_string()),
None,
)
.await;
@@ -562,8 +539,7 @@ pub async fn transform_json_value<'c>(
for (a, b) in m.clone().into_iter() {
m.insert(
a.clone(),
transform_json_value(authed, user_db.clone(), db, workspace, b, job_id, token)
.await?,
transform_json_value(authed, user_db, db, workspace, b, job_id, token).await?,
);
}
Ok(Value::Object(m))
@@ -572,18 +548,6 @@ pub async fn transform_json_value<'c>(
}
}
async fn authed_transaction_or_default<'c>(
authed: &ApiAuthed,
user_db: Option<UserDB>,
db: &DB,
) -> sqlx::error::Result<Transaction<'c, Postgres>> {
if let Some(user_db) = user_db {
user_db.begin(authed).await
} else {
db.clone().begin().await
}
}
async fn check_path_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,

View File

@@ -69,7 +69,6 @@ pub struct NewSchedule {
pub on_recovery_extra_args: Option<serde_json::Value>,
pub ws_error_handler_muted: Option<bool>,
pub retry: Option<serde_json::Value>,
pub tag: Option<String>,
}
#[derive(Serialize, Deserialize)]
@@ -156,9 +155,9 @@ async fn create_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, \
ws_error_handler_muted, retry, summary, no_flow_overlap, tag \
ws_error_handler_muted, retry, summary, no_flow_overlap \
) VALUES ( \
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22 \
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 \
) RETURNING *",
w_id,
ns.path,
@@ -181,7 +180,6 @@ async fn create_schedule(
ns.retry,
ns.summary,
ns.no_flow_overlap.unwrap_or(false),
ns.tag,
)
.fetch_one(&mut tx)
.await
@@ -235,9 +233,8 @@ async fn edit_schedule(
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, ws_error_handler_muted = $11, retry = $12, summary = $13, \
no_flow_overlap = $14, tag = $15
WHERE path = $16 AND workspace_id = $17 RETURNING *",
on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14 \
WHERE path = $15 AND workspace_id = $16 RETURNING *",
es.schedule,
es.timezone,
es.args,
@@ -252,7 +249,6 @@ async fn edit_schedule(
es.retry,
es.summary,
es.no_flow_overlap.unwrap_or(false),
es.tag,
path,
w_id,
)
@@ -348,7 +344,6 @@ pub struct ScheduleWJobs {
pub jobs: Option<Vec<serde_json::Value>>,
pub summary: Option<String>,
pub no_flow_overlap: bool,
pub tag: Option<String>,
}
async fn list_schedule_with_jobs(
@@ -697,7 +692,6 @@ pub struct EditSchedule {
pub ws_error_handler_muted: Option<bool>,
pub retry: Option<serde_json::Value>,
pub no_flow_overlap: Option<bool>,
pub tag: Option<String>,
}
pub async fn clear_schedule<'c>(

View File

@@ -143,7 +143,7 @@ pub async fn get_users(
Extension(db): Extension<DB>,
Query(query): Query<ScimQuery>,
) -> Result<JsonScim<serde_json::Value>> {
let mut sqlb = SqlBuilder::select_from("password")
let mut sqlb = SqlBuilder::select_from("usr")
.fields(&["email"])
.limit(query.count.unwrap_or(100000))
.offset(query.startIndex.map(|x| x - 1).unwrap_or(0))
@@ -200,30 +200,24 @@ pub async fn create_user(
pub async fn get_groups(
Extension(db): Extension<DB>,
Query(query): Query<ScimQuery>,
) -> Result<Json<Vec<serde_json::Value>>> {
) -> Result<JsonScim<serde_json::Value>> {
let sqlb = SqlBuilder::select_from("instance_group")
.fields(&[
"name",
"external_id",
"scim_display_name",
"array_remove(array_agg(email_to_igroup.email), null) as emails",
])
.right()
.join("email_to_igroup")
.on("instance_group.name = email_to_igroup.igroup")
.group_by("name, external_id")
.fields(&["name"])
.limit(query.count.unwrap_or(100000))
.offset(query.startIndex.map(|x| x - 1).unwrap_or(0))
.clone();
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let groups = sqlx::query_as::<_, Group>(&sql)
let users = sqlx::query_scalar(&sql)
.fetch_all(&db)
.await?
.into_iter()
.map(|x| group_response(x).0)
.map(|x: String| User { id: x.clone(), userName: x, active: true })
.collect();
Ok(Json(groups))
Ok(resource_response(
"urn:ietf:params:scim:api:messages:2.0:ListResponse",
users,
))
}
// {
@@ -237,19 +231,17 @@ pub async fn get_groups(
// }
#[cfg(feature = "enterprise")]
#[derive(Serialize, sqlx::FromRow)]
pub struct Group {
#[derive(Serialize)]
struct Group {
name: String,
emails: Option<Vec<String>>,
external_id: Option<String>,
scim_display_name: Option<String>,
}
#[cfg(feature = "enterprise")]
fn group_response(group: Group) -> JsonScim<serde_json::Value> {
let json = json!({
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName": group.scim_display_name.unwrap_or_default(),
"id": group.external_id.unwrap_or_else(|| convert_name(&group.name)),
"displayName": group.name,
"id": convert_name(&group.name),
"members": group.emails.unwrap_or_default().into_iter().map(|x| json!({"value": x, "display": x})).collect::<Vec<serde_json::Value>>(),
"meta": {
"resourceType": "Group"
@@ -266,7 +258,7 @@ pub async fn get_group(
) -> Result<JsonScim<serde_json::Value>> {
let group= sqlx::query_as!(
Group,
"SELECT name, external_id, scim_display_name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE external_id = $1 group by name, external_id",
"SELECT name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE name = $1 GROUP BY name",
id
)
.fetch_optional(&db)
@@ -300,41 +292,26 @@ pub async fn create_group(
Extension(db): Extension<DB>,
Json(body): Json<CreateGroup>,
) -> Result<JsonScim<serde_json::Value>> {
use uuid::Uuid;
tracing::info!("SCIM creating group: {:?}", body);
let mut tx: sqlx::Transaction<'_, sqlx::Postgres> = db.begin().await?;
let id = Uuid::new_v4().to_string();
let scim_display_name = Some(body.displayName.clone());
let name = convert_name(&body.displayName.clone());
sqlx::query!(
"INSERT INTO instance_group (name, scim_display_name, external_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
name,
body.displayName,
id,
"INSERT INTO instance_group (name) VALUES ($1) ON CONFLICT DO NOTHING",
convert_name(&body.displayName)
)
.execute(&mut *tx)
.await?;
tracing::info!(
"SCIM created group: {} with external_id: {} (display name: {})",
name,
id,
body.displayName
);
for member in &body.members {
sqlx::query!(
"INSERT INTO email_to_igroup (email, igroup) VALUES ($1, $2) ON CONFLICT DO NOTHING",
convert_name(&member.display),
name,
body.displayName,
)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(group_response(Group {
external_id: Some(id),
name,
scim_display_name,
name: body.displayName.clone(),
emails: Some(
body.members
.clone()
@@ -372,50 +349,32 @@ pub async fn update_group(
if body.schemas.len() == 1 {
let schema = body.schemas.get(0).unwrap();
if schema == "urn:ietf:params:scim:schemas:core:2.0:Group" {
let group= sqlx::query_as!(
Group,
"SELECT name, external_id, scim_display_name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE external_id = $1 GROUP BY name",
id
)
.fetch_optional(&db)
.await?;
let mut group = not_found_if_none(group, "Group", id.clone())?;
sqlx::query!("DELETE FROM email_to_igroup WHERE igroup = $1", group.name)
sqlx::query!("DELETE FROM email_to_igroup WHERE igroup = $1", id)
.execute(&mut *tx)
.await?;
let new_name = if let Some(name) = body.displayName.clone() {
let new_name = convert_name(name.as_str());
sqlx::query!(
"UPDATE instance_group SET scim_display_name = $1, name = $2 where external_id = $3",
name,
new_name,
id
)
.execute(&mut *tx)
.await?;
group.scim_display_name = Some(name);
new_name
} else {
group.name.clone()
};
if let Some(members) = body.members.clone() {
let mut emails = vec![];
for m in members {
emails.push(m.display.clone());
sqlx::query!(
"INSERT INTO email_to_igroup (email, igroup) VALUES ($1, $2) ON CONFLICT DO NOTHING",
m.display,
new_name,
id
)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
group.emails = Some(emails);
Ok(group_response(group))
Ok(group_response(Group {
name: body.displayName.unwrap_or_default(),
emails: Some(
body.members
.unwrap_or_default()
.clone()
.into_iter()
.map(|x| x.display.clone())
.collect(),
),
}))
} else {
Err(Error::BadRequest("expected members".to_string()))
}
@@ -430,15 +389,10 @@ pub async fn update_group(
#[cfg(feature = "enterprise")]
pub async fn delete_group(Extension(db): Extension<DB>, Path(id): Path<String>) -> Result<()> {
tracing::info!("SCIM delete group: {:?}", id);
let group = sqlx::query_scalar!("SELECT name FROM instance_group WHERE external_id = $1", id)
.fetch_optional(&db)
.await?;
let group = not_found_if_none(group, "Group", id.clone())?;
sqlx::query!("DELETE FROM email_to_igroup WHERE igroup = $1", group)
sqlx::query!("DELETE FROM email_to_igroup WHERE igroup = $1", id)
.execute(&db)
.await?;
sqlx::query!("DELETE FROM instance_group WHERE name = $1", group)
sqlx::query!("DELETE FROM instance_group WHERE name = $1", id)
.execute(&db)
.await?;
Ok(())

View File

@@ -27,7 +27,7 @@ use sql_builder::prelude::*;
use sql_builder::SqlBuilder;
use sqlx::{FromRow, Postgres, Transaction};
use std::{
collections::{hash_map::DefaultHasher, HashMap},
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
sync::Arc,
};
@@ -47,8 +47,9 @@ use windmill_common::{
},
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_parser_ts::remove_pinned_imports;
use windmill_queue::{self, schedule::push_scheduled_job, PushIsolationLevel, QueueTransaction};
use windmill_queue::{
self, schedule::push_scheduled_job, PushArgs, PushIsolationLevel, QueueTransaction,
};
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
@@ -87,8 +88,6 @@ pub struct ScriptWDraft {
pub delete_after_use: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_key: Option<String>,
}
pub fn global_service() -> Router {
@@ -116,7 +115,6 @@ pub fn workspaced_service() -> Router {
.route("/get/draft/*path", get(get_script_by_path_w_draft))
.route("/get/p/*path", get(get_script_by_path))
.route("/raw/p/*path", get(raw_script_by_path))
.route("/raw_unpinned/p/*path", get(raw_script_by_path_unpinned))
.route("/exists/p/*path", get(exists_script_by_path))
.route("/archive/h/:hash", post(archive_script_by_hash))
.route("/delete/h/:hash", post(delete_script_by_hash))
@@ -460,6 +458,8 @@ async fn create_script(
Some(String::new())
} else {
ns.lock
.as_ref()
.map(|x| x.join("\n"))
.and_then(|e| if e.is_empty() { None } else { Some(e) })
};
@@ -477,8 +477,8 @@ async fn create_script(
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, \
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
delete_after_use, timeout, concurrency_key) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27)",
delete_after_use, timeout) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)",
&w_id,
&hash.0,
ns.path,
@@ -505,13 +505,11 @@ async fn create_script(
ns.restart_unless_cancelled,
ns.delete_after_use,
ns.timeout,
ns.concurrency_key,
)
.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 {
if let Some(p_path) = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone()) {
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
p_path,
@@ -613,15 +611,6 @@ async fn create_script(
} else {
ns.tag
};
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
if let Some(dm) = ns.deployment_message {
args.insert("deployment_message".to_string(), json!(dm));
}
if let Some(ref p_path) = p_path_opt {
args.insert("parent_path".to_string(), json!(p_path));
}
let tx = PushIsolationLevel::Transaction(tx);
let (_, new_tx) = windmill_queue::push(
&db,
@@ -632,8 +621,9 @@ async fn create_script(
language: ns.language,
path: ns.path,
dedicated_worker: ns.dedicated_worker,
deployment_message: ns.deployment_message,
},
args,
PushArgs::empty(),
&authed.username,
&authed.email,
permissioned_as,
@@ -659,14 +649,9 @@ async fn create_script(
&authed.username,
&db,
&w_id,
DeployedObject::Script {
hash: hash.clone(),
path: script_path.clone(),
parent_path: p_path_opt,
},
DeployedObject::Script { hash: hash, path: script_path },
ns.deployment_message,
rsmq,
false,
)
.await?;
tx.commit().await?;
@@ -723,7 +708,7 @@ async fn get_script_by_path_w_draft(
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key FROM script LEFT JOIN draft ON
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout FROM script LEFT JOIN draft ON
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
WHERE script.path = $1 AND script.workspace_id = $2 \
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \
@@ -875,24 +860,6 @@ async fn raw_script_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
raw_script_by_path_internal(path, user_db, authed, w_id, false).await
}
async fn raw_script_by_path_unpinned(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
raw_script_by_path_internal(path, user_db, authed, w_id, true).await
}
async fn raw_script_by_path_internal(
path: StripPath,
user_db: UserDB,
authed: ApiAuthed,
w_id: String,
unpin: bool,
) -> Result<String> {
let path = path.to_path();
if !path.ends_with(".py")
@@ -927,12 +894,7 @@ async fn raw_script_by_path_internal(
tx.commit().await?;
let content = not_found_if_none(content_o, "Script", path)?;
if unpin {
return Ok(remove_pinned_imports(&content)?);
} else {
return Ok(content);
}
Ok(content)
}
async fn exists_script_by_path(
@@ -1039,7 +1001,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();
@@ -1066,23 +1027,6 @@ async fn archive_script_by_path(
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Script {
hash: ScriptHash(0), // dummy hash as it will not get inserted in db
path: path.to_string(),
parent_path: Some(path.to_string()),
},
Some(format!("Script '{}' archived", path)),
rsmq,
true,
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
@@ -1172,7 +1116,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();
@@ -1226,35 +1169,6 @@ async fn delete_script_by_path(
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Script {
hash: ScriptHash(0), // Temporary value as it will get removed right after
path: path.to_string(),
parent_path: Some(path.to_string()),
},
Some(format!("Script '{}' deleted", path)),
rsmq,
true,
)
.await?;
sqlx::query!(
"DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND script_hash IS NOT NULL",
path,
w_id
)
.execute(&db)
.await
.map_err(|e| {
Error::InternalErr(format!(
"error deleting deployment metadata for script with path {path} in workspace {w_id}: {e}"
))
})?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScriptPath { workspace: w_id, path: path.to_string() },

View File

@@ -56,8 +56,7 @@ pub async fn test_email(
require_super_admin(&db, &authed.email).await?;
let smtp = test_email.smtp;
let to = test_email.to;
let client = SmtpClientBuilder::new(smtp.host, smtp.port)
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
let client = SmtpClientBuilder::new(smtp.host, smtp.port).implicit_tls(smtp.tls_implicit);
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
if !username.is_empty() {
client.credentials((username, password))

View File

@@ -1666,8 +1666,7 @@ pub fn send_email_if_possible(subject: &str, content: &str, to: &str) {
pub async fn send_email_if_possible_intern(subject: &str, content: &str, to: &str) -> Result<()> {
if let Some(smtp) = SERVER_CONFIG.read().await.smtp.clone() {
let client = SmtpClientBuilder::new(smtp.host, smtp.port)
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
let client = SmtpClientBuilder::new(smtp.host, smtp.port).implicit_tls(smtp.tls_implicit);
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
if !username.is_empty() {
client.credentials((username, password))

View File

@@ -25,9 +25,7 @@ use windmill_common::{
db::UserDB,
error::{Error, JsonResult, Result},
utils::{not_found_if_none, StripPath},
variables::{
build_crypt, get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable,
},
variables::{get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable},
};
use lazy_static::lazy_static;
@@ -69,8 +67,6 @@ async fn list_contextual_variables(
Some("u/user/encapsulating_flow_path".to_string()),
Some("u/user/triggering_flow_path".to_string()),
Some("c".to_string()),
Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()),
Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c".to_string()),
)
.await
.to_vec(),
@@ -196,6 +192,57 @@ async fn get_value(
.map(Json);
}
pub async fn get_value_internal<'c>(
mut tx: Transaction<'c, Postgres>,
db: &DB,
w_id: &str,
path: &str,
username: &str,
) -> Result<String> {
let variable_o = sqlx::query!(
"SELECT value, account, (now() > account.expires_at) as is_expired, is_secret, path from variable
LEFT JOIN account ON variable.account = account.id WHERE variable.path = $1 AND variable.workspace_id = $2", path, w_id
)
.fetch_optional(&mut *tx)
.await?;
let variable = if let Some(variable) = variable_o {
variable
} else {
explain_variable_perm_error(path, w_id, db).await?;
unreachable!()
};
let r = if variable.is_secret {
audit_log(
&mut *tx,
username,
"variables.decrypt_secret",
ActionKind::Execute,
&w_id,
Some(&variable.path),
None,
)
.await?;
let value = variable.value;
if variable.is_expired.unwrap_or(false) && variable.account.is_some() {
_refresh_token(tx, &variable.path, &w_id, variable.account.unwrap()).await?
} else if !value.is_empty() {
let mc = build_crypt(&mut tx, &w_id).await?;
tx.commit().await?;
mc.decrypt_base64_to_string(value)
.map_err(|e| Error::InternalErr(e.to_string()))?
} else {
"".to_string()
}
} else {
variable.value
};
Ok(r)
}
async fn explain_variable_perm_error(
path: &str,
w_id: &str,
@@ -557,55 +604,31 @@ fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value {
}
}
pub async fn get_value_internal<'c>(
mut tx: Transaction<'c, Postgres>,
db: &DB,
pub async fn build_crypt<'c>(
db: &mut Transaction<'c, Postgres>,
w_id: &str,
path: &str,
username: &str,
) -> Result<MagicCrypt256> {
let key = get_workspace_key(w_id, db).await?;
let crypt_key = if let Some(ref salt) = SECRET_SALT.as_ref() {
format!("{}{}", key, salt)
} else {
key
};
Ok(magic_crypt::new_magic_crypt!(crypt_key, 256))
}
pub async fn get_workspace_key<'c>(
w_id: &str,
db: &mut Transaction<'c, Postgres>,
) -> Result<String> {
let variable_o = sqlx::query!(
"SELECT value, account, (now() > account.expires_at) as is_expired, is_secret, path from variable
LEFT JOIN account ON variable.account = account.id WHERE variable.path = $1 AND variable.workspace_id = $2", path, w_id
let key = sqlx::query_scalar!(
"SELECT key FROM workspace_key WHERE workspace_id = $1 AND kind = 'cloud'",
w_id
)
.fetch_optional(&mut *tx)
.await?;
let variable = if let Some(variable) = variable_o {
variable
} else {
explain_variable_perm_error(path, w_id, db).await?;
unreachable!()
};
let r = if variable.is_secret {
audit_log(
&mut *tx,
username,
"variables.decrypt_secret",
ActionKind::Execute,
&w_id,
Some(&variable.path),
None,
)
.await?;
let value = variable.value;
if variable.is_expired.unwrap_or(false) && variable.account.is_some() {
_refresh_token(tx, &variable.path, &w_id, variable.account.unwrap()).await?
} else if !value.is_empty() {
let mc = build_crypt(&mut tx, &w_id).await?;
tx.commit().await?;
mc.decrypt_base64_to_string(value)
.map_err(|e| Error::InternalErr(e.to_string()))?
} else {
"".to_string()
}
} else {
variable.value
};
Ok(r)
.fetch_one(&mut **db)
.await
.map_err(|e| Error::InternalErr(format!("fetching workspace key: {e}")))?;
Ok(key)
}
pub fn encrypt(mc: &MagicCrypt256, value: &str) -> String {

View File

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
#[cfg(feature = "stripe")]
#[cfg(feature = "enterprise")]
use std::str::FromStr;
use crate::db::ApiAuthed;
@@ -18,9 +18,10 @@ use crate::{
resources::{Resource, ResourceType},
users::{send_email_if_possible, WorkspaceInvite, VALID_USERNAME},
utils::require_super_admin,
variables::build_crypt,
webhook_util::{InstanceEvent, WebhookShared},
};
#[cfg(feature = "stripe")]
#[cfg(feature = "enterprise")]
use axum::response::Redirect;
use axum::{
body::StreamBody,
@@ -31,10 +32,10 @@ use axum::{
Json, Router,
};
use chrono::Utc;
#[cfg(feature = "stripe")]
#[cfg(feature = "enterprise")]
use chrono::{Datelike, TimeZone, Timelike};
use magic_crypt::MagicCryptTrait;
#[cfg(feature = "stripe")]
#[cfg(feature = "enterprise")]
use stripe::CustomerId;
use uuid::Uuid;
use windmill_audit::{audit_log, ActionKind};
@@ -42,7 +43,6 @@ use windmill_common::db::UserDB;
use windmill_common::s3_helpers::LargeFileStorage;
use windmill_common::schedule::Schedule;
use windmill_common::users::username_to_permissioned_as;
use windmill_common::variables::build_crypt;
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::workspaces::WorkspaceGitRepo;
use windmill_common::{
@@ -83,6 +83,7 @@ pub fn workspaced_service() -> Router {
.route("/edit_deploy_to", post(edit_deploy_to))
.route("/tarball", get(tarball_workspace))
.route("/is_premium", get(is_premium))
.route("/premium_info", get(premium_info))
.route("/edit_copilot_config", post(edit_copilot_config))
.route("/get_copilot_info", get(get_copilot_info))
.route("/edit_error_handler", post(edit_error_handler))
@@ -91,11 +92,9 @@ pub fn workspaced_service() -> Router {
post(edit_large_file_storage_config),
)
.route("/edit_git_sync_config", post(edit_git_sync_config))
.route("/edit_default_app", post(edit_default_app))
.route("/default_app", get(get_default_app))
.route("/leave", post(leave_workspace));
#[cfg(feature = "stripe")]
#[cfg(feature = "enterprise")]
{
if STRIPE_KEY.is_none() {
return router;
@@ -103,13 +102,12 @@ pub fn workspaced_service() -> Router {
tracing::info!("stripe enabled");
return router
.route("/premium_info", get(premium_info))
.route("/checkout", get(stripe_checkout))
.route("/billing_portal", get(stripe_portal));
}
}
#[cfg(not(feature = "stripe"))]
#[cfg(not(feature = "enterprise"))]
router
}
pub fn global_service() -> Router {
@@ -164,7 +162,6 @@ pub struct WorkspaceSettings {
pub error_handler_muted_on_cancel: Option<bool>,
pub large_file_storage: Option<serde_json::Value>, // effectively: DatasetsStorage
pub git_sync: Option<serde_json::Value>, // effectively: WorkspaceGitRepo
pub default_app: Option<String>,
}
#[derive(FromRow, Serialize, Debug)]
@@ -327,7 +324,6 @@ pub struct PremiumWorkspaceInfo {
pub usage: Option<i32>,
pub seats: Option<i32>,
}
#[cfg(feature = "stripe")]
async fn premium_info(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -399,14 +395,14 @@ async fn premium_info(
Ok(Json(result))
}
#[cfg(feature = "stripe")]
#[cfg(feature = "enterprise")]
#[derive(Deserialize)]
struct PlanQuery {
plan: String,
seats: Option<i32>,
}
#[cfg(feature = "stripe")]
#[cfg(feature = "enterprise")]
async fn stripe_checkout(
authed: ApiAuthed,
Path(w_id): Path<String>,
@@ -495,7 +491,7 @@ async fn stripe_checkout(
}
}
#[cfg(feature = "stripe")]
#[cfg(feature = "enterprise")]
async fn stripe_portal(
authed: ApiAuthed,
Path(w_id): Path<String>,
@@ -1047,7 +1043,7 @@ async fn edit_large_file_storage_config(
#[derive(Deserialize)]
struct EditGitSyncConfig {
git_sync_settings: Option<Vec<WorkspaceGitRepo>>,
git_sync_settings: Option<WorkspaceGitRepo>,
}
async fn edit_git_sync_config(
@@ -1081,7 +1077,7 @@ async fn edit_git_sync_config(
.await?;
if let Some(git_sync_settings) = new_config.git_sync_settings {
let serialized_config = serde_json::to_value::<Vec<WorkspaceGitRepo>>(git_sync_settings)
let serialized_config = serde_json::to_value::<WorkspaceGitRepo>(git_sync_settings)
.map_err(|err| Error::InternalErr(err.to_string()))?;
sqlx::query!(
@@ -1104,84 +1100,6 @@ async fn edit_git_sync_config(
Ok(format!("Edit git sync config for workspace {}", &w_id))
}
#[derive(Deserialize)]
struct EditDefaultApp {
default_app_path: Option<String>,
}
async fn edit_default_app(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
ApiAuthed { is_admin, username, .. }: ApiAuthed,
Json(new_config): Json<EditDefaultApp>,
) -> Result<String> {
#[cfg(not(feature = "enterprise"))]
{
return Err(Error::BadRequest(
"Setting a workspace default app is only available on Windmill Enterprise Edition"
.to_string(),
));
}
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
let args_for_audit = format!("{:?}", new_config.default_app_path);
audit_log(
&mut *tx,
&authed.username,
"workspaces.edit_default_app",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("args_for_audit", args_for_audit.as_str())].into()),
)
.await?;
if let Some(default_app_path) = new_config.default_app_path {
sqlx::query!(
"UPDATE workspace_settings SET default_app = $1 WHERE workspace_id = $2",
default_app_path,
&w_id
)
.execute(&mut *tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET default_app = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(format!("Edit default app for workspace {}", &w_id))
}
#[derive(Serialize)]
struct WorkspaceDefaultApp {
pub default_app_path: Option<String>,
}
async fn get_default_app(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<WorkspaceDefaultApp> {
let mut tx = db.begin().await?;
let default_app_path = sqlx::query_scalar!(
"SELECT default_app FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&mut *tx)
.await
.map_err(|err| Error::InternalErr(format!("getting default_app: {err}")))?;
tx.commit().await?;
Ok(Json(WorkspaceDefaultApp { default_app_path }))
}
async fn edit_error_handler(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -1198,7 +1116,7 @@ async fn edit_error_handler(
"INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
w_id,
"error_handler",
"The group the error handler acts on behalf of",
"The group the error handler acts on belhalf of",
serde_json::json!({username_to_permissioned_as(&authed.username): true})
)
.execute(&mut *tx)
@@ -1893,7 +1811,8 @@ struct ScriptMetadata {
summary: String,
description: String,
schema: Option<Schema>,
lock: Option<String>,
is_template: bool,
lock: Vec<String>,
kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
envs: Option<Vec<String>>,
@@ -1981,7 +1900,6 @@ struct ArchiveQueryParams {
skip_variables: Option<bool>,
skip_resources: Option<bool>,
include_schedules: Option<bool>,
default_ts: Option<String>,
}
#[inline]
@@ -1989,10 +1907,11 @@ pub fn to_string_without_metadata<T>(value: &T, preserve_extra_perms: bool) -> R
where
T: ?Sized + Serialize,
{
let mut value = serde_json::to_value(value).map_err(to_anyhow)?;
let value = serde_json::to_value(value).map_err(to_anyhow)?;
value
.as_object_mut()
.as_object()
.map(|obj| {
let mut obj = obj.clone();
for key in [
"workspace_id",
"path",
@@ -2015,10 +1934,6 @@ where
}
}
if let Some(o2) = obj.get_mut("policy").and_then(|x| x.as_object_mut()) {
o2.remove("on_behalf_of");
o2.remove("on_behalf_of_email");
}
if !preserve_extra_perms && obj.contains_key("extra_perms") {
obj.remove("extra_perms");
}
@@ -2031,7 +1946,6 @@ where
async fn tarball_workspace(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Query(ArchiveQueryParams {
@@ -2042,12 +1956,9 @@ async fn tarball_workspace(
skip_secrets,
skip_variables,
include_schedules,
default_ts,
}): Query<ArchiveQueryParams>,
) -> Result<([(headers::HeaderName, String); 2], impl IntoResponse)> {
// require_admin(authed.is_admin, &authed.username)?;
let mut tx = user_db.begin(&authed).await?;
require_admin(authed.is_admin, &authed.username)?;
let tmp_dir = TempDir::new_in("/tmp/windmill/")?;
@@ -2066,7 +1977,7 @@ async fn tarball_workspace(
{
let folders = sqlx::query_as::<_, Folder>("SELECT * FROM folder WHERE workspace_id = $1")
.bind(&w_id)
.fetch_all(&mut *tx)
.fetch_all(&db)
.await?;
for folder in folders {
@@ -2086,19 +1997,13 @@ async fn tarball_workspace(
workspace_id = $1)",
)
.bind(&w_id)
.fetch_all(&mut *tx)
.fetch_all(&db)
.await?;
for script in scripts {
let ext = match script.language {
ScriptLang::Python3 => "py",
ScriptLang::Deno => {
if default_ts.as_ref().is_some_and(|x| x == "bun") {
"deno.ts"
} else {
"ts"
}
}
ScriptLang::Deno => "ts",
ScriptLang::Go => "go",
ScriptLang::Bash => "sh",
ScriptLang::Powershell => "ps1",
@@ -2109,24 +2014,25 @@ async fn tarball_workspace(
ScriptLang::Mssql => "ms.sql",
ScriptLang::Graphql => "gql",
ScriptLang::Nativets => "fetch.ts",
ScriptLang::Bun => {
if default_ts.as_ref().is_some_and(|x| x == "bun") {
"ts"
} else {
"bun.ts"
}
}
ScriptLang::Bun => "bun.ts",
};
archive
.write_to_archive(&script.content, &format!("{}.{}", script.path, ext))
.await?;
let lock = script
.lock
.unwrap_or_else(|| "".to_string())
.lines()
.map(|x| x.to_string())
.collect();
let metadata = ScriptMetadata {
summary: script.summary,
description: script.description,
schema: script.schema,
is_template: script.is_template,
kind: script.kind.to_string(),
lock: script.lock,
lock,
envs: script.envs,
concurrent_limit: script.concurrent_limit,
concurrency_time_window_s: script.concurrency_time_window_s,
@@ -2152,7 +2058,7 @@ async fn tarball_workspace(
"SELECT * FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'",
&w_id
)
.fetch_all(&mut *tx)
.fetch_all(&db)
.await?;
for resource in resources {
@@ -2169,7 +2075,7 @@ async fn tarball_workspace(
"SELECT * FROM resource_type WHERE workspace_id = $1",
&w_id
)
.fetch_all(&mut *tx)
.fetch_all(&db)
.await?;
for resource_type in resource_types {
@@ -2188,7 +2094,7 @@ async fn tarball_workspace(
"SELECT * FROM flow WHERE workspace_id = $1 AND archived = false",
)
.bind(&w_id)
.fetch_all(&mut *tx)
.fetch_all(&db)
.await?;
for flow in flows {
@@ -2207,7 +2113,7 @@ async fn tarball_workspace(
"SELECT * FROM variable WHERE workspace_id = $1 AND is_secret = false"
})
.bind(&w_id)
.fetch_all(&mut *tx)
.fetch_all(&db)
.await?;
let mc = build_crypt(&mut db.begin().await?, &w_id).await?;
@@ -2238,7 +2144,7 @@ async fn tarball_workspace(
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
&w_id
)
.fetch_all(&mut *tx)
.fetch_all(&db)
.await?;
for app in apps {
@@ -2256,7 +2162,7 @@ async fn tarball_workspace(
WHERE workspace_id = $1",
&w_id
)
.fetch_all(&mut *tx)
.fetch_all(&db)
.await?;
for schedule in schedules {

View File

@@ -50,5 +50,4 @@ git-version.workspace = true
cron.workspace = true
tracing-loki = { version = "^0", optional = true }
aws-sdk-s3.workspace = true
aws-config.workspace = true
magic-crypt.workspace = true
aws-config.workspace = true

View File

@@ -96,9 +96,9 @@ impl IntoResponse for Error {
};
if matches!(status, axum::http::StatusCode::NOT_FOUND) {
tracing::warn!(message = e.to_string());
tracing::warn!(not_found = e.to_string());
} else {
tracing::error!(message = e.to_string());
tracing::error!(error = e.to_string());
};
axum::response::Response::builder()

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