Compare commits
164 Commits
v1.210.0
...
fix-signin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43caa4ee0b | ||
|
|
e2cac7c57f | ||
|
|
2f72be36e5 | ||
|
|
d47d4ccf85 | ||
|
|
583dae6a72 | ||
|
|
8fc0afce71 | ||
|
|
6eaec47162 | ||
|
|
a3f1111ca7 | ||
|
|
60c4860233 | ||
|
|
85805bdf82 | ||
|
|
225f675fb3 | ||
|
|
a122b6cab6 | ||
|
|
dd980ee296 | ||
|
|
45648bbf7f | ||
|
|
1d90434f12 | ||
|
|
cb42f10dc6 | ||
|
|
62de305908 | ||
|
|
49803e26f6 | ||
|
|
592d7839d1 | ||
|
|
f195320e99 | ||
|
|
fdb546ea2a | ||
|
|
772e3b8a32 | ||
|
|
1bef60dd50 | ||
|
|
d4374a0103 | ||
|
|
fc5f054b94 | ||
|
|
4774e03be5 | ||
|
|
007d5be23e | ||
|
|
f816ad01d3 | ||
|
|
bb7f11fcda | ||
|
|
830dec0f90 | ||
|
|
920cc9a576 | ||
|
|
1f99fcd3af | ||
|
|
52a2746e50 | ||
|
|
ec25856b7c | ||
|
|
2d72facbdf | ||
|
|
06411c8a63 | ||
|
|
0bf3685cbd | ||
|
|
56c0d5a755 | ||
|
|
06656924ae | ||
|
|
70c504edfa | ||
|
|
0d93986b18 | ||
|
|
651f74b00e | ||
|
|
16d10aedf7 | ||
|
|
72bb15f6a0 | ||
|
|
772c04c9b3 | ||
|
|
03d3bd33d0 | ||
|
|
3789b34dae | ||
|
|
3d6fb15a90 | ||
|
|
e6d67f4e59 | ||
|
|
1a1d1db96f | ||
|
|
15ebf46abc | ||
|
|
e9a7f0cf17 | ||
|
|
4eb25216f3 | ||
|
|
b7fe5ddf15 | ||
|
|
f7d93c56df | ||
|
|
f78f14e268 | ||
|
|
5a66806c76 | ||
|
|
c8046af9d0 | ||
|
|
15b2c9f171 | ||
|
|
89abb68f63 | ||
|
|
9525ab7bba | ||
|
|
bd31979a62 | ||
|
|
76a387f4a1 | ||
|
|
46e0f91387 | ||
|
|
55e49fbd4b | ||
|
|
c990f856aa | ||
|
|
2d3ce8a49c | ||
|
|
726866b410 | ||
|
|
c3eaf0bf4a | ||
|
|
2bbd0b34b5 | ||
|
|
36e46e2e47 | ||
|
|
eec7d83d98 | ||
|
|
d2b3026032 | ||
|
|
8275602372 | ||
|
|
095da9e76e | ||
|
|
731f92b907 | ||
|
|
dbaef0aa5f | ||
|
|
19ab924fa2 | ||
|
|
bae0f45f21 | ||
|
|
5fa653d154 | ||
|
|
94e9b80e19 | ||
|
|
0c0f43dd3a | ||
|
|
3bb2a0c960 | ||
|
|
55e34d8cdd | ||
|
|
81ef24b3ce | ||
|
|
603e7ff67f | ||
|
|
f1cabb40f6 | ||
|
|
e8b1f220dd | ||
|
|
16be2300ff | ||
|
|
21765922f8 | ||
|
|
f4aa76897e | ||
|
|
4c42836cfc | ||
|
|
9d32bdf5a6 | ||
|
|
e3f2b43748 | ||
|
|
f5e098d03b | ||
|
|
cfd3da41ef | ||
|
|
b4bbb794b5 | ||
|
|
1c2abcda23 | ||
|
|
9783abba0a | ||
|
|
2859bbe7b4 | ||
|
|
8f8ea227c8 | ||
|
|
44775a4de6 | ||
|
|
217e69498f | ||
|
|
449974404a | ||
|
|
7fe3bca624 | ||
|
|
cbdaf3b1d8 | ||
|
|
31fbc5867c | ||
|
|
bfdb559b47 | ||
|
|
c42c54e69b | ||
|
|
7ae84fce50 | ||
|
|
45ee1d7703 | ||
|
|
391f4ab551 | ||
|
|
6377605821 | ||
|
|
6797d4d114 | ||
|
|
37ffdaed60 | ||
|
|
0317c26d5b | ||
|
|
3ebe6d7a62 | ||
|
|
ad199afd06 | ||
|
|
60d2f79677 | ||
|
|
26fdf20f49 | ||
|
|
07dae13f44 | ||
|
|
c1bb97d990 | ||
|
|
62a3d79266 | ||
|
|
6dd91adb13 | ||
|
|
51ce2f8cb3 | ||
|
|
89f42c0a51 | ||
|
|
ded0bb890b | ||
|
|
f8a1bd8d69 | ||
|
|
baac93f401 | ||
|
|
de1e1f545d | ||
|
|
6d426b4ec4 | ||
|
|
cd8919d46f | ||
|
|
8ea98c2c8d | ||
|
|
c2598b3304 | ||
|
|
6000c5dbc0 | ||
|
|
d31559c56b | ||
|
|
624b4d0e98 | ||
|
|
895fedc8cb | ||
|
|
1acf78e782 | ||
|
|
c7a30f7c9d | ||
|
|
aa6bf4027f | ||
|
|
b667317d44 | ||
|
|
cd260e7062 | ||
|
|
47ad8d6013 | ||
|
|
c90f7f167e | ||
|
|
4882d94dfe | ||
|
|
19907e4012 | ||
|
|
2f15ebc5f9 | ||
|
|
70f121035e | ||
|
|
c476543570 | ||
|
|
4023bca192 | ||
|
|
eef6432637 | ||
|
|
2692737418 | ||
|
|
6ae1a69b75 | ||
|
|
5a72ca9b24 | ||
|
|
75021c444e | ||
|
|
ee243dedc6 | ||
|
|
36379d6db0 | ||
|
|
cee8206c42 | ||
|
|
06905b252f | ||
|
|
ab9adacfad | ||
|
|
08c14e51c7 | ||
|
|
c07e9056f1 | ||
|
|
837d3716d3 |
19
.github/workflows/backend-test.yml
vendored
19
.github/workflows/backend-test.yml
vendored
@@ -15,7 +15,7 @@ on:
|
||||
|
||||
jobs:
|
||||
cargo_test:
|
||||
runs-on: [self-hosted, new]
|
||||
runs-on: ubicloud-standard-8
|
||||
container:
|
||||
image: ghcr.io/windmill-labs/backend-tests
|
||||
services:
|
||||
@@ -31,15 +31,16 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: |
|
||||
backend
|
||||
backend -> target
|
||||
# - uses: Swatinem/rust-cache@v2
|
||||
# with:
|
||||
# workspaces: |
|
||||
# backend
|
||||
# backend -> target
|
||||
- name: cargo test
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
run:
|
||||
mkdir frontend/build && cd backend && touch
|
||||
windmill-api/openapi-deref.yaml &&
|
||||
DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill DISABLE_EMBEDDING=true cargo
|
||||
test --features enterprise --all -- --nocapture
|
||||
DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill
|
||||
DISABLE_EMBEDDING=true RUST_LOG=info cargo test --features enterprise --all --
|
||||
--nocapture
|
||||
|
||||
13
.github/workflows/benchmark.yml
vendored
13
.github/workflows/benchmark.yml
vendored
@@ -7,14 +7,13 @@ on:
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
runs-on: [self-hosted, new]
|
||||
runs-on: ubicloud-standard-8
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_DB: windmill
|
||||
POSTGRES_PASSWORD: changeme
|
||||
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5
|
||||
@@ -35,12 +34,12 @@ jobs:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
DISABLE_SERVER: true
|
||||
MODE: worker
|
||||
WORKER_GROUP: dedicated
|
||||
DEDICATED_WORKER: "admins:f/benchmarks/dedicated"
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
options: >-
|
||||
--pull always
|
||||
--pull always --restart unless-stopped
|
||||
steps:
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
@@ -50,9 +49,11 @@ jobs:
|
||||
ref: benchmarks
|
||||
- name: benchmark
|
||||
timeout-minutes: 20
|
||||
run: deno run --unstable -A -r
|
||||
run:
|
||||
deno run --unstable -A -r
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
|
||||
-c https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
|
||||
-c
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
|
||||
- name: Push changes
|
||||
run: |
|
||||
pwd
|
||||
|
||||
2
.github/workflows/change-versions.yml
vendored
2
.github/workflows/change-versions.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- "version.txt"
|
||||
jobs:
|
||||
change_version:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud
|
||||
container: node:18
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
13
.github/workflows/clean-docker.yml
vendored
13
.github/workflows/clean-docker.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: Clean docker
|
||||
on:
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: "0 0 */2 * *"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, new]
|
||||
steps:
|
||||
- name: clean docker
|
||||
run: |
|
||||
sudo docker system prune -f
|
||||
4
.github/workflows/deno_on_release.yml
vendored
4
.github/workflows/deno_on_release.yml
vendored
@@ -8,7 +8,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build_deno_and_push_to_repo:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: generate_deno
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
tag_repo:
|
||||
needs: [build_deno_and_push_to_repo]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
||||
@@ -16,7 +16,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build_ee:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
61
.github/workflows/docker-image.yml
vendored
61
.github/workflows/docker-image.yml
vendored
@@ -20,7 +20,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
org.opencontainers.image.licenses=AGPLv3
|
||||
|
||||
build_ee:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -108,6 +108,52 @@ jobs:
|
||||
${{ steps.meta-ee-public.outputs.labels }}
|
||||
org.opencontainers.image.licenses=Windmill-Enterprise-License
|
||||
|
||||
build_ee_reports_privately:
|
||||
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.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}-ee-reports
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=short
|
||||
|
||||
- name: Login to ECR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.ECR_REGISTRY }}
|
||||
username: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Build and push publicly ee
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
file: "./docker/DockerfileReports"
|
||||
tags: |
|
||||
${{ steps.meta-ee-public.outputs.tags }}
|
||||
labels: |
|
||||
${{ steps.meta-ee-public.outputs.labels }}
|
||||
org.opencontainers.image.licenses=Windmill-Enterprise-License
|
||||
|
||||
# disabled until we make it 100% reliable and add more meaningful tests
|
||||
# playwright:
|
||||
# runs-on: [self-hosted, new]
|
||||
@@ -143,7 +189,7 @@ jobs:
|
||||
|
||||
deploy_s3:
|
||||
needs: [build_ee]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud
|
||||
if: github.event_name != 'pull_request'
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
@@ -165,10 +211,9 @@ jobs:
|
||||
bucket: windmill-frontend
|
||||
bucket-region: us-east-1
|
||||
|
||||
|
||||
attach_amd64_binary_to_release:
|
||||
needs: [build, build_ee]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
env:
|
||||
ARCH: amd64
|
||||
@@ -203,7 +248,6 @@ jobs:
|
||||
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:
|
||||
@@ -213,7 +257,7 @@ jobs:
|
||||
|
||||
attach_arm64_binary_to_release:
|
||||
needs: [build, build_ee]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicoud
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
env:
|
||||
ARCH: arm64
|
||||
@@ -248,7 +292,6 @@ jobs:
|
||||
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:
|
||||
@@ -258,7 +301,7 @@ jobs:
|
||||
|
||||
publish_ecr:
|
||||
needs: [build_ee]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -5,8 +5,6 @@ env:
|
||||
name: Build windmill:mssql
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 */4 * *"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}-mssql
|
||||
8
.github/workflows/frontend-check.yml
vendored
8
.github/workflows/frontend-check.yml
vendored
@@ -1,14 +1,14 @@
|
||||
name: check frontend build
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened,synchronize,reopened,closed]
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
paths:
|
||||
- "frontend/**"
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
npm_check:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
@@ -16,4 +16,6 @@ jobs:
|
||||
node-version: 18
|
||||
- name: "npm check"
|
||||
timeout-minutes: 2
|
||||
run: cd frontend && npm ci && npm run generate-backend-client && npm run check
|
||||
run:
|
||||
cd frontend && npm ci && npm run generate-backend-client && npm run
|
||||
check
|
||||
|
||||
2
.github/workflows/go_on_release.yml
vendored
2
.github/workflows/go_on_release.yml
vendored
@@ -10,7 +10,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build_go_and_push_to_repo:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
|
||||
2
.github/workflows/npm_on_release.yml
vendored
2
.github/workflows/npm_on_release.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build_npm:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
11
.github/workflows/publish_lsp.yml
vendored
11
.github/workflows/publish_lsp.yml
vendored
@@ -16,14 +16,17 @@ permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish_lsp:
|
||||
runs-on: ubuntu-latest
|
||||
sleep:
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- name: Sleep for 900 seconds waiting for pypi to update index
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: sleep 900
|
||||
shell: bash
|
||||
|
||||
publish_lsp:
|
||||
needs: [sleep]
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
@@ -62,7 +65,7 @@ jobs:
|
||||
|
||||
publish_lsp_private:
|
||||
needs: [publish_lsp]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
||||
2
.github/workflows/pypi_on_release.yml
vendored
2
.github/workflows/pypi_on_release.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish_pypi:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud-standard-8
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
container:
|
||||
image: ghcr.io/windmill-labs/python-client-builder
|
||||
|
||||
2
.github/workflows/release-please.yml
vendored
2
.github/workflows/release-please.yml
vendored
@@ -6,7 +6,7 @@ name: release-please
|
||||
jobs:
|
||||
release-please:
|
||||
name: "Release please"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: GoogleCloudPlatform/release-please-action@v3
|
||||
with:
|
||||
|
||||
194
CHANGELOG.md
194
CHANGELOG.md
@@ -1,6 +1,200 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## [1.219.1](https://github.com/windmill-labs/windmill/compare/v1.219.0...v1.219.1) (2023-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix editorContext in preview ([cb42f10](https://github.com/windmill-labs/windmill/commit/cb42f10dc605f00ba70fadc61069aba98af8ec88))
|
||||
* maps mapRegion update on move ([62de305](https://github.com/windmill-labs/windmill/commit/62de305908ba7e6ac75aa5e8ffba822522b08345))
|
||||
|
||||
## [1.219.0](https://github.com/windmill-labs/windmill/compare/v1.218.0...v1.219.0) (2023-12-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* eval preview for apps ([d4374a0](https://github.com/windmill-labs/windmill/commit/d4374a0103d2244b31b5cec7649dbfb96b2af1b4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** rename menu component ([#2738](https://github.com/windmill-labs/windmill/issues/2738)) ([f816ad0](https://github.com/windmill-labs/windmill/commit/f816ad01d3e39917714abadcf3833a4d8619e4f3))
|
||||
* improve quickstyle to be in static ccomponents only ([4774e03](https://github.com/windmill-labs/windmill/commit/4774e03be5663017bccbe95f026cc39d4c43f536))
|
||||
* improve stat card ([fc5f054](https://github.com/windmill-labs/windmill/commit/fc5f054b94fa58ee889d10d06e486b6e8c4f885e))
|
||||
* limit log pull from queued jobs ([592d783](https://github.com/windmill-labs/windmill/commit/592d7839d183843b425521a319b802fb8cac3f21))
|
||||
* support INET in pg ([007d5be](https://github.com/windmill-labs/windmill/commit/007d5be23e038fbf82907c3b335b3d747791295d))
|
||||
|
||||
## [1.218.0](https://github.com/windmill-labs/windmill/compare/v1.217.0...v1.218.0) (2023-11-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** add menu component ([#2721](https://github.com/windmill-labs/windmill/issues/2721)) ([0665692](https://github.com/windmill-labs/windmill/commit/06656924ae8173e906a243aab2af658a1689af01))
|
||||
* **frontend:** resource rework ([#2725](https://github.com/windmill-labs/windmill/issues/2725)) ([ec25856](https://github.com/windmill-labs/windmill/commit/ec25856b7c8a7d33a50d1beb5f3e99c7b912e1ca))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make REST language support URLSearchParams, headers, FormData ([830dec0](https://github.com/windmill-labs/windmill/commit/830dec0f90e0189b4dea4de8c44a03e437acf440))
|
||||
* Pull patched version of gosyn ([#2734](https://github.com/windmill-labs/windmill/issues/2734)) ([0bf3685](https://github.com/windmill-labs/windmill/commit/0bf3685cbdecae0a8e7a24a5198ae2fed98fe340))
|
||||
* s3 resource openapi spec ([#2730](https://github.com/windmill-labs/windmill/issues/2730)) ([16d10ae](https://github.com/windmill-labs/windmill/commit/16d10aedf79cddf7cdc15d55c42c72f4948b3ee2))
|
||||
* update deployed even when draft only ([#2694](https://github.com/windmill-labs/windmill/issues/2694)) ([1f99fcd](https://github.com/windmill-labs/windmill/commit/1f99fcd3af21208676aa90015559359740b0534a))
|
||||
* Using latest gosyn ([#2737](https://github.com/windmill-labs/windmill/issues/2737)) ([920cc9a](https://github.com/windmill-labs/windmill/commit/920cc9a576db0112ffd9572480c3d0a8aa08055b))
|
||||
* Workspace error handler creation was not adding the slack resource ([#2733](https://github.com/windmill-labs/windmill/issues/2733)) ([70c504e](https://github.com/windmill-labs/windmill/commit/70c504edfaf7c114c91af2b3ca39dfee073f04f2))
|
||||
|
||||
## [1.217.0](https://github.com/windmill-labs/windmill/compare/v1.216.0...v1.217.0) (2023-11-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for raw query args ([8275602](https://github.com/windmill-labs/windmill/commit/82756023728fe392fee93a7bba0567ce582aad8c))
|
||||
* **frontend:** add "hide schedules" filter ([#2710](https://github.com/windmill-labs/windmill/issues/2710)) ([46e0f91](https://github.com/windmill-labs/windmill/commit/46e0f913878c9f688594cfe1c9184438e4facf49))
|
||||
* **frontend:** Added tailwind classes auto-complete ([#2712](https://github.com/windmill-labs/windmill/issues/2712)) ([2d3ce8a](https://github.com/windmill-labs/windmill/commit/2d3ce8a49c952accb4f1ceb0e6ea608df539f179))
|
||||
* **frontend:** Stat card improvement ([#2709](https://github.com/windmill-labs/windmill/issues/2709)) ([89abb68](https://github.com/windmill-labs/windmill/commit/89abb68f635d062778485c15d88fd6272ebd40a2))
|
||||
* scheduled app reports ([#2714](https://github.com/windmill-labs/windmill/issues/2714)) ([3789b34](https://github.com/windmill-labs/windmill/commit/3789b34dae72ff57bb9e0bb7e93439446c78095d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ai fix popup placement + update edit/fix prompt to return complete code ([#2715](https://github.com/windmill-labs/windmill/issues/2715)) ([76a387f](https://github.com/windmill-labs/windmill/commit/76a387f4a181d21afad0138c07f947aa292978cc))
|
||||
* better error for moved openai resource ([#2724](https://github.com/windmill-labs/windmill/issues/2724)) ([15b2c9f](https://github.com/windmill-labs/windmill/commit/15b2c9f171122ab074f8430a785f8ed577921fcc))
|
||||
* Deno can talk to private NPM registries behind HTTPS ([#2713](https://github.com/windmill-labs/windmill/issues/2713)) ([726866b](https://github.com/windmill-labs/windmill/commit/726866b410863ca9583a8145ebd9f4c4557cef08))
|
||||
* Error handler now supports flows ([#2707](https://github.com/windmill-labs/windmill/issues/2707)) ([36e46e2](https://github.com/windmill-labs/windmill/commit/36e46e2e47e1e949e1e235fa4ec0365b46d80de1))
|
||||
* **frontend:** fix separator z-index ([#2720](https://github.com/windmill-labs/windmill/issues/2720)) ([9525ab7](https://github.com/windmill-labs/windmill/commit/9525ab7bbab9f820d1f6fee44f9a2e9ad19d1b54))
|
||||
* **frontend:** Fix table initial ordering ([#2727](https://github.com/windmill-labs/windmill/issues/2727)) ([1a1d1db](https://github.com/windmill-labs/windmill/commit/1a1d1db96fd069434291f4b5f37ae1d0e21c5e44))
|
||||
* **frontend:** Improve error message + fix overflow when file name is too long ([#2691](https://github.com/windmill-labs/windmill/issues/2691)) ([c990f85](https://github.com/windmill-labs/windmill/commit/c990f856aaf821899cc54db20a653af396ceae7f))
|
||||
* generate cargo lock file ([#2722](https://github.com/windmill-labs/windmill/issues/2722)) ([bd31979](https://github.com/windmill-labs/windmill/commit/bd31979a62a30071e68385b5eb5dc28f37ea8fb5))
|
||||
* improve autocomplete reactivity ([c3eaf0b](https://github.com/windmill-labs/windmill/commit/c3eaf0bf4aab531b52a1b3ac276c6840b0925786))
|
||||
* make dedicated workers able to redeploy automatically ([e6d67f4](https://github.com/windmill-labs/windmill/commit/e6d67f4e5994360e7ae83c1303b31514b6062d1d))
|
||||
* minor fixes to private NPM and python registries to get everything working ([#2728](https://github.com/windmill-labs/windmill/issues/2728)) ([3d6fb15](https://github.com/windmill-labs/windmill/commit/3d6fb15a90f4d5aa18a6d6158760a380295e3d9e))
|
||||
* only list session and permanent token in user settings ([c8046af](https://github.com/windmill-labs/windmill/commit/c8046af9d01ade6891ea97e56974bb742bbf3e6e))
|
||||
* trim .bun.ts for local imports ([d2b3026](https://github.com/windmill-labs/windmill/commit/d2b3026032d288e1e5de1f37979d92d02094b3e4))
|
||||
|
||||
## [1.216.0](https://github.com/windmill-labs/windmill/compare/v1.215.0...v1.216.0) (2023-11-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add early return for flows ([dbaef0a](https://github.com/windmill-labs/windmill/commit/dbaef0aa5f22c55b691f6205ae053c155a0e025c))
|
||||
* fill pg resource from string ([#2699](https://github.com/windmill-labs/windmill/issues/2699)) ([f1cabb4](https://github.com/windmill-labs/windmill/commit/f1cabb40f6cbec05b105563dad58da048d9187ec))
|
||||
* **frontend:** add currency format + add enum autocomplete + fix run… ([#2670](https://github.com/windmill-labs/windmill/issues/2670)) ([0c0f43d](https://github.com/windmill-labs/windmill/commit/0c0f43dd3ac0541d889046c0c6df8dbf78650b9c))
|
||||
* **frontend:** add stat card ([#2687](https://github.com/windmill-labs/windmill/issues/2687)) ([81ef24b](https://github.com/windmill-labs/windmill/commit/81ef24b3cec18f86e407b343b4d72ac566d268e1))
|
||||
* **python:** Update return type for 'get_resource' function ([#2695](https://github.com/windmill-labs/windmill/issues/2695)) ([603e7ff](https://github.com/windmill-labs/windmill/commit/603e7ff67f5f68d291db37173b89325d5de911a1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* expand enum type narrowing to forms ([94e9b80](https://github.com/windmill-labs/windmill/commit/94e9b80e1993a217415118bf48b8575cd8363746))
|
||||
* **frontend:** correctly handle when result is undefined ([#2693](https://github.com/windmill-labs/windmill/issues/2693)) ([f4aa768](https://github.com/windmill-labs/windmill/commit/f4aa76897ea0f261c1e47976f517058601c479f9))
|
||||
* improve multiselect from form ([5fa653d](https://github.com/windmill-labs/windmill/commit/5fa653d154c6e8697119796a025bdc380f68de9a))
|
||||
* improve resource pages ([16be230](https://github.com/windmill-labs/windmill/commit/16be2300ff66cad8b740856a6435d1c1e53bce99))
|
||||
* lighten monaco editor workers ([#2690](https://github.com/windmill-labs/windmill/issues/2690)) ([4c42836](https://github.com/windmill-labs/windmill/commit/4c42836cfcad63da023a658213f03b45c925efb2))
|
||||
* Priority tags FE buggy when missing from config ([#2702](https://github.com/windmill-labs/windmill/issues/2702)) ([e8b1f22](https://github.com/windmill-labs/windmill/commit/e8b1f220dd299922a32b04451756f2b7fa735d5d))
|
||||
* S3 bucket browser small improvements and fixes ([#2700](https://github.com/windmill-labs/windmill/issues/2700)) ([55e34d8](https://github.com/windmill-labs/windmill/commit/55e34d8cdd64362615f2c1bc2698c0538c287784))
|
||||
|
||||
## [1.215.0](https://github.com/windmill-labs/windmill/compare/v1.214.1...v1.215.0) (2023-11-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* query embeddings from s3 ([#2683](https://github.com/windmill-labs/windmill/issues/2683)) ([e3f2b43](https://github.com/windmill-labs/windmill/commit/e3f2b43748e8e0853dd0dbae87cba37b950dc76e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bigquery schema and date inputs ([#2688](https://github.com/windmill-labs/windmill/issues/2688)) ([f5e098d](https://github.com/windmill-labs/windmill/commit/f5e098d03b467002a8deab4d2c519945c01458d4))
|
||||
* improve wmill go client ([cfd3da4](https://github.com/windmill-labs/windmill/commit/cfd3da41efe4e89e5d8672c08433442b05d11f37))
|
||||
|
||||
## [1.214.1](https://github.com/windmill-labs/windmill/compare/v1.214.0...v1.214.1) (2023-11-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **python:** Quality of Life improvements to Python client ([#2686](https://github.com/windmill-labs/windmill/issues/2686)) ([1c2abcd](https://github.com/windmill-labs/windmill/commit/1c2abcda231b2d10e7a358a4e4b7973785cb6199))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add image base64 source kinds ([7ae84fc](https://github.com/windmill-labs/windmill/commit/7ae84fce5014d97bf17803e2ccefafee009b28bb))
|
||||
* fix backend build ([45ee1d7](https://github.com/windmill-labs/windmill/commit/45ee1d770345f03c03cc48a3ddcc130ca3655a4c))
|
||||
* **frontend:** disable active interaction to avoid broken state ([#2675](https://github.com/windmill-labs/windmill/issues/2675)) ([bfdb559](https://github.com/windmill-labs/windmill/commit/bfdb559b47786e37f135ca345c55828cc70e49e9))
|
||||
* **frontend:** improve tutorial ux ([#2677](https://github.com/windmill-labs/windmill/issues/2677)) ([7fe3bca](https://github.com/windmill-labs/windmill/commit/7fe3bca624fa3b434e3fbe9b33b213449044accc))
|
||||
* **frontend:** use popover for schedule for later on the runs page ([#2678](https://github.com/windmill-labs/windmill/issues/2678)) ([31fbc58](https://github.com/windmill-labs/windmill/commit/31fbc5867cc72befea5733811148126289ce8bb9))
|
||||
* graphql variables ([#2682](https://github.com/windmill-labs/windmill/issues/2682)) ([217e694](https://github.com/windmill-labs/windmill/commit/217e69498fa5958e83c595003b2a086528e51388))
|
||||
* relax tags constraints ([8f8ea22](https://github.com/windmill-labs/windmill/commit/8f8ea227c850f9d01f913660547b6a6394c6e142))
|
||||
* Various fixes and improvements for Windmill S3 capabilities ([#2674](https://github.com/windmill-labs/windmill/issues/2674)) ([c42c54e](https://github.com/windmill-labs/windmill/commit/c42c54e69b5acdcbe44a0c26ae7fbaa2189e6b5f))
|
||||
|
||||
## [1.214.0](https://github.com/windmill-labs/windmill/compare/v1.213.0...v1.214.0) (2023-11-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* hub path scripts + nested inputs glue ([#2668](https://github.com/windmill-labs/windmill/issues/2668)) ([ad199af](https://github.com/windmill-labs/windmill/commit/ad199afd06814540bb7d36669709902be56eeb8a))
|
||||
* **python:** Refactor Windmill Python client (remove `windmill-api`) ([#2665](https://github.com/windmill-labs/windmill/issues/2665)) ([37ffdae](https://github.com/windmill-labs/windmill/commit/37ffdaed60fb750f1466b440353e1d8409eaea90))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix flow primary schedule clearing ([3ebe6d7](https://github.com/windmill-labs/windmill/commit/3ebe6d7a620e37fd6c81bcf4c8713a48eb081f81))
|
||||
|
||||
## [1.213.0](https://github.com/windmill-labs/windmill/compare/v1.212.0...v1.213.0) (2023-11-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* code completion UI + other nits ([#2657](https://github.com/windmill-labs/windmill/issues/2657)) ([6d426b4](https://github.com/windmill-labs/windmill/commit/6d426b4ec49d4749a89c86dc5eb1f11bf705ca26))
|
||||
* Expanding an s3object result now opens the S3 file browser ([#2656](https://github.com/windmill-labs/windmill/issues/2656)) ([baac93f](https://github.com/windmill-labs/windmill/commit/baac93f40140ee37548a273885c028a8e6500b6d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ask to return value ([#2659](https://github.com/windmill-labs/windmill/issues/2659)) ([de1e1f5](https://github.com/windmill-labs/windmill/commit/de1e1f545d4cd42f46d9af9a0349af86acf1901c))
|
||||
* fix embedded approval step timeouts ([51ce2f8](https://github.com/windmill-labs/windmill/commit/51ce2f8cb308da285dab0dc433bf596caa5eeed0))
|
||||
* fix error handling for list of errors ([c1bb97d](https://github.com/windmill-labs/windmill/commit/c1bb97d990810c9b3d909c9a045f0ce84be6c25e))
|
||||
* set session code completion to enabled by default ([#2664](https://github.com/windmill-labs/windmill/issues/2664)) ([ded0bb8](https://github.com/windmill-labs/windmill/commit/ded0bb890bb54ef80c857395474e692865ee4717))
|
||||
|
||||
## [1.212.0](https://github.com/windmill-labs/windmill/compare/v1.211.0...v1.212.0) (2023-11-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* S3 file picker as a drawer ([#2640](https://github.com/windmill-labs/windmill/issues/2640)) ([624b4d0](https://github.com/windmill-labs/windmill/commit/624b4d0e9898dddcce3cb2ce989ce1f9e4736061))
|
||||
* test an iteration ([c2598b3](https://github.com/windmill-labs/windmill/commit/c2598b330450f9885f7d10e2b5baa54d6ef88cc5))
|
||||
* upgrade to gpt-4-turbo ([#2655](https://github.com/windmill-labs/windmill/issues/2655)) ([8ea98c2](https://github.com/windmill-labs/windmill/commit/8ea98c2c8d636209954a267116eb03ab13217ef8))
|
||||
|
||||
## [1.211.0](https://github.com/windmill-labs/windmill/compare/v1.210.1...v1.211.0) (2023-11-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* agent mode v0 ([cd260e7](https://github.com/windmill-labs/windmill/commit/cd260e7062802ab39115025577a2456c66435899))
|
||||
* agent mode v0 ([47ad8d6](https://github.com/windmill-labs/windmill/commit/47ad8d6013e7144a5d018f43c827b171228b5f42))
|
||||
* ai regex ([19907e4](https://github.com/windmill-labs/windmill/commit/19907e4012e88a8fd28a5f0564a7ea48ec12020c))
|
||||
* custom error messages for forms ([2f15ebc](https://github.com/windmill-labs/windmill/commit/2f15ebc5f9628b0d26ec08b3527ddc96c6d42ba0))
|
||||
* **python:** Add functionality and resiliency to wmill python client ([#2650](https://github.com/windmill-labs/windmill/issues/2650)) ([c7a30f7](https://github.com/windmill-labs/windmill/commit/c7a30f7c9db26252f0ee69e1276ddccc0d52acb3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add refresh button to item picker ([4882d94](https://github.com/windmill-labs/windmill/commit/4882d94dfe18c156662fe483e1dd4f5d3e3be3af))
|
||||
* fix base64 assignment (file input) ([70f1210](https://github.com/windmill-labs/windmill/commit/70f121035edd94b3940f530a5603b1ff4bf03839))
|
||||
* main broken tests ([#2652](https://github.com/windmill-labs/windmill/issues/2652)) ([c90f7f1](https://github.com/windmill-labs/windmill/commit/c90f7f167e0b9c20008a62eeeef7819e24fc3da9))
|
||||
* token expiry is equal to timeout ([b667317](https://github.com/windmill-labs/windmill/commit/b667317d44f37bb50f24e356a1a3d231ebe8b4b4))
|
||||
|
||||
## [1.210.1](https://github.com/windmill-labs/windmill/compare/v1.210.0...v1.210.1) (2023-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add toggle to invites ([36379d6](https://github.com/windmill-labs/windmill/commit/36379d6db05de170e0237b12e767b6d6f4a6f2ef))
|
||||
* auto-invite all instead of by domain ([c07e905](https://github.com/windmill-labs/windmill/commit/c07e9056f1872cae70b8c3bebdbbf47daeee7ac5))
|
||||
* flow copilot arg types ([#2648](https://github.com/windmill-labs/windmill/issues/2648)) ([08c14e5](https://github.com/windmill-labs/windmill/commit/08c14e51c792fe65d4f993379eff0e5c8a75215b))
|
||||
* improve error message for unauthorized variables/resources ([5a72ca9](https://github.com/windmill-labs/windmill/commit/5a72ca9b24c5c4e9fe94c7865b9145283aceff53))
|
||||
* leave workspace + instance api ([ee243de](https://github.com/windmill-labs/windmill/commit/ee243dedc6df28a64f15e0b274b7fa96f6428474))
|
||||
* make wmill compatible with python 3.7 ([6ae1a69](https://github.com/windmill-labs/windmill/commit/6ae1a69b75fe039586956064880c274a21fc5970))
|
||||
* migrate old state env variable to new env variable ([2692737](https://github.com/windmill-labs/windmill/commit/2692737418ed601c0a36c368f59ffb8d10d9ad38))
|
||||
|
||||
## [1.210.0](https://github.com/windmill-labs/windmill/compare/v1.209.0...v1.210.0) (2023-11-17)
|
||||
|
||||
|
||||
|
||||
62
Dockerfile
62
Dockerfile
@@ -1,4 +1,8 @@
|
||||
FROM debian:bookworm-slim as nsjail
|
||||
ARG DEBIAN_IMAGE=debian:bookworm-slim
|
||||
ARG RUST_IMAGE=rust:slim-bookworm
|
||||
ARG PYTHON_IMAGE=python:3.11.4-slim-bookworm
|
||||
|
||||
FROM ${DEBIAN_IMAGE} as nsjail
|
||||
|
||||
WORKDIR /nsjail
|
||||
|
||||
@@ -22,7 +26,7 @@ RUN if [ "$nsjail" = "true" ]; then git clone -b master --single-branch https://
|
||||
&& git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800; fi
|
||||
RUN if [ "$nsjail" = "true" ]; then make; else touch nsjail; fi
|
||||
|
||||
FROM rust:slim-bookworm AS rust_base
|
||||
FROM ${RUST_IMAGE} AS rust_base
|
||||
|
||||
RUN apt-get update && apt-get install -y git libssl-dev pkg-config npm
|
||||
|
||||
@@ -87,7 +91,7 @@ COPY .git/ .git/
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
|
||||
|
||||
|
||||
FROM debian:bookworm-slim as downloader
|
||||
FROM ${DEBIAN_IMAGE} as downloader
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
@@ -101,29 +105,44 @@ RUN [ "$TARGETPLATFORM" == "linux/amd64" ] && curl -Lsf https://github.com/denol
|
||||
|
||||
RUN unzip deno.zip && rm deno.zip
|
||||
|
||||
FROM python:3.11.4-slim-bookworm
|
||||
FROM ${PYTHON_IMAGE}
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ARG POWERSHELL_VERSION=7.3.5
|
||||
ARG POWERSHELL_DEB_VERSION=7.3.5-1
|
||||
ARG RCLONE_VERSION=1.60.1
|
||||
ARG KUBECTL_VERSION=1.27.2
|
||||
ARG HELM_VERSION=3.12.0
|
||||
ARG APP=/usr/src/app
|
||||
ARG WITH_POWERSHELL=true
|
||||
ARG WITH_RCLONE=true
|
||||
ARG WITH_KUBECTL=true
|
||||
ARG WITH_HELM=true
|
||||
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y ca-certificates wget curl git jq libprotobuf-dev libnl-route-3-dev unzip build-essential unixodbc xmlsec1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then apt-get update -y && apt install libicu-dev -y && wget -O 'pwsh.deb' 'https://github.com/PowerShell/PowerShell/releases/download/v7.3.5/powershell_7.3.5-1.deb_amd64.deb' && \
|
||||
dpkg --install 'pwsh.deb' && \
|
||||
rm 'pwsh.deb'; else echo 'pwshell not on amd64'; fi
|
||||
RUN if [ "$WITH_POWERSHELL" = "true" ]; then \
|
||||
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then apt-get update -y && apt install libicu-dev -y && wget -O 'pwsh.deb' "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell_${POWERSHELL_DEB_VERSION}.deb_amd64.deb" && \
|
||||
dpkg --install 'pwsh.deb' && \
|
||||
rm 'pwsh.deb'; else echo 'pwshell not on amd64'; fi; \
|
||||
else echo 'Building the image without powershell'; fi
|
||||
|
||||
RUN arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
wget https://get.helm.sh/helm-v3.12.0-linux-$arch.tar.gz && \
|
||||
tar -zxvf helm-v3.12.0-linux-$arch.tar.gz && \
|
||||
mv linux-$arch/helm /usr/local/bin/helm &&\
|
||||
chmod +x /usr/local/bin/helm
|
||||
RUN if [ "$WITH_HELM" = "true" ]; then \
|
||||
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
wget "https://get.helm.sh/helm-v${HELM_VERSION}-linux-$arch.tar.gz" && \
|
||||
tar -zxvf "helm-v${HELM_VERSION}-linux-$arch.tar.gz" && \
|
||||
mv linux-$arch/helm /usr/local/bin/helm &&\
|
||||
chmod +x /usr/local/bin/helm; \
|
||||
else echo 'Building the image without helm'; fi
|
||||
|
||||
RUN arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
curl -LO "https://dl.k8s.io/release/v1.27.2/bin/linux/$arch/kubectl" && \
|
||||
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
|
||||
RUN if [ "$WITH_KUBECTL" = "true" ]; then \
|
||||
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
curl -LO "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/$arch/kubectl" && \
|
||||
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl; \
|
||||
else echo 'Building the image without kubectl'; fi
|
||||
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
@@ -140,10 +159,13 @@ RUN set -eux; \
|
||||
unzip awscliv2.zip && \
|
||||
./aws/install && rm awscliv2.zip
|
||||
|
||||
RUN arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
curl -o rclone.zip "https://downloads.rclone.org/v1.60.1/rclone-v1.60.1-linux-$arch.zip"; \
|
||||
unzip -p rclone.zip rclone-v1.60.1-linux-$arch/rclone > /usr/bin/rclone; rm rclone.zip; \
|
||||
chown root:root /usr/bin/rclone; chmod 755 /usr/bin/rclone
|
||||
RUN if [ "$WITH_RCLONE" = "true" ]; then \
|
||||
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
curl -o rclone.zip "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-$arch.zip"; \
|
||||
unzip -p rclone.zip rclone-v${RCLONE_VERSION}-linux-$arch/rclone > /usr/bin/rclone; rm rclone.zip; \
|
||||
chown root:root /usr/bin/rclone; chmod 755 /usr/bin/rclone; \
|
||||
else echo 'Building the image without rclone'; fi
|
||||
|
||||
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
|
||||
80
README.md
80
README.md
@@ -1,14 +1,11 @@
|
||||
<p align="center">
|
||||
<a href="https://www.windmill.dev/"><img src="./imgs/windmill-banner.png" alt="windmill.dev"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<em>.</em>
|
||||
</p>
|
||||
<p align=center>
|
||||
Open-source developer infrastructure for internal tools. Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIsm and custom UIs to trigger workflows and scripts as internal apps.
|
||||
Open-source developer infrastructure for internal tools (APIs, background jobs, workflows and UIs). Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIsm and custom UIs to trigger workflows and scripts as internal apps.
|
||||
|
||||
<p align=center>
|
||||
Scripts are turned into UIs and no-code modules, no-code modules can be composed into very rich flows, and script and flows can be triggered from internal UIs made with a low-code builder. The script languages supported are: Python, TypeScript, Go, Bash, SQL. Scripts can be generated by an AI assistant powered by OpenAI.
|
||||
Scripts are turned into UIs and no-code modules, no-code modules can be composed into rich flows, and script and flows can be triggered from internal UIs made with a low-code builder. Supported script languages supported are: Python, TypeScript, Go, Bash, SQL.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -36,7 +33,7 @@ Scripts are turned into UIs and no-code modules, no-code modules can be composed
|
||||
<a href="https://app.windmill.dev">Try it</a> - <a href="https://www.windmill.dev/docs/intro/">Docs</a> - <a href="https://discord.gg/V7PM2YHsPB">Discord</a> - <a href="https://hub.windmill.dev">Hub</a> - <a href="https://www.windmill.dev/docs/misc/contributing">Contributor's guide</a>
|
||||
</p>
|
||||
|
||||
# Windmill - Turn scripts into workflows and UIs that you can share and run at scale
|
||||
# Windmill - Developer platform for APIs, background jobs, workflows and UIs
|
||||
|
||||
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers
|
||||
dedicated instance and commercial support and licenses.
|
||||
@@ -45,12 +42,13 @@ dedicated instance and commercial support and licenses.
|
||||
|
||||
https://github.com/windmill-labs/windmill/assets/122811744/0b132cd1-ee67-4505-822f-0c7ee7104252
|
||||
|
||||
- [Windmill - Turn scripts into workflows and UIs that you can share and run at scale](#windmill---turn-scripts-into-workflows-and-uis-that-you-can-share-and-run-at-scale)
|
||||
- [Windmill - Developer platform for APIs, background jobs, workflows and UIs](#windmill---developer-platform-for-apis-background-jobs-workflows-and-uis)
|
||||
- [Main Concepts](#main-concepts)
|
||||
- [Show me some actual script code](#show-me-some-actual-script-code)
|
||||
- [CLI](#cli)
|
||||
- [Running scripts locally](#running-scripts-locally)
|
||||
- [Stack](#stack)
|
||||
- [Fastest Self-Hostable Workflow Engine](#fastest-self-hostable-workflow-engine)
|
||||
- [Security](#security)
|
||||
- [Sandboxing](#sandboxing)
|
||||
- [Secrets, credentials and sensitive values](#secrets-credentials-and-sensitive-values)
|
||||
@@ -59,12 +57,10 @@ https://github.com/windmill-labs/windmill/assets/122811744/0b132cd1-ee67-4505-82
|
||||
- [How to self-host](#how-to-self-host)
|
||||
- [Docker compose](#docker-compose)
|
||||
- [Kubernetes (k8s) and Helm charts](#kubernetes-k8s-and-helm-charts)
|
||||
- [Postgres without superuser](#postgres-without-superuser)
|
||||
- [Run from binaries](#run-from-binaries)
|
||||
- [OAuth, SSO \& SMTP](#oauth-sso--smtp)
|
||||
- [Commercial license](#commercial-license)
|
||||
- [OAuth for self-hosting](#oauth-for-self-hosting)
|
||||
- [smtp for self-hosting](#smtp-for-self-hosting)
|
||||
- [Resource types](#resource-types)
|
||||
- [Manually fetch latest Windmill binary](#manually-fetch-latest-windmill-binary)
|
||||
- [Integrations](#integrations)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Run a local dev setup](#run-a-local-dev-setup)
|
||||
- [only Frontend](#only-frontend)
|
||||
@@ -75,22 +71,28 @@ https://github.com/windmill-labs/windmill/assets/122811744/0b132cd1-ee67-4505-82
|
||||
## Main Concepts
|
||||
|
||||
1. Define a minimal and generic script in Python, TypeScript, Go or Bash that
|
||||
solves a specific task. Here sending a POST request. The code can be defined
|
||||
in the provided Web IDE or synchronized with your own github repo:
|
||||
solves a specific task. The code can be defined
|
||||
in the [provided Web IDE](https://www.windmill.dev/docs/code_editor) or [synchronized with your own GitHub repo](https://www.windmill.dev/docs/advanced/cli/sync) (e.g. through [VS Code](https://www.windmill.dev/docs/cli_local_dev/vscode-extension) extension):
|
||||
|
||||

|
||||
|
||||
2. Your scripts parameters are automatically parsed and generate a frontend.
|
||||
 
|
||||
2. Your scripts parameters are automatically parsed and [generate a frontend](https://www.windmill.dev/docs/core_concepts/auto_generated_uis).
|
||||
|
||||
3. Make it flow! You can chain your scripts or scripts made by the community
|
||||

|
||||
|
||||

|
||||
|
||||
3. Make it [flow](https://www.windmill.dev/docs/flows/flow_editor)! You can chain your scripts or scripts made by the community
|
||||
shared on [WindmillHub](https://hub.windmill.dev).
|
||||
|
||||

|
||||
|
||||
4. Build complex UIs on top of your scripts and flows.
|
||||
4. Build [complex UIs](https://www.windmill.dev/docs/apps/app_editor) on top of your scripts and flows.
|
||||
|
||||

|
||||
|
||||
Scripts and flows can also be triggered by a cron schedule '_/5 _ \* \* \*' or
|
||||
through webhooks.
|
||||
Scripts and flows can also be triggered by a [cron schedule](https://www.windmill.dev/docs/core_concepts/scheduling) (e.g. '_/5 _ \* \* \*') or
|
||||
through [webhooks](https://www.windmill.dev/docs/core_concepts/webhooks).
|
||||
|
||||
You can build your entire infra on top of Windmill!
|
||||
|
||||
@@ -146,7 +148,9 @@ instance from local commands. See
|
||||
You can run your script locally easily, you simply need to pass the right
|
||||
environment variables for the `wmill` client library to fetch resources and
|
||||
variables from your instance if necessary. See more:
|
||||
<https://www.windmill.dev/docs/advanced/local_development/>.
|
||||
<https://www.windmill.dev/docs/advanced/local_development>.
|
||||
|
||||
To develop & test locally scripts & flows, we recommend using the Windmill VS Code extension: <https://www.windmill.dev/docs/cli_local_dev/vscode-extension>.
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -167,6 +171,14 @@ variables from your instance if necessary. See more:
|
||||
- python runtime is python3
|
||||
- golang runtime is 1.19.1
|
||||
|
||||
## Fastest Self-Hostable Workflow Engine
|
||||
|
||||
We have compared Windmill to other self-hostable workflow engines (Airflow, Prefect & Temporal) and Windmill is the most performant solution for both benchmarks: one flow composed of 40 lightweight tasks & one flow composed of 10 long-running tasks.
|
||||
|
||||
All methodology & results on our [Benchmarks](https://www.windmill.dev/docs/misc/benchmarks/competitors#airflow-setup) page.
|
||||
|
||||

|
||||
|
||||
## Security
|
||||
|
||||
### Sandboxing
|
||||
@@ -204,10 +216,14 @@ back to the database is ~50ms. A typical lightweight deno job will take around
|
||||
|
||||
We only provide docker-compose setup here. For more advanced setups, like
|
||||
compiling from source or using without a postgres super user, see
|
||||
[documentation](https://www.windmill.dev/docs/advanced/self_host).
|
||||
[Self-Host documentation](https://www.windmill.dev/docs/advanced/self_host).
|
||||
|
||||
### Docker compose
|
||||
|
||||
Windmill can be deployed using 3 files: ([docker-compose.yml](./docker-compose.yml), [Caddyfile](./Caddyfile) and a [.env](./.env)) in a single command.
|
||||
|
||||
Make sure Docker is started, and run:
|
||||
|
||||
```
|
||||
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
|
||||
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile -o Caddyfile
|
||||
@@ -222,6 +238,8 @@ The default super-admin user is: admin@windmill.dev / changeme.
|
||||
|
||||
From there, you can follow the setup app and create other users.
|
||||
|
||||
More details in [Self-Host Documention](https://www.windmill.dev/docs/advanced/self_host#docker).
|
||||
|
||||
### Kubernetes (k8s) and Helm charts
|
||||
|
||||
We publish helm charts at:
|
||||
@@ -247,14 +265,16 @@ Workspace SSO, Microsoft/Azure and Okta) directly from the UI in the superadmin
|
||||
settings. Do note that there is a limit of 50 SSO users on the community
|
||||
edition.
|
||||
|
||||
[See documentation](https://www.windmill.dev/docs/misc/setup_oauth).
|
||||
|
||||
### Commercial license
|
||||
|
||||
To self-host Windmill, you must respect the terms of the AGPLv3 license which
|
||||
To self-host Windmill, you must respect the terms of the [AGPLv3 license](https://www.gnu.org/licenses/agpl-3.0.en.html) which
|
||||
you do not need to worry about for personal uses. For business uses, you should
|
||||
be fine if you do not re-expose it in any way Windmill to your users and are
|
||||
be fine if you do not re-expose Windmill in any way to your users and are
|
||||
comfortable with AGPLv3.
|
||||
|
||||
To re-expose any Windmill parts to your users as a feature of your product, or
|
||||
To [re-expose any Windmill parts to your users](https://www.windmill.dev/docs/misc/white_labelling) as a feature of your product, or
|
||||
to build a feature on top of Windmill, to comply with AGPLv3 your product must
|
||||
be AGPLv3 or you must get a commercial license. Contact us at
|
||||
<ruben@windmill.dev> if you have any doubts.
|
||||
@@ -264,9 +284,11 @@ your current infrastructure to Windmill, support with tight SLA, and our global
|
||||
cache sync for high-performance/no dependency cache miss of cluster from 10+
|
||||
nodes to 200+ nodes.
|
||||
|
||||
### Resource types
|
||||
### Integrations
|
||||
|
||||
You will also want to import all the approved resource types from
|
||||
In Windmill, integrations are referred to as [resources and resource types](https://www.windmill.dev/docs/core_concepts/resources_and_types). Each Resource has a Resource Type that defines the schema that the resource needs to implement.
|
||||
|
||||
On self-hosted instances, you might want to import all the approved resource types from
|
||||
[WindmillHub](https://hub.windmill.dev). A setup script will prompt you to have
|
||||
it being synced automatically everyday.
|
||||
|
||||
@@ -341,11 +363,11 @@ it being synced automatically everyday.
|
||||
| SMTP_PASSWORD | None | password for the smtp server to send invite emails | Server |
|
||||
| SMTP_TLS_IMPLICIT | false | https://docs.rs/mail-send/latest/mail_send/struct.SmtpClientBuilder.html#method.implicit_tlsemails | Server |
|
||||
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmin can create workspaces | Server |
|
||||
| GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE | None | Path to a script to run when a root job fails. The script will be run in and from the admins workspace | Server |
|
||||
| GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE | None | Path to a script or flow to run when a root job fails. The path needs to be prefixed with either `script/` or `flow/` to indicate the kind of error handler being used (assuming `script/` by default). The error handler will be run in and from the admins workspace | Server |
|
||||
| WHITELIST_ENVS | None | List of envs variables, separated by a ',' that are whitelisted as being safe to passthrough the workers | Worker |
|
||||
| SAML_METADATA | None | SAML Metadata URL to enable SAML SSO (EE only) | Server |
|
||||
| SECRET_SALT | None | Secret Salt used for encryption and decryption of secrets. If defined, the secrets will not be decryptable unless the right salt is passed in, which is the case for the workers and the server | Server + Worker |
|
||||
| OPENAI_AZURE_BASE_PATH | None | Azure OpenAI API base path (no trailing slash) | Server |
|
||||
| OPENAI_AZURE_BASE_PATH | None | Azure OpenAI path to be used instead of the OpenAI path. All Windmill AI features will run on the specified deployed model. Format: `https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}` | Server |
|
||||
| DISABLE_EMBEDDING | false | Disable local embedding search of hub scripts | Server |
|
||||
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
|
||||
| DISABLE_SERVER | false | Disable the external API, operate as a worker only instance | Worker |
|
||||
|
||||
15
backend/.sqlx/query-00be497354f5375e9ccffb998d126a853da91d607ff9e57e10d0e5481e4d3848.json
generated
Normal file
15
backend/.sqlx/query-00be497354f5375e9ccffb998d126a853da91d607ff9e57e10d0e5481e4d3848.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM usr WHERE workspace_id = $1 AND email = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "00be497354f5375e9ccffb998d126a853da91d607ff9e57e10d0e5481e4d3848"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE $2::text = '*' OR email LIKE CONCAT('%', $2::text) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING",
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -12,5 +12,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9739c91f85f36367cec44b12c921e2917afbcb249dbf52c82f06c943f0e1185d"
|
||||
"hash": "0360207b5fb2a7f877c2608566454f40f7cbbcd20bcb84e5968ac3e21b6ea0f6"
|
||||
}
|
||||
23
backend/.sqlx/query-108e4c505168381b51ad298b5294aa73e84d35bb68a5911985139bbf94c1d231.json
generated
Normal file
23
backend/.sqlx/query-108e4c505168381b51ad298b5294aa73e84d35bb68a5911985139bbf94c1d231.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT extra_perms from resource WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "108e4c505168381b51ad298b5294aa73e84d35bb68a5911985139bbf94c1d231"
|
||||
}
|
||||
20
backend/.sqlx/query-67624d479cc293b0306398f9e855cf38a72dd3d7d75b01e93cfd3ac4875b6e37.json
generated
Normal file
20
backend/.sqlx/query-67624d479cc293b0306398f9e855cf38a72dd3d7d75b01e93cfd3ac4875b6e37.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM password",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "67624d479cc293b0306398f9e855cf38a72dd3d7d75b01e93cfd3ac4875b6e37"
|
||||
}
|
||||
23
backend/.sqlx/query-7813908c080e239fd6b9df3711fd704d6bb5d5686a02b72de2cb61e3c127cb54.json
generated
Normal file
23
backend/.sqlx/query-7813908c080e239fd6b9df3711fd704d6bb5d5686a02b72de2cb61e3c127cb54.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT extra_perms from folder WHERE name = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7813908c080e239fd6b9df3711fd704d6bb5d5686a02b72de2cb61e3c127cb54"
|
||||
}
|
||||
35
backend/.sqlx/query-8e0679c2b1bd451691fe5c69a2841ddc9f211311316ec6b9d4699b2c70997a19.json
generated
Normal file
35
backend/.sqlx/query-8e0679c2b1bd451691fe5c69a2841ddc9f211311316ec6b9d4699b2c70997a19.json
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT tag, dedicated_worker, value->>'early_return' as early_return from flow WHERE path = $1 and workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "dedicated_worker",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "early_return",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8e0679c2b1bd451691fe5c69a2841ddc9f211311316ec6b9d4699b2c70997a19"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes FROM token WHERE email = $1\n ORDER BY created_at DESC",
|
||||
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes FROM token WHERE email = $1\n ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -48,5 +48,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "773c145013623e4eb29a8df70e46d805dc5b15942a36a0d988521abb0cb34e41"
|
||||
"hash": "9ce6eecfa10c2f71cc536957ead498ad829ea8023ce449eab27225ec66738525"
|
||||
}
|
||||
23
backend/.sqlx/query-a90924854cbd98bb16a2ed02e7966b0726e11ddd7511f1d503b1df29dee71419.json
generated
Normal file
23
backend/.sqlx/query-a90924854cbd98bb16a2ed02e7966b0726e11ddd7511f1d503b1df29dee71419.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT extra_perms from variable WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a90924854cbd98bb16a2ed02e7966b0726e11ddd7511f1d503b1df29dee71419"
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE queue SET canceled = true, canceled_reason = 'http connection broke', canceled_by = $1 WHERE id = $2 AND workspace_id = $3",
|
||||
"query": "UPDATE queue SET canceled = true, canceled_reason = 'http connection broke', canceled_by = queue.created_by WHERE id = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "460a3c3161899e172d2d20004e12ad3f331c73f260d099749a9e8fb85ae5614e"
|
||||
"hash": "a96dd57b127a1adbdca13867a76d7fd9f9a9c76b02bcebd2830d928821f3abc0"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT content FROM script WHERE path = $1 AND workspace_id = $2\n AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \n workspace_id = $2)\n ",
|
||||
"query": "\n SELECT content FROM script WHERE path = $1 AND workspace_id = $2\n AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND\n workspace_id = $2)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -19,5 +19,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d581bfb507d3a875f07677187717a469ec6ab1db1585835c961409612d81ec7d"
|
||||
"hash": "ac01e45d3335015f53f3d63fe159e631efb65c3d326b6b6ae8361a2116bff145"
|
||||
}
|
||||
52
backend/.sqlx/query-bc1f2f169b4960dae02f62e37bb4ae081146e598109860ee1f42cd3778c5ebaf.json
generated
Normal file
52
backend/.sqlx/query-bc1f2f169b4960dae02f62e37bb4ae081146e598109860ee1f42cd3778c5ebaf.json
generated
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes FROM token WHERE email = $1 AND label != 'ephemeral-script'\n ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "token_prefix",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "expiration",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bc1f2f169b4960dae02f62e37bb4ae081146e598109860ee1f42cd3778c5ebaf"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n VALUES ($1, $2, $3, $4)",
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, email)\n DO UPDATE SET is_admin = $3, operator = $4",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -13,5 +13,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "be7a99a5bb6858323ca61dd51077010f51ba58ae76b9a413339255024dcb524d"
|
||||
"hash": "d970a0b07a2b5840d1feb1baacb834dbaf91c633d3e7e1e29c8eb7eedc53e888"
|
||||
}
|
||||
1313
backend/Cargo.lock
generated
1313
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.210.0"
|
||||
version = "1.219.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -21,7 +21,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.210.0"
|
||||
version = "1.219.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -97,8 +97,8 @@ headers = "^0"
|
||||
hyper = { version = "^0", features = ["full"] }
|
||||
tokio = { version = "^1", features = ["full", "tracing"] }
|
||||
tower = "^0"
|
||||
tower-http = { version = "^0", features = ["trace", "cors"] }
|
||||
tower-cookies = "^0"
|
||||
tower-http = { version = "^0.4", features = ["trace", "cors"] }
|
||||
tower-cookies = "0.9.0"
|
||||
serde = "^1"
|
||||
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
|
||||
uuid = { version = "^1", features = ["serde", "v4"] }
|
||||
@@ -137,6 +137,7 @@ json-pointer = "^0"
|
||||
itertools = "^0"
|
||||
regex = "^1"
|
||||
deno_fetch = "0.139.0"
|
||||
deno_tls = "0.102.0"
|
||||
deno_console = "0.115.0"
|
||||
deno_url = "0.115.0"
|
||||
deno_webidl = "0.115.0"
|
||||
@@ -180,7 +181,7 @@ async-stripe = { version = "0.25.2", features = [
|
||||
async_zip = { version = "0.0.11", features = ["full"] }
|
||||
once_cell = "1.17.1"
|
||||
rsmq_async = { version = "5.1.5" }
|
||||
gosyn = "0.2.2"
|
||||
gosyn = "0.2.6"
|
||||
bytes = "1.4.0"
|
||||
gethostname = "0.4.3"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE queue ALTER COLUMN tag TYPE varchar(255);
|
||||
ALTER TABLE completed_job ALTER COLUMN tag TYPE varchar(255);
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
INSERT INTO config (name, config) VALUES
|
||||
('worker__reports', '{"init_bash": "apt-get update\napt-get install -y chromium", "worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun", "chromium"]}') ON CONFLICT DO NOTHING;
|
||||
@@ -45,7 +45,8 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
|
||||
"shapefile" => "pyshp",
|
||||
"sklearn" => "scikit-learn",
|
||||
"umap" => "umap-learn",
|
||||
"cv2" => "opencv-python"
|
||||
"cv2" => "opencv-python",
|
||||
"atlassian" => "atlassian-python-api"
|
||||
};
|
||||
|
||||
fn replace_import(x: String) -> String {
|
||||
@@ -167,7 +168,7 @@ pub async fn parse_python_imports(
|
||||
let code = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT content FROM script WHERE path = $1 AND workspace_id = $2
|
||||
AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND
|
||||
AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND
|
||||
workspace_id = $2)
|
||||
"#,
|
||||
&rpath,
|
||||
|
||||
@@ -311,6 +311,12 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
||||
} else {
|
||||
let literals = types
|
||||
.into_iter()
|
||||
.filter(|x| match ***x {
|
||||
TsType::TsKeywordType(TsKeywordType { kind, .. }) => {
|
||||
kind != TsKeywordTypeKind::TsStringKeyword
|
||||
}
|
||||
_ => true,
|
||||
})
|
||||
.map(|x| match &**x {
|
||||
TsType::TsLitType(TsLitType {
|
||||
lit: TsLit::Str(Str { value, .. }), ..
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"collaborators": [
|
||||
"Ruben Fiszel <ruben@windmill.dev>"
|
||||
],
|
||||
"version": "1.202.1",
|
||||
"version": "1.219.1",
|
||||
"files": [
|
||||
"windmill_parser_wasm_bg.wasm",
|
||||
"windmill_parser_wasm.js",
|
||||
|
||||
@@ -97,15 +97,6 @@ function getInt32Memory0() {
|
||||
return cachedInt32Memory0;
|
||||
}
|
||||
|
||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
function addHeapObject(obj) {
|
||||
if (heap_next === heap.length) heap.push(heap.length + 1);
|
||||
const idx = heap_next;
|
||||
@@ -115,6 +106,15 @@ function addHeapObject(obj) {
|
||||
return idx;
|
||||
}
|
||||
|
||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
let cachedFloat64Memory0 = null;
|
||||
|
||||
function getFloat64Memory0() {
|
||||
@@ -526,14 +526,10 @@ function __wbg_get_imports() {
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
};
|
||||
imports.wbg.__wbg_eval_f742bccbdcf34b02 = function(arg0, arg1) {
|
||||
imports.wbg.__wbg_eval_a7fb8f2e206bad71 = function(arg0, arg1) {
|
||||
const ret = eval(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
|
||||
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_boolean_get = function(arg0) {
|
||||
const v = getObject(arg0);
|
||||
const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2;
|
||||
@@ -551,6 +547,14 @@ function __wbg_get_imports() {
|
||||
const ret = getObject(arg0) === getObject(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
|
||||
const ret = BigInt.asUintN(64, arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
|
||||
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_number_get = function(arg0, arg1) {
|
||||
const obj = getObject(arg1);
|
||||
const ret = typeof(obj) === 'number' ? obj : undefined;
|
||||
@@ -566,19 +570,15 @@ function __wbg_get_imports() {
|
||||
const ret = getObject(arg0) in getObject(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
|
||||
const ret = BigInt.asUintN(64, arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
|
||||
const ret = getObject(arg0) == getObject(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_get_4a9aa5157afeb382 = function(arg0, arg1) {
|
||||
imports.wbg.__wbg_get_f01601b5a68d10e3 = function(arg0, arg1) {
|
||||
const ret = getObject(arg0)[arg1 >>> 0];
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_length_cace2e0b3ddc0502 = function(arg0) {
|
||||
imports.wbg.__wbg_length_1009b1af0c481d7b = function(arg0) {
|
||||
const ret = getObject(arg0).length;
|
||||
return ret;
|
||||
};
|
||||
@@ -586,39 +586,39 @@ function __wbg_get_imports() {
|
||||
const ret = typeof(getObject(arg0)) === 'function';
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_next_15da6a3df9290720 = function(arg0) {
|
||||
imports.wbg.__wbg_next_9b877f231f476d01 = function(arg0) {
|
||||
const ret = getObject(arg0).next;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_next_1989a20442400aaa = function() { return handleError(function (arg0) {
|
||||
imports.wbg.__wbg_next_6529ee0cca8d57ed = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).next();
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_done_bc26bf4ada718266 = function(arg0) {
|
||||
imports.wbg.__wbg_done_5fe336b092d60cf2 = function(arg0) {
|
||||
const ret = getObject(arg0).done;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_value_0570714ff7d75f35 = function(arg0) {
|
||||
imports.wbg.__wbg_value_0c248a78fdc8e19f = function(arg0) {
|
||||
const ret = getObject(arg0).value;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_iterator_7ee1a391d310f8e4 = function() {
|
||||
imports.wbg.__wbg_iterator_db7ca081358d4fb2 = function() {
|
||||
const ret = Symbol.iterator;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_get_2aff440840bb6202 = function() { return handleError(function (arg0, arg1) {
|
||||
imports.wbg.__wbg_get_7b48513de5dc5ea4 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = Reflect.get(getObject(arg0), getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_call_669127b9d730c650 = function() { return handleError(function (arg0, arg1) {
|
||||
imports.wbg.__wbg_call_90c26b09837aba1c = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg0).call(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_isArray_38525be7442aa21e = function(arg0) {
|
||||
imports.wbg.__wbg_isArray_74fb723e24f76012 = function(arg0) {
|
||||
const ret = Array.isArray(getObject(arg0));
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_ArrayBuffer_c7cc317e5c29cc0d = function(arg0) {
|
||||
imports.wbg.__wbg_instanceof_ArrayBuffer_e7d53d51371448e2 = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof ArrayBuffer;
|
||||
@@ -628,30 +628,30 @@ function __wbg_get_imports() {
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_isSafeInteger_c38b0a16d0c7cef7 = function(arg0) {
|
||||
imports.wbg.__wbg_isSafeInteger_f93fde0dca9820f8 = function(arg0) {
|
||||
const ret = Number.isSafeInteger(getObject(arg0));
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_entries_6d727b73ee02b7ce = function(arg0) {
|
||||
imports.wbg.__wbg_entries_9e2e2aa45aa5094a = function(arg0) {
|
||||
const ret = Object.entries(getObject(arg0));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_buffer_344d9b41efe96da7 = function(arg0) {
|
||||
imports.wbg.__wbg_buffer_a448f833075b71ba = function(arg0) {
|
||||
const ret = getObject(arg0).buffer;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_new_d8a000788389a31e = function(arg0) {
|
||||
imports.wbg.__wbg_new_8f67e318f15d7254 = function(arg0) {
|
||||
const ret = new Uint8Array(getObject(arg0));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_set_dcfd613a3420f908 = function(arg0, arg1, arg2) {
|
||||
imports.wbg.__wbg_set_2357bf09366ee480 = function(arg0, arg1, arg2) {
|
||||
getObject(arg0).set(getObject(arg1), arg2 >>> 0);
|
||||
};
|
||||
imports.wbg.__wbg_length_a5587d6cd79ab197 = function(arg0) {
|
||||
imports.wbg.__wbg_length_1d25fa9e4ac21ce7 = function(arg0) {
|
||||
const ret = getObject(arg0).length;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_Uint8Array_19e6f142a5e7e1e1 = function(arg0) {
|
||||
imports.wbg.__wbg_instanceof_Uint8Array_bced6f43aed8c1aa = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof Uint8Array;
|
||||
|
||||
Binary file not shown.
@@ -97,6 +97,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
} else if &x == "worker" {
|
||||
tracing::info!("Binary is in 'worker' mode");
|
||||
Mode::Worker
|
||||
} else if &x == "agent" {
|
||||
tracing::info!("Binary is in 'agent' mode");
|
||||
if std::env::var("BASE_INTERNAL_URL").is_err() {
|
||||
panic!("BASE_INTERNAL_URL is required in agent mode")
|
||||
}
|
||||
if std::env::var("JOB_TOKEN").is_err() {
|
||||
tracing::warn!("JOB_TOKEN is not passed, hence workers will still create one ephemeral token per job and the DATABASE_URL need to be of a role that can INSERT into the token table")
|
||||
}
|
||||
Mode::Agent
|
||||
} else {
|
||||
if &x != "standalone" {
|
||||
tracing::error!("mode not recognized, defaulting to standalone: {x}");
|
||||
@@ -130,7 +139,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false)
|
||||
&& mode != Mode::Worker;
|
||||
&& (mode == Mode::Server || mode == Mode::Standalone);
|
||||
|
||||
let server_bind_address: IpAddr = if server_mode {
|
||||
std::env::var("SERVER_BIND_ADDR")
|
||||
@@ -191,31 +200,36 @@ async fn main() -> anyhow::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let last_mig_version =
|
||||
sqlx::query_scalar!("select version from _sqlx_migrations order by version desc limit 1;")
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let is_agent = mode == Mode::Agent;
|
||||
|
||||
tracing::info!(
|
||||
if !is_agent {
|
||||
let last_mig_version = sqlx::query_scalar!(
|
||||
"select version from _sqlx_migrations order by version desc limit 1;"
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
tracing::info!(
|
||||
"Last migration version: {last_mig_version:?}. Starting potential migration of the db if first connection on a new windmill version (can take a while depending on the migration) ...",
|
||||
);
|
||||
|
||||
// migration code to avoid break
|
||||
windmill_api::migrate_db(&db).await?;
|
||||
// migration code to avoid break
|
||||
windmill_api::migrate_db(&db).await?;
|
||||
|
||||
let last_mig_version =
|
||||
sqlx::query_scalar!("select version from _sqlx_migrations order by version desc limit 1;")
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
tracing::info!(
|
||||
"Completed potential migration of the db. Last migration version: {last_mig_version:?}",
|
||||
);
|
||||
let last_mig_version = sqlx::query_scalar!(
|
||||
"select version from _sqlx_migrations order by version desc limit 1;"
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
tracing::info!(
|
||||
"Completed potential migration of the db. Last migration version: {last_mig_version:?}",
|
||||
);
|
||||
}
|
||||
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
|
||||
let shutdown_signal = windmill_common::shutdown_signal(tx.clone(), rx.resubscribe());
|
||||
|
||||
@@ -248,8 +262,18 @@ Windmill Community Edition {GIT_VERSION}
|
||||
port_var.unwrap_or(0)
|
||||
};
|
||||
|
||||
let default_base_internal_url = format!("http://localhost:{}", port.to_string());
|
||||
// since it's only on server mode, the port is statically defined
|
||||
let base_internal_url: String = format!("http://localhost:{}", port.to_string());
|
||||
let base_internal_url: String = if let Ok(base_url) = std::env::var("BASE_INTERNAL_URL") {
|
||||
if !is_agent {
|
||||
tracing::warn!("BASE_INTERNAL_URL is now unecessary and ignored unless the mode is 'agent', you can remove it.");
|
||||
default_base_internal_url.clone()
|
||||
} else {
|
||||
base_url
|
||||
}
|
||||
} else {
|
||||
default_base_internal_url.clone()
|
||||
};
|
||||
|
||||
initial_load(&db, tx.clone(), worker_mode, server_mode).await;
|
||||
|
||||
@@ -257,14 +281,10 @@ Windmill Community Edition {GIT_VERSION}
|
||||
|
||||
monitor_pool(&db).await;
|
||||
|
||||
if std::env::var("BASE_INTERNAL_URL").is_ok() {
|
||||
tracing::warn!("BASE_INTERNAL_URL is now unecessary and ignored, you can remove it.");
|
||||
}
|
||||
|
||||
let addr = SocketAddr::from((server_bind_address, port));
|
||||
|
||||
let rsmq2 = rsmq.clone();
|
||||
let (port_tx, port_rx) = tokio::sync::oneshot::channel::<u16>();
|
||||
let (base_internal_tx, base_internal_rx) = tokio::sync::oneshot::channel::<String>();
|
||||
|
||||
DirBuilder::new()
|
||||
.recursive(true)
|
||||
@@ -273,21 +293,28 @@ Windmill Community Edition {GIT_VERSION}
|
||||
.expect("could not create initial server dir");
|
||||
|
||||
let server_f = async {
|
||||
windmill_api::run_server(
|
||||
db.clone(),
|
||||
rsmq2,
|
||||
addr,
|
||||
rx.resubscribe(),
|
||||
port_tx,
|
||||
server_mode,
|
||||
)
|
||||
.await?;
|
||||
if !is_agent {
|
||||
windmill_api::run_server(
|
||||
db.clone(),
|
||||
rsmq2,
|
||||
addr,
|
||||
rx.resubscribe(),
|
||||
base_internal_tx,
|
||||
server_mode,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
base_internal_tx
|
||||
.send(base_internal_url.clone())
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!("Could not send base_internal_url to agent: {e}")
|
||||
})?;
|
||||
}
|
||||
Ok(()) as anyhow::Result<()>
|
||||
};
|
||||
|
||||
let workers_f = async {
|
||||
let port = port_rx.await?;
|
||||
let base_internal_url: String = format!("http://localhost:{}", port.to_string());
|
||||
let base_internal_url = base_internal_rx.await?;
|
||||
if worker_mode {
|
||||
run_workers(
|
||||
db.clone(),
|
||||
|
||||
@@ -719,7 +719,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
&db,
|
||||
&job.workspace_id,
|
||||
&job.permissioned_as,
|
||||
"ephemeral-zombie-jobs",
|
||||
"ephemeral-script",
|
||||
*SCRIPT_TOKEN_EXPIRY,
|
||||
&job.email,
|
||||
)
|
||||
|
||||
@@ -124,7 +124,7 @@ impl ApiServer {
|
||||
|
||||
let addr = sock.local_addr().unwrap();
|
||||
drop(sock);
|
||||
let (port_tx, _port_rx) = tokio::sync::oneshot::channel::<u16>();
|
||||
let (port_tx, _port_rx) = tokio::sync::oneshot::channel::<String>();
|
||||
|
||||
let task = tokio::task::spawn(windmill_api::run_server(
|
||||
db.clone(),
|
||||
@@ -961,7 +961,7 @@ fn spawn_test_worker(
|
||||
tokio::sync::broadcast::Sender<()>,
|
||||
tokio::task::JoinHandle<()>,
|
||||
) {
|
||||
for x in [windmill_worker::LOCK_CACHE_DIR] {
|
||||
for x in [windmill_worker::LOCK_CACHE_DIR, windmill_worker::GO_BIN_CACHE_DIR] {
|
||||
std::fs::DirBuilder::new()
|
||||
.recursive(true)
|
||||
.create(x)
|
||||
|
||||
@@ -82,5 +82,8 @@ tokenizers.workspace = true
|
||||
candle-core.workspace = true
|
||||
candle-transformers.workspace = true
|
||||
candle-nn.workspace = true
|
||||
aws-sdk-s3 = "0.36.0"
|
||||
aws-config = "0.57.2"
|
||||
aws-sdk-s3 = "0.39.1"
|
||||
aws-config = "1.0.0"
|
||||
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"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
version: 1.206.0
|
||||
version: 1.213.0
|
||||
title: Windmill API
|
||||
contact:
|
||||
name: Windmill Team
|
||||
@@ -952,6 +952,19 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/users/leave_instance:
|
||||
post:
|
||||
summary: leave instance
|
||||
operationId: leaveInstance
|
||||
tags:
|
||||
- user
|
||||
responses:
|
||||
'200':
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/users/usage:
|
||||
get:
|
||||
summary: get current usage outside of premium workspaces
|
||||
@@ -1124,24 +1137,6 @@ paths:
|
||||
- disabled
|
||||
- folders
|
||||
- folders_owners
|
||||
/w/{workspace}/users/leave_workspace:
|
||||
post:
|
||||
summary: leave workspace
|
||||
operationId: leaveWorkspace
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
responses:
|
||||
'200':
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/users/accept_invite:
|
||||
post:
|
||||
summary: accept invite to workspace
|
||||
@@ -1360,6 +1355,24 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/w/{workspace}/workspaces/leave:
|
||||
post:
|
||||
summary: leave workspace
|
||||
operationId: leaveWorkspace
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
responses:
|
||||
'200':
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/w/{workspace}/users/whois/{username}:
|
||||
get:
|
||||
summary: whois
|
||||
@@ -1534,6 +1547,24 @@ paths:
|
||||
properties:
|
||||
deploy_to:
|
||||
type: string
|
||||
/w/{workspace}/workspaces/is_premium:
|
||||
get:
|
||||
summary: get if workspace is premium
|
||||
operationId: getIsPremium
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
responses:
|
||||
'200':
|
||||
description: status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
/w/{workspace}/workspaces/premium_info:
|
||||
get:
|
||||
summary: get premium info
|
||||
@@ -1557,6 +1588,8 @@ paths:
|
||||
type: boolean
|
||||
usage:
|
||||
type: number
|
||||
seats:
|
||||
type: number
|
||||
required:
|
||||
- premium
|
||||
/w/{workspace}/workspaces/edit_slack_command:
|
||||
@@ -1670,6 +1703,8 @@ paths:
|
||||
properties:
|
||||
operator:
|
||||
type: boolean
|
||||
invite_all:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: status
|
||||
@@ -3266,32 +3301,32 @@ paths:
|
||||
id:
|
||||
type: string
|
||||
value:
|
||||
oneOf: &ref_177
|
||||
oneOf: &ref_182
|
||||
- type: object
|
||||
properties: &ref_161
|
||||
properties: &ref_166
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
oneOf: &ref_24
|
||||
- type: object
|
||||
properties: &ref_157
|
||||
properties: &ref_162
|
||||
value: {}
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- javascript
|
||||
required: &ref_158
|
||||
required: &ref_163
|
||||
- expr
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_159
|
||||
properties: &ref_164
|
||||
expr:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- javascript
|
||||
required: &ref_160
|
||||
required: &ref_165
|
||||
- expr
|
||||
- type
|
||||
discriminator: &ref_25
|
||||
@@ -3331,13 +3366,13 @@ paths:
|
||||
type: number
|
||||
concurrency_time_window_s:
|
||||
type: number
|
||||
required: &ref_162
|
||||
required: &ref_167
|
||||
- type
|
||||
- content
|
||||
- language
|
||||
- input_transforms
|
||||
- type: object
|
||||
properties: &ref_163
|
||||
properties: &ref_168
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@@ -3351,12 +3386,12 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
required: &ref_164
|
||||
required: &ref_169
|
||||
- type
|
||||
- path
|
||||
- input_transforms
|
||||
- type: object
|
||||
properties: &ref_165
|
||||
properties: &ref_170
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@@ -3368,12 +3403,12 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- flow
|
||||
required: &ref_166
|
||||
required: &ref_171
|
||||
- type
|
||||
- path
|
||||
- input_transforms
|
||||
- type: object
|
||||
properties: &ref_167
|
||||
properties: &ref_172
|
||||
modules:
|
||||
type: array
|
||||
items:
|
||||
@@ -3395,13 +3430,13 @@ paths:
|
||||
type: boolean
|
||||
parallelism:
|
||||
type: integer
|
||||
required: &ref_168
|
||||
required: &ref_173
|
||||
- modules
|
||||
- iterator
|
||||
- skip_failures
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_169
|
||||
properties: &ref_174
|
||||
branches:
|
||||
type: array
|
||||
items:
|
||||
@@ -3432,12 +3467,12 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- branchone
|
||||
required: &ref_170
|
||||
required: &ref_175
|
||||
- branches
|
||||
- default
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_171
|
||||
properties: &ref_176
|
||||
branches:
|
||||
type: array
|
||||
items:
|
||||
@@ -3462,28 +3497,28 @@ paths:
|
||||
- branchall
|
||||
parallel:
|
||||
type: boolean
|
||||
required: &ref_172
|
||||
required: &ref_177
|
||||
- branches
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_173
|
||||
properties: &ref_178
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- identity
|
||||
flow:
|
||||
type: boolean
|
||||
required: &ref_174
|
||||
required: &ref_179
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_175
|
||||
properties: &ref_180
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- graphql
|
||||
required: &ref_176
|
||||
required: &ref_181
|
||||
- type
|
||||
discriminator: &ref_178
|
||||
discriminator: &ref_183
|
||||
propertyName: type
|
||||
mapping:
|
||||
rawscript: '#/components/schemas/RawScript'
|
||||
@@ -3539,7 +3574,7 @@ paths:
|
||||
type: number
|
||||
retry:
|
||||
type: object
|
||||
properties: &ref_179
|
||||
properties: &ref_184
|
||||
constant:
|
||||
type: object
|
||||
properties:
|
||||
@@ -5132,6 +5167,8 @@ paths:
|
||||
type: boolean
|
||||
priority:
|
||||
type: integer
|
||||
dedicated_worker:
|
||||
type: boolean
|
||||
required: &ref_144
|
||||
- path
|
||||
- edited_by
|
||||
@@ -5279,6 +5316,8 @@ paths:
|
||||
type: boolean
|
||||
priority:
|
||||
type: integer
|
||||
dedicated_worker:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- type: object
|
||||
@@ -9448,6 +9487,57 @@ paths:
|
||||
properties:
|
||||
connection_settings_str:
|
||||
type: string
|
||||
/w/{workspace}/job_helpers/polars_connection_settings:
|
||||
post:
|
||||
summary: >-
|
||||
Converts an S3 resource to the set of arguments necessary to connect
|
||||
Polars to an S3 bucket
|
||||
operationId: polarsConnectionSettings
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
requestBody:
|
||||
description: S3 resource to connect to
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
s3_resource:
|
||||
$ref: '#/components/schemas/S3Resource'
|
||||
responses:
|
||||
'200':
|
||||
description: Connection settings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
endpoint_url:
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
secret:
|
||||
type: string
|
||||
use_ssl:
|
||||
type: boolean
|
||||
cache_regions:
|
||||
type: boolean
|
||||
client_kwargs:
|
||||
type: object
|
||||
properties: &ref_155
|
||||
region_name:
|
||||
type: string
|
||||
required: &ref_156
|
||||
- region_name
|
||||
required:
|
||||
- endpoint_url
|
||||
- use_ssl
|
||||
- cache_regions
|
||||
- client_kwargs
|
||||
/w/{workspace}/job_helpers/test_connection:
|
||||
get:
|
||||
summary: Test connection to the workspace datasets storage
|
||||
@@ -9465,10 +9555,10 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
/w/{workspace}/job_helpers/list_stored_datasets:
|
||||
post:
|
||||
summary: List the dataset keys available in the worspace datasets storage
|
||||
operationId: polarsConnectionSettings
|
||||
/w/{workspace}/job_helpers/list_stored_files:
|
||||
get:
|
||||
summary: List the file keys available in the worspace files storage (S3)
|
||||
operationId: listStoredFiles
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
@@ -9476,25 +9566,129 @@ paths:
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
- name: max_keys
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: marker
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Connection settings
|
||||
description: List of file keys
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
dataset_keys:
|
||||
next_marker:
|
||||
type: string
|
||||
windmill_large_files:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties: &ref_155
|
||||
properties: &ref_157
|
||||
s3:
|
||||
type: string
|
||||
s3_bucket:
|
||||
type: string
|
||||
required: &ref_156
|
||||
required: &ref_158
|
||||
- s3
|
||||
required:
|
||||
- windmill_large_files
|
||||
/w/{workspace}/job_helpers/load_file_metadata:
|
||||
get:
|
||||
summary: Load metadata of the file
|
||||
operationId: loadFileMetadata
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
- name: file_key
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: FileMetadata
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: &ref_159
|
||||
mime_type:
|
||||
type: string
|
||||
size_in_bytes:
|
||||
type: integer
|
||||
last_modified:
|
||||
type: string
|
||||
format: date-time
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
version_id:
|
||||
type: string
|
||||
/w/{workspace}/job_helpers/load_file_preview:
|
||||
get:
|
||||
summary: Load a preview of the file
|
||||
operationId: loadFilePreview
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
- name: file_key
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: file_size_in_bytes
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
- name: file_mime_type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: csv_separator
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: read_bytes_from
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
- name: read_bytes_length
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: FilePreview
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: &ref_160
|
||||
msg:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
content_type:
|
||||
type: string
|
||||
enum:
|
||||
- RawText
|
||||
- Csv
|
||||
- Parquet
|
||||
- Unknown
|
||||
required: &ref_161
|
||||
- content_type
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
@@ -10093,13 +10287,24 @@ components:
|
||||
HubScriptKind:
|
||||
name: kind
|
||||
schema: *ref_29
|
||||
PolarsClientKwargs:
|
||||
type: object
|
||||
properties: *ref_155
|
||||
required: *ref_156
|
||||
LargeFileStorage:
|
||||
type: object
|
||||
properties: *ref_15
|
||||
WindmillLargeFile:
|
||||
type: object
|
||||
properties: *ref_155
|
||||
required: *ref_156
|
||||
properties: *ref_157
|
||||
required: *ref_158
|
||||
WindmillFileMetadata:
|
||||
type: object
|
||||
properties: *ref_159
|
||||
WindmillFilePreview:
|
||||
type: object
|
||||
properties: *ref_160
|
||||
required: *ref_161
|
||||
S3Resource:
|
||||
type: object
|
||||
properties:
|
||||
@@ -10125,57 +10330,57 @@ components:
|
||||
- pathStyle
|
||||
StaticTransform:
|
||||
type: object
|
||||
properties: *ref_157
|
||||
required: *ref_158
|
||||
properties: *ref_162
|
||||
required: *ref_163
|
||||
JavascriptTransform:
|
||||
type: object
|
||||
properties: *ref_159
|
||||
required: *ref_160
|
||||
properties: *ref_164
|
||||
required: *ref_165
|
||||
InputTransform:
|
||||
oneOf: *ref_24
|
||||
discriminator: *ref_25
|
||||
RawScript:
|
||||
type: object
|
||||
properties: *ref_161
|
||||
required: *ref_162
|
||||
properties: *ref_166
|
||||
required: *ref_167
|
||||
PathScript:
|
||||
type: object
|
||||
properties: *ref_163
|
||||
required: *ref_164
|
||||
properties: *ref_168
|
||||
required: *ref_169
|
||||
PathFlow:
|
||||
type: object
|
||||
properties: *ref_165
|
||||
required: *ref_166
|
||||
properties: *ref_170
|
||||
required: *ref_171
|
||||
FlowModule:
|
||||
type: object
|
||||
properties: *ref_26
|
||||
required: *ref_27
|
||||
ForloopFlow:
|
||||
type: object
|
||||
properties: *ref_167
|
||||
required: *ref_168
|
||||
properties: *ref_172
|
||||
required: *ref_173
|
||||
BranchOne:
|
||||
type: object
|
||||
properties: *ref_169
|
||||
required: *ref_170
|
||||
properties: *ref_174
|
||||
required: *ref_175
|
||||
BranchAll:
|
||||
type: object
|
||||
properties: *ref_171
|
||||
required: *ref_172
|
||||
properties: *ref_176
|
||||
required: *ref_177
|
||||
Identity:
|
||||
type: object
|
||||
properties: *ref_173
|
||||
required: *ref_174
|
||||
properties: *ref_178
|
||||
required: *ref_179
|
||||
Graphql:
|
||||
type: object
|
||||
properties: *ref_175
|
||||
required: *ref_176
|
||||
properties: *ref_180
|
||||
required: *ref_181
|
||||
FlowModuleValue:
|
||||
oneOf: *ref_177
|
||||
discriminator: *ref_178
|
||||
oneOf: *ref_182
|
||||
discriminator: *ref_183
|
||||
Retry:
|
||||
type: object
|
||||
properties: *ref_179
|
||||
properties: *ref_184
|
||||
FlowValue:
|
||||
type: object
|
||||
properties: *ref_48
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.210.0
|
||||
version: 1.219.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -731,6 +731,20 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/users/leave_instance:
|
||||
post:
|
||||
summary: leave instance
|
||||
operationId: leaveInstance
|
||||
tags:
|
||||
- user
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/users/usage:
|
||||
get:
|
||||
summary: get current usage outside of premium workspaces
|
||||
@@ -827,22 +841,6 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/User"
|
||||
|
||||
/w/{workspace}/users/leave_workspace:
|
||||
post:
|
||||
summary: leave workspace
|
||||
operationId: leaveWorkspace
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/users/accept_invite:
|
||||
post:
|
||||
summary: accept invite to workspace
|
||||
@@ -1051,6 +1049,22 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/leave:
|
||||
post:
|
||||
summary: leave workspace
|
||||
operationId: leaveWorkspace
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/users/whois/{username}:
|
||||
get:
|
||||
summary: whois
|
||||
@@ -1349,6 +1363,8 @@ paths:
|
||||
properties:
|
||||
operator:
|
||||
type: boolean
|
||||
invite_all:
|
||||
type: boolean
|
||||
|
||||
responses:
|
||||
"200":
|
||||
@@ -1618,6 +1634,11 @@ paths:
|
||||
operationId: listTokens
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- name: exclude_ephemeral
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: truncated token
|
||||
@@ -6296,7 +6317,9 @@ paths:
|
||||
|
||||
/w/{workspace}/job_helpers/duckdb_connection_settings:
|
||||
post:
|
||||
summary: Converts an S3 resource to the set of instructions necessary to connect DuckDB to an S3 bucket
|
||||
summary:
|
||||
Converts an S3 resource to the set of instructions necessary to connect
|
||||
DuckDB to an S3 bucket
|
||||
operationId: duckdbConnectionSettings
|
||||
tags:
|
||||
- helpers
|
||||
@@ -6308,8 +6331,10 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
s3_resource:
|
||||
$ref: "#/components/schemas/S3Resource"
|
||||
type: object
|
||||
properties:
|
||||
s3_resource:
|
||||
$ref: "#/components/schemas/S3Resource"
|
||||
responses:
|
||||
"200":
|
||||
description: Connection settings
|
||||
@@ -6320,6 +6345,51 @@ paths:
|
||||
properties:
|
||||
connection_settings_str:
|
||||
type: string
|
||||
/w/{workspace}/job_helpers/polars_connection_settings:
|
||||
post:
|
||||
summary:
|
||||
Converts an S3 resource to the set of arguments necessary to connect
|
||||
Polars to an S3 bucket
|
||||
operationId: polarsConnectionSettings
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: S3 resource to connect to
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
s3_resource:
|
||||
$ref: "#/components/schemas/S3Resource"
|
||||
responses:
|
||||
"200":
|
||||
description: Connection settings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
endpoint_url:
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
secret:
|
||||
type: string
|
||||
use_ssl:
|
||||
type: boolean
|
||||
cache_regions:
|
||||
type: boolean
|
||||
client_kwargs:
|
||||
$ref: "#/components/schemas/PolarsClientKwargs"
|
||||
required:
|
||||
- endpoint_url
|
||||
- use_ssl
|
||||
- cache_regions
|
||||
- client_kwargs
|
||||
|
||||
/w/{workspace}/job_helpers/test_connection:
|
||||
get:
|
||||
@@ -6336,26 +6406,101 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/job_helpers/list_stored_datasets:
|
||||
post:
|
||||
summary: List the dataset keys available in the worspace datasets storage
|
||||
operationId: polarsConnectionSettings
|
||||
/w/{workspace}/job_helpers/list_stored_files:
|
||||
get:
|
||||
summary: List the file keys available in the worspace files storage (S3)
|
||||
operationId: listStoredFiles
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: max_keys
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: marker
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Connection settings
|
||||
description: List of file keys
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
dataset_keys:
|
||||
next_marker:
|
||||
type: string
|
||||
windmill_large_files:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WindmillLargeFile"
|
||||
required:
|
||||
- windmill_large_files
|
||||
|
||||
/w/{workspace}/job_helpers/load_file_metadata:
|
||||
get:
|
||||
summary: Load metadata of the file
|
||||
operationId: loadFileMetadata
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: file_key
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: FileMetadata
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/WindmillFileMetadata"
|
||||
|
||||
/w/{workspace}/job_helpers/load_file_preview:
|
||||
get:
|
||||
summary: Load a preview of the file
|
||||
operationId: loadFilePreview
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: file_key
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: file_size_in_bytes
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
- name: file_mime_type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: csv_separator
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: read_bytes_from
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
- name: read_bytes_length
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
description: FilePreview
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/WindmillFilePreview"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
@@ -8340,6 +8485,14 @@ components:
|
||||
type: string
|
||||
enum: [script, failure, trigger, approval]
|
||||
|
||||
PolarsClientKwargs:
|
||||
type: object
|
||||
properties:
|
||||
region_name:
|
||||
type: string
|
||||
required:
|
||||
- region_name
|
||||
|
||||
LargeFileStorage:
|
||||
type: object
|
||||
properties:
|
||||
@@ -8354,11 +8507,38 @@ components:
|
||||
properties:
|
||||
s3:
|
||||
type: string
|
||||
s3_bucket:
|
||||
type: string
|
||||
required:
|
||||
- s3
|
||||
|
||||
WindmillFileMetadata:
|
||||
type: object
|
||||
properties:
|
||||
mime_type:
|
||||
type: string
|
||||
size_in_bytes:
|
||||
type: integer
|
||||
last_modified:
|
||||
type: string
|
||||
format: date-time
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
version_id:
|
||||
type: string
|
||||
|
||||
WindmillFilePreview:
|
||||
type: object
|
||||
properties:
|
||||
msg:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
content_type:
|
||||
type: string
|
||||
enum: ["RawText", "Csv", "Parquet", "Unknown"]
|
||||
required:
|
||||
- content_type
|
||||
|
||||
S3Resource:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{self, Error, Result};
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
routing::get,
|
||||
@@ -228,14 +228,31 @@ impl EmbeddingsDb {
|
||||
self.db
|
||||
.create_collection("resource_types".to_string(), 384, Distance::Cosine)?;
|
||||
|
||||
let response = http_get_from_hub(
|
||||
&HTTP_CLIENT,
|
||||
"https://hub.windmill.dev/scripts/embeddings",
|
||||
false,
|
||||
None,
|
||||
pg_db,
|
||||
)
|
||||
.await?;
|
||||
let response = HTTP_CLIENT
|
||||
.get("https://bucket.windmillhub.com/embeddings/scripts_embeddings.json")
|
||||
.send()
|
||||
.await;
|
||||
let response =
|
||||
if response.is_err() || response.as_ref().unwrap().error_for_status_ref().is_err() {
|
||||
tracing::warn!("Failed to get scripts embeddings from bucket, trying hub...");
|
||||
http_get_from_hub(
|
||||
&HTTP_CLIENT,
|
||||
"https://hub.windmill.dev/scripts/embeddings",
|
||||
false,
|
||||
None,
|
||||
pg_db,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
response.unwrap()
|
||||
};
|
||||
if response.error_for_status_ref().is_err() {
|
||||
return Err(anyhow!(
|
||||
"Failed to get scripts embeddings from hub with error code: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
let hub_scripts = response.json::<Vec<HubScript>>().await?;
|
||||
|
||||
for script in &hub_scripts {
|
||||
@@ -257,14 +274,31 @@ impl EmbeddingsDb {
|
||||
self.db.insert_into_collection("scripts", embedding)?;
|
||||
}
|
||||
|
||||
let response = http_get_from_hub(
|
||||
&HTTP_CLIENT,
|
||||
"https://hub.windmill.dev/resource_types/embeddings",
|
||||
false,
|
||||
None,
|
||||
pg_db,
|
||||
)
|
||||
.await?;
|
||||
let response = HTTP_CLIENT
|
||||
.get("https://bucket.windmillhub.com/embeddings/resource_types_embeddings.json")
|
||||
.send()
|
||||
.await;
|
||||
let response = if response.is_err()
|
||||
|| response.as_ref().unwrap().error_for_status_ref().is_err()
|
||||
{
|
||||
tracing::warn!("Failed to get resource types embeddings from bucket, trying hub...");
|
||||
http_get_from_hub(
|
||||
&HTTP_CLIENT,
|
||||
"https://hub.windmill.dev/resource_types/embeddings",
|
||||
false,
|
||||
None,
|
||||
pg_db,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
response.unwrap()
|
||||
};
|
||||
if response.error_for_status_ref().is_err() {
|
||||
return Err(anyhow!(
|
||||
"Failed to get resource types embeddings from hub with error code: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
let hub_resource_types = response.json::<Vec<HubResourceType>>().await?;
|
||||
|
||||
let resource_types: Vec<ResourceType> =
|
||||
|
||||
@@ -520,6 +520,7 @@ async fn update_flow(
|
||||
.await?;
|
||||
|
||||
if let Some(schedule) = schedule {
|
||||
clear_schedule(tx.transaction_mut(), &flow_path, &w_id).await?;
|
||||
schedulables.push(schedule);
|
||||
}
|
||||
|
||||
@@ -896,8 +897,8 @@ mod tests {
|
||||
concurrency_time_window_s: None,
|
||||
skip_expr: None,
|
||||
cache_ttl: None,
|
||||
ws_error_handler_muted: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
};
|
||||
let expect = serde_json::json!({
|
||||
"modules": [
|
||||
|
||||
@@ -204,9 +204,9 @@ async fn _check_nb_of_groups(db: &DB) -> Result<()> {
|
||||
let nb_groups = sqlx::query_scalar!("SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack'",)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
if nb_groups.unwrap_or(0) >= 5 {
|
||||
if nb_groups.unwrap_or(0) >= 3 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of groups (5 outside of native groups 'all', 'slack' and 'error_handler') without an enterprise license"
|
||||
"You have reached the maximum number of groups (3 outside of native groups 'all', 'slack' and 'error_handler') without an enterprise license"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
use crate::{resources::transform_json_value, users::Tokened, workspaces::LargeFileStorage};
|
||||
use aws_sdk_s3::config::{Credentials, Region};
|
||||
use std::cmp;
|
||||
|
||||
use crate::{
|
||||
db::DB, resources::transform_json_value, users::Tokened, workspaces::LargeFileStorage,
|
||||
};
|
||||
use aws_sdk_s3::config::{BehaviorVersion, Credentials, Region};
|
||||
use axum::{
|
||||
extract::Path,
|
||||
extract::{Path, Query},
|
||||
routing::{get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use hyper::http;
|
||||
use object_store::ClientConfigKey;
|
||||
use polars::{
|
||||
io::{
|
||||
cloud::{AmazonS3ConfigKey, CloudOptions},
|
||||
SerReader,
|
||||
},
|
||||
lazy::{
|
||||
dsl::col,
|
||||
frame::{LazyFrame, ScanArgsParquet},
|
||||
},
|
||||
prelude::CsvReader,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use windmill_common::{db::UserDB, error};
|
||||
@@ -29,8 +45,16 @@ pub fn workspaced_service() -> Router {
|
||||
)
|
||||
.route("/test_connection", get(test_connection).layer(cors.clone()))
|
||||
.route(
|
||||
"/list_stored_datasets",
|
||||
get(list_stored_datasets).layer(cors.clone()),
|
||||
"/list_stored_files",
|
||||
get(list_stored_files).layer(cors.clone()),
|
||||
)
|
||||
.route(
|
||||
"/load_file_metadata",
|
||||
get(load_file_metadata).layer(cors.clone()),
|
||||
)
|
||||
.route(
|
||||
"/load_file_preview",
|
||||
get(load_file_preview).layer(cors.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,7 +142,7 @@ async fn polars_connection_settings(
|
||||
let s3_resource = query.s3_resource;
|
||||
|
||||
let response = PolarsConnectionSettingsResponse {
|
||||
endpoint_url: s3_resource.endpoint,
|
||||
endpoint_url: render_endpoint(&s3_resource),
|
||||
key: s3_resource.access_key,
|
||||
secret: s3_resource.secret_key,
|
||||
use_ssl: s3_resource.use_ssl,
|
||||
@@ -129,31 +153,28 @@ async fn polars_connection_settings(
|
||||
return Ok(Json(response));
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ListStoredDatasetsResponse {
|
||||
windmill_large_files: Vec<WindmillLargeFile>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct WindmillLargeFile {
|
||||
s3: String,
|
||||
s3_bucket: Option<String>,
|
||||
}
|
||||
|
||||
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, &user_db, &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(),
|
||||
));
|
||||
}
|
||||
|
||||
let s3_resource = s3_resource_opt.unwrap();
|
||||
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);
|
||||
s3_client
|
||||
.list_objects()
|
||||
@@ -165,64 +186,301 @@ async fn test_connection(
|
||||
return Ok(Json(()));
|
||||
}
|
||||
|
||||
async fn list_stored_datasets(
|
||||
#[derive(Deserialize)]
|
||||
struct ListStoredFilesQuery {
|
||||
pub max_keys: i32,
|
||||
pub marker: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ListStoredDatasetsResponse {
|
||||
windmill_large_files: Vec<WindmillLargeFile>,
|
||||
pub next_marker: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_stored_files(
|
||||
authed: ApiAuthed,
|
||||
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 s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &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.unwrap();
|
||||
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 mut stored_datasets = Vec::<WindmillLargeFile>::new();
|
||||
let s3_bucket = s3_resource.bucket;
|
||||
loop {
|
||||
let mut list_object_query = s3_client
|
||||
.list_objects()
|
||||
.bucket(s3_bucket.clone())
|
||||
.max_keys(32);
|
||||
|
||||
if let Some(last_dataset_name) = stored_datasets.last() {
|
||||
// if stored_datasets already has some elements, it means we're looping through pages
|
||||
// according to AWS SDK docs, we can set the marker to the last returned dataset
|
||||
list_object_query = list_object_query.set_marker(Some(last_dataset_name.clone().s3))
|
||||
}
|
||||
let list_object_query = s3_client
|
||||
.list_objects()
|
||||
.bucket(s3_bucket.clone())
|
||||
.max_keys(query.max_keys)
|
||||
.set_marker(query.marker);
|
||||
|
||||
let bucket_objects = list_object_query
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
|
||||
let bucket_objects = list_object_query
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
|
||||
|
||||
let page_datasets = bucket_objects
|
||||
.contents()
|
||||
.iter()
|
||||
.filter(|object| object.key().is_some())
|
||||
.map(|object| object.key())
|
||||
.map(Option::unwrap)
|
||||
.map(&str::to_string)
|
||||
.map(|object_key| WindmillLargeFile {
|
||||
s3: object_key.clone(),
|
||||
s3_bucket: Some(s3_bucket.clone()),
|
||||
})
|
||||
.collect::<Vec<WindmillLargeFile>>();
|
||||
let stored_datasets = bucket_objects
|
||||
.contents()
|
||||
.iter()
|
||||
.filter(|object| object.key().is_some())
|
||||
.map(|object| object.key())
|
||||
.map(Option::unwrap)
|
||||
.map(&str::to_string)
|
||||
.map(|object_key| WindmillLargeFile { s3: object_key.clone() })
|
||||
.collect::<Vec<WindmillLargeFile>>();
|
||||
|
||||
stored_datasets.extend(page_datasets.clone());
|
||||
|
||||
if !bucket_objects.is_truncated() {
|
||||
break;
|
||||
}
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if stored_datasets.len() > 20 {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
"The workspace s3 bucket contains more than 20 files. Consider upgrading to Windmill Enterprise Edition to continue to use this feature."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let next_marker = if bucket_objects.is_truncated().unwrap_or(false) {
|
||||
if bucket_objects.next_marker().is_some() {
|
||||
// some S3 providers returns the next marker for us. If that's the case just re-use it
|
||||
bucket_objects.next_marker().map(|v| v.to_owned())
|
||||
} else {
|
||||
// others, like AWS, doesn't and implicitly expect users to return the last key of the current page
|
||||
stored_datasets.last().map(|v| v.s3.clone())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
return Ok(Json(ListStoredDatasetsResponse {
|
||||
windmill_large_files: stored_datasets,
|
||||
next_marker: next_marker,
|
||||
}));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoadFileMetadataQuery {
|
||||
pub file_key: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LoadFileMetadataResponse {
|
||||
pub mime_type: Option<String>,
|
||||
pub size_in_bytes: Option<i64>,
|
||||
pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub expires: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub version_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoadFilePreviewQuery {
|
||||
pub file_key: String,
|
||||
|
||||
// The two options below are requested from s3 with an additional query is not set
|
||||
pub file_size_in_bytes: Option<i64>,
|
||||
pub file_mime_type: Option<String>,
|
||||
|
||||
// For CSV files, the separator needs to be specify
|
||||
pub csv_separator: Option<String>,
|
||||
|
||||
// Specify the content length to be read. Both will be taken into account when reading files, except for:
|
||||
// - CSVs: only the length will be taken into account, a CSV file larger than this will be truncated.
|
||||
// Note that truncated CSV files might not be valid CSV files anymore, and therefore the preview might fail
|
||||
// - Parquet files: Parquet files are lazy-loaded. Therefore none of those params will be taken into account
|
||||
pub read_bytes_from: i64,
|
||||
pub read_bytes_length: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LoadFilePreviewResponse {
|
||||
pub content: Option<String>,
|
||||
pub content_type: WindmillContentType,
|
||||
pub msg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
enum WindmillContentType {
|
||||
RawText,
|
||||
Csv,
|
||||
Parquet,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
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, &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(),
|
||||
))?;
|
||||
let s3_client = build_s3_client(&s3_resource);
|
||||
|
||||
let s3_bucket = s3_resource.bucket.clone();
|
||||
let s3_object_metadata = s3_client
|
||||
.head_object()
|
||||
.bucket(&s3_bucket)
|
||||
.key(&file_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
|
||||
|
||||
let response = LoadFileMetadataResponse {
|
||||
mime_type: s3_object_metadata.content_type().map(&str::to_string),
|
||||
size_in_bytes: s3_object_metadata.content_length(),
|
||||
last_modified: s3_object_metadata
|
||||
.last_modified()
|
||||
.map(|dt| chrono::DateTime::from_timestamp(dt.secs(), dt.subsec_nanos()))
|
||||
.flatten(),
|
||||
expires: s3_object_metadata
|
||||
.expires()
|
||||
.map(|dt| chrono::DateTime::from_timestamp(dt.secs(), dt.subsec_nanos()))
|
||||
.flatten(),
|
||||
version_id: s3_object_metadata.version_id().map(&str::to_string),
|
||||
};
|
||||
return Ok(Json(response));
|
||||
}
|
||||
|
||||
async fn load_file_preview(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<LoadFilePreviewQuery>,
|
||||
) -> error::JsonResult<LoadFilePreviewResponse> {
|
||||
// query validation
|
||||
if query.read_bytes_length > 8 * 1024 * 1024 {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Cannot load file bigger than 8MB".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
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(),
|
||||
))?;
|
||||
let s3_client = build_s3_client(&s3_resource);
|
||||
|
||||
let s3_bucket = s3_resource.bucket.clone();
|
||||
|
||||
// if content length is provided in the request, use it, otherwise get it from s3
|
||||
let (s3_object_mime_type, s3_object_content_length) =
|
||||
if query.file_size_in_bytes.is_some() || query.file_mime_type.is_some() {
|
||||
(query.file_mime_type.clone(), query.file_size_in_bytes)
|
||||
} else {
|
||||
let s3_object_metadata = s3_client
|
||||
.head_object()
|
||||
.bucket(&s3_bucket)
|
||||
.key(&file_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
|
||||
(
|
||||
s3_object_metadata.content_type().map(|v| v.to_owned()),
|
||||
s3_object_metadata.content_length(),
|
||||
)
|
||||
};
|
||||
|
||||
let file_chunk_length = if s3_object_content_length.is_some() {
|
||||
cmp::min(
|
||||
query.read_bytes_length,
|
||||
s3_object_content_length.unwrap() - query.read_bytes_from,
|
||||
)
|
||||
} else {
|
||||
query.read_bytes_length
|
||||
};
|
||||
|
||||
let content_type: WindmillContentType;
|
||||
let content_preview = match s3_object_mime_type.as_deref() {
|
||||
Some("text/csv") => {
|
||||
content_type = WindmillContentType::Csv;
|
||||
read_s3_csv_object_head(
|
||||
&s3_client,
|
||||
&s3_bucket,
|
||||
&file_key,
|
||||
file_chunk_length,
|
||||
query.csv_separator,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(mt)
|
||||
if mt.starts_with("text/")
|
||||
|| mt == "application/json"
|
||||
|| mt == "application/x-yaml" =>
|
||||
{
|
||||
content_type = WindmillContentType::RawText;
|
||||
read_s3_text_object_head(
|
||||
&s3_client,
|
||||
&s3_bucket,
|
||||
&file_key,
|
||||
query.read_bytes_from,
|
||||
file_chunk_length,
|
||||
)
|
||||
.await
|
||||
}
|
||||
mt_opt => {
|
||||
// sometimes S3 doesn't infer the content type on upload. Guess it from the file extension
|
||||
if file_key.to_lowercase().ends_with(".parquet") {
|
||||
content_type = WindmillContentType::Parquet;
|
||||
read_s3_parquet_object_head(&s3_resource, &file_key).await
|
||||
} else if file_key.to_lowercase().ends_with(".csv") {
|
||||
content_type = WindmillContentType::Csv;
|
||||
read_s3_csv_object_head(
|
||||
&s3_client,
|
||||
&s3_bucket,
|
||||
&file_key,
|
||||
file_chunk_length,
|
||||
query.csv_separator,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
content_type = WindmillContentType::Unknown;
|
||||
let msg = match mt_opt {
|
||||
Some(mt) => {
|
||||
format!("Preview is not available for content of type '{}'", mt).to_string()
|
||||
}
|
||||
None => "Preview is not available. Content type is unknown or not supported"
|
||||
.to_string(),
|
||||
};
|
||||
Err(error::Error::ExecutionErr(msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let response: LoadFilePreviewResponse = match content_preview {
|
||||
Ok(content) => LoadFilePreviewResponse {
|
||||
content_type: content_type,
|
||||
content: Some(content),
|
||||
msg: None,
|
||||
},
|
||||
|
||||
Err(err) => LoadFilePreviewResponse {
|
||||
content_type: content_type,
|
||||
content: None,
|
||||
msg: Some(err.to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
return Ok(Json(response));
|
||||
}
|
||||
|
||||
async fn get_workspace_s3_resource<'c>(
|
||||
authed: &ApiAuthed,
|
||||
user_db: &UserDB,
|
||||
db: &DB,
|
||||
token: &str,
|
||||
w_id: &str,
|
||||
) -> error::Result<Option<S3Resource>> {
|
||||
@@ -261,6 +519,7 @@ async fn get_workspace_s3_resource<'c>(
|
||||
let interpolated_value = transform_json_value(
|
||||
authed,
|
||||
user_db,
|
||||
db,
|
||||
&w_id,
|
||||
resource_path_json_value,
|
||||
&Option::None,
|
||||
@@ -276,18 +535,10 @@ async fn get_workspace_s3_resource<'c>(
|
||||
fn build_s3_client(s3_resource_ref: &S3Resource) -> aws_sdk_s3::Client {
|
||||
let s3_resource = s3_resource_ref.clone();
|
||||
|
||||
let endpoint_with_prefix = if s3_resource.endpoint.starts_with("http://")
|
||||
|| s3_resource.endpoint.starts_with("https://")
|
||||
{
|
||||
s3_resource.endpoint.clone()
|
||||
} else if s3_resource.use_ssl {
|
||||
format!("https://{}", s3_resource.endpoint)
|
||||
} else {
|
||||
format!("http://{}", s3_resource.endpoint)
|
||||
};
|
||||
|
||||
let endpoint = render_endpoint(&s3_resource);
|
||||
let mut s3_config_builder = aws_sdk_s3::Config::builder()
|
||||
.endpoint_url(endpoint_with_prefix)
|
||||
.endpoint_url(endpoint)
|
||||
.behavior_version(BehaviorVersion::latest())
|
||||
.region(Region::new(s3_resource.region));
|
||||
if s3_resource.access_key.is_some() {
|
||||
s3_config_builder = s3_config_builder.credentials_provider(Credentials::new(
|
||||
@@ -304,3 +555,176 @@ fn build_s3_client(s3_resource_ref: &S3Resource) -> aws_sdk_s3::Client {
|
||||
let s3_config = s3_config_builder.build();
|
||||
return aws_sdk_s3::Client::from_conf(s3_config);
|
||||
}
|
||||
|
||||
fn build_polars_s3_config(s3_resource_ref: &S3Resource) -> CloudOptions {
|
||||
let s3_resource = s3_resource_ref.to_owned();
|
||||
let mut s3_configs: Vec<(AmazonS3ConfigKey, String)> = vec![
|
||||
(AmazonS3ConfigKey::Region, s3_resource.region),
|
||||
(AmazonS3ConfigKey::Bucket, s3_resource.bucket),
|
||||
(
|
||||
AmazonS3ConfigKey::Endpoint,
|
||||
render_endpoint(s3_resource_ref),
|
||||
),
|
||||
(
|
||||
AmazonS3ConfigKey::Client(ClientConfigKey::AllowHttp),
|
||||
(!s3_resource.use_ssl).to_string(),
|
||||
),
|
||||
(
|
||||
AmazonS3ConfigKey::VirtualHostedStyleRequest,
|
||||
(!s3_resource.path_style).to_string(),
|
||||
),
|
||||
];
|
||||
if let Some(access_key) = s3_resource.access_key {
|
||||
s3_configs.push((AmazonS3ConfigKey::AccessKeyId, access_key));
|
||||
}
|
||||
if let Some(secret_key) = s3_resource.secret_key {
|
||||
s3_configs.push((AmazonS3ConfigKey::SecretAccessKey, secret_key));
|
||||
}
|
||||
return CloudOptions::default().with_aws(s3_configs);
|
||||
}
|
||||
|
||||
fn render_endpoint(s3_resource: &S3Resource) -> String {
|
||||
if s3_resource.endpoint.starts_with("http://") || s3_resource.endpoint.starts_with("https://") {
|
||||
s3_resource.endpoint.clone()
|
||||
} else if s3_resource.use_ssl {
|
||||
format!("https://{}", s3_resource.endpoint)
|
||||
} else {
|
||||
format!("http://{}", s3_resource.endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_s3_text_object_head(
|
||||
s3_client: &aws_sdk_s3::Client,
|
||||
s3_bucket: &str,
|
||||
file_key: &str,
|
||||
from_char: i64,
|
||||
length: i64,
|
||||
) -> error::Result<String> {
|
||||
let s3_object = s3_client
|
||||
.get_object()
|
||||
.range(format!("bytes={}-{}", from_char, length).to_string())
|
||||
.bucket(s3_bucket)
|
||||
.key(file_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::warn!("Error fetching text file from S3: {:?}", err);
|
||||
error::Error::InternalErr(err.to_string())
|
||||
})?;
|
||||
|
||||
let payload = s3_object
|
||||
.body
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::warn!(
|
||||
"Error reading raw text file {}. Error was: {:?}",
|
||||
file_key,
|
||||
err
|
||||
);
|
||||
error::Error::InternalErr("File encoding is not supported".to_string())
|
||||
})?
|
||||
.into_bytes()
|
||||
.to_vec();
|
||||
|
||||
let file_header_str = String::from_utf8(payload).map_err(|err| {
|
||||
tracing::warn!(
|
||||
"Encoding of file {} unsupported. Error was: {:?}",
|
||||
file_key,
|
||||
err
|
||||
);
|
||||
error::Error::InternalErr("File encoding is not supported".to_string())
|
||||
})?;
|
||||
return Ok(file_header_str);
|
||||
}
|
||||
|
||||
async fn read_s3_parquet_object_head(
|
||||
s3_resource_ref: &S3Resource,
|
||||
file_key: &str,
|
||||
) -> error::Result<String> {
|
||||
let s3_cloud_config = build_polars_s3_config(s3_resource_ref);
|
||||
|
||||
let args: ScanArgsParquet = ScanArgsParquet {
|
||||
n_rows: Some(1),
|
||||
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();
|
||||
let polars_df_result = 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("*")])
|
||||
.limit(10) // for now read only first 10 lines
|
||||
.collect()
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
|
||||
return Ok(format!("{:?}", df).to_string());
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
|
||||
|
||||
return polars_df_result;
|
||||
}
|
||||
|
||||
async fn read_s3_csv_object_head(
|
||||
s3_client: &aws_sdk_s3::Client,
|
||||
s3_bucket: &str,
|
||||
file_key: &str,
|
||||
length: i64,
|
||||
separator: Option<String>,
|
||||
) -> error::Result<String> {
|
||||
let separator_final = if let Some(separator_char) = separator {
|
||||
if separator_char.len() != 1 {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Separator must be a single character".to_string(),
|
||||
));
|
||||
}
|
||||
separator_char.as_bytes()[0]
|
||||
} else {
|
||||
",".as_bytes()[0] // polars uses the comma as default, doing the same here
|
||||
};
|
||||
|
||||
let s3_object = s3_client
|
||||
.get_object()
|
||||
.bucket(s3_bucket)
|
||||
.range(format!("bytes=0-{}", length).to_string())
|
||||
.key(file_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
|
||||
|
||||
// TODO: polars does not seem to support lazy csv reader, unfortunately. We can implement it ourselves if needed
|
||||
// Right now it's fine b/c we limit the download from AWS to 32MB. We should recomment users to use parquet
|
||||
// for larger files
|
||||
let file_content_bytes = s3_object
|
||||
.body
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?
|
||||
.into_bytes();
|
||||
let cursor = std::io::Cursor::new(file_content_bytes);
|
||||
|
||||
let csv_df = CsvReader::new(cursor)
|
||||
.with_n_rows(Some(10)) // for now read only first 10 lines
|
||||
.with_separator(separator_final)
|
||||
.finish()
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
|
||||
|
||||
return Ok(format!("{:?}", csv_df).to_string());
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ use sqlx::{query_scalar, types::Uuid, FromRow, Postgres, Transaction};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use urlencoding::encode;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::worker::{CUSTOM_TAGS_PER_WORKSPACE, SERVER_CONFIG};
|
||||
use windmill_common::worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE, SERVER_CONFIG};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{self, to_anyhow, Error},
|
||||
@@ -50,8 +50,8 @@ use windmill_common::{
|
||||
};
|
||||
use windmill_common::{get_latest_deployed_hash_for_path, BASE_URL};
|
||||
use windmill_queue::{
|
||||
add_completed_job_error, empty_args, get_queued_job, job_is_complete, push, CanceledBy,
|
||||
PushArgs, PushIsolationLevel,
|
||||
add_completed_job_error, get_queued_job, get_result_by_id_from_running_flow, job_is_complete,
|
||||
push, CanceledBy, PushArgs, PushIsolationLevel,
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -364,7 +364,7 @@ async fn get_job(
|
||||
async fn get_job_internal(db: &DB, workspace_id: &str, job_id: Uuid) -> error::Result<Job> {
|
||||
let cjob_maybe = sqlx::query_as::<_, CompletedJob>("SELECT
|
||||
id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
|
||||
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
|
||||
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, right(logs, 20000000) as logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
|
||||
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
|
||||
raw_lock, email, visible_to_owner, mem_peak, tag, priority
|
||||
FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
@@ -378,7 +378,7 @@ async fn get_job_internal(db: &DB, workspace_id: &str, job_id: Uuid) -> error::R
|
||||
} else {
|
||||
let job_o = sqlx::query_as::<_, QueuedJob>(
|
||||
"SELECT id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for, running,
|
||||
script_hash, script_path, CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, logs, raw_code, canceled, canceled_by, canceled_reason, last_ping,
|
||||
script_hash, script_path, CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, right(logs, 20000000) as logs, raw_code, canceled, canceled_by, canceled_reason, last_ping,
|
||||
job_kind, env_id, schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language,
|
||||
suspend, suspend_until, same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
|
||||
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id, cache_ttl, priority
|
||||
@@ -1564,6 +1564,53 @@ fn decode_payload<D: DeserializeOwned>(t: String) -> anyhow::Result<D> {
|
||||
serde_json::from_slice(vec.as_slice()).context("invalid json")
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DecodeQuery {
|
||||
pub include_query: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct IncludeQuery {
|
||||
pub include_query: Option<String>,
|
||||
}
|
||||
|
||||
pub struct DecodeQueries(pub HashMap<String, Box<RawValue>>);
|
||||
|
||||
#[axum::async_trait]
|
||||
impl<S> FromRequest<S, axum::body::Body> for DecodeQueries
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request(
|
||||
req: Request<axum::body::Body>,
|
||||
_state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let query = req.uri().query().unwrap_or("");
|
||||
let include_query = serde_urlencoded::from_str::<IncludeQuery>(query)
|
||||
.map(|x| x.include_query)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
let parse_query_args = include_query
|
||||
.split(",")
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let mut args = HashMap::new();
|
||||
if !parse_query_args.is_empty() {
|
||||
let queries =
|
||||
serde_urlencoded::from_str::<HashMap<String, String>>(query).unwrap_or_default();
|
||||
parse_query_args.iter().for_each(|h| {
|
||||
if let Some(v) = queries.get(h) {
|
||||
args.insert(h.to_string(), to_raw_value(v));
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(DecodeQueries(args))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_raw_string(
|
||||
raw_string: Option<String>,
|
||||
mut args: serde_json::Map<String, serde_json::Value>,
|
||||
@@ -1802,33 +1849,25 @@ struct Guard {
|
||||
done: bool,
|
||||
id: Uuid,
|
||||
w_id: String,
|
||||
db: UserDB,
|
||||
authed: ApiAuthed,
|
||||
db: DB,
|
||||
}
|
||||
|
||||
impl Drop for Guard {
|
||||
fn drop(&mut self) {
|
||||
if !&self.done {
|
||||
let id = self.id;
|
||||
let username = self.authed.username.clone();
|
||||
let w_id = self.w_id.clone();
|
||||
let db = self.db.clone();
|
||||
let authed = self.authed.clone();
|
||||
|
||||
tracing::info!("http connection broke, marking job {id} as canceled");
|
||||
tokio::spawn(async move {
|
||||
let tx = db.begin(&authed).await.ok();
|
||||
if let Some(mut tx) = tx {
|
||||
let _ = sqlx::query!(
|
||||
"UPDATE queue SET canceled = true, canceled_reason = 'http connection broke', canceled_by = $1 WHERE id = $2 AND workspace_id = $3",
|
||||
username,
|
||||
let _ = sqlx::query!(
|
||||
"UPDATE queue SET canceled = true, canceled_reason = 'http connection broke', canceled_by = queue.created_by WHERE id = $1 AND workspace_id = $2",
|
||||
id,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.execute(&db)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1840,10 +1879,10 @@ pub struct WindmillStatusCode {
|
||||
result: Option<Box<RawValue>>,
|
||||
}
|
||||
async fn run_wait_result<T>(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
db: &DB,
|
||||
uuid: Uuid,
|
||||
Path((w_id, _)): Path<(String, T)>,
|
||||
node_id: Option<String>,
|
||||
) -> error::Result<Response> {
|
||||
let mut result;
|
||||
let timeout = SERVER_CONFIG.read().await.timeout_wait_result.clone();
|
||||
@@ -1853,27 +1892,29 @@ async fn run_wait_result<T>(
|
||||
(timeout * 1000) as u64
|
||||
};
|
||||
|
||||
let mut g = Guard {
|
||||
done: false,
|
||||
id: uuid,
|
||||
w_id: w_id.clone(),
|
||||
db: user_db.clone(),
|
||||
authed: authed.clone(),
|
||||
};
|
||||
let mut g = Guard { done: false, id: uuid, w_id: w_id.clone(), db: db.clone() };
|
||||
|
||||
let fast_poll_duration = *WAIT_RESULT_FAST_POLL_DURATION_SECS as u64 * 1000;
|
||||
let mut accumulated_delay = 0 as u64;
|
||||
|
||||
loop {
|
||||
let mut tx = user_db.clone().begin(&authed).await?;
|
||||
result =
|
||||
sqlx::query("SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
.bind(uuid)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
drop(tx);
|
||||
if let Some(node_id) = node_id.as_ref() {
|
||||
result = get_result_by_id_from_running_flow(&db, &w_id, &uuid, node_id, None)
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
let row =
|
||||
sqlx::query("SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
.bind(uuid)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
if let Some(row) = row {
|
||||
result = Some(RawResult::from_row(&row)?.result.to_owned());
|
||||
} else {
|
||||
result = None;
|
||||
}
|
||||
}
|
||||
|
||||
if result.is_some() {
|
||||
break;
|
||||
@@ -1892,7 +1933,6 @@ async fn run_wait_result<T>(
|
||||
}
|
||||
if let Some(result) = result {
|
||||
g.done = true;
|
||||
let result = RawResult::from_row(&result)?.result;
|
||||
|
||||
let status_code = serde_json::from_str::<WindmillStatusCode>(result.get());
|
||||
match status_code {
|
||||
@@ -1956,6 +1996,7 @@ pub async fn run_wait_result_job_by_path_get(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, script_path)): Path<(String, StripPath)>,
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
DecodeQueries(queries): DecodeQueries,
|
||||
) -> error::Result<Response> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
@@ -1968,13 +2009,17 @@ pub async fn run_wait_result_job_by_path_get(
|
||||
.map(decode_payload)
|
||||
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
|
||||
|
||||
let payload_args = if let Some(payload) = payload_r {
|
||||
let mut payload_args = if let Some(payload) = payload_r {
|
||||
payload?
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
queries.iter().for_each(|(k, v)| {
|
||||
payload_args.insert(k.to_string(), v.clone());
|
||||
});
|
||||
|
||||
let args = PushArgs { extra: payload_args, args: sqlx::types::Json(empty_args()) };
|
||||
let inner_args: HashMap<String, Box<RawValue>> = HashMap::new();
|
||||
let args = PushArgs { extra: payload_args, args: sqlx::types::Json(inner_args) };
|
||||
|
||||
check_queue_too_long(&db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
|
||||
let script_path = script_path.to_path();
|
||||
@@ -2010,7 +2055,7 @@ pub async fn run_wait_result_job_by_path_get(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_path))).await
|
||||
run_wait_result(&db, uuid, Path((w_id, script_path)), None).await
|
||||
}
|
||||
|
||||
pub async fn run_wait_result_flow_by_path_get(
|
||||
@@ -2020,8 +2065,8 @@ pub async fn run_wait_result_flow_by_path_get(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
||||
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
DecodeQueries(queries): DecodeQueries,
|
||||
) -> error::Result<Response> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
@@ -2035,12 +2080,16 @@ pub async fn run_wait_result_flow_by_path_get(
|
||||
.map(decode_payload)
|
||||
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
|
||||
|
||||
let payload_args = if let Some(payload) = payload_r {
|
||||
let mut payload_args = if let Some(payload) = payload_r {
|
||||
payload?
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
queries.iter().for_each(|(k, v)| {
|
||||
payload_args.insert(k.to_string(), v.clone());
|
||||
});
|
||||
|
||||
let args = PushArgs { extra: payload_args, args: sqlx::types::Json(HashMap::new()) };
|
||||
|
||||
run_wait_result_flow_by_path_internal(
|
||||
@@ -2119,7 +2168,7 @@ async fn run_wait_result_script_by_path_internal(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_path))).await
|
||||
run_wait_result(&db, uuid, Path((w_id, script_path)), None).await
|
||||
}
|
||||
|
||||
pub async fn run_wait_result_script_by_hash(
|
||||
@@ -2187,7 +2236,7 @@ pub async fn run_wait_result_script_by_hash(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_hash))).await
|
||||
run_wait_result(&db, uuid, Path((w_id, script_hash)), None).await
|
||||
}
|
||||
|
||||
pub async fn run_wait_result_flow_by_path(
|
||||
@@ -2225,15 +2274,15 @@ async fn run_wait_result_flow_by_path_internal(
|
||||
|
||||
let scheduled_for = run_query.get_scheduled_for(&db).await?;
|
||||
|
||||
let (tag, dedicated_worker) = sqlx::query!(
|
||||
"SELECT tag, dedicated_worker from flow WHERE path = $1 and workspace_id = $2",
|
||||
let (tag, dedicated_worker, early_return) = sqlx::query!(
|
||||
"SELECT tag, dedicated_worker, value->>'early_return' as early_return from flow WHERE path = $1 and workspace_id = $2",
|
||||
flow_path,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.map(|x| (x.tag, x.dedicated_worker))
|
||||
.unwrap_or_else(|| (None, None));
|
||||
.map(|x| (x.tag, x.dedicated_worker, x.early_return))
|
||||
.unwrap_or_else(|| (None, None, None));
|
||||
|
||||
check_tag_available_for_workspace(&w_id, &tag).await?;
|
||||
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
|
||||
@@ -2264,7 +2313,7 @@ async fn run_wait_result_flow_by_path_internal(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, flow_path))).await
|
||||
run_wait_result(&db, uuid, Path((w_id, flow_path)), early_return).await
|
||||
}
|
||||
|
||||
async fn run_preview_job(
|
||||
@@ -2829,7 +2878,7 @@ async fn get_completed_job<'a>(
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::Result<Response> {
|
||||
let job_o = sqlx::query("SELECT id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
|
||||
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
|
||||
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, right(logs, 20000000) as logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
|
||||
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
|
||||
raw_lock, email, visible_to_owner, mem_peak, tag, priority FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
.bind(id)
|
||||
|
||||
@@ -112,7 +112,7 @@ pub async fn run_server(
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
addr: SocketAddr,
|
||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||
port_tx: tokio::sync::oneshot::Sender<u16>,
|
||||
port_tx: tokio::sync::oneshot::Sender<String>,
|
||||
server_mode: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(mut rsmq) = rsmq.clone() {
|
||||
@@ -284,7 +284,7 @@ pub async fn run_server(
|
||||
);
|
||||
|
||||
port_tx
|
||||
.send(server.local_addr().port())
|
||||
.send(format!("http://localhost:{}", server.local_addr().port()))
|
||||
.expect("Failed to send port");
|
||||
|
||||
let server = server.with_graceful_shutdown(async {
|
||||
|
||||
@@ -102,9 +102,13 @@ async fn proxy(
|
||||
&openai_resource_path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
create_openai_json_error(format!(
|
||||
"Could not find the OpenAI resource at path {openai_resource_path}, update the resource path in the workspace settings"
|
||||
))
|
||||
})?;
|
||||
|
||||
if resource.is_none() {
|
||||
return Err(create_openai_json_error(
|
||||
@@ -126,8 +130,9 @@ async fn proxy(
|
||||
"https://api.openai.com/v1"
|
||||
};
|
||||
|
||||
let url = format!("{}/{}", base_url, openai_path);
|
||||
let mut request = HTTP_CLIENT
|
||||
.post(base_url.to_string() + "/" + &openai_path)
|
||||
.post(url)
|
||||
.header("content-type", "application/json")
|
||||
.body(body);
|
||||
|
||||
|
||||
@@ -247,6 +247,7 @@ async fn list_resources(
|
||||
async fn get_resource(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<ListableResource> {
|
||||
let path = path.to_path();
|
||||
@@ -269,7 +270,9 @@ async fn get_resource(
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
if resource_o.is_none() {
|
||||
explain_resource_perm_error(&path, &w_id, &db).await?;
|
||||
}
|
||||
let resource = not_found_if_none(resource_o, "Resource", path)?;
|
||||
Ok(Json(resource))
|
||||
}
|
||||
@@ -295,6 +298,7 @@ async fn exists_resource(
|
||||
async fn get_resource_value(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<Option<serde_json::Value>> {
|
||||
let path = path.to_path();
|
||||
@@ -307,12 +311,55 @@ async fn get_resource_value(
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
if value_o.is_none() {
|
||||
explain_resource_perm_error(&path, &w_id, &db).await?;
|
||||
}
|
||||
|
||||
let value = not_found_if_none(value_o, "Resource", path)?;
|
||||
Ok(Json(value))
|
||||
}
|
||||
|
||||
async fn explain_resource_perm_error(
|
||||
path: &str,
|
||||
w_id: &str,
|
||||
db: &sqlx::Pool<Postgres>,
|
||||
) -> windmill_common::error::Result<()> {
|
||||
let extra_perms = sqlx::query_scalar!(
|
||||
"SELECT extra_perms from resource WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound(format!("Resource {} not found", path)))?;
|
||||
if path.starts_with("f/") {
|
||||
let folder = path.split("/").nth(1).ok_or_else(|| {
|
||||
Error::BadRequest(format!(
|
||||
"path {} should have at least 2 components separated by /",
|
||||
path
|
||||
))
|
||||
})?;
|
||||
let folder_extra_perms = sqlx::query_scalar!(
|
||||
"SELECT extra_perms from folder WHERE name = $1 AND workspace_id = $2",
|
||||
folder,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
return Err(Error::NotAuthorized(format!(
|
||||
"Resource exists but you don't have access to it:\nresource perms: {}\nfolder perms: {}",
|
||||
serde_json::to_string_pretty(&extra_perms).unwrap_or_default(), serde_json::to_string_pretty(&folder_extra_perms).unwrap_or_default()
|
||||
)));
|
||||
} else {
|
||||
return Err(Error::NotAuthorized(format!(
|
||||
"Resource exists but you don't have access to it:\nresource perms: {}",
|
||||
serde_json::to_string_pretty(&extra_perms).unwrap_or_default()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
async fn custom_component(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -347,6 +394,7 @@ struct JobInfo {
|
||||
async fn get_resource_value_interpolated(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(job_info): Query<JobInfo>,
|
||||
@@ -362,11 +410,23 @@ async fn get_resource_value_interpolated(
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
if value_o.is_none() {
|
||||
explain_resource_perm_error(&path, &w_id, &db).await?;
|
||||
}
|
||||
|
||||
let value = not_found_if_none(value_o, "Resource", path)?;
|
||||
if let Some(value) = value {
|
||||
Ok(Json(Some(
|
||||
transform_json_value(&authed, &user_db, &w_id, value, &job_info.job_id, &token).await?,
|
||||
transform_json_value(
|
||||
&authed,
|
||||
&user_db,
|
||||
&db,
|
||||
&w_id,
|
||||
value,
|
||||
&job_info.job_id,
|
||||
&token,
|
||||
)
|
||||
.await?,
|
||||
)))
|
||||
} else {
|
||||
Ok(Json(None))
|
||||
@@ -379,6 +439,7 @@ use async_recursion::async_recursion;
|
||||
pub async fn transform_json_value<'c>(
|
||||
authed: &ApiAuthed,
|
||||
user_db: &UserDB,
|
||||
db: &DB,
|
||||
workspace: &str,
|
||||
v: Value,
|
||||
job_id: &Option<Uuid>,
|
||||
@@ -388,8 +449,8 @@ pub async fn transform_json_value<'c>(
|
||||
Value::String(y) if y.starts_with("$var:") => {
|
||||
let path = y.strip_prefix("$var:").unwrap();
|
||||
let tx: Transaction<'_, Postgres> = user_db.clone().begin(authed).await?;
|
||||
let v =
|
||||
crate::variables::get_value_internal(tx, workspace, path, &authed.username).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:") => {
|
||||
@@ -408,7 +469,7 @@ 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, workspace, v, job_id, token).await
|
||||
transform_json_value(authed, user_db, db, workspace, v, job_id, token).await
|
||||
} else {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
@@ -466,7 +527,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, workspace, b, job_id, token).await?,
|
||||
transform_json_value(authed, user_db, db, workspace, b, job_id, token).await?,
|
||||
);
|
||||
}
|
||||
Ok(Value::Object(m))
|
||||
|
||||
@@ -42,6 +42,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/delete/*path", delete(delete_schedule))
|
||||
.route("/setenabled/*path", post(set_enabled))
|
||||
.route("/setdefaulthandler", post(set_default_error_handler))
|
||||
// .route("/catchup/*path", post(do_catchup).get(list_catchup))
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
@@ -455,6 +456,54 @@ pub async fn set_enabled(
|
||||
))
|
||||
}
|
||||
|
||||
// pub async fn do_catchup(
|
||||
// authed: ApiAuthed,
|
||||
// Extension(db): Extension<DB>,
|
||||
// Extension(user_db): Extension<UserDB>,
|
||||
// Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
// Path((w_id, path)): Path<(String, StripPath)>,
|
||||
// Json(payload): Json<SetEnabled>,
|
||||
// ) -> Result<String> {
|
||||
// let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
|
||||
// (rsmq, user_db.begin(&authed).await?).into();
|
||||
// let path = path.to_path();
|
||||
// let schedule_o = sqlx::query_as!(
|
||||
// Schedule,
|
||||
// "UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *",
|
||||
// &payload.enabled,
|
||||
// authed.email,
|
||||
// path,
|
||||
// w_id
|
||||
// )
|
||||
// .fetch_optional(&mut tx)
|
||||
// .await?;
|
||||
|
||||
// let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
|
||||
|
||||
// clear_schedule(tx.transaction_mut(), path, &w_id).await?;
|
||||
|
||||
// audit_log(
|
||||
// &mut tx,
|
||||
// &authed.username,
|
||||
// "schedule.setenabled",
|
||||
// ActionKind::Update,
|
||||
// &w_id,
|
||||
// Some(path),
|
||||
// Some([("enabled", payload.enabled.to_string().as_ref())].into()),
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
// if payload.enabled {
|
||||
// tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
// }
|
||||
// tx.commit().await?;
|
||||
|
||||
// Ok(format!(
|
||||
// "succesfully updated schedule at path {} to status {}",
|
||||
// path, payload.enabled
|
||||
// ))
|
||||
// }
|
||||
|
||||
async fn delete_schedule(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -642,3 +691,9 @@ pub async fn clear_schedule<'c>(
|
||||
pub struct SetEnabled {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Catchup {
|
||||
pub from: DateTime<Utc>,
|
||||
pub to: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
@@ -814,6 +814,8 @@ async fn raw_script_by_path(
|
||||
}
|
||||
let path = path
|
||||
.trim_end_matches(".py")
|
||||
.trim_end_matches(".bun.ts")
|
||||
.trim_end_matches(".deno.ts")
|
||||
.trim_end_matches(".ts")
|
||||
.trim_end_matches(".go")
|
||||
.trim_end_matches(".sh");
|
||||
@@ -1087,6 +1089,14 @@ async fn delete_script_by_path(
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("deleting script by path {w_id}: {e}")))?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed.username,
|
||||
|
||||
@@ -94,6 +94,7 @@ pub fn global_service() -> Router {
|
||||
"/tutorial_progress",
|
||||
post(update_tutorial_progress).get(get_tutorial_progress),
|
||||
)
|
||||
.route("/leave_instance", post(leave_instance))
|
||||
// .route("/list_invite_codes", get(list_invite_codes))
|
||||
// .route("/create_invite_code", post(create_invite_code))
|
||||
// .route("/signup", post(signup))
|
||||
@@ -1416,6 +1417,29 @@ async fn add_user_to_workspace<'c>(
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
async fn leave_instance(
|
||||
Extension(db): Extension<DB>,
|
||||
ApiAuthed { email, username, .. }: ApiAuthed,
|
||||
) -> Result<String> {
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!("DELETE FROM password WHERE email = $1", &email)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&username,
|
||||
"workspaces.leave",
|
||||
ActionKind::Delete,
|
||||
"global",
|
||||
Some(&email),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Left instance",))
|
||||
}
|
||||
async fn update_workspace_user(
|
||||
ApiAuthed { username, is_admin, .. }: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -1578,6 +1602,9 @@ async fn create_user(
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
_check_nb_of_user(&db).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, \
|
||||
company)
|
||||
@@ -2100,19 +2127,37 @@ async fn impersonate(
|
||||
Ok((StatusCode::CREATED, token))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListTokenQuery {
|
||||
exclude_ephemeral: Option<bool>,
|
||||
}
|
||||
|
||||
async fn list_tokens(
|
||||
Extension(db): Extension<DB>,
|
||||
ApiAuthed { email, .. }: ApiAuthed,
|
||||
Query(query): Query<ListTokenQuery>,
|
||||
) -> JsonResult<Vec<TruncatedToken>> {
|
||||
let rows = sqlx::query_as!(
|
||||
TruncatedToken,
|
||||
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, \
|
||||
last_used_at, scopes FROM token WHERE email = $1
|
||||
ORDER BY created_at DESC",
|
||||
email,
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
let rows = if query.exclude_ephemeral.unwrap_or(false) {
|
||||
sqlx::query_as!(
|
||||
TruncatedToken,
|
||||
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, \
|
||||
last_used_at, scopes FROM token WHERE email = $1 AND label != 'ephemeral-script'
|
||||
ORDER BY created_at DESC",
|
||||
email,
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
TruncatedToken,
|
||||
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, \
|
||||
last_used_at, scopes FROM token WHERE email = $1
|
||||
ORDER BY created_at DESC",
|
||||
email,
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
};
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
@@ -2290,16 +2335,26 @@ pub struct LoginUserInfo {
|
||||
}
|
||||
|
||||
async fn _check_nb_of_user(db: &DB) -> Result<()> {
|
||||
let nb_users =
|
||||
let nb_users_sso =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM password WHERE login_type != 'password'",)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
if nb_users.unwrap_or(0) >= 10 {
|
||||
if nb_users_sso.unwrap_or(0) >= 10 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of oauth users accounts (10) without an enterprise license"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let nb_users = sqlx::query_scalar!("SELECT COUNT(*) FROM password",)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
if nb_users.unwrap_or(0) >= 50 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of accounts (50) without an enterprise license"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
use windmill_common::{
|
||||
error::{self, Error},
|
||||
users::SUPERADMIN_SECRET_EMAIL,
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
|
||||
DB,
|
||||
};
|
||||
|
||||
pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
|
||||
if email == SUPERADMIN_SECRET_EMAIL {
|
||||
if email == SUPERADMIN_SECRET_EMAIL || email == SUPERADMIN_NOTIFICATION_EMAIL {
|
||||
return Ok(());
|
||||
}
|
||||
let is_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
|
||||
|
||||
@@ -107,6 +107,7 @@ struct GetVariableQuery {
|
||||
async fn get_variable(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Query(q): Query<GetVariableQuery>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<ListableVariable> {
|
||||
@@ -128,7 +129,12 @@ async fn get_variable(
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let variable = not_found_if_none(variable_o, "Variable", &path)?;
|
||||
let variable = if let Some(variable) = variable_o {
|
||||
variable
|
||||
} else {
|
||||
explain_variable_perm_error(&path, &w_id, &db).await?;
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let decrypt_secret = q.decrypt_secret.unwrap_or(true);
|
||||
|
||||
@@ -172,17 +178,20 @@ async fn get_variable(
|
||||
async fn get_value(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<String> {
|
||||
let path = path.to_path();
|
||||
let tx = user_db.begin(&authed).await?;
|
||||
return get_value_internal(tx, &w_id, &path, &authed.username)
|
||||
return get_value_internal(tx, &db, &w_id, &path, &authed.username)
|
||||
.await
|
||||
.map(Json);
|
||||
}
|
||||
|
||||
pub async fn get_value_internal<'c>(
|
||||
mut tx: Transaction<'c, Postgres>,
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
path: &str,
|
||||
username: &str,
|
||||
@@ -194,7 +203,12 @@ pub async fn get_value_internal<'c>(
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let variable = not_found_if_none(variable_o, "Variable", &path)?;
|
||||
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(
|
||||
@@ -226,6 +240,45 @@ pub async fn get_value_internal<'c>(
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
async fn explain_variable_perm_error(
|
||||
path: &str,
|
||||
w_id: &str,
|
||||
db: &sqlx::Pool<Postgres>,
|
||||
) -> windmill_common::error::Result<()> {
|
||||
let extra_perms = sqlx::query_scalar!(
|
||||
"SELECT extra_perms from variable WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound(format!("Variable {} not found", path)))?;
|
||||
if path.starts_with("f/") {
|
||||
let folder = path.split("/").nth(1).ok_or_else(|| {
|
||||
Error::BadRequest(format!(
|
||||
"path {} should have at least 2 components separated by /",
|
||||
path
|
||||
))
|
||||
})?;
|
||||
let folder_extra_perms = sqlx::query_scalar!(
|
||||
"SELECT extra_perms from folder WHERE name = $1 AND workspace_id = $2",
|
||||
folder,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
return Err(Error::NotAuthorized(format!(
|
||||
"Variable exists but you don't have access to it:\nvariable perms: {}\nfolder perms: {}",
|
||||
serde_json::to_string_pretty(&extra_perms).unwrap_or_default(), serde_json::to_string_pretty(&folder_extra_perms).unwrap_or_default()
|
||||
)));
|
||||
} else {
|
||||
return Err(Error::NotAuthorized(format!(
|
||||
"Variable exists but you don't have access to it:\nvariable perms: {}",
|
||||
serde_json::to_string_pretty(&extra_perms).unwrap_or_default()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
async fn exists_variable(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
|
||||
@@ -36,7 +36,7 @@ use magic_crypt::MagicCryptTrait;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use stripe::CustomerId;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use chrono::{TimeZone, Datelike};
|
||||
use chrono::{TimeZone, Datelike, Timelike};
|
||||
use uuid::Uuid;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::db::UserDB;
|
||||
@@ -83,7 +83,8 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/edit_copilot_config", post(edit_copilot_config))
|
||||
.route("/get_copilot_info", get(get_copilot_info) )
|
||||
.route("/edit_error_handler", post(edit_error_handler))
|
||||
.route("/edit_large_file_storage_config", post(edit_large_file_storage_config));
|
||||
.route("/edit_large_file_storage_config", post(edit_large_file_storage_config))
|
||||
.route("/leave", post(leave_workspace));
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
@@ -423,22 +424,25 @@ async fn stripe_checkout(
|
||||
_ => params.customer_email = Some(&authed.email),
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
|
||||
metadata: {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert("workspace_id".to_string(), w_id.clone());
|
||||
Some(map)
|
||||
},
|
||||
billing_cycle_anchor: Some({
|
||||
// first of the next month (and possibly next year) at noon UTC
|
||||
let now = Utc::now();
|
||||
let date = if now.month() == 12 {
|
||||
Utc.with_ymd_and_hms(now.year() + 1, 1, 1, 12, 0, 0).single().unwrap()
|
||||
} else {
|
||||
Utc.with_ymd_and_hms(now.year(), now.month() + 1, 1, 12, 0, 0).single().unwrap()
|
||||
};
|
||||
date.timestamp()
|
||||
}),
|
||||
billing_cycle_anchor: if now.day() == 1 && now.hour() < 12 {
|
||||
// no need to prorate so close to the billing cycle renew date
|
||||
None
|
||||
} else {
|
||||
// first of the next month (and possibly next year) at noon UTC
|
||||
let date = if now.month() == 12 {
|
||||
Utc.with_ymd_and_hms(now.year() + 1, 1, 1, 12, 0, 0).single().unwrap()
|
||||
} else {
|
||||
Utc.with_ymd_and_hms(now.year(), now.month() + 1, 1, 12, 0, 0).single().unwrap()
|
||||
};
|
||||
Some(date.timestamp())
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
@@ -748,13 +752,17 @@ async fn edit_auto_invite(
|
||||
// ));
|
||||
// }
|
||||
|
||||
if ea.invite_all.is_some_and(|x| x) && *CLOUD_HOSTED {
|
||||
let domain = if ea.invite_all.is_some_and(|x| x) {
|
||||
if *CLOUD_HOSTED {
|
||||
return Err(Error::BadRequest(
|
||||
"invite_all is only available locally".to_string(),
|
||||
));
|
||||
|
||||
}
|
||||
let domain = email.split('@').last().unwrap();
|
||||
} else {
|
||||
"*"
|
||||
}
|
||||
} else {
|
||||
email.split('@').last().unwrap()
|
||||
};
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
@@ -778,7 +786,7 @@ async fn edit_auto_invite(
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_invite
|
||||
(workspace_id, email, is_admin, operator)
|
||||
SELECT $1::text, email, false, $3 FROM password WHERE $2::text = '*' OR email LIKE CONCAT('%', $2::text) AND NOT EXISTS (
|
||||
SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (
|
||||
SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email
|
||||
)
|
||||
ON CONFLICT DO NOTHING",
|
||||
@@ -1104,9 +1112,9 @@ async fn _check_nb_of_workspaces(db: &DB) -> Result<()> {
|
||||
let nb_workspaces = sqlx::query_scalar!("SELECT COUNT(*) FROM workspace WHERE id != 'admins' AND deleted = false",)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
if nb_workspaces.unwrap_or(0) >= 3 {
|
||||
if nb_workspaces.unwrap_or(0) >= 2 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of workspaces (3 outside of default worskapce 'admins') without an enterprise license. Archive/delete another workspace to create a new one"
|
||||
"You have reached the maximum number of workspaces (2 outside of default worskapce 'admins') without an enterprise license. Archive/delete another workspace to create a new one"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
@@ -1302,6 +1310,31 @@ async fn archive_workspace(
|
||||
Ok(format!("Archived workspace {}", &w_id))
|
||||
}
|
||||
|
||||
async fn leave_workspace(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
ApiAuthed { email, username, .. }: ApiAuthed,
|
||||
) -> Result<String> {
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!("DELETE FROM usr WHERE workspace_id = $1 AND email = $2", &w_id, &email)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&username,
|
||||
"workspaces.leave",
|
||||
ActionKind::Delete,
|
||||
&w_id,
|
||||
Some(&email),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Left workspace {}", &w_id))
|
||||
}
|
||||
|
||||
async fn unarchive_workspace(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
@@ -1477,7 +1510,8 @@ async fn invite_user(
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_invite
|
||||
(workspace_id, email, is_admin, operator)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, email)
|
||||
DO UPDATE SET is_admin = $3, operator = $4",
|
||||
&w_id,
|
||||
nu.email,
|
||||
nu.is_admin,
|
||||
|
||||
@@ -95,7 +95,7 @@ pub struct FlowValue {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_ttl: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ws_error_handler_muted: Option<bool>,
|
||||
pub early_return: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
// Priority at the flow level
|
||||
pub priority: Option<i16>,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
pub const SUPERADMIN_SECRET_EMAIL: &str = "superadmin_secret@windmill.dev";
|
||||
pub const SUPERADMIN_NOTIFICATION_EMAIL: &str = "superadmin_notification@windmill.dev";
|
||||
|
||||
pub fn username_to_permissioned_as(user: &str) -> String {
|
||||
if user.contains('@') {
|
||||
|
||||
@@ -167,6 +167,7 @@ pub async fn get_uid(db: &DB) -> Result<String> {
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Mode {
|
||||
Worker,
|
||||
Agent,
|
||||
Server,
|
||||
Standalone,
|
||||
}
|
||||
|
||||
@@ -75,25 +75,6 @@ pub async fn get_reserved_variables(
|
||||
step_id: Option<String>,
|
||||
) -> [ContextualVariable; 15] {
|
||||
let state_path = {
|
||||
let flow_path = flow_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| "NO_FLOW_PATH".to_string());
|
||||
let script_path = path.clone().unwrap_or_else(|| "NO_JOB_PATH".to_string());
|
||||
let schedule_path = schedule_path
|
||||
.clone()
|
||||
.map(|x| format!("/{x}"))
|
||||
.unwrap_or_else(String::new);
|
||||
|
||||
let script_path = if script_path.ends_with("/") {
|
||||
"NO_NAME".to_string()
|
||||
} else {
|
||||
script_path
|
||||
};
|
||||
|
||||
format!("{permissioned_as}/{flow_path}/{script_path}{schedule_path}")
|
||||
};
|
||||
|
||||
let state_path_2 = {
|
||||
let trigger = if schedule_path.is_some() {
|
||||
username.to_string()
|
||||
} else {
|
||||
@@ -200,13 +181,13 @@ pub async fn get_reserved_variables(
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_STATE_PATH".to_string(),
|
||||
value: state_path,
|
||||
description: "State resource path unique to a script and its trigger (legacy, in a migration period against WM_STATE_PATH_NEW)".to_string(),
|
||||
value: state_path.clone(),
|
||||
description: "State resource path unique to a script and its trigger".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_STATE_PATH_NEW".to_string(),
|
||||
value: state_path_2,
|
||||
description: "State resource path unique to a script and its trigger".to_string(),
|
||||
value: state_path,
|
||||
description: "State resource path unique to a script and its trigger (legacy)".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_FLOW_STEP_ID".to_string(),
|
||||
|
||||
@@ -38,4 +38,5 @@ futures-core.workspace = true
|
||||
itertools.workspace = true
|
||||
async-recursion.workspace = true
|
||||
bigdecimal.workspace = true
|
||||
axum.workspace = true
|
||||
axum.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
@@ -50,13 +50,12 @@ use windmill_common::{
|
||||
},
|
||||
flows::{add_virtual_items_if_necessary, FlowModuleValue, FlowValue},
|
||||
jobs::{
|
||||
get_payload_tag_from_prefixed_path, script_path_to_payload, CompletedJob, JobKind,
|
||||
JobPayload, QueuedJob, RawCode,
|
||||
get_payload_tag_from_prefixed_path, CompletedJob, JobKind, JobPayload, QueuedJob, RawCode,
|
||||
},
|
||||
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
|
||||
schedule::Schedule,
|
||||
scripts::{ScriptHash, ScriptLang},
|
||||
users::SUPERADMIN_SECRET_EMAIL,
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
|
||||
worker::{to_raw_value, WORKER_CONFIG},
|
||||
DB, METRICS_ENABLED,
|
||||
};
|
||||
@@ -489,6 +488,10 @@ pub async fn add_completed_job<
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
tracing::info!(
|
||||
"inserted completed job: {} (success: {success})",
|
||||
queued_job.id
|
||||
);
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
if *CLOUD_HOSTED && !is_flow && _duration > 1000 {
|
||||
@@ -516,7 +519,22 @@ pub async fn add_completed_job<
|
||||
&& queued_job.parent_job.is_none()
|
||||
&& !success
|
||||
{
|
||||
if let Err(e) = send_error_to_global_handler(rsmq.clone(), &queued_job, db, result).await {
|
||||
let result = serde_json::from_str(
|
||||
&serde_json::to_string(result.0).unwrap_or_else(|_| "{}".to_string()),
|
||||
)
|
||||
.unwrap_or_else(|_| json!({}));
|
||||
let result = if result.is_object() || result.is_null() {
|
||||
result
|
||||
} else {
|
||||
json!({ "error": result })
|
||||
};
|
||||
tracing::info!(
|
||||
"Sending error of job {} to error handlers (if any)",
|
||||
queued_job.id
|
||||
);
|
||||
if let Err(e) =
|
||||
send_error_to_global_handler(rsmq.clone(), &queued_job, db, Json(&result)).await
|
||||
{
|
||||
tracing::error!(
|
||||
"Could not run global error handler for job {}: {}",
|
||||
&queued_job.id,
|
||||
@@ -529,7 +547,7 @@ pub async fn add_completed_job<
|
||||
&queued_job,
|
||||
canceled_by.is_some(),
|
||||
db,
|
||||
result,
|
||||
Json(&result),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -541,7 +559,6 @@ pub async fn add_completed_job<
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Added completed job {}", queued_job.id);
|
||||
// tracing::error!("4 {:?}", start.elapsed());
|
||||
|
||||
Ok(queued_job.id)
|
||||
@@ -561,12 +578,13 @@ pub async fn run_error_handler<
|
||||
is_global: bool,
|
||||
) -> Result<(), Error> {
|
||||
let w_id = &queued_job.workspace_id;
|
||||
let script_w_id = if is_global { "admins" } else { w_id }; // script workspace id
|
||||
let handler_w_id = if is_global { "admins" } else { w_id }; // script workspace id
|
||||
let job_id = queued_job.id;
|
||||
let (job_payload, tag) = script_path_to_payload(&error_handler_path, db, script_w_id).await?;
|
||||
let (job_payload, tag) =
|
||||
get_payload_tag_from_prefixed_path(&error_handler_path, db, handler_w_id).await?;
|
||||
|
||||
let mut extra = HashMap::new();
|
||||
extra.insert("workspace_id".to_string(), to_raw_value(&w_id));
|
||||
extra.insert("workspace_id".to_string(), to_raw_value(&handler_w_id));
|
||||
extra.insert("job_id".to_string(), to_raw_value(&job_id));
|
||||
extra.insert("path".to_string(), to_raw_value(&queued_job.script_path));
|
||||
extra.insert(
|
||||
@@ -586,7 +604,7 @@ pub async fn run_error_handler<
|
||||
// TODO(gbouv): REMOVE THIS after December 1st 2023 and ping users to re-save their error handlers
|
||||
if error_handler_path
|
||||
.to_string()
|
||||
.eq("hub/5792/workspace-or-schedule-error-handler-slack")
|
||||
.eq("script/hub/5792/workspace-or-schedule-error-handler-slack")
|
||||
{
|
||||
// default slack error handler being used -> we need to inject the slack token
|
||||
let slack_resource = format!("$res:{WORKSPACE_SLACK_BOT_TOKEN_PATH}");
|
||||
@@ -610,7 +628,7 @@ pub async fn run_error_handler<
|
||||
let (uuid, tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
script_w_id,
|
||||
handler_w_id,
|
||||
job_payload,
|
||||
PushArgs { extra, args: result.to_owned() },
|
||||
if is_global {
|
||||
@@ -664,12 +682,19 @@ pub async fn send_error_to_global_handler<
|
||||
result: Json<&'a T>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(ref global_error_handler) = *GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE {
|
||||
let prefixed_global_error_handler_path = if global_error_handler.starts_with("script/")
|
||||
|| global_error_handler.starts_with("flow/")
|
||||
{
|
||||
global_error_handler.clone()
|
||||
} else {
|
||||
format!("script/{}", global_error_handler)
|
||||
};
|
||||
run_error_handler(
|
||||
rsmq,
|
||||
queued_job,
|
||||
db,
|
||||
result,
|
||||
global_error_handler,
|
||||
&prefixed_global_error_handler_path,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
@@ -728,13 +753,15 @@ pub async fn send_error_to_workspace_handler<
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if !ws_error_handler_muted.unwrap_or(false) {
|
||||
let muted = ws_error_handler_muted.unwrap_or(false);
|
||||
if !muted {
|
||||
tracing::info!("workspace error handled for job {}", &queued_job.id);
|
||||
run_error_handler(
|
||||
rsmq,
|
||||
queued_job,
|
||||
db,
|
||||
result,
|
||||
&error_handler.strip_prefix("script/").unwrap(),
|
||||
&error_handler,
|
||||
error_handler_extra_args,
|
||||
false,
|
||||
)
|
||||
@@ -1430,7 +1457,13 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
|
||||
, last_ping = now()
|
||||
, suspend_until = null
|
||||
WHERE id = $1
|
||||
RETURNING *",
|
||||
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
|
||||
running, script_hash, script_path, args, right(logs, 20000000) as logs, raw_code, canceled, canceled_by,
|
||||
canceled_reason, last_ping, job_kind, env_id, schedule_path, permissioned_as,
|
||||
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
|
||||
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
|
||||
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
|
||||
timeout, flow_step_id, cache_ttl, priority",
|
||||
)
|
||||
.bind(uuid)
|
||||
.fetch_optional(db)
|
||||
@@ -1481,7 +1514,13 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 1
|
||||
)
|
||||
RETURNING *")
|
||||
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
|
||||
running, script_hash, script_path, args, right(logs, 20000000) as logs, raw_code, canceled, canceled_by,
|
||||
canceled_reason, last_ping, job_kind, env_id, schedule_path, permissioned_as,
|
||||
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
|
||||
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
|
||||
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
|
||||
timeout, flow_step_id, cache_ttl, priority")
|
||||
.bind(tags)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
@@ -1513,7 +1552,13 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 1
|
||||
)
|
||||
RETURNING *",
|
||||
RETURNING id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for,
|
||||
running, script_hash, script_path, args, right(logs, 20000000) as logs, raw_code, canceled, canceled_by,
|
||||
canceled_reason, last_ping, job_kind, env_id, schedule_path, permissioned_as,
|
||||
flow_status, raw_flow, is_flow_step, language, suspend, suspend_until,
|
||||
same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
|
||||
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s,
|
||||
timeout, flow_step_id, cache_ttl, priority",
|
||||
)
|
||||
.bind(priority_tags.tags.clone())
|
||||
.fetch_optional(db)
|
||||
@@ -1544,7 +1589,7 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
struct ResultR {
|
||||
pub struct ResultR {
|
||||
result: Option<Json<Box<RawValue>>>,
|
||||
}
|
||||
|
||||
@@ -1596,7 +1641,7 @@ pub async fn get_result_by_id(
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn get_result_by_id_from_running_flow(
|
||||
pub async fn get_result_by_id_from_running_flow(
|
||||
db: &Pool<Postgres>,
|
||||
w_id: &str,
|
||||
flow_id: &Uuid,
|
||||
@@ -1959,12 +2004,9 @@ where
|
||||
let content_type_header = headers_map.get(CONTENT_TYPE);
|
||||
let content_type = content_type_header.and_then(|value| value.to_str().ok());
|
||||
let query = Query::<RequestQuery>::try_from_uri(req.uri()).unwrap().0;
|
||||
let extra = build_extra(&headers_map, query.include_header);
|
||||
let raw = query.raw.as_ref().is_some_and(|x| *x);
|
||||
(
|
||||
content_type,
|
||||
build_extra(&headers_map, query.include_header),
|
||||
raw,
|
||||
)
|
||||
(content_type, extra, raw)
|
||||
};
|
||||
|
||||
if content_type.is_none() || content_type.unwrap().starts_with("application/json") {
|
||||
@@ -2047,7 +2089,7 @@ impl PushArgs<HashMap<String, Box<RawValue>>> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_args() -> Box<RawValue> {
|
||||
pub fn empty_result() -> Box<RawValue> {
|
||||
return JsonRawValue::from_string("{}".to_string()).unwrap();
|
||||
}
|
||||
|
||||
@@ -2071,8 +2113,8 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
job_payload: JobPayload,
|
||||
args: T,
|
||||
user: &str,
|
||||
email: &str,
|
||||
permissioned_as: String,
|
||||
mut email: &str,
|
||||
mut permissioned_as: String,
|
||||
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
|
||||
schedule_path: Option<String>,
|
||||
parent_job: Option<Uuid>,
|
||||
@@ -2225,6 +2267,10 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
priority,
|
||||
),
|
||||
JobPayload::ScriptHub { path } => {
|
||||
if path == "hub/7771/slack" {
|
||||
permissioned_as = SUPERADMIN_NOTIFICATION_EMAIL.to_string();
|
||||
email = SUPERADMIN_NOTIFICATION_EMAIL;
|
||||
}
|
||||
(
|
||||
None,
|
||||
Some(path),
|
||||
|
||||
@@ -60,6 +60,7 @@ deno_console.workspace = true
|
||||
deno_url.workspace = true
|
||||
deno_core.workspace = true
|
||||
deno_ast.workspace = true
|
||||
deno_tls.workspace = true
|
||||
postgres-native-tls.workspace = true
|
||||
native-tls.workspace = true
|
||||
mysql_async.workspace = true
|
||||
|
||||
@@ -220,7 +220,7 @@ fn convert_val(arg_t: String, arg_v: Value) -> Value {
|
||||
|
||||
match arg_t.as_str() {
|
||||
"timestamp" | "datetime" => {
|
||||
v = v + ":00";
|
||||
v = v.trim_end_matches("Z").to_string();
|
||||
}
|
||||
"date" => {
|
||||
let arr = v.split("T").collect::<Vec<&str>>();
|
||||
@@ -235,7 +235,7 @@ fn convert_val(arg_t: String, arg_v: Value) -> Value {
|
||||
let arr = v.split("T").collect::<Vec<&str>>();
|
||||
match arr.as_slice() {
|
||||
[_, time] => {
|
||||
v = time.to_string() + ":00";
|
||||
v = time.trim_end_matches("Z").to_string();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
start_child_process, write_file, write_file_binary,
|
||||
},
|
||||
AuthedClientBackgroundTask, BUN_CACHE_DIR, BUN_PATH, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV,
|
||||
NPM_CONFIG_REGISTRY, NSJAIL_PATH, PATH_ENV, TZ_ENV,
|
||||
NSJAIL_PATH, PATH_ENV, TZ_ENV,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
@@ -512,7 +512,7 @@ plugin(p)
|
||||
}
|
||||
|
||||
pub async fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String, String> {
|
||||
let mut bun_envs: HashMap<String, String> = HashMap::from([
|
||||
let bun_envs: HashMap<String, String> = HashMap::from([
|
||||
(String::from("PATH"), PATH_ENV.clone()),
|
||||
(String::from("HOME"), HOME_ENV.clone()),
|
||||
(String::from("TZ"), TZ_ENV.clone()),
|
||||
@@ -529,10 +529,6 @@ pub async fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String
|
||||
BUN_CACHE_DIR.to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
|
||||
bun_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
|
||||
}
|
||||
return bun_envs;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ lazy_static::lazy_static! {
|
||||
.map(|x| format!(";{x}"))
|
||||
.unwrap_or_else(|| String::new());
|
||||
|
||||
static ref DENO_CERT: String = std::env::var("DENO_CERT").ok().unwrap_or_else(|| String::new());
|
||||
static ref DENO_TLS_CA_STORE: String = std::env::var("DENO_TLS_CA_STORE").ok().unwrap_or_else(|| String::new());
|
||||
|
||||
}
|
||||
async fn get_common_deno_proc_envs(
|
||||
@@ -70,6 +72,12 @@ async fn get_common_deno_proc_envs(
|
||||
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
|
||||
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
|
||||
}
|
||||
if DENO_CERT.len() > 0 {
|
||||
deno_envs.insert(String::from("DENO_CERT"), DENO_CERT.clone());
|
||||
}
|
||||
if DENO_TLS_CA_STORE.len() > 0 {
|
||||
deno_envs.insert(String::from("DENO_TLS_CA_STORE"), DENO_TLS_CA_STORE.clone());
|
||||
}
|
||||
return deno_envs;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ pub async fn handle_go_job(
|
||||
) -> Result<Box<RawValue>, Error> {
|
||||
//go does not like executing modules at temp root
|
||||
let job_dir = &format!("{job_dir}/go");
|
||||
let bin_path = Some(format!(
|
||||
let bin_path = format!(
|
||||
"{}/{}",
|
||||
GO_BIN_CACHE_DIR,
|
||||
calculate_hash(&format!(
|
||||
@@ -61,13 +61,8 @@ pub async fn handle_go_job(
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default()
|
||||
))
|
||||
));
|
||||
|
||||
let bin_exists = if let Some(bin_path) = bin_path.clone() {
|
||||
tokio::fs::metadata(bin_path).await.is_ok()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
);
|
||||
let bin_exists = tokio::fs::metadata(&bin_path).await.is_ok();
|
||||
|
||||
let (skip_go_mod, skip_tidy) = if bin_exists {
|
||||
create_dir(job_dir).await?;
|
||||
@@ -215,12 +210,11 @@ func Run(req Req) (interface{{}}, error){{
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(bin_path) = bin_path.clone() {
|
||||
tokio::fs::copy(format!("{job_dir}/main"), &bin_path).await?;
|
||||
logs.push_str(&format!("write cached binary: {}\n", bin_path));
|
||||
}
|
||||
create_dir(&bin_path).await?;
|
||||
tokio::fs::copy(format!("{job_dir}/main"), format!("{bin_path}/main")).await?;
|
||||
logs.push_str(&format!("write cached binary: {}\n", bin_path));
|
||||
} else {
|
||||
let path = bin_path.unwrap().clone();
|
||||
let path = format!("{bin_path}/main");
|
||||
logs.push_str(&format!("found cached binary: {path}\n"));
|
||||
tokio::fs::copy(&path, format!("{job_dir}/main"))
|
||||
.await
|
||||
|
||||
@@ -4,6 +4,7 @@ use serde_json::{json, value::RawValue};
|
||||
use sqlx::types::Json;
|
||||
use windmill_common::error::Error;
|
||||
use windmill_common::jobs::QueuedJob;
|
||||
use windmill_parser_graphql::parse_graphql_sig;
|
||||
use windmill_queue::HTTP_CLIENT;
|
||||
|
||||
use serde::Deserialize;
|
||||
@@ -48,9 +49,26 @@ pub async fn do_graphql(
|
||||
return Err(Error::BadRequest("Missing api argument".to_string()));
|
||||
};
|
||||
|
||||
// variables is job_args except for api
|
||||
let mut variables = HashMap::new();
|
||||
let sig = parse_graphql_sig(&query)
|
||||
.map_err(|x| Error::ExecutionErr(x.to_string()))?
|
||||
.args;
|
||||
if let Some(job_args) = job_args {
|
||||
for arg in &sig {
|
||||
variables.insert(
|
||||
arg.name.clone(),
|
||||
job_args
|
||||
.get(&arg.name)
|
||||
.map(|x| x.to_owned())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut request = HTTP_CLIENT.post(api.base_url).json(&json!({
|
||||
"query": query,
|
||||
"variables": job_args
|
||||
"variables": variables
|
||||
}));
|
||||
|
||||
if let Some(token) = &api.bearer_token {
|
||||
|
||||
@@ -6,16 +6,17 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc, env, io::{BufReader, self}};
|
||||
|
||||
use deno_ast::{ParseParams, SourceTextInfo};
|
||||
use deno_core::{
|
||||
op, serde_v8,
|
||||
v8::IsolateHandle,
|
||||
v8::{self},
|
||||
Extension, JsRuntime, Op, OpState, RuntimeOptions, Snapshot,
|
||||
Extension, JsRuntime, Op, OpState, RuntimeOptions, Snapshot, error::AnyError,
|
||||
};
|
||||
use deno_fetch::FetchPermissions;
|
||||
use deno_tls::{rustls::RootCertStore, rustls_pemfile};
|
||||
use deno_web::{BlobStore, TimersPermission};
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
@@ -38,6 +39,33 @@ pub struct IdContext {
|
||||
pub previous_id: String,
|
||||
}
|
||||
|
||||
pub struct ContainerRootCertStoreProvider {
|
||||
root_cert_store: RootCertStore,
|
||||
}
|
||||
|
||||
impl ContainerRootCertStoreProvider {
|
||||
fn new() -> ContainerRootCertStoreProvider {
|
||||
return ContainerRootCertStoreProvider {
|
||||
root_cert_store: deno_tls::create_default_root_cert_store(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_certificate(&mut self, cert_path: String) -> io::Result<()> {
|
||||
let cert_file = std::fs::File::open(cert_path)?;
|
||||
let mut reader = BufReader::new(cert_file);
|
||||
let pem_file = rustls_pemfile::certs(&mut reader)?;
|
||||
|
||||
self.root_cert_store.add_parsable_certificates(&pem_file);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl deno_tls::RootCertStoreProvider for ContainerRootCertStoreProvider {
|
||||
fn get_or_try_init(&self) -> Result<&RootCertStore, AnyError> {
|
||||
Ok(&self.root_cert_store)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PermissionsContainer;
|
||||
|
||||
impl FetchPermissions for PermissionsContainer {
|
||||
@@ -532,6 +560,18 @@ pub async fn eval_fetch_timeout(
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let deno_fetch_options = if let Some(cert_path) = env::var("DENO_CERT").ok() {
|
||||
let mut cert_store_provider = ContainerRootCertStoreProvider::new();
|
||||
cert_store_provider.add_certificate(cert_path)?;
|
||||
|
||||
deno_fetch::Options {
|
||||
root_cert_store_provider: Some(Arc::new(cert_store_provider)),
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let exts: Vec<Extension> = vec![
|
||||
deno_webidl::deno_webidl::init_ops(),
|
||||
deno_url::deno_url::init_ops(),
|
||||
@@ -540,9 +580,7 @@ pub async fn eval_fetch_timeout(
|
||||
Arc::new(BlobStore::default()),
|
||||
None,
|
||||
),
|
||||
deno_fetch::deno_fetch::init_ops::<PermissionsContainer>(
|
||||
Default::default(),
|
||||
),
|
||||
deno_fetch::deno_fetch::init_ops::<PermissionsContainer>(deno_fetch_options),
|
||||
ext
|
||||
];
|
||||
|
||||
@@ -645,8 +683,8 @@ import("file:///eval.ts").then((module) => module.main(...args)).then(JSON.strin
|
||||
let local = v8::Local::new(scope, global);
|
||||
// Deserialize a `v8` object into a Rust type using `serde_v8`,
|
||||
// in this case deserialize to a JSON `Value`.
|
||||
let r = serde_v8::from_v8::<String>(scope, local)?;
|
||||
Ok(unsafe_raw(r))
|
||||
let r = serde_v8::from_v8::<Option<String>>(scope, local)?;
|
||||
Ok(unsafe_raw(r.unwrap_or_else(|| "null".to_string())))
|
||||
}
|
||||
|
||||
#[op]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::net::IpAddr;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -404,6 +405,9 @@ pub fn pg_cell_to_json_value(
|
||||
Type::UUID => get_basic(row, column, column_i, |a: uuid::Uuid| {
|
||||
Ok(JSONValue::String(a.to_string()))
|
||||
})?,
|
||||
Type::INET => get_basic(row, column, column_i, |a: IpAddr| {
|
||||
Ok(JSONValue::String(a.to_string()))
|
||||
})?,
|
||||
Type::JSON | Type::JSONB => get_basic(row, column, column_i, |a: JSONValue| Ok(a))?,
|
||||
Type::FLOAT4 => get_basic(row, column, column_i, |a: f32| {
|
||||
Ok(f64_to_json_number(a.into())?)
|
||||
|
||||
@@ -30,6 +30,7 @@ lazy_static::lazy_static! {
|
||||
|
||||
static ref PIP_INDEX_URL: Option<String> = std::env::var("PIP_INDEX_URL").ok();
|
||||
static ref PIP_TRUSTED_HOST: Option<String> = std::env::var("PIP_TRUSTED_HOST").ok();
|
||||
static ref PIP_INDEX_CERT: Option<String> = std::env::var("PIP_INDEX_CERT").ok();
|
||||
|
||||
|
||||
static ref RELATIVE_IMPORT_REGEX: Regex = Regex::new(r#"(import|from)\s(((u|f)\.)|\.)"#).unwrap();
|
||||
@@ -121,6 +122,9 @@ pub async fn pip_compile(
|
||||
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
|
||||
args.extend(["--trusted-host", host]);
|
||||
}
|
||||
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
|
||||
args.extend(["--cert", cert_path]);
|
||||
}
|
||||
|
||||
let mut child_cmd = Command::new("pip-compile");
|
||||
child_cmd
|
||||
@@ -410,6 +414,13 @@ async fn prepare_wrapper(
|
||||
.to_lowercase();
|
||||
let module_dir = format!("{}/{}", job_dir, dirs);
|
||||
tokio::fs::create_dir_all(format!("{module_dir}/")).await?;
|
||||
|
||||
let last = if last.starts_with(|x: char| x.is_ascii_digit()) {
|
||||
format!("_{}", last)
|
||||
} else {
|
||||
last
|
||||
};
|
||||
|
||||
let _ = write_file(&module_dir, &format!("{last}.py"), inner_content).await?;
|
||||
if relative_imports {
|
||||
let _ = write_file(job_dir, "loader.py", RELATIVE_PYTHON_LOADER).await?;
|
||||
@@ -484,11 +495,6 @@ if args["{name}"] is None:
|
||||
|
||||
let module_dir_dot = dirs.replace("/", ".").replace("-", "_");
|
||||
|
||||
let last = if last.starts_with(|x: char| x.is_ascii_digit()) {
|
||||
format!("_{}", last)
|
||||
} else {
|
||||
last
|
||||
};
|
||||
Ok((
|
||||
import_loader,
|
||||
import_base64,
|
||||
@@ -603,6 +609,9 @@ pub async fn handle_python_reqs(
|
||||
if let Some(url) = PIP_INDEX_URL.as_ref() {
|
||||
vars.push(("INDEX_URL", url));
|
||||
}
|
||||
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
|
||||
vars.push(("PIP_INDEX_CERT", cert_path));
|
||||
}
|
||||
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
|
||||
vars.push(("TRUSTED_HOST", host));
|
||||
}
|
||||
@@ -702,6 +711,9 @@ pub async fn handle_python_reqs(
|
||||
if let Some(url) = PIP_INDEX_URL.as_ref() {
|
||||
command_args.extend(["--index-url", url]);
|
||||
}
|
||||
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
|
||||
command_args.extend(["--cert", cert_path]);
|
||||
}
|
||||
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
|
||||
command_args.extend(["--trusted-host", &host]);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,15 @@ import "ext:deno_web/13_message_port.js";
|
||||
import "ext:deno_web/14_compression.js";
|
||||
import "ext:deno_web/15_performance.js";
|
||||
|
||||
globalThis.atob = base64.atob;
|
||||
globalThis.btoa = base64.btoa;
|
||||
globalThis.fetch = fetch.fetch;
|
||||
globalThis.Request = request.Request;
|
||||
globalThis.Blob = file.Blob;
|
||||
globalThis.URL = url.URL;
|
||||
globalThis.FormData = formData.FormData;
|
||||
globalThis.URLSearchParams = url.URLSearchParams;
|
||||
globalThis.Headers = headers.Headers;
|
||||
globalThis.FileReader = fileReader.FileReader;
|
||||
globalThis.console = new console.Console((msg, level) =>
|
||||
globalThis.Deno.core.ops.op_log([msg])
|
||||
|
||||
@@ -32,7 +32,7 @@ use windmill_common::{
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue},
|
||||
jobs::{JobKind, QueuedJob},
|
||||
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
|
||||
users::SUPERADMIN_SECRET_EMAIL,
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
|
||||
utils::{rd_string, StripPath},
|
||||
worker::{
|
||||
to_raw_value, to_raw_value_owned, update_ping, CLOUD_HOSTED, WORKER_CONFIG, WORKER_GROUP,
|
||||
@@ -40,7 +40,7 @@ use windmill_common::{
|
||||
DB, IS_READY, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
};
|
||||
use windmill_queue::{
|
||||
canceled_job_to_result, empty_args, get_queued_job, pull, push, register_metric, CanceledBy,
|
||||
canceled_job_to_result, empty_result, get_queued_job, pull, push, register_metric, CanceledBy,
|
||||
PushArgs, PushIsolationLevel, WrappedError, HTTP_CLIENT,
|
||||
};
|
||||
|
||||
@@ -128,17 +128,22 @@ pub async fn create_token_for_owner(
|
||||
w_id: &str,
|
||||
owner: &str,
|
||||
label: &str,
|
||||
expires_in: i32,
|
||||
expires_in: u64,
|
||||
email: &str,
|
||||
) -> error::Result<String> {
|
||||
// TODO: Bad implementation. We should not have access to this DB here.
|
||||
if let Some(token) = JOB_TOKEN.as_ref() {
|
||||
return Ok(token.clone());
|
||||
}
|
||||
|
||||
let token: String = rd_string(30);
|
||||
let is_super_admin =
|
||||
sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.unwrap_or(false)
|
||||
|| email == SUPERADMIN_SECRET_EMAIL;
|
||||
|| email == SUPERADMIN_SECRET_EMAIL
|
||||
|| email == SUPERADMIN_NOTIFICATION_EMAIL;
|
||||
|
||||
sqlx::query_scalar!(
|
||||
"INSERT INTO token
|
||||
@@ -195,6 +200,8 @@ pub const MAX_BUFFERED_DEDICATED_JOBS: usize = 3;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
|
||||
static ref JOB_TOKEN: Option<String> = std::env::var("JOB_TOKEN").ok();
|
||||
|
||||
static ref SLEEP_QUEUE: u64 = std::env::var("SLEEP_QUEUE")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<u64>().ok())
|
||||
@@ -267,10 +274,10 @@ lazy_static::lazy_static! {
|
||||
|
||||
pub static ref TIMEOUT_DURATION: Duration = Duration::from_secs(*TIMEOUT);
|
||||
|
||||
pub static ref SCRIPT_TOKEN_EXPIRY: i32 = std::env::var("SCRIPT_TOKEN_EXPIRY")
|
||||
pub static ref SCRIPT_TOKEN_EXPIRY: u64 = std::env::var("SCRIPT_TOKEN_EXPIRY")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<i32>().ok())
|
||||
.unwrap_or(900);
|
||||
.and_then(|x| x.parse::<u64>().ok())
|
||||
.unwrap_or(*TIMEOUT);
|
||||
|
||||
pub static ref GLOBAL_CACHE_INTERVAL: u64 = std::env::var("GLOBAL_CACHE_INTERVAL")
|
||||
.ok()
|
||||
@@ -951,6 +958,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
let worker_flow_transition_duration2 = worker_flow_transition_duration.clone();
|
||||
|
||||
let worker_name2 = worker_name.clone();
|
||||
let killpill_tx2 = killpill_tx.clone();
|
||||
let send_result = tokio::spawn(async move {
|
||||
while let Some(jc) = job_completed_rx.recv().await {
|
||||
if let Some(wj) = worker_job_completed_channel_queue2.as_ref() {
|
||||
@@ -962,7 +970,6 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
let same_worker_tx2 = same_worker_tx2.clone();
|
||||
let rsmq2 = rsmq2.clone();
|
||||
let worker_name = worker_name2.clone();
|
||||
|
||||
if matches!(jc.job.job_kind, JobKind::Noop) || is_dedicated_worker {
|
||||
thread_count.fetch_add(1, Ordering::SeqCst);
|
||||
let thread_count = thread_count.clone();
|
||||
@@ -986,7 +993,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
let worker_save_completed_job_duration2 =
|
||||
worker_save_completed_job_duration.clone();
|
||||
let worker_flow_transition_duration2 = worker_flow_transition_duration.clone();
|
||||
|
||||
let killpill_tx = killpill_tx2.clone();
|
||||
tokio::spawn(async move {
|
||||
#[cfg(feature = "benchmark")]
|
||||
let process_start = Instant::now();
|
||||
@@ -1049,6 +1056,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
.execute(&db2)
|
||||
.await
|
||||
.expect("update config to trigger restart of all dedicated workers at that config");
|
||||
killpill_tx.send(()).unwrap_or_default();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -1099,79 +1107,71 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
let is_flow_worker;
|
||||
if let Some(flow_path) = _wp.path.strip_prefix("flow/") {
|
||||
is_flow_worker = true;
|
||||
loop {
|
||||
let value = sqlx::query_scalar!(
|
||||
"SELECT value FROM flow WHERE path = $1 AND workspace_id = $2",
|
||||
flow_path,
|
||||
_wp.workspace_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await;
|
||||
if let Ok(v) = value {
|
||||
if let Some(v) = v {
|
||||
let value = serde_json::from_value::<FlowValue>(v).map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"could not convert json to flow for {flow_path}: {err:?}"
|
||||
))
|
||||
let value = sqlx::query_scalar!(
|
||||
"SELECT value FROM flow WHERE path = $1 AND workspace_id = $2",
|
||||
flow_path,
|
||||
_wp.workspace_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await;
|
||||
if let Ok(v) = value {
|
||||
if let Some(v) = v {
|
||||
let value = serde_json::from_value::<FlowValue>(v).map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"could not convert json to flow for {flow_path}: {err:?}"
|
||||
))
|
||||
});
|
||||
if let Ok(flow) = value {
|
||||
let workers = spawn_dedicated_workers_for_flow(
|
||||
&flow.modules,
|
||||
&_wp.path,
|
||||
&_wp.workspace_id,
|
||||
killpill_tx.clone(),
|
||||
&killpill_rx,
|
||||
db,
|
||||
&worker_dir,
|
||||
base_internal_url,
|
||||
&worker_name,
|
||||
&job_completed_tx,
|
||||
)
|
||||
.await;
|
||||
workers.into_iter().for_each(|(path, sender, handle)| {
|
||||
dedicated_handles.push(handle);
|
||||
hm.insert(path, sender);
|
||||
});
|
||||
if let Ok(flow) = value {
|
||||
let workers = spawn_dedicated_workers_for_flow(
|
||||
&flow.modules,
|
||||
&_wp.path,
|
||||
&_wp.workspace_id,
|
||||
killpill_tx.clone(),
|
||||
&killpill_rx,
|
||||
db,
|
||||
&worker_dir,
|
||||
base_internal_url,
|
||||
&worker_name,
|
||||
&job_completed_tx,
|
||||
)
|
||||
.await;
|
||||
workers.into_iter().for_each(|(path, sender, handle)| {
|
||||
dedicated_handles.push(handle);
|
||||
hm.insert(path, sender);
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
tracing::error!(
|
||||
"flow present but value not found for {}, waiting 10s",
|
||||
flow_path
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::error!("flow not found for {}, waiting 10s,", flow_path);
|
||||
tracing::error!(
|
||||
"flow present but value not found for dedicated worker. {}",
|
||||
flow_path
|
||||
);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10000)).await;
|
||||
} else {
|
||||
tracing::error!("flow not found for dedicated worker: {}. Waiting for dependency job and expected to restart.", flow_path);
|
||||
}
|
||||
} else {
|
||||
is_flow_worker = false;
|
||||
loop {
|
||||
if let Some((path, sender, handle)) = spawn_dedicated_worker(
|
||||
SpawnWorker::Script { path: _wp.path.clone(), hash: None },
|
||||
&_wp.workspace_id,
|
||||
killpill_tx.clone(),
|
||||
&killpill_rx,
|
||||
db,
|
||||
&worker_dir,
|
||||
base_internal_url,
|
||||
&worker_name,
|
||||
&job_completed_tx,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
dedicated_handles.push(handle);
|
||||
hm.insert(path, sender);
|
||||
break;
|
||||
} else {
|
||||
tracing::error!(
|
||||
"failed to spawn dedicated worker for {}, script found but not in a compatible language. Retrying 10s",
|
||||
if let Some((path, sender, handle)) = spawn_dedicated_worker(
|
||||
SpawnWorker::Script { path: _wp.path.clone(), hash: None },
|
||||
&_wp.workspace_id,
|
||||
killpill_tx.clone(),
|
||||
&killpill_rx,
|
||||
db,
|
||||
&worker_dir,
|
||||
base_internal_url,
|
||||
&worker_name,
|
||||
&job_completed_tx,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
dedicated_handles.push(handle);
|
||||
hm.insert(path, sender);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"failed to spawn dedicated worker for {}, script not found",
|
||||
_wp.path
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(10000)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
(hm, is_flow_worker)
|
||||
@@ -1245,6 +1245,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
|
||||
if (jobs_executed as u32 + vacuum_shift) % VACUUM_PERIOD == 0 {
|
||||
if let Err(e) = sqlx::query!("VACUUM queue").execute(db).await {
|
||||
jobs_executed += 1;
|
||||
tracing::error!(worker = %worker_name, "failed to vacuum queue: {}", e);
|
||||
}
|
||||
tracing::info!(worker = %worker_name, "vacuumed queue and completed_job");
|
||||
@@ -1460,7 +1461,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
.send(JobCompleted {
|
||||
job: Arc::new(job),
|
||||
success: true,
|
||||
result: empty_args(),
|
||||
result: empty_result(),
|
||||
logs: String::new(),
|
||||
mem_peak: 0,
|
||||
cached_res_path: None,
|
||||
@@ -1845,9 +1846,8 @@ async fn spawn_dedicated_worker(
|
||||
{
|
||||
let (dedicated_worker_tx, dedicated_worker_rx) =
|
||||
mpsc::channel::<Arc<QueuedJob>>(MAX_BUFFERED_DEDICATED_JOBS);
|
||||
let mut killpill_rx = killpill_rx.resubscribe();
|
||||
let killpill_rx = killpill_rx.resubscribe();
|
||||
let db = db.clone();
|
||||
let worker_dir = worker_dir.clone();
|
||||
let base_internal_url = base_internal_url.to_string();
|
||||
let worker_name = worker_name.to_string();
|
||||
let job_completed_tx = job_completed_tx.clone();
|
||||
@@ -1865,16 +1865,12 @@ async fn spawn_dedicated_worker(
|
||||
|
||||
let (content, lock, language, envs) = match sw {
|
||||
SpawnWorker::Script { path, hash } => {
|
||||
let r;
|
||||
loop {
|
||||
let q = if let Some(hash) = hash {
|
||||
get_script_content_by_hash(&hash, &w_id, &db).await.map(
|
||||
|r: ContentReqLangEnvs| {
|
||||
Some((r.content, r.lockfile, r.language, r.envs))
|
||||
},
|
||||
)
|
||||
} else {
|
||||
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>)>(
|
||||
let q = if let Some(hash) = hash {
|
||||
get_script_content_by_hash(&hash, &w_id, &db).await.map(
|
||||
|r: ContentReqLangEnvs| Some((r.content, r.lockfile, r.language, r.envs)),
|
||||
)
|
||||
} else {
|
||||
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>)>(
|
||||
"SELECT content, lock, language, envs FROM script WHERE path = $1 AND workspace_id = $2 AND
|
||||
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
|
||||
deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
|
||||
@@ -1884,37 +1880,23 @@ async fn spawn_dedicated_worker(
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("expected content and lock: {e}")))
|
||||
};
|
||||
|
||||
if let Ok(q) = q {
|
||||
if let Some(wp) = q {
|
||||
r = wp;
|
||||
break;
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed to fetch script `{}` in workspace {} for dedicated worker. Retrying in 10s.",
|
||||
path,
|
||||
w_id
|
||||
);
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
tracing::info!("Killing dedicated worker while it was attempting to fetch script");
|
||||
return None;
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(10)) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Ok(q) = q {
|
||||
if let Some(wp) = q {
|
||||
wp
|
||||
} else {
|
||||
tracing::error!("Failed to fetch script for dedicated worker");
|
||||
killpill_tx.send(()).expect("send");
|
||||
tracing::error!(
|
||||
"Failed to fetch script `{}` in workspace {} for dedicated worker.",
|
||||
path,
|
||||
w_id
|
||||
);
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Failed to fetch script for dedicated worker");
|
||||
killpill_tx.send(()).expect("send");
|
||||
return None;
|
||||
}
|
||||
r
|
||||
}
|
||||
SpawnWorker::RawScript { content, lock, lang, .. } => (content, lock, Some(lang), None),
|
||||
};
|
||||
@@ -1925,21 +1907,26 @@ async fn spawn_dedicated_worker(
|
||||
}
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let token = rd_string(30);
|
||||
if let Err(e) = sqlx::query_scalar!(
|
||||
"INSERT INTO token
|
||||
let token = if let Some(token) = JOB_TOKEN.as_ref() {
|
||||
token.clone()
|
||||
} else {
|
||||
let token = rd_string(30);
|
||||
if let Err(e) = sqlx::query_scalar!(
|
||||
"INSERT INTO token
|
||||
(token, label, super_admin, email)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
token,
|
||||
"dedicated_worker",
|
||||
true,
|
||||
"dedicated_worker@windmill.dev"
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("failed to create token for dedicated worker: {:?}", e);
|
||||
killpill_tx.clone().send(()).expect("send");
|
||||
token,
|
||||
"dedicated_worker",
|
||||
true,
|
||||
"dedicated_worker@windmill.dev"
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("failed to create token for dedicated worker: {:?}", e);
|
||||
killpill_tx.clone().send(()).expect("send");
|
||||
};
|
||||
token
|
||||
};
|
||||
|
||||
let worker_envs = build_envs(envs).expect("failed to build envs");
|
||||
|
||||
@@ -1383,10 +1383,29 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
logs,
|
||||
0,
|
||||
canceled_by,
|
||||
rsmq,
|
||||
rsmq.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if flow_job.is_flow_step {
|
||||
if let Some(parent_job) = flow_job.parent_job {
|
||||
update_flow_status_after_job_completion(
|
||||
db,
|
||||
client,
|
||||
parent_job,
|
||||
&flow_job.id,
|
||||
&flow_job.workspace_id,
|
||||
true,
|
||||
&to_raw_value(&result),
|
||||
false,
|
||||
same_worker_tx.clone(),
|
||||
&worker_dir,
|
||||
None,
|
||||
rsmq,
|
||||
worker_name,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -2348,8 +2367,8 @@ async fn compute_next_flow_transform(
|
||||
concurrency_time_window_s: None,
|
||||
skip_expr: None,
|
||||
cache_ttl: None,
|
||||
ws_error_handler_muted: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
},
|
||||
path: inner_path,
|
||||
restarted_from: None,
|
||||
@@ -2387,8 +2406,8 @@ async fn compute_next_flow_transform(
|
||||
concurrency_time_window_s: None,
|
||||
skip_expr: None,
|
||||
cache_ttl: None,
|
||||
ws_error_handler_muted: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
},
|
||||
path: Some(format!("{}/forloop", flow_job.script_path())),
|
||||
restarted_from: None,
|
||||
@@ -2483,8 +2502,8 @@ async fn compute_next_flow_transform(
|
||||
concurrency_time_window_s: None,
|
||||
skip_expr: None,
|
||||
cache_ttl: None,
|
||||
ws_error_handler_muted: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
},
|
||||
path: Some(format!(
|
||||
"{}/branchone-{}",
|
||||
@@ -2532,8 +2551,8 @@ async fn compute_next_flow_transform(
|
||||
concurrency_time_window_s: None,
|
||||
skip_expr: None,
|
||||
cache_ttl: None,
|
||||
ws_error_handler_muted: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
},
|
||||
path: Some(format!(
|
||||
"{}/branchall-{}",
|
||||
@@ -2597,8 +2616,8 @@ async fn compute_next_flow_transform(
|
||||
concurrency_time_window_s: None,
|
||||
skip_expr: None,
|
||||
cache_ttl: None,
|
||||
ws_error_handler_muted: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
},
|
||||
path: Some(format!(
|
||||
"{}/branchall-{}",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
|
||||
import { JSDOM } from "https://jspm.dev/jsdom";
|
||||
import { JSDOM } from "https://jspm.dev/jsdom@22";
|
||||
|
||||
type DataPoint = {
|
||||
value: number;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.210.0";
|
||||
export const VERSION = "v1.219.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function pushFlow(
|
||||
|
||||
if (flow) {
|
||||
if (isSuperset(localFlow, flow)) {
|
||||
log.info(colors.bold.green("Flow is up to date"));
|
||||
log.info(colors.green(`Flow ${remotePath} is up to date`));
|
||||
return;
|
||||
}
|
||||
log.info(colors.bold.yellow(`Updating flow ${remotePath}...`));
|
||||
|
||||
@@ -31,7 +31,7 @@ addEventListener("error", (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
export const VERSION = "v1.210.0";
|
||||
export const VERSION = "v1.219.1";
|
||||
|
||||
let command: any = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -124,14 +124,18 @@ export async function handleFile(
|
||||
typed.is_template === remote.is_template &&
|
||||
typed.kind == remote.kind &&
|
||||
!remote.archived &&
|
||||
remote?.lock == typed.lock?.join("\n") &&
|
||||
JSON.stringify(typed.schema) == JSON.stringify(remote.schema))
|
||||
(remote?.lock ?? "") == (typed.lock?.join("\n") ?? "") &&
|
||||
JSON.stringify(typed.schema) == JSON.stringify(remote.schema) &&
|
||||
typed.tag == remote.tag &&
|
||||
(typed.ws_error_handler_muted ?? false) ==
|
||||
remote.ws_error_handler_muted &&
|
||||
typed.dedicated_worker == remote.dedicated_worker &&
|
||||
typed.cache_ttl == remote.cache_ttl &&
|
||||
typed.concurrency_time_window_s ==
|
||||
remote.concurrency_time_window_s &&
|
||||
typed.concurrent_limit == remote.concurrent_limit)
|
||||
) {
|
||||
log.info(
|
||||
colors.yellow(
|
||||
`No change to push for script ${remotePath}, skipping`
|
||||
)
|
||||
);
|
||||
log.info(colors.green(`Script ${remotePath} is up to date`));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,8 +561,8 @@ async function pull(
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
colors.green.underline(
|
||||
`Done! All ${changes.length} changes applied locally.`
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes applied locally.`
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -831,8 +831,8 @@ async function push(
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
colors.green.underline(
|
||||
`Done! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}.`
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export async function resolveDefaultResource(obj: any): Promise<any> {
|
||||
}
|
||||
|
||||
export function getStatePath(): string {
|
||||
const state_path = Deno.env.get("WM_STATE_PATH_NEW");
|
||||
const state_path = Deno.env.get("WM_STATE_PATH_NEW") ?? Deno.env.get("WM_STATE_PATH");
|
||||
if (state_path === undefined) {
|
||||
throw Error("State path not set");
|
||||
}
|
||||
|
||||
4
docker/DockerfileReports
Normal file
4
docker/DockerfileReports
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM ghcr.io/windmill-labs/windmill-ee:main
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y chromium
|
||||
16
examples/deploy/private-package-registry-tls/Caddyfile
Normal file
16
examples/deploy/private-package-registry-tls/Caddyfile
Normal file
@@ -0,0 +1,16 @@
|
||||
{$BASE_URL} {
|
||||
bind {$ADDRESS}
|
||||
tls /certs/caddy.crt /certs/caddy.key
|
||||
handle_path /static/* {
|
||||
root * /static/
|
||||
file_server {
|
||||
browse
|
||||
}
|
||||
}
|
||||
handle_path /npm/* {
|
||||
reverse_proxy * http://npm_registry:4873
|
||||
}
|
||||
handle_path /* {
|
||||
reverse_proxy * http://pypi_server:8080
|
||||
}
|
||||
}
|
||||
146
examples/deploy/private-package-registry-tls/README.md
Normal file
146
examples/deploy/private-package-registry-tls/README.md
Normal file
@@ -0,0 +1,146 @@
|
||||
Private package registry with self-signed certificates
|
||||
==================================================
|
||||
|
||||
Setup a private NPM registry and Pypi server behind a revers proxy (Caddy) exposing an HTTPS endpoint with self signed certificates
|
||||
|
||||
### Setup
|
||||
|
||||
1. Generate self signed certificates (or bring your own)
|
||||
```bash
|
||||
cd certs
|
||||
# feel free to read the file anc change the
|
||||
./generate_certs.sh
|
||||
# choose a password for the RootCA key. You'll need to input it for pretty much all following steps
|
||||
```
|
||||
At the end of the script, you should have multiple files in the `certs/` folder. The most important ones are:
|
||||
- `windmill-root.key` (Root CA private key)
|
||||
- `windmill-root.crt` (Root CA certificate)
|
||||
- `caddy.key` (caddy server private key)
|
||||
- `caddy.crt` (caddy registry server certificate)
|
||||
|
||||
2. Start the docker compose stack
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
This will start the private NPM registry, the private Pypi server and Caddy as a reverse proxy sitting in front of the two. Separately, for the purpose of proving an end to end setup, it will also start minimal Windmill stack composed of just one Windmill server/worker and the associated database.
|
||||
|
||||
For a more complete Windmill setup, we invite you to refer to the latest [docker compose](/docker-compose.yml) at the root of this repository to setup a more evolved Windmill stack.
|
||||
|
||||
For the private registries/repositories, it's using [Verdaccio](https://verdaccio.org/) as an easy-to-deploy NPM registry and [pypiserver](https://pypi.org/project/pypiserver/) for Python.
|
||||
|
||||
## Deno pulling package from private NPM registry
|
||||
|
||||
Upload the custom `helloworld_npm_package` package to the private NPM registry and use it in a Windmill script
|
||||
|
||||
```bash
|
||||
cd helloworld_npm_package
|
||||
# you need to first create a registry user
|
||||
# you might need to run `npm config set strict-ssl false` if the below fails. If you do, then change it back to `true` after running the 2 commands
|
||||
npm adduser --registry https://localhost/npm/
|
||||
npm publish --registry https://localhost/npm/
|
||||
```
|
||||
|
||||
Go to Windmill at `http://localhost:8000`. Create a simple Deno script:
|
||||
```ts
|
||||
import * as testpackage from "npm:@windmill/helloworld@0.0.1"
|
||||
|
||||
export async function main() {
|
||||
console.log(testpackage.sayHello("Windmill"))
|
||||
}
|
||||
```
|
||||
and execute it. It should return successfully with:
|
||||
```
|
||||
Hello Windmill
|
||||
```
|
||||
|
||||
Note: Native fetches to HTTPS endpoints with self signed certificates also takes into account the DENO_CERT environment variable
|
||||
|
||||
Go to Windmill and create a new script of type "REST" with the following content:
|
||||
```ts
|
||||
export async function main() {
|
||||
const res = await fetch("https://caddy/static/helloworld.json", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
It will fetch a static json file from Caddy exposed behind its HTTPS endpoint with self signed certificate.
|
||||
|
||||
## Python pulling package from private NPM registry
|
||||
|
||||
Upload the custom `helloworld_python_module` python module to the private Pypi server
|
||||
|
||||
```bash
|
||||
cd helloworld_python_module
|
||||
python setup.py sdist
|
||||
# using twine to upload the module here, get it with `pip install twine`
|
||||
twine upload --repository-url https://localhost/ dist/* --cert ../certs/windmill-root.crt
|
||||
# no username and password, just press enter. For the purpose of the demo we're running pypiserver completely unauthenticated
|
||||
```
|
||||
You can check that the package is uploaded by visiting [https://localhost/simple](https://localhost/simple).
|
||||
|
||||
Go to Windmill at `http://localhost:8000`. Create a simple Python script:
|
||||
```python
|
||||
#requirements:
|
||||
#windmill-helloworld==0.0.1
|
||||
import windmill_helloworld
|
||||
|
||||
def main():
|
||||
print(windmill_helloworld.say_hello("Windmill"))
|
||||
```
|
||||
and execute it. It should return successfully with:
|
||||
```
|
||||
Hello Windmill
|
||||
```
|
||||
|
||||
### MISC
|
||||
|
||||
1. `DENO_TLS_CA_STORE` VS `DENO_CERT`
|
||||
Both works. `DENO_CERT` is better b/c you just have to set it to the path of the trusted Root CA certificate, and deno will trust this certificate.
|
||||
When using `DENO_TLS_CA_STORE=system`, you _have to_ make the server trust the custom Root CA certificate with the following commands:
|
||||
```bash
|
||||
# in the windmill-server container:
|
||||
cp /custom-certs/windmill-root.crt /usr/local/share/ca-certificates/
|
||||
update-ca-certificates # as root
|
||||
# the output should tell (among other things): " ... 1 added, 0 removed; done. ..."
|
||||
```
|
||||
|
||||
2. Running Deno scripts manually in the Windmill container
|
||||
This could be useful for debugging purposes.
|
||||
|
||||
```bash
|
||||
# log into the Windmill Server container
|
||||
docker exec -it <WINDMILL_SERVER_CONTAINER> /bin/bash
|
||||
|
||||
# Go into Deno REPL
|
||||
deno
|
||||
|
||||
# try to import the package
|
||||
import * as testpackage from "npm:@windmill/helloworld@0.0.1"
|
||||
> undefined
|
||||
|
||||
# if the above returns undefined, there's a good chance it's working. If you want to double check:
|
||||
testpackage.sayHello("Windmill")
|
||||
> Hello Windmill
|
||||
|
||||
# potentially inspect the env var available to DENO. Is you used DENO_CERT in the docker compose, check its value:
|
||||
console.log(Deno.env.get("DENO_CERT"))
|
||||
> /custom-certs/windmill-root.crt
|
||||
```
|
||||
|
||||
3. Manually testing Pypi private server integration
|
||||
Can be useful to debug as well:
|
||||
|
||||
```bash
|
||||
# install the package
|
||||
pip3 install --cert ../certs/windmill-root.crt -i https://localhost/simple/ windmill-helloworld
|
||||
|
||||
# go to python CLI and try to import it and use it
|
||||
python
|
||||
>>> import windmill_helloworld
|
||||
>>> windmill_helloworld.say_hello("world")
|
||||
'Hello world'
|
||||
```
|
||||
5
examples/deploy/private-package-registry-tls/certs/.gitignore
vendored
Normal file
5
examples/deploy/private-package-registry-tls/certs/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
*.crt
|
||||
*.csr
|
||||
*.key
|
||||
*.ext
|
||||
*.srl
|
||||
35
examples/deploy/private-package-registry-tls/certs/generate_certs.sh
Executable file
35
examples/deploy/private-package-registry-tls/certs/generate_certs.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
set eou -pipefail
|
||||
|
||||
CANAME='windmill-root'
|
||||
|
||||
COUNTRY='FR'
|
||||
STATE='Paris'
|
||||
CITY='Paris'
|
||||
ORGANIZATION='WindmillLabs'
|
||||
ROOT_CA_CN='WindmillRootCA'
|
||||
SERVER_CA_CN='caddy' # IMPORTANT: set this to the FQDN of the caddy server. Here in the docker compose stack, it will be caddy
|
||||
|
||||
echo "Generating RootCA key"
|
||||
openssl genrsa -aes256 -out $CANAME.key 4096
|
||||
echo "Generating RootCA certificate"
|
||||
openssl req -x509 -new -nodes -key $CANAME.key -sha256 -days 1826 -out $CANAME.crt -subj "/CN=${ROOT_CA_CN}/C=${COUNTRY}/ST=${STATE}/L=${CITY}/O=${ORGANIZATION}"
|
||||
|
||||
CERTNAME=caddy
|
||||
echo "Generating server certificate private key and cert signing request"
|
||||
openssl req -new -nodes -out $CERTNAME.csr -newkey rsa:4096 -keyout $CERTNAME.key -subj "/CN=${SERVER_CA_CN}/C=${COUNTRY}/ST=${STATE}/L=${CITY}/O=${ORGANIZATION}"
|
||||
|
||||
echo "Generating server certificate"
|
||||
cat > $CERTNAME.v3.ext << EOF
|
||||
[v3_req]
|
||||
authorityKeyIdentifier=keyid,issuer
|
||||
basicConstraints=CA:FALSE
|
||||
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
DNS.1 = caddy
|
||||
DNS.2 = localhost
|
||||
EOF
|
||||
# ^ HERE ^ in the above alt_names, feel free to add any alternate CN
|
||||
|
||||
openssl x509 -req -in $CERTNAME.csr -CA $CANAME.crt -CAkey $CANAME.key -CAcreateserial -out $CERTNAME.crt -days 1826 -sha256 -extensions v3_req -extfile $CERTNAME.v3.ext
|
||||
@@ -0,0 +1,69 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
npm_registry:
|
||||
image: verdaccio/verdaccio
|
||||
environment:
|
||||
- VERDACCIO_PROTOCOL=http
|
||||
- VERDACCIO_PUBLIC_URL=http://caddy/npm/
|
||||
volumes:
|
||||
- ./verdaccio_conf:/verdaccio/conf
|
||||
- npm_registry_data:/verdaccio/storage
|
||||
|
||||
pypi_server:
|
||||
image: pypiserver/pypiserver:latest
|
||||
platform: linux/x86_64
|
||||
volumes:
|
||||
- pypi_data:/data/packages
|
||||
command: run -P . -a . /data/packages
|
||||
|
||||
caddy:
|
||||
image: caddy:2.5.2-alpine
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- ./certs:/certs
|
||||
- ./helloworld_static:/static/
|
||||
ports:
|
||||
- 443:443
|
||||
environment:
|
||||
- BASE_URL=":443"
|
||||
|
||||
db:
|
||||
image: postgres:14
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_DB: windmill
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
windmill_server:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
ports:
|
||||
- 8000:8000
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:changeme@db/windmill?sslmode=disable
|
||||
- WORKER_TAGS=deno,go,python3,bash,flow,hub,dependency,nativets
|
||||
|
||||
# For DENO and REST scripts:
|
||||
- NPM_CONFIG_REGISTRY=http://caddy/npm/
|
||||
- DENO_CERT=/custom-certs/windmill-root.crt # this will make deno trust this RootCA for all sessions
|
||||
# - DENO_TLS_CA_STORE=system # alternatively, you can use this but you'll need to manually trust the RootCA at the host level, see README.md
|
||||
|
||||
# For Python scripts:
|
||||
- PIP_INDEX_URL=https://caddy/simple/
|
||||
- PIP_INDEX_CERT=/custom-certs/windmill-root.crt # this will make pip trust this RootCA for all sessions
|
||||
volumes:
|
||||
- ./certs:/custom-certs
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
npm_registry_data: null
|
||||
pypi_data: null
|
||||
db_data: null
|
||||
1
examples/deploy/private-package-registry-tls/helloworld_npm_package/.gitignore
vendored
Normal file
1
examples/deploy/private-package-registry-tls/helloworld_npm_package/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -0,0 +1,4 @@
|
||||
Hello world test package
|
||||
========================
|
||||
|
||||
Dummy NPM package to test imports from a private NPM registry in Windmill
|
||||
@@ -0,0 +1,3 @@
|
||||
exports.sayHello = function(x) {
|
||||
return `Hello ${x}`
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@windmill/helloworld",
|
||||
"version": "0.0.1",
|
||||
"description": "hello world test package",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "Windmill",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
3
examples/deploy/private-package-registry-tls/helloworld_python_module/.gitignore
vendored
Normal file
3
examples/deploy/private-package-registry-tls/helloworld_python_module/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
@@ -0,0 +1,4 @@
|
||||
Hello world test module
|
||||
=======================
|
||||
|
||||
Dummy Python module to test imports from a private Pypi server in Windmill
|
||||
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
|
||||
import setuptools
|
||||
|
||||
module_path = os.path.join(os.path.dirname(__file__), "windmill_helloworld.py")
|
||||
|
||||
setuptools.setup(
|
||||
name="windmill-helloworld",
|
||||
version="0.0.1",
|
||||
url="https://github.com/windmill-labs/windmill/blob//examples/deploy/private-package-registry-tls/README.md",
|
||||
author="WindmillLabs",
|
||||
author_email="contact@windmill.dev",
|
||||
description="Simple hello world python module to host on a private Pypi server",
|
||||
long_description=open("README.md").read(),
|
||||
py_modules=["windmill_helloworld"],
|
||||
zip_safe=False,
|
||||
platforms="any",
|
||||
install_requires=[],
|
||||
classifiers=[
|
||||
"Development Status :: 2 - Pre-Alpha",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
def say_hello(x: str) -> str:
|
||||
return "Hello {}".format(x)
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"hello": "world"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user