Compare commits

..

2 Commits

Author SHA1 Message Date
Ruben Fiszel
4fceae4f85 progress 2023-02-01 20:54:13 +01:00
Ruben Fiszel
c1cfed840a all 2023-02-01 16:27:50 +01:00
622 changed files with 21486 additions and 293685 deletions

7
.env
View File

@@ -1,7 +1,2 @@
DB_PASSWORD=changeme DB_PASSWORD=changeme
WM_BASE_URL=localhost
# this is the url that your instance is publicly exposed to
WM_BASE_URL=http://localhost
# this is the url that caddy will reverse proxy from. It might be different than WM_BASE_URL if you re using a second proxy/load balancer in front
CADDY_REVERSE_PROXY=http://localhost

View File

@@ -4,7 +4,7 @@ VERSION=$1
echo "Updating versions to: $VERSION" echo "Updating versions to: $VERSION"
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" backend/Cargo.toml sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" backend/Cargo.toml
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" cli/main.ts sed -i -e "/^const VERSION =/s/= .*/= \"v$VERSION\";/" cli/main.ts
sed -i -e "/version: /s/: .*/: $VERSION/" backend/windmill-api/openapi.yaml sed -i -e "/version: /s/: .*/: $VERSION/" backend/windmill-api/openapi.yaml
sed -i -e "/version: /s/: .*/: $VERSION/" openflow.openapi.yaml sed -i -e "/version: /s/: .*/: $VERSION/" openflow.openapi.yaml
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" frontend/package.json sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" frontend/package.json

View File

@@ -40,4 +40,4 @@ jobs:
backend -> target backend -> target
- name: cargo test - name: cargo test
timeout-minutes: 10 timeout-minutes: 10
run: mkdir frontend/build && cd backend && touch windmill-api/openapi-deref.yaml && DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill DISABLE_NSJAIL=false cargo test --all -- --nocapture run: mkdir frontend/build && cd backend && touch windmill-api/openapi-deref.yaml && DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill cargo test --all -- --nocapture

View File

@@ -10,7 +10,6 @@ jobs:
container: node:18 container: node:18
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- run: git config --system --add safe.directory /__w/windmill/windmill
- name: Change versions - name: Change versions
run: ./.github/change-versions.sh "$(cat version.txt)" run: ./.github/change-versions.sh "$(cat version.txt)"
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1

View File

@@ -0,0 +1,20 @@
name: Deploy to windmill.dev
on:
push:
branches: [main]
paths:
- "community/**"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to windmill.dev
uses: windmill-labs/windmill-gh-action-deploy@v2.0.0
with:
dry_run: false
input_dir: community
windmill_workspace: starter
windmill_token: ${{ secrets.WINDMILL_API_TOKEN }}

View File

@@ -18,7 +18,7 @@ permissions:
contents: read contents: read
id-token: write id-token: write
packages: write packages: write
jobs: jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@@ -50,6 +50,7 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
- name: Build and push publicly - name: Build and push publicly
uses: depot/build-push-action@v1 uses: depot/build-push-action@v1
with: with:
@@ -62,7 +63,7 @@ jobs:
labels: | labels: |
${{ steps.meta-public.outputs.labels }} ${{ steps.meta-public.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3 org.opencontainers.image.licenses=AGPLv3
build_ee: build_ee:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@@ -108,61 +109,40 @@ jobs:
${{ steps.meta-ee-public.outputs.labels }} ${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License 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]
# needs: [build]
# services:
# postgres:
# image: postgres
# env:
# POSTGRES_DB: windmill
# POSTGRES_USER: admin
# POSTGRES_PASSWORD: changeme
# ports:
# - 5432:5432
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# steps:
# - uses: actions/checkout@v3
# - name: "Docker"
# run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
# id: docker-container
# - uses: actions/setup-node@v3
# with:
# node-version: 16
# - name: "Playwright run"
# timeout-minutes: 2
# run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
# - name: "Clean up"
# run: docker kill ${{ steps.docker-container.outputs.id }}
# if: always()
deploy_s3: playwright:
needs: [build_ee] runs-on: [self-hosted, new]
runs-on: ubuntu-latest needs: [build]
env: services:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} postgres:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} image: postgres
env:
POSTGRES_DB: windmill
POSTGRES_USER: admin
POSTGRES_PASSWORD: changeme
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: "Docker"
run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
id: docker-container
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 16
- uses: shrink/actions-docker-extract@v2 - name: "Playwright run"
id: extract timeout-minutes: 2
with: run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest - name: "Clean up"
path: "/static_frontend/." run: docker kill ${{ steps.docker-container.outputs.id }}
if: always()
- uses: reggionick/s3-deploy@v3
with:
folder: ${{ steps.extract.outputs.destination }}
bucket: windmill-frontend
bucket-region: us-east-1
publish_privately_heavy: publish_privately_heavy:
needs: [build_ee] needs: [build_ee]
runs-on: [self-hosted, new] runs-on: [self-hosted, new]
@@ -202,7 +182,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push privately - name: Build and push privately
uses: docker/build-push-action@v4 uses: docker/build-push-action@v3
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
with: with:
context: . context: .
@@ -211,12 +191,8 @@ jobs:
tags: | tags: |
${{ steps.meta-heavy.outputs.tags }} ${{ steps.meta-heavy.outputs.tags }}
labels: ${{ steps.meta-heavy.outputs.labels }} labels: ${{ steps.meta-heavy.outputs.labels }}
cache-from: cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-heavy:buildcache
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-heavy:buildcache,mode=max
}}-heavy:buildcache
cache-to:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-heavy:buildcache,mode=max
publish_privately_helm: publish_privately_helm:
runs-on: [self-hosted, new] runs-on: [self-hosted, new]
@@ -228,6 +204,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Login to registry - name: Login to registry
uses: docker/login-action@v2 uses: docker/login-action@v2
@@ -243,9 +220,9 @@ jobs:
registry: ${{ env.ECR_REGISTRY }} registry: ${{ env.ECR_REGISTRY }}
username: ${{ secrets.AWS_ACCESS_KEY_ID }} username: ${{ secrets.AWS_ACCESS_KEY_ID }}
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Build and push privately - name: Build and push privately
uses: docker/build-push-action@v4 uses: docker/build-push-action@v3
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
with: with:
context: . context: .
@@ -253,9 +230,5 @@ jobs:
file: ./docker/DockerfileHelm file: ./docker/DockerfileHelm
tags: | tags: |
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:helm ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:helm
cache-from: cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-helm:buildcache
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-helm:buildcache,mode=max
}}-helm:buildcache
cache-to:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-helm:buildcache,mode=max

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v3
- name: generate_go - name: generate_go
run: | run: |
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0 go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0

View File

@@ -65,7 +65,7 @@ jobs:
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Build and push publicly - name: Build and push publicly
uses: docker/build-push-action@v4 uses: docker/build-push-action@v3
with: with:
context: "{{defaultContext}}:lsp" context: "{{defaultContext}}:lsp"
push: true push: true

View File

@@ -25,12 +25,12 @@ jobs:
run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata - name: Docker metadata
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v3
with: with:
images: registry.uffizzi.com/${{ env.UUID_TAG_APP }} images: registry.uffizzi.com/${{ env.UUID_TAG_APP }}
tags: type=raw,value=60d tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry - name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v4 uses: docker/build-push-action@v2
with: with:
push: true push: true
context: ./ context: ./

View File

@@ -1,609 +1,6 @@
# Changelog # Changelog
## [1.87.0](https://github.com/windmill-labs/windmill/compare/v1.86.0...v2.0.0) (2023-04-11)
### ⚠ BREAKING CHANGES
* **frontend:** Add option to return file names ([#1380](https://github.com/windmill-labs/windmill/issues/1380))
### Features
* **backend:** add instance events webhook ([f2d3c82](https://github.com/windmill-labs/windmill/commit/f2d3c8208b6daa49f304f355752145de47138a3c))
* **backend:** extend cached resolution for go ([dac61d1](https://github.com/windmill-labs/windmill/commit/dac61d1c982576d7589e16ab01c8cc8bad6e1686))
* **backend:** Redis based queue ([#1324](https://github.com/windmill-labs/windmill/issues/1324)) ([d45e6c9](https://github.com/windmill-labs/windmill/commit/d45e6c94abed609357b18d4daa7de6b2ea0ba978))
* **frontend:** Add option to return file names ([#1380](https://github.com/windmill-labs/windmill/issues/1380)) ([3dabac1](https://github.com/windmill-labs/windmill/commit/3dabac153f302f48210d15ebaec514e72717300f))
* **python:** cache dependency resolution ([facb670](https://github.com/windmill-labs/windmill/commit/facb67093ce7d3b0874d0d559fb272ed822ce360))
### Bug Fixes
* **backend:** nested deno relative imports ([955a213](https://github.com/windmill-labs/windmill/commit/955a213a504c1f3b8811c930823e87fe7dba101a))
* **cli:** overwrite archived scripts ([1f705ca](https://github.com/windmill-labs/windmill/commit/1f705cab2ce8c79829f22fc6af9e06ecba7450b1))
* **frontend:** Add missing stopPropagation ([#1394](https://github.com/windmill-labs/windmill/issues/1394)) ([58d4b55](https://github.com/windmill-labs/windmill/commit/58d4b556ebbd76c6f07f1a16d601a9d824b99f7e))
* **frontend:** fix app init issue ([d0e0e1f](https://github.com/windmill-labs/windmill/commit/d0e0e1fdf27d9a7fb86c66e43398786b64d8b6b7))
* **frontend:** Fix frontend dependencies ([#1379](https://github.com/windmill-labs/windmill/issues/1379)) ([8e9c491](https://github.com/windmill-labs/windmill/commit/8e9c49165060a4a7f831b8be075593f89d867784))
* **frontend:** Fix icon picker input ([#1389](https://github.com/windmill-labs/windmill/issues/1389)) ([8a44f8e](https://github.com/windmill-labs/windmill/commit/8a44f8e7796f13698e2a99af9f3772f5e676604b))
* **frontend:** Fix mac shortcuts ([#1381](https://github.com/windmill-labs/windmill/issues/1381)) ([41831d5](https://github.com/windmill-labs/windmill/commit/41831d58ed593bb283600b76170f6e76783e0eae))
* **frontend:** fix popover configuration to avoid content shift ([#1377](https://github.com/windmill-labs/windmill/issues/1377)) ([2031e1e](https://github.com/windmill-labs/windmill/commit/2031e1ebd0dc020da104ee84a0294c86babcefaf))
* **frontend:** remove stopPropagation that was preventing components dnd ([#1378](https://github.com/windmill-labs/windmill/issues/1378)) ([de8dc1e](https://github.com/windmill-labs/windmill/commit/de8dc1e9cd7beea2ce62656e9e7676214f77a110))
### Performance Improvements
* parallelize more operations for deno jobs ([e911869](https://github.com/windmill-labs/windmill/commit/e911869d990956463834ac9ff35c52ba8236e362))
## [1.86.0](https://github.com/windmill-labs/windmill/compare/v1.85.0...v1.86.0) (2023-04-08)
### Features
* **backend:** add /ready endpoint for workers ([94eecea](https://github.com/windmill-labs/windmill/commit/94eecea02b6295ad5674db4b010bf6ab7984fa17))
* **backend:** add GET endpoint to trigger scripts ([15c75d9](https://github.com/windmill-labs/windmill/commit/15c75d9d00a69ae97123ed371b9657e298345bdb))
* **backend:** lowercase all emails in relevant endpoints ([#1361](https://github.com/windmill-labs/windmill/issues/1361)) ([7f9050b](https://github.com/windmill-labs/windmill/commit/7f9050b285cf8f7f6baf05452b673f58988c452c))
* **cli:** add getFullResource ([3a232db](https://github.com/windmill-labs/windmill/commit/3a232dbb5792c28b26747e1ba260fffcdd4a8416))
* do cache bucket syncing in background + check tar before pushing it ([#1360](https://github.com/windmill-labs/windmill/issues/1360)) ([3e5ff86](https://github.com/windmill-labs/windmill/commit/3e5ff8682a298ba9e59b2662c4c04c5698447204))
* **frontend:** add flow expand button ([34a8b01](https://github.com/windmill-labs/windmill/commit/34a8b01b762c0b210d76101e7da7bd2397258e8d))
* **frontend:** add impersonate api + local resolution of import by lsp v0 ([7675f08](https://github.com/windmill-labs/windmill/commit/7675f08b7bfe319e496a86a7ef1ab7cc8c1d12d2))
* **frontend:** add workspace to ctx ([8f7a11b](https://github.com/windmill-labs/windmill/commit/8f7a11b8964e2c3405ce3689f9cf2298f9e71c75))
* **frontend:** Improve login + toasts ([#1363](https://github.com/windmill-labs/windmill/issues/1363)) ([92be102](https://github.com/windmill-labs/windmill/commit/92be102a070b1f17b9d3e40524cd21b54301b5a7))
* **frontend:** make script editor a single page ([b84be60](https://github.com/windmill-labs/windmill/commit/b84be60c53ca1ef65826123f39099d33c1f549c0))
* **frontend:** Tone down text + display whole text ([#1366](https://github.com/windmill-labs/windmill/issues/1366)) ([f214d5f](https://github.com/windmill-labs/windmill/commit/f214d5f96b6ac26cd3ef90a6ab696a6dfe02b3f0))
* improved cron/schedule editor ([#1362](https://github.com/windmill-labs/windmill/issues/1362)) ([17176bb](https://github.com/windmill-labs/windmill/commit/17176bb8d112b35228ce9183f4b2f81abe9e5b6e))
### Bug Fixes
* **backend:** allow cors ([8a594a8](https://github.com/windmill-labs/windmill/commit/8a594a89adba9915508884f900f58c4ab53cdfec))
* **backend:** allow longer name/company ([eff61bb](https://github.com/windmill-labs/windmill/commit/eff61bb8d3496bc1c5be4b1051f99ed4470a47ff))
* **backend:** always flush bash output ([517b2c9](https://github.com/windmill-labs/windmill/commit/517b2c9cca54628c8ee692d65c05bc2513eaaf22))
* **backend:** always flush bash output ([7a9091f](https://github.com/windmill-labs/windmill/commit/7a9091fed6aa99201b75bab88d4faddbe041eee4))
* **backend:** inline script app python fix ([8c72722](https://github.com/windmill-labs/windmill/commit/8c72722710db8e3720b01180b504cbc66e79f5ca))
* **frontend:** Add FlowGraph display on Safari ([#1351](https://github.com/windmill-labs/windmill/issues/1351)) ([2819b09](https://github.com/windmill-labs/windmill/commit/2819b09ce5011a467e994ee8b1f09cf33145003d))
* **frontend:** Fix button poppup ([#1368](https://github.com/windmill-labs/windmill/issues/1368)) ([a344928](https://github.com/windmill-labs/windmill/commit/a344928f251d697f53e40c517b0b86bd90e0ad52))
* **frontend:** Fix connected property ([#1371](https://github.com/windmill-labs/windmill/issues/1371)) ([4af39f0](https://github.com/windmill-labs/windmill/commit/4af39f081bf3d07aaade39e5a5a221741fe8f973))
* **frontend:** Fix flow templateEditor ([#1367](https://github.com/windmill-labs/windmill/issues/1367)) ([51fc436](https://github.com/windmill-labs/windmill/commit/51fc436456104c2d6a3cd6f6d62f08929e40d450))
* **frontend:** make croninput a builder rather than a tab ([266b5b0](https://github.com/windmill-labs/windmill/commit/266b5b00da3bd7643eaa5dba1b8c1456f11c5e30))
* **frontend:** Minor fixes ([#1374](https://github.com/windmill-labs/windmill/issues/1374)) ([76a2a1d](https://github.com/windmill-labs/windmill/commit/76a2a1db363facbaf9a0e9618f169d6cc66e946f))
* no need to map internal ports to hosts ([#1365](https://github.com/windmill-labs/windmill/issues/1365)) ([4ec035b](https://github.com/windmill-labs/windmill/commit/4ec035b09a58f8859bc576b03c24cc73f335f32d))
## [1.85.0](https://github.com/windmill-labs/windmill/compare/v1.84.1...v1.85.0) (2023-04-03)
### Features
* add local cache for folder path used + invalidate cache on folder creation ([018b051](https://github.com/windmill-labs/windmill/commit/018b051781e3f40b9d1da8ccdd5edb1cd49877ba))
* **frontend:** add agGrid api hooks + ready ([de1e294](https://github.com/windmill-labs/windmill/commit/de1e29492c9aefdfc59f605ba81f7c51a96bf2f3))
* **frontend:** Add ID renaming popup ([#1344](https://github.com/windmill-labs/windmill/issues/1344)) ([0b8a08c](https://github.com/windmill-labs/windmill/commit/0b8a08cb49644da7c354c3631751e925ac5353b9))
### Bug Fixes
* **backend:** improve handling subflow with many depth using tailrec ([8c53598](https://github.com/windmill-labs/windmill/commit/8c53598aba3fb89f4174d1c0ab3912096ac07c96))
* **backend:** improve subflow processing ([390a988](https://github.com/windmill-labs/windmill/commit/390a988d4c96256a4fbd6a9302fc47a5648c2c43))
* **frontend:** PDF reader header positioning ([#1350](https://github.com/windmill-labs/windmill/issues/1350)) ([daf8276](https://github.com/windmill-labs/windmill/commit/daf827666b13917f8c9abeab5bb2b072bd0fef0b))
## [1.84.1](https://github.com/windmill-labs/windmill/compare/v1.84.0...v1.84.1) (2023-03-31)
### Bug Fixes
* **cli:** overwrite instead of smart diff ([b6d5eef](https://github.com/windmill-labs/windmill/commit/b6d5eef5479e38cc36af2db67d4c45f78c622b9a))
## [1.84.0](https://github.com/windmill-labs/windmill/compare/v1.83.1...v1.84.0) (2023-03-31)
### Features
* add force cancel ([fbe5c18](https://github.com/windmill-labs/windmill/commit/fbe5c18da02763371e6f32c898b31a6a29984b45))
* add the ability to edit previous versions ([2368da2](https://github.com/windmill-labs/windmill/commit/2368da214660ff1835b49b4c2c87256c9bd565cf))
* **backend:** reduce memory allocation for big forloops of flows ([c7506e4](https://github.com/windmill-labs/windmill/commit/c7506e4daec5b12bf908e6954bf6f3521a97b3ba))
* **frontend:** App component style input grouping ([#1334](https://github.com/windmill-labs/windmill/issues/1334)) ([01564f0](https://github.com/windmill-labs/windmill/commit/01564f0a1c26ee9f065bb0adeb7d5e8df0b2e5b5))
* **frontend:** Display frontend execution result in Debug Runs ([#1341](https://github.com/windmill-labs/windmill/issues/1341)) ([57f8dd9](https://github.com/windmill-labs/windmill/commit/57f8dd9570577a58fe91d93c7a9d1a9b4dc69598))
* **frontend:** improve input connection UI ([#1333](https://github.com/windmill-labs/windmill/issues/1333)) ([5ac646e](https://github.com/windmill-labs/windmill/commit/5ac646e859a07efb65542aae9365aa7791ce1097))
### Bug Fixes
* **backend:** add a refresh button to workspace script/hub ([bb61cef](https://github.com/windmill-labs/windmill/commit/bb61cef0e56bf7fa7f8a5f91dabd590afd5db791))
* **backend:** backend compatability on macos ([#1340](https://github.com/windmill-labs/windmill/issues/1340)) ([dfd2abc](https://github.com/windmill-labs/windmill/commit/dfd2abc76466cddca98f93fd82be91ba5d3076e0))
* **frontend:** Export python code as string ([#1339](https://github.com/windmill-labs/windmill/issues/1339)) ([2779891](https://github.com/windmill-labs/windmill/commit/277989141100b033b26b496b8a55d97d48cf7e81))
* **frontend:** improve app tables ([cd1f9b6](https://github.com/windmill-labs/windmill/commit/cd1f9b6baa0dadfb14fee3a586a4b6b164e5e402))
* **frontend:** improve loading of big args in job details ([71619ac](https://github.com/windmill-labs/windmill/commit/71619acdfac010822c1eac496a6f3f869e6ca6fb))
* **frontend:** improve loading of big jobs in run form ([b325493](https://github.com/windmill-labs/windmill/commit/b3254938fe58d8c00a0c4347e7ef519e3a6e4031))
## [1.83.1](https://github.com/windmill-labs/windmill/compare/v1.83.0...v1.83.1) (2023-03-28)
### Bug Fixes
* **cli:** plain secrets might be undefined ([569a55e](https://github.com/windmill-labs/windmill/commit/569a55e45b34641b0fb4569387166f3aa89ce35f))
## [1.83.0](https://github.com/windmill-labs/windmill/compare/v1.82.0...v1.83.0) (2023-03-28)
### Features
* **backend:** allow relative imports for python ([a5500ea](https://github.com/windmill-labs/windmill/commit/a5500ea40a77b2e0408e2a644190a8f65b18cd1d))
* **backend:** execute /bin/bash instead of /bin/sh for bash scripts ([021fa23](https://github.com/windmill-labs/windmill/commit/021fa23f9ffcd11548977a4589eb9bc2815243cf))
* **backend:** improve relative importsfor deno ([eaac598](https://github.com/windmill-labs/windmill/commit/eaac598af308cedea8f0f8fc7c189a4640b4366b))
* **backend:** increase timeout for premium workspace ([00b70d9](https://github.com/windmill-labs/windmill/commit/00b70d9aaac8ae979782492d7754060a3c2c9567))
* **frontend:** add pagination ([33c07d3](https://github.com/windmill-labs/windmill/commit/33c07d3e63f96673719ecb15e45f4cd9e18be80e))
* **frontend:** Add quick style settings to app editor ([#1308](https://github.com/windmill-labs/windmill/issues/1308)) ([ac24862](https://github.com/windmill-labs/windmill/commit/ac2486219cd91df3a7fe11d37894797a881cac6c))
* **frontend:** add recompute as a primitive ([449d3ae](https://github.com/windmill-labs/windmill/commit/449d3ae5ddeceef3fbcb7a815a4dba16c9639fb3))
* **frontend:** add textareacomponent + fix multiselect style + select multi components ([2b31653](https://github.com/windmill-labs/windmill/commit/2b31653a8aa06807678e8609cfa62cf0f2f55dce))
* **frontend:** multiselect components for apps ([577dec5](https://github.com/windmill-labs/windmill/commit/577dec5c5733cdf88e8586ce6c27159920c69c8a))
* **frontend:** use rich json editor for arrays of objects and for object in ArgInput ([b95afaa](https://github.com/windmill-labs/windmill/commit/b95afaa9bb41b102181657453a564f44f4511983))
### Bug Fixes
* **apps:** improve app table actionButtons behavior under many clicks ([8e3d8ac](https://github.com/windmill-labs/windmill/commit/8e3d8acc80de971ee115d6903d24864d8263f08b))
* **cli:** add --plain-secrets ([98d51e2](https://github.com/windmill-labs/windmill/commit/98d51e219df1680507114f9b57ec0b0a4a234b5c))
* **frontend:** add a modal that is always mounted to make sure compon… ([#1328](https://github.com/windmill-labs/windmill/issues/1328)) ([a527cb8](https://github.com/windmill-labs/windmill/commit/a527cb8222a2ff80dae38ebae7dc5ea0979d74c5))
* **frontend:** Disable app keyboard navigation on focused inputs ([#1326](https://github.com/windmill-labs/windmill/issues/1326)) ([da24e9a](https://github.com/windmill-labs/windmill/commit/da24e9ab0625a7503c498c179022ea4011a03170))
* **frontend:** persist description for schemas ([1a48673](https://github.com/windmill-labs/windmill/commit/1a4867302f72aaae8f422ac8f53812c116cc383d))
* **frontend:** Revert app upload input ([#1330](https://github.com/windmill-labs/windmill/issues/1330)) ([fa457bb](https://github.com/windmill-labs/windmill/commit/fa457bb7099bd31c2315eaf7f7f2c40900b2ae39))
* **frontend:** Small app fixes ([#1331](https://github.com/windmill-labs/windmill/issues/1331)) ([75306c8](https://github.com/windmill-labs/windmill/commit/75306c831616d9a01cc3a4681732aab93153f1a9))
## [1.82.0](https://github.com/windmill-labs/windmill/compare/v1.81.0...v1.82.0) (2023-03-24)
### Features
* **backend:** introduce RESTART_ZOMBIE_JOBS and ZOMBIE_JOB_TIMEOUT ([47a7f71](https://github.com/windmill-labs/windmill/commit/47a7f7163aae3fe807e766c824085b4d1b75c8c8))
### Bug Fixes
* **backend:** do not consider FlowPreview as potential zombie job ([f7c30b5](https://github.com/windmill-labs/windmill/commit/f7c30b5d2f16e15f36208e07126557fd7ed84801))
* **backend:** increase dynamic js timeout + improve client passing ([34e25f0](https://github.com/windmill-labs/windmill/commit/34e25f0f96fe637cc42f4017a064c40def5d67ef))
* **cli:** improve diff speed + fix replacing cli ([b999c98](https://github.com/windmill-labs/windmill/commit/b999c9894b4011b735f37df485fe403c22c00512))
* **frontend:** Fix AppTable error display + clear errors when removing a component + properly detect that latest component run had an error ([#1322](https://github.com/windmill-labs/windmill/issues/1322)) ([c15bc8a](https://github.com/windmill-labs/windmill/commit/c15bc8a7bfb3bef2634e6093088967137cd06239))
* **frontend:** fix refresh with manual dependencies ([#1319](https://github.com/windmill-labs/windmill/issues/1319)) ([a47031a](https://github.com/windmill-labs/windmill/commit/a47031a41e6a3392101e280dcd1aea098f898447))
* **frontend:** fix settings panel ([#1323](https://github.com/windmill-labs/windmill/issues/1323)) ([30b8e47](https://github.com/windmill-labs/windmill/commit/30b8e474df5b71b7e7b36d3fe5974a289cf0dfae))
* **frontend:** Fix transformer ([#1321](https://github.com/windmill-labs/windmill/issues/1321)) ([addabcc](https://github.com/windmill-labs/windmill/commit/addabcceb0c90782ba4a934bb3822f8cc9865069))
* **frontend:** remove unnecessary div ([#1318](https://github.com/windmill-labs/windmill/issues/1318)) ([e193a0b](https://github.com/windmill-labs/windmill/commit/e193a0bcdf6690b007594d2f1325a7ec26603129))
## [1.81.0](https://github.com/windmill-labs/windmill/compare/v1.80.1...v1.81.0) (2023-03-21)
### Features
* **apps:** add action on form/button/formbutton ([2593218](https://github.com/windmill-labs/windmill/commit/2593218cbf07c05521a270797055ddb22dc22b8d))
### Bug Fixes
* **frontend:** Remove action outline on preview mode ([#1313](https://github.com/windmill-labs/windmill/issues/1313)) ([a7c4f1a](https://github.com/windmill-labs/windmill/commit/a7c4f1a12e02e8627a5955b75d572e9cf11d8122))
## [1.80.1](https://github.com/windmill-labs/windmill/compare/v1.80.0...v1.80.1) (2023-03-21)
### Bug Fixes
* **cli:** add support for non metadataed scripts ([42f6d2e](https://github.com/windmill-labs/windmill/commit/42f6d2e0ee6294f8a1d97f5f62f2adb6edfd2fed))
## [1.80.0](https://github.com/windmill-labs/windmill/compare/v1.79.0...v1.80.0) (2023-03-20)
### Features
* **apps:** add transformers for data sources ([0abacac](https://github.com/windmill-labs/windmill/commit/0abacac06c7dd586b48c66ff47b7589fe692205b))
* **frontend:** App set tab ([#1307](https://github.com/windmill-labs/windmill/issues/1307)) ([48413a7](https://github.com/windmill-labs/windmill/commit/48413a78c5e7e0ee8208711f15135d81136b7386))
### Bug Fixes
* **frontend:** add missing optional chaining ([#1306](https://github.com/windmill-labs/windmill/issues/1306)) ([29b1cc6](https://github.com/windmill-labs/windmill/commit/29b1cc6ff0ebc5edcad24a1780113889c507075d))
* **frontend:** App button triggered by ([#1304](https://github.com/windmill-labs/windmill/issues/1304)) ([cf2d031](https://github.com/windmill-labs/windmill/commit/cf2d031e8e89faa2cd7fa58436cbe7cf4d9045f9))
## [1.79.0](https://github.com/windmill-labs/windmill/compare/v1.78.0...v1.79.0) (2023-03-17)
### Features
* **frontend:** add listeners for frontend scripts ([597e38e](https://github.com/windmill-labs/windmill/commit/597e38ef367d38fa97fc443ccb2c721e5964fece))
* **frontend:** add table actions navigation ([#1298](https://github.com/windmill-labs/windmill/issues/1298)) ([c3ba1a6](https://github.com/windmill-labs/windmill/commit/c3ba1a6ab97484a08a5a20187bb858a5af7025cb))
* **frontend:** App component triggers ([#1303](https://github.com/windmill-labs/windmill/issues/1303)) ([078cb1b](https://github.com/windmill-labs/windmill/commit/078cb1bf3e4de08cb018578f04d24392a6462f69))
* **frontend:** Component control ([#1293](https://github.com/windmill-labs/windmill/issues/1293)) ([bd927a2](https://github.com/windmill-labs/windmill/commit/bd927a27ed9581dbf67ea3694f9d989f8d71d2ed))
### Bug Fixes
* **frontend:** App panel styling ([#1284](https://github.com/windmill-labs/windmill/issues/1284)) ([c1dd35c](https://github.com/windmill-labs/windmill/commit/c1dd35c3f0fcbc1be43273f82a873c3c07863417))
* **frontend:** Display app context search on top ([#1300](https://github.com/windmill-labs/windmill/issues/1300)) ([bd3ee81](https://github.com/windmill-labs/windmill/commit/bd3ee81b14846f16ccd16461de99b46fe68be6ba))
* **frontend:** fix horizontal splitpanes ([#1301](https://github.com/windmill-labs/windmill/issues/1301)) ([ea3dab4](https://github.com/windmill-labs/windmill/commit/ea3dab411b3d5dd772e04c8831e789e2470aaf28))
* **frontend:** fix map render ([#1297](https://github.com/windmill-labs/windmill/issues/1297)) ([0092721](https://github.com/windmill-labs/windmill/commit/00927210fd68c31cb793ef4f0efea05711ebcf00))
* **frontend:** Hide archive toggle with empty list ([#1296](https://github.com/windmill-labs/windmill/issues/1296)) ([bac831b](https://github.com/windmill-labs/windmill/commit/bac831b23ce85a683ddbd4537900670a0b7d12a8))
## [1.78.0](https://github.com/windmill-labs/windmill/compare/v1.77.0...v1.78.0) (2023-03-16)
### Features
* **frontend:** app textcomponent editable + tooltip ([11567d6](https://github.com/windmill-labs/windmill/commit/11567d6280ea60f1a8c3c6607c724179775cbbe3))
### Bug Fixes
* **backend:** whitelist for include_header was ignored in some cases ([183a459](https://github.com/windmill-labs/windmill/commit/183a4591df700ab4720de6e92a83631256940089))
* **frontend:** improve rendering performance after component moving ([6f890f2](https://github.com/windmill-labs/windmill/commit/6f890f2120885f90d986fbd655096b45bf9de539))
* **frontend:** remove staticOutputs from apps ([dbdfd62](https://github.com/windmill-labs/windmill/commit/dbdfd626386398180ecba7976714f86365eeccd8))
## [1.77.0](https://github.com/windmill-labs/windmill/compare/v1.76.0...v1.77.0) (2023-03-14)
### Features
* **apps:** state can be used as input in apps ([2f0acb9](https://github.com/windmill-labs/windmill/commit/2f0acb9ffa8dace4a886527dcee49809d019b271))
* **apps:** tabs can be made pages or invisible + better frontend scripts reactivity ([cd645d0](https://github.com/windmill-labs/windmill/commit/cd645d0935f2d06e0ff71f14d2cf63accd378ff3))
* **deno:** add support for custom npm repo ([#1291](https://github.com/windmill-labs/windmill/issues/1291)) ([944795f](https://github.com/windmill-labs/windmill/commit/944795f6eeaa7d01ab1a35a80570a55c363723e6))
* **frontend:** add setTab to frontend scripts ([c2a97c5](https://github.com/windmill-labs/windmill/commit/c2a97c53cfff0fdb35dd8bc249490566eebdc1a9))
* **frontend:** app components output panel ([#1283](https://github.com/windmill-labs/windmill/issues/1283)) ([751edcf](https://github.com/windmill-labs/windmill/commit/751edcf9b8e0976a1d073603c9eff5dc6e714490))
### Bug Fixes
* **backend:** do not cache reference to workspace scripts ([eb73f2a](https://github.com/windmill-labs/windmill/commit/eb73f2a687f6faad301b9038ab8585450bec7481))
* **frontend:** fix app tabs ([#1288](https://github.com/windmill-labs/windmill/issues/1288)) ([c71a577](https://github.com/windmill-labs/windmill/commit/c71a577fead90c9cd01a736b54d859ec4f0b7807))
* **frontend:** fix container deletion ([#1287](https://github.com/windmill-labs/windmill/issues/1287)) ([bc870bd](https://github.com/windmill-labs/windmill/commit/bc870bd03eb76cb8bc0e0c861f6cd8a9c661186b))
* **frontend:** Update setting accordion ([#1285](https://github.com/windmill-labs/windmill/issues/1285)) ([dea12e8](https://github.com/windmill-labs/windmill/commit/dea12e8870ece998bb6607723cbaab9b9a958f22))
## [1.76.0](https://github.com/windmill-labs/windmill/compare/v1.75.0...v1.76.0) (2023-03-13)
### Features
* **frontend:** add frontend (JS) scripts to apps ([f0b1b1f](https://github.com/windmill-labs/windmill/commit/f0b1b1f752731ba434b960a75624118152f53c00))
* **frontend:** Copy, Cut and Paste ([#1279](https://github.com/windmill-labs/windmill/issues/1279)) ([82c139e](https://github.com/windmill-labs/windmill/commit/82c139ed0992be401e250cfb7ecc0fca61b76772))
* **frontend:** disabled for action buttons can now depend on row ([75f87e7](https://github.com/windmill-labs/windmill/commit/75f87e7e1117a9c12afcf626379e94b134a9a493))
* **frontend:** improve drag-n-drop behavior ([cfd489a](https://github.com/windmill-labs/windmill/commit/cfd489a55059e7b6843f99bab261c70b3852e6a2))
### Bug Fixes
* **backend:** improve worker ping api ([c958480](https://github.com/windmill-labs/windmill/commit/c958480ce83844a989f58dd5a70eb288582e2194))
* **frontend:** General fixes and updates ([#1281](https://github.com/windmill-labs/windmill/issues/1281)) ([3e5a179](https://github.com/windmill-labs/windmill/commit/3e5a179eb8cd8001f49c92305141dade1571e20f))
## [1.75.0](https://github.com/windmill-labs/windmill/compare/v1.74.2...v1.75.0) (2023-03-11)
### Features
* add filter jobs by args or result ([3b44f9a](https://github.com/windmill-labs/windmill/commit/3b44f9a72ca0466a44963a4b9657a0ee59b44753))
* **apps:** add resource picker ([8681e83](https://github.com/windmill-labs/windmill/commit/8681e83b574141acbf7e5a389a9e8a4f340336d1))
* **bash:** add default argument handling for bash ([1d5c194](https://github.com/windmill-labs/windmill/commit/1d5c194f09ffba963d52e418c5954843d84ae337))
* **frontend-apps:** add variable picker for static string input on apps ([bc440f8](https://github.com/windmill-labs/windmill/commit/bc440f8d4154ce464c0e027d93b7a0a3b76d782e))
* **frontend:** make runs filters synced with query args ([61a5e1f](https://github.com/windmill-labs/windmill/commit/61a5e1f1accc988628b785b3b9be04c4ea719874))
### Bug Fixes
* **backend:** add killpill for lines reading ([7c825c2](https://github.com/windmill-labs/windmill/commit/7c825c212dd0f1e8be427eabd9a9756303241d1b))
* **cli:** many small fixes ([ce32370](https://github.com/windmill-labs/windmill/commit/ce323709a94d27fb24214719180ea1aafc66d646))
## [1.74.2](https://github.com/windmill-labs/windmill/compare/v1.74.1...v1.74.2) (2023-03-09)
### Bug Fixes
* **frontend:** fix splitpanes navigation ([#1276](https://github.com/windmill-labs/windmill/issues/1276)) ([8d5c5b8](https://github.com/windmill-labs/windmill/commit/8d5c5b88a35d7a3bad1d8ddf2d940026825241eb))
## [1.74.1](https://github.com/windmill-labs/windmill/compare/v1.74.0...v1.74.1) (2023-03-09)
### Bug Fixes
* **apps:** proper reactivity for non rendered static components ([ae53baf](https://github.com/windmill-labs/windmill/commit/ae53bafaf6777f928113f84b2c6ed6a2ed341844))
* **ci:** make windmill compile again by pinning swc deps ([2ea15d5](https://github.com/windmill-labs/windmill/commit/2ea15d5035e5e15473968db3c0501a4dddff5cd0))
## [1.74.0](https://github.com/windmill-labs/windmill/compare/v1.73.1...v1.74.0) (2023-03-09)
### Features
* add delete by path for scripts ([0c2cf92](https://github.com/windmill-labs/windmill/commit/0c2cf92dd3df9610e649f15e23921a4ca0d94e6a))
* **frontend:** Add color picker input to app ([#1270](https://github.com/windmill-labs/windmill/issues/1270)) ([88e537a](https://github.com/windmill-labs/windmill/commit/88e537ad1fb4c207f38fbe951c82106bef6491a3))
* **frontend:** add expand ([#1268](https://github.com/windmill-labs/windmill/issues/1268)) ([b854ee3](https://github.com/windmill-labs/windmill/commit/b854ee34393534bde104e2e6f606108fd66d38dc))
* **frontend:** add hash to ctx in apps ([b1a45b1](https://github.com/windmill-labs/windmill/commit/b1a45b1e708aa6f19f8be9c949507083e044f2d8))
* **frontend:** Add key navigation in app editor ([#1273](https://github.com/windmill-labs/windmill/issues/1273)) ([6b0fb75](https://github.com/windmill-labs/windmill/commit/6b0fb75d23e2151c88b07814139d203c1bd0578d))
### Bug Fixes
* **cli:** improve visibility of the active workspace ([e6344da](https://github.com/windmill-labs/windmill/commit/e6344dac6d1be04b46231fa8ef8579fd12ca8f37))
* **frontend:** add confirmation modal to delete script/flow/app ([a4adcb5](https://github.com/windmill-labs/windmill/commit/a4adcb5192c11f7bf47a0d259825e474779378d7))
* **frontend:** Clean up app editor ([#1267](https://github.com/windmill-labs/windmill/issues/1267)) ([0a5e181](https://github.com/windmill-labs/windmill/commit/0a5e181a3aa966fb8211bee0d9174fc16353b31f))
* **frontend:** Minor changes ([#1272](https://github.com/windmill-labs/windmill/issues/1272)) ([3b6ae0c](https://github.com/windmill-labs/windmill/commit/3b6ae0cc49461b858d9cfff79eae9a7569465235))
* **frontend:** simplify input bindings ([b2de531](https://github.com/windmill-labs/windmill/commit/b2de531a46e4b120d7106d361b727746bec516dd))
## [1.73.1](https://github.com/windmill-labs/windmill/compare/v1.73.0...v1.73.1) (2023-03-07)
### Bug Fixes
* **frontend:** load flow is not initialized ([719d475](https://github.com/windmill-labs/windmill/commit/719d4752621d462b1cfaa0d27930fba7586be779))
## [1.73.0](https://github.com/windmill-labs/windmill/compare/v1.72.0...v1.73.0) (2023-03-07)
### Features
* **frontend:** add a way to automatically resize ([#1259](https://github.com/windmill-labs/windmill/issues/1259)) ([24f58ef](https://github.com/windmill-labs/windmill/commit/24f58efd9994a2201c1b1d9bbfb11734c57068e3))
* **frontend:** add ability to move nodes ([614fb50](https://github.com/windmill-labs/windmill/commit/614fb5022aa7d5428fb96b7ee3a20794edd1e9d3))
* **frontend:** Add app PDF viewer ([#1254](https://github.com/windmill-labs/windmill/issues/1254)) ([3e5d09e](https://github.com/windmill-labs/windmill/commit/3e5d09ef0b5619186bee5ec6d442cbfd12a6e8d5))
* **frontend:** add fork/save buttons + consistent styling for slider/range ([9e9f8ef](https://github.com/windmill-labs/windmill/commit/9e9f8efb8ee389ea75e99b67ef720756959ca737))
* **frontend:** add history to flows and apps ([9e4d90a](https://github.com/windmill-labs/windmill/commit/9e4d90ad37a57ff1f515eea0c82cf603649e915d))
* **frontend:** Fix object viewer style ([#1255](https://github.com/windmill-labs/windmill/issues/1255)) ([94f1aad](https://github.com/windmill-labs/windmill/commit/94f1aadef2b09ac1962478f11b27cc708b8328f1))
* **frontend:** refactor entire flow builder UX ([2ac51b0](https://github.com/windmill-labs/windmill/commit/2ac51b0af08bdef7ce3c7e874e9983b9fc00478a))
### Bug Fixes
* **frontend:** arginput + apppreview fixes ([e2c4545](https://github.com/windmill-labs/windmill/commit/e2c45452401022b00285b21551ffaf35a114be33))
* **frontend:** fix app map reactivity ([#1260](https://github.com/windmill-labs/windmill/issues/1260)) ([2557e13](https://github.com/windmill-labs/windmill/commit/2557e136bd0df1a023819b7d9b2235e30d7140b6))
* **frontend:** fix branch deletion ([#1261](https://github.com/windmill-labs/windmill/issues/1261)) ([a999eb2](https://github.com/windmill-labs/windmill/commit/a999eb21121a7c0010621448324e0c77caf2b3f6))
* **frontend:** Side menu z-index issue ([#1265](https://github.com/windmill-labs/windmill/issues/1265)) ([c638897](https://github.com/windmill-labs/windmill/commit/c638897fdcd58f55b0929f91641b21a6f9d25ead))
## [1.72.0](https://github.com/windmill-labs/windmill/compare/v1.71.0...v1.72.0) (2023-03-02)
### Features
* **backend:** get_result_by_id do a downward pass to find node at any depth ([#1249](https://github.com/windmill-labs/windmill/issues/1249)) ([4c913dc](https://github.com/windmill-labs/windmill/commit/4c913dc4b6be03571a015c97a13829adffb61479))
* **frontend:** Add app map component ([#1251](https://github.com/windmill-labs/windmill/issues/1251)) ([ed25d9f](https://github.com/windmill-labs/windmill/commit/ed25d9f186d9925f75404cb193a025d8a41c4540))
* **frontend:** app splitpanes ([#1248](https://github.com/windmill-labs/windmill/issues/1248)) ([f4d79ee](https://github.com/windmill-labs/windmill/commit/f4d79ee2633e6cdab0fa2410108b31cfa77e10da))
### Bug Fixes
* **backend:** improve result retrieval ([c4463bb](https://github.com/windmill-labs/windmill/commit/c4463bb029907f3c8d77abb194f872aae7876bf6))
* **backend:** incorrect get_result_by_id for list_result job ([2a75cd2](https://github.com/windmill-labs/windmill/commit/2a75cd250ea5e01849fc8bbb69bf44f147d0acb8))
* **cli:** fix workspace option + run script/flow + whoami ([35ea2b2](https://github.com/windmill-labs/windmill/commit/35ea2b27b12159c68c8507ec1f8686028c975387))
* **frontend:** background script not showing inputs ([55eb48c](https://github.com/windmill-labs/windmill/commit/55eb48c55332431304cedbf3bcbbbcff61ec3645))
* **frontend:** fix table bindings ([2679386](https://github.com/windmill-labs/windmill/commit/2679386bf87a56352269911bd89e52df5ee9f314))
* **frontend:** rework app reactivity ([94b20d2](https://github.com/windmill-labs/windmill/commit/94b20d2f5e3b551974c57ea82b6e3dc16e97b9b8))
* **frontend:** rework app reactivity ([1753cb7](https://github.com/windmill-labs/windmill/commit/1753cb7da658f47be974c15da82c71a8e19309a6))
## [1.71.0](https://github.com/windmill-labs/windmill/compare/v1.70.1...v1.71.0) (2023-02-28)
### Features
* **backend:** use counter for sleep/execution/pull durations ([e568690](https://github.com/windmill-labs/windmill/commit/e56869092a03fec4703ddd9ef65c89edb8122962))
* **cli:** add autocompletions ([287b2db](https://github.com/windmill-labs/windmill/commit/287b2db22f7b56e90bcd0c4727c00096695c2e0d))
* **frontend:** App drawer ([#1246](https://github.com/windmill-labs/windmill/issues/1246)) ([8a0d115](https://github.com/windmill-labs/windmill/commit/8a0d1158c4d7e970cb91e1adf4838e5efdbb39ff))
* **frontend:** drawer for editing workspace scripts in flows ([6adc875](https://github.com/windmill-labs/windmill/commit/6adc87561070d8aceaba1838008cd7e6be2e2660))
### Bug Fixes
* **frontend:** Add more app custom css ([#1229](https://github.com/windmill-labs/windmill/issues/1229)) ([a4e4d18](https://github.com/windmill-labs/windmill/commit/a4e4d188ad10443dd0b7f104389594efc768dc59))
* **frontend:** Add more app custom css ([#1247](https://github.com/windmill-labs/windmill/issues/1247)) ([1bb5ed9](https://github.com/windmill-labs/windmill/commit/1bb5ed9ae01fd7998b06833b6222e5dd5d774d35))
* **frontend:** display currently selected filter even if not in list ([42d1cd6](https://github.com/windmill-labs/windmill/commit/42d1cd6456620ba917c560c87d736dc93634adff))
* **frontend:** Fix deeply nested move ([#1245](https://github.com/windmill-labs/windmill/issues/1245)) ([a67f10e](https://github.com/windmill-labs/windmill/commit/a67f10eeb6fdb44bbb3a510badcc5ad0ae187a2b))
* **frontend:** invisible subgrids have h-0 + app policies fix ([2244e83](https://github.com/windmill-labs/windmill/commit/2244e83b9da803a4cf46ab0825d7cb6cb0e24872))
## [1.70.1](https://github.com/windmill-labs/windmill/compare/v1.70.0...v1.70.1) (2023-02-27)
### Bug Fixes
* **cli:** make cli resilient to systems without openable browsers ([c051ffe](https://github.com/windmill-labs/windmill/commit/c051ffeb42c1cff609f93da7745036ea722e17d4))
* **frontend:** Disable move in nested subgrid ([#1238](https://github.com/windmill-labs/windmill/issues/1238)) ([70eab30](https://github.com/windmill-labs/windmill/commit/70eab303bd45111ae198d9b710bfd6f9f59e53b0))
* **frontend:** Fix inline scripts list ([#1240](https://github.com/windmill-labs/windmill/issues/1240)) ([97602ac](https://github.com/windmill-labs/windmill/commit/97602ac6db1404d36d160a431ffcea6c0f567a48))
* **frontend:** Fix subgrid lock ([#1232](https://github.com/windmill-labs/windmill/issues/1232)) ([8ee9d67](https://github.com/windmill-labs/windmill/commit/8ee9d67f4faa91446338b41c664ef91913eb8b81))
## [1.70.1](https://github.com/windmill-labs/windmill/compare/v1.70.0...v1.70.1) (2023-02-27)
### Bug Fixes
* **cli:** make cli resilient to systems without openable browsers ([c051ffe](https://github.com/windmill-labs/windmill/commit/c051ffeb42c1cff609f93da7745036ea722e17d4))
* **frontend:** Disable move in nested subgrid ([#1238](https://github.com/windmill-labs/windmill/issues/1238)) ([70eab30](https://github.com/windmill-labs/windmill/commit/70eab303bd45111ae198d9b710bfd6f9f59e53b0))
* **frontend:** Fix subgrid lock ([#1232](https://github.com/windmill-labs/windmill/issues/1232)) ([8ee9d67](https://github.com/windmill-labs/windmill/commit/8ee9d67f4faa91446338b41c664ef91913eb8b81))
## [1.70.0](https://github.com/windmill-labs/windmill/compare/v1.69.3...v1.70.0) (2023-02-27)
### Features
* **apps:** add ag grid ([b690d80](https://github.com/windmill-labs/windmill/commit/b690d801d4aa5695ee558e81d1ed114074dfcb83))
* **frontend:** move to other grid ([#1230](https://github.com/windmill-labs/windmill/issues/1230)) ([104e4ac](https://github.com/windmill-labs/windmill/commit/104e4ac5e790c30e6fb6b27726776693038d4f19))
### Bug Fixes
* app setup and sync now uses 1.69.3 ([d38aff2](https://github.com/windmill-labs/windmill/commit/d38aff2fe228f23eb18c3991392928c064e6aca2))
* **frontend:** Fix duplication ([#1237](https://github.com/windmill-labs/windmill/issues/1237)) ([e87f4fc](https://github.com/windmill-labs/windmill/commit/e87f4fc44b847a573f5acafc0348fbcbfcb2258f))
* **frontend:** fix graph viewer id assignment ([e1f686d](https://github.com/windmill-labs/windmill/commit/e1f686d8508cfc1f73c43be08facc44217ca8de0))
## [1.69.3](https://github.com/windmill-labs/windmill/compare/v1.69.2...v1.69.3) (2023-02-24)
### Bug Fixes
* **deno:** fix denoify buffer handling ([c2e5afd](https://github.com/windmill-labs/windmill/commit/c2e5afd4e07fb63375832f308da8c744616ee188))
## [1.69.2](https://github.com/windmill-labs/windmill/compare/v1.69.1...v1.69.2) (2023-02-24)
### Bug Fixes
* **app:** fix all nested behavior ([dd28308](https://github.com/windmill-labs/windmill/commit/dd28308c3cf1877ba3f19dcd2bd20bf1c7896a99))
* **frontend:** delete grid item ([008c30f](https://github.com/windmill-labs/windmill/commit/008c30fcaad64af512407f9889a9881fafac0868))
* **frontend:** duplicate ([483407c](https://github.com/windmill-labs/windmill/commit/483407cdf0e1ed61de180a904934e950fed4adc3))
* **frontend:** Fix findGridItem ([a8295d0](https://github.com/windmill-labs/windmill/commit/a8295d0b5acd08cec42b7939d907df5c25132644))
* **frontend:** Fix findGridItem ([5bb77ed](https://github.com/windmill-labs/windmill/commit/5bb77edf45740a75e969b1bef31580271c9d5505))
* **frontend:** Fix next id ([8ddcf4d](https://github.com/windmill-labs/windmill/commit/8ddcf4d9c1a8d6dd20ee241a3f308811c49e58f1))
* **frontend:** gridtab ([fa105b4](https://github.com/windmill-labs/windmill/commit/fa105b4caeaa2d0e9704a48f6caf8d846839c23e))
* **frontend:** rewrote utils ([ea1b2c2](https://github.com/windmill-labs/windmill/commit/ea1b2c29b95282df347ef9c5973917fa3880e843))
* **frontend:** wip ([33ebe2d](https://github.com/windmill-labs/windmill/commit/33ebe2da8e81476be62a2567d5012573a8a010b6))
## [1.69.1](https://github.com/windmill-labs/windmill/compare/v1.69.0...v1.69.1) (2023-02-24)
### Bug Fixes
* **deno:** remove mysql support waiting for deno fix ([dd7e8c7](https://github.com/windmill-labs/windmill/commit/dd7e8c742c83f6a1d13e4343ca626c0b5efc06fb))
* **deno:** remove mysql support waiting for deno fix ([2f78132](https://github.com/windmill-labs/windmill/commit/2f78132e081bdf3d7468e022f0e981ebfa52cfb3))
* **frontend:** containers and tab fixes v1 ([27cac3f](https://github.com/windmill-labs/windmill/commit/27cac3ffe69c4dac160e9e55ffd1eb8ea348d487))
* **frontend:** containers and tab fixes v1 ([705703a](https://github.com/windmill-labs/windmill/commit/705703a5e2f2dc7ceb4c215221f72bf624799841))
* **frontend:** containers and tab fixes v1 ([fac31c6](https://github.com/windmill-labs/windmill/commit/fac31c6628b289ad6aae92434e312c4be281a4d2))
## [1.69.0](https://github.com/windmill-labs/windmill/compare/v1.68.0...v1.69.0) (2023-02-23)
### Features
* **frontend:** Duplicate component ([#1228](https://github.com/windmill-labs/windmill/issues/1228)) ([089a6b6](https://github.com/windmill-labs/windmill/commit/089a6b6ae52e8d28dd15e2f9a6ad900c5853d0a1))
* **frontend:** Properly delete tab content ([#1227](https://github.com/windmill-labs/windmill/issues/1227)) ([857ee5f](https://github.com/windmill-labs/windmill/commit/857ee5f318466d12bf0d41515451798df087ab74))
* **frontend:** Support deeply nested components ([#1225](https://github.com/windmill-labs/windmill/issues/1225)) ([6ad876e](https://github.com/windmill-labs/windmill/commit/6ad876ebb45a934b7a4dc980cf38a5228d7d11f1))
### Bug Fixes
* **cli:** .wmillignore whitelist behavior ([d543650](https://github.com/windmill-labs/windmill/commit/d543650b313c434e794ad800aefe4aeda83c0fed))
## [1.68.0](https://github.com/windmill-labs/windmill/compare/v1.67.4...v1.68.0) (2023-02-23)
### Features
* **frontend:** Add more app component CSS customisation ([#1218](https://github.com/windmill-labs/windmill/issues/1218)) ([6044e3b](https://github.com/windmill-labs/windmill/commit/6044e3b6ef92e89b8f15f38bc2d0986ec64105d5))
### Bug Fixes
* **cli:** better ergonomics around workspace add ([40c12e6](https://github.com/windmill-labs/windmill/commit/40c12e6139c7b42d7ab169bab2dd37f8b43bea06))
* **cli:** better ergonomics around workspaces ([3b7160e](https://github.com/windmill-labs/windmill/commit/3b7160e84aa454bdb5f343da99cfd97a6b319937))
## [1.67.4](https://github.com/windmill-labs/windmill/compare/v1.67.3...v1.67.4) (2023-02-23)
### Bug Fixes
* **backend:** workflow check for has_failure_module ([e54dc3f](https://github.com/windmill-labs/windmill/commit/e54dc3ff97e4454a15b9efe25cc12f6c9e1e176b))
## [1.67.3](https://github.com/windmill-labs/windmill/compare/v1.67.2...v1.67.3) (2023-02-23)
### Bug Fixes
* **cli:** ignone non wmill looking files ([ec57c59](https://github.com/windmill-labs/windmill/commit/ec57c5977f122b629a07e05bc3551662d518ce30))
## [1.67.2](https://github.com/windmill-labs/windmill/compare/v1.67.1...v1.67.2) (2023-02-23)
### Bug Fixes
* **cli:** ignone non wmill looking files ([969e89f](https://github.com/windmill-labs/windmill/commit/969e89f8bbc10f6712920321b70ede35f19ab9ed))
## [1.67.1](https://github.com/windmill-labs/windmill/compare/v1.67.0...v1.67.1) (2023-02-22)
### Bug Fixes
* **cli:** coloring nits ([3fa24ad](https://github.com/windmill-labs/windmill/commit/3fa24adad0a07ba2f469c545b28251b035efdf90))
## [1.67.0](https://github.com/windmill-labs/windmill/compare/v1.66.1...v1.67.0) (2023-02-22)
### Features
* **frontend:** Add app sub grids ([#1208](https://github.com/windmill-labs/windmill/issues/1208)) ([dbc59e9](https://github.com/windmill-labs/windmill/commit/dbc59e952143ee5813780ad13794cef4e036911c))
### Bug Fixes
* **cli:** add --fail-conflicts to ci push ([0085b46](https://github.com/windmill-labs/windmill/commit/0085b46c1e3b8267fcafcb06ce72b4d820e49df5))
## [1.66.1](https://github.com/windmill-labs/windmill/compare/v1.66.0...v1.66.1) (2023-02-22)
### Bug Fixes
* **cli:** delete workspace instead of archiving them ([70dfc8b](https://github.com/windmill-labs/windmill/commit/70dfc8b8d0293d80da7db14caa1b9eb0ed67653d))
## [1.66.0](https://github.com/windmill-labs/windmill/compare/v1.65.0...v1.66.0) (2023-02-22)
### Features
* add delete flows ([e81f7bd](https://github.com/windmill-labs/windmill/commit/e81f7bd7239b73710da2a4ddec0da7805c13da06))
* CLI refactor v1 ([e31d2ae](https://github.com/windmill-labs/windmill/commit/e31d2ae27f886e774ffc429eea80057f4f9f4213))
* **frontend:** Add image app component ([#1213](https://github.com/windmill-labs/windmill/issues/1213)) ([a4b773a](https://github.com/windmill-labs/windmill/commit/a4b773af294554c5787f02ebda363c8d9a3eff1b))
## [1.65.0](https://github.com/windmill-labs/windmill/compare/v1.64.0...v1.65.0) (2023-02-21)
### Features
* **apps:** add asJson for customcss ([71d6dad](https://github.com/windmill-labs/windmill/commit/71d6dad37cc239952ce7799609c02474b0b1fc81))
* **apps:** add custom css for apps ([7f00e1c](https://github.com/windmill-labs/windmill/commit/7f00e1c1a8f2e905b0677d82ba547f55dc23b3e0))
* **backend:** Zip Workspace Export ([#1201](https://github.com/windmill-labs/windmill/issues/1201)) ([5d109b3](https://github.com/windmill-labs/windmill/commit/5d109b3cd4b7749788f9cb9fcbe1949c45eedf1f))
* **frontend:** Add divider app component ([#1209](https://github.com/windmill-labs/windmill/issues/1209)) ([c33e79e](https://github.com/windmill-labs/windmill/commit/c33e79e0b8d5ba1103d87fdd47fcd0e1071e19de))
* **frontend:** Add file input app component ([#1211](https://github.com/windmill-labs/windmill/issues/1211)) ([d4b6d69](https://github.com/windmill-labs/windmill/commit/d4b6d691264bf21e4e2c97548aaad9aa80678a6b))
* **frontend:** Add icon app component ([#1207](https://github.com/windmill-labs/windmill/issues/1207)) ([e4791c2](https://github.com/windmill-labs/windmill/commit/e4791c2b7e3a0e6b90c37bc1200f9cd0ab3b6845))
## [1.64.0](https://github.com/windmill-labs/windmill/compare/v1.63.2...v1.64.0) (2023-02-16)
### Features
* **frontend:** Trigger settings drawer with URL hash ([#1185](https://github.com/windmill-labs/windmill/issues/1185)) ([8445697](https://github.com/windmill-labs/windmill/commit/8445697e31394ac11f3b8aa10af1546cc9c0041c))
## [1.63.2](https://github.com/windmill-labs/windmill/compare/v1.63.1...v1.63.2) (2023-02-15)
### Bug Fixes
* **psql:** update pg client ([a2fbc57](https://github.com/windmill-labs/windmill/commit/a2fbc5702509bb259bae106baa9a6146360ec5dd))
## [1.63.1](https://github.com/windmill-labs/windmill/compare/v1.63.0...v1.63.1) (2023-02-14)
### Bug Fixes
* update hub sync script ([03eb144](https://github.com/windmill-labs/windmill/commit/03eb1444c4a5dfbd170ba8d200784e530ca2f771))
## [1.63.0](https://github.com/windmill-labs/windmill/compare/v1.62.0...v1.63.0) (2023-02-14)
### Features
* add mem peak info ([f584062](https://github.com/windmill-labs/windmill/commit/f584062f13aa7da8e767fd35de1aef7bbb67c3c8))
* **frontend:** Minimal support for custom filenames ([#1190](https://github.com/windmill-labs/windmill/issues/1190)) ([b03b3be](https://github.com/windmill-labs/windmill/commit/b03b3be154efb0984f9623c27acc05617f125bc5))
* **worker:** set oom_adj to 1000 to prioritize killing subprocess ([265fbc5](https://github.com/windmill-labs/windmill/commit/265fbc5835d029d510a794e171392884cb20bdae))
### Bug Fixes
* **python:** return none if argument is missing ([3f2754b](https://github.com/windmill-labs/windmill/commit/3f2754b3305f6cb65373d532ff0db6020bf07e45))
* Update references to the docs ([#1191](https://github.com/windmill-labs/windmill/issues/1191)) ([a574270](https://github.com/windmill-labs/windmill/commit/a574270bc259f423c984259cd7d9a6d91b77815c))
## [1.62.0](https://github.com/windmill-labs/windmill/compare/v1.61.1...v1.62.0) (2023-02-03)
### Features
* add INCLUDE_HEADERS env variable to pass value from request headers ([0921ba0](https://github.com/windmill-labs/windmill/commit/0921ba008535e945f2ec3255728c2e8c1f4c36dc))
* add WHITELIST_WORKSPACES and BLACKLIST_WORKSPACES ([99568ea](https://github.com/windmill-labs/windmill/commit/99568eaa473d57123a7dde4007f8812e0053fb3f))
* Add workspace webhook ([#1158](https://github.com/windmill-labs/windmill/issues/1158)) ([b9ac60f](https://github.com/windmill-labs/windmill/commit/b9ac60f8bb0662e364606c4b7b8a6e3c1e7e4041))
* adding worker_busy ([23007f7](https://github.com/windmill-labs/windmill/commit/23007f7a71630fc2040e1be39db83ba56689e3c4))
* **cli:** 2-Way sync ([#1071](https://github.com/windmill-labs/windmill/issues/1071)) ([cdd1619](https://github.com/windmill-labs/windmill/commit/cdd16195aeaf32e1f1d0648f48e4843954d16d9c))
* **frontend:** App initial loading animations ([#1176](https://github.com/windmill-labs/windmill/issues/1176)) ([3305481](https://github.com/windmill-labs/windmill/commit/3305481d5d4ce598ceb57256cea851869cdaf25e))
* **python:** add ADDITIONAL_PYTHON_PATHS ([14b32be](https://github.com/windmill-labs/windmill/commit/14b32be8b229372c57a167fd74cb958a96f0e8e6))
### Bug Fixes
* **frontend:** Render popups above components in app editor ([#1171](https://github.com/windmill-labs/windmill/issues/1171)) ([bc8d1a3](https://github.com/windmill-labs/windmill/commit/bc8d1a375ec7886357ce0ef5971bb35013c94d61))
* **frontend:** Various fixes and improvements ([#1177](https://github.com/windmill-labs/windmill/issues/1177)) ([9f5500c](https://github.com/windmill-labs/windmill/commit/9f5500c1965ea50796d3bf289c0f9e0c929427f4))
* navigate to new script page before saving script ([f171cd8](https://github.com/windmill-labs/windmill/commit/f171cd8b7c46677173572bac256cbb489a1b8526))
## [1.61.1](https://github.com/windmill-labs/windmill/compare/v1.61.0...v1.61.1) (2023-01-31) ## [1.61.1](https://github.com/windmill-labs/windmill/compare/v1.61.0...v1.61.1) (2023-01-31)

View File

@@ -1,5 +1,5 @@
{$BASE_URL} { {$BASE_URL} {
bind {$ADDRESS} bind {$ADDRESS}
reverse_proxy /ws/* http://lsp:3001 reverse_proxy /ws/* http://lsp:3001
reverse_proxy /* http://windmill_server:8000 reverse_proxy /* http://windmill:8000
} }

View File

@@ -73,7 +73,7 @@ ARG features=""
COPY --from=planner /windmill/recipe.json recipe.json COPY --from=planner /windmill/recipe.json recipe.json
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef cook --release --features "$features" --recipe-path recipe.json
COPY ./openflow.openapi.yaml /openflow.openapi.yaml COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./ COPY ./backend ./
@@ -85,8 +85,7 @@ COPY .git/ .git/
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features" RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
FROM python:3.11.3-slim-buster FROM python:3.11.1-slim-buster
ARG TARGETPLATFORM
ARG APP=/usr/src/app ARG APP=/usr/src/app
@@ -124,17 +123,12 @@ ENV TZ=Etc/UTC
RUN /usr/local/bin/python3 -m pip install pip-tools RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=frontend /frontend/build /static_frontend
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=nsjail /nsjail/nsjail /bin/nsjail COPY --from=nsjail /nsjail/nsjail /bin/nsjail
COPY --from=denoland/deno:latest /usr/bin/deno /usr/bin/deno COPY --from=denoland/deno:latest /usr/bin/deno /usr/bin/deno
# docker does not support conditional COPY and we want to use the same Dockerfile for both amd64 and arm64 and privilege the official image
COPY --from=lukechannings/deno:latest /usr/bin/deno /usr/bin/deno-arm
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then rm /usr/bin/deno-arm; elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then mv /usr/bin/deno-arm /usr/bin/deno; fi
RUN mkdir -p ${APP} RUN mkdir -p ${APP}
WORKDIR ${APP} WORKDIR ${APP}

View File

@@ -8,9 +8,5 @@ or belonging to one of the below cases:
The files under backend/ are AGPL Licensed. The files under backend/ are AGPL Licensed.
The files under frontend/ are AGPL Licensed. The files under frontend/ are AGPL Licensed.
The files under python-client/ deno-client/ go-client/ are Apache 2.0 Licensed. The files under python-client/ are Apache 2.0 Licensed.
The files under community/ are Apache 2.0 Licensed.
The openapi files, including the OpenFlow spec is Apache 2.0 Licensed.
All third party components incorporated into the Windmill Software are licensed under the
original license provided by the owner of the applicable component.

173
README.md
View File

@@ -5,7 +5,7 @@
<em>.</em> <em>.</em>
</p> </p>
<p align=center> <p align=center>
Open-source developer infrastructure for internal tools. Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs to trigger workflows and scripts as internal apps. 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. Open-source developer infrastructure for internal tools. Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs to trigger workflows and scripts as internal apps. 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.
</p> </p>
<p align="center"> <p align="center">
@@ -20,36 +20,55 @@ Open-source developer infrastructure for internal tools. Self-hostable alternati
</a> </a>
</p> </p>
---
**Try it (personal workspaces are free forever)**: <https://app.windmill.dev>
**Documentation**: <https://docs.windmill.dev>
**Discord**: <https://discord.gg/V7PM2YHsPB>
**Hub**: <https://hub.windmill.dev>
**Contributor's guide**: <https://docs.windmill.dev/docs/contributors_guide>
**Roadmap**: <https://github.com/orgs/windmill-labs/projects/2>
You can show your support for the project by starring this repo.
Windmill Labs offers commercial licenses, an enterprise edition, local hub
mirrors, and support: contact ruben@windmill.dev.
---
# Windmill
<p align="center"> <p align="center">
<a href="https://app.windmill.dev">Try it</a> - <a href="https://docs.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://docs.windmill.dev/docs/misc/contributing">Contributor's guide</a> <b>Disclaimer: </b>Windmill is in <b>BETA</b>. It is secure to run in production but we are still <a href="https://github.com/orgs/windmill-labs/projects/2">improving the product fast<a/>.
</p> </p>
# Windmill - Turn scripts into workflows and UIs that you can share and run at scale ![Windmill Screenshot](./imgs/windmill-flow.png)
![Windmill Screenshot](./imgs/windmill.png)
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers Windmill is <b>fully open-sourced (AGPLv3)</b>:
dedicated instance and commercial support and licenses.
![Windmill Diagram](/imgs/stacks.svg) - [Windmill](#windmill)
https://user-images.githubusercontent.com/275584/218350457-bc2fdc3b-e667-4da5-a2bd-3bacc1f0ec79.mp4
- [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)
- [Main Concepts](#main-concepts) - [Main Concepts](#main-concepts)
- [Show me some actual script code](#show-me-some-actual-script-code)
- [CLI](#cli) - [CLI](#cli)
- [Running scripts locally](#running-scripts-locally) - [Layout](#layout)
- [Stack](#stack) - [Stack](#stack)
- [Security](#security) - [Security](#security)
- [Sandboxing](#sandboxing) - [Sandboxing and workload isolation](#sandboxing-and-workload-isolation)
- [Secrets, credentials and sensitive values](#secrets-credentials-and-sensitive-values) - [Secrets, credentials and sensitive values](#secrets-credentials-and-sensitive-values)
- [Performance](#performance) - [Performance](#performance)
- [Architecture](#architecture) - [Architecture](#architecture)
- [Big-picture Architecture](#big-picture-architecture)
- [Technical Architecture](#technical-architecture)
- [How to self-host](#how-to-self-host) - [How to self-host](#how-to-self-host)
- [Docker compose](#docker-compose) - [Docker compose](#docker-compose)
- [Kubernetes (k8s) and Helm charts](#kubernetes-k8s-and-helm-charts) - [Kubernetes (k8s) and Helm charts](#kubernetes-k8s-and-helm-charts)
- [Postgres without superuser](#postgres-without-superuser)
- [Commercial license](#commercial-license) - [Commercial license](#commercial-license)
- [OAuth for self-hosting](#oauth-for-self-hosting) - [OAuth for self-hosting (very optional)](#oauth-for-self-hosting-very-optional)
- [Resource types](#resource-types) - [Resource types](#resource-types)
- [Environment Variables](#environment-variables) - [Environment Variables](#environment-variables)
- [Run a local dev setup](#run-a-local-dev-setup) - [Run a local dev setup](#run-a-local-dev-setup)
@@ -70,62 +89,30 @@ https://user-images.githubusercontent.com/275584/218350457-bc2fdc3b-e667-4da5-a2
3. Make it flow! You can chain your scripts or scripts made by the community 3. Make it flow! You can chain your scripts or scripts made by the community
shared on [WindmillHub](https://hub.windmill.dev). shared on [WindmillHub](https://hub.windmill.dev).
![Step 3](./imgs/windmill-flow.png) ![Step 4](./imgs/windmill-flow.png)
4. Build complex UI on top of your scripts and flows. 4. Build complex UI on top of your scripts and flows.
![Step 4](./imgs/windmill-builder.png) ![Step 5](./imgs/windmill-builder.png)
Scripts and flows can also be triggered by a cron schedule '_/5 _ \* \* \*' or Scripts and flows can also be triggered by a cron schedule '*/5 * * * *' or
through webhooks. through webhooks.
You can build your entire infra on top of Windmill! You can build your entire infra on top of Windmill!
## Show me some actual script code
```typescript
import * as wmill from "https://deno.land/x/windmill@v1.62.0/mod.ts";
//import any dependency from npm
import cowsay from "npm:cowsay@1.5.0";
export async function main(
a: number,
// unions generate enums
b: "my" | "enum",
// default parameters prefill the field
d = "default arg",
// nested objects work c = { nested: "object" },
// permissioned and typed json
db: wmill.Resource<"postgresql">
) {
const email = Deno.env.get("WM_EMAIL");
// variables are permissioned and by path
let variable = await wmill.getVariable("f/company-folder/my_secret");
const lastTimeRun = await wmill.getState();
// logs are printed and always inspectable
console.log(cowsay.say({ text: "hello " + email + " " + lastTimeRun }));
await wmill.setState(Date.now());
// return is serialized as JSON
return { foo: d, variable };
}
```
## CLI ## CLI
We have a powerful CLI to interact with the windmill platform and sync your We have a powerful CLI to interact with the windmill platform and sync your
scripts from local files, github repos and to run scripts and flows on the scripts from your own github repo. See
instance from local commands. See
[more details](https://github.com/windmill-labs/windmill/tree/main/cli) [more details](https://github.com/windmill-labs/windmill/tree/main/cli)
![CLI Screencast](./cli/vhs/output/setup.gif) ![CLI Screencast](./cli/vhs/output/setup.gif)
### Running scripts locally ## Layout
You can run your script locally easily, you simply need to pass the right - `backend/`: Rust backend
environment variables for the `wmill` client library to fetch resources and - `frontend`: Svelte frontend
variables from your instance if necessary. See more: - `lsp/`: Lsp asssistant for the monaco editor
<https://docs.windmill.dev/docs/advanced/local_development/> - `<lang>-client/`: Windmill client for the given `<lang>`
## Stack ## Stack
@@ -148,7 +135,7 @@ variables from your instance if necessary. See more:
## Security ## Security
### Sandboxing ### Sandboxing and workload isolation
Windmill uses [nsjail](https://github.com/google/nsjail) on top of the deno Windmill uses [nsjail](https://github.com/google/nsjail) on top of the deno
sandboxing. It is production multi-tenant grade secure. Do not take our word for sandboxing. It is production multi-tenant grade secure. Do not take our word for
@@ -174,42 +161,41 @@ back to the database is ~50ms. A typical lightweight deno job will take around
<p align="center"> <p align="center">
### Big-picture Architecture
<img src="./imgs/diagram.svg"> <img src="./imgs/diagram.svg">
### Technical Architecture
<img src="./imgs/architecture.svg">
</p> </p>
## How to self-host ## How to self-host
We only provide docker-compose setup here. For more advanced setups, like We only provide docker-compose setup here. For more advanced setups, like
compiling from source or using without a postgres super user, see compiling from source or using without a postgres super user, see
[documentation](https://docs.windmill.dev/docs/advanced/self_host) [documentation](https://docs.windmill.dev/docs/how-tos/self_host)
### Docker compose ### Docker compose
``` `docker compose up` with the following docker-compose is sufficient:
curl https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml -o docker-compose.yml <https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml>
curl https://github.com/windmill-labs/windmill/blob/main/CaddyFile -o Caddyfile
curl https://github.com/windmill-labs/windmill/blob/main/.env -o .env
docker compose up -d --pull always Go to https://localhost et voilà :)
```
Go to http://localhost et voilà :) For older kernels < 4.18, set `DISABLE_NUSER=true` as env variable, otherwise
nsjail will not be able to launch the isolated scripts.
To disable nsjail altogether, set `DISABLE_NSJAIL=true`.
The default super-admin user is: admin@windmill.dev / changeme The default super-admin user is: admin@windmill.dev / changeme
From there, you can follow the setup app and creat other users. From there, you can create other users (do not forget to change the password!)
### Kubernetes (k8s) and Helm charts ### Kubernetes (k8s) and Helm charts
We publish helm charts at: We publish helm charts at: <https://github.com/windmill-labs/windmill-helm-charts>
<https://github.com/windmill-labs/windmill-helm-charts>
### Postgres without superuser
If you do not want, or cannot (for instance, in AWS Aurora or Cloud sql) use a
postgres superuser, you can run `./init-db-as-superuser.sql` to init the
required users for windmill.
### Commercial license ### Commercial license
@@ -221,14 +207,14 @@ 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 as a feature of your product, or
to build a feature on top of Windmill, to comply with AGPLv3 your product must 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 be AGPLv3 or you must get a commercial license. Contact us at
<ruben@windmill.dev> if you have any doubts. <license@windmill.dev> if you have any doubts.
In addition, a commercial license grants you a dedicated engineer to transition In addition, a commercial license grants you a dedicated engineer to transition
your current infrastructure to Windmill, support with tight SLA, audit logs your current infrastructure to Windmill, support with tight SLA, audit logs
export features, SSO, unlimited users creation, advanced permission managing export features, SSO, unlimited users creation, advanced permission managing
features such as groups and the ability to create more than one workspace. features such as groups and the ability to create more than one workspace.
### OAuth for self-hosting ### OAuth for self-hosting (very optional)
To get the same oauth integrations as Windmill Cloud, mount `oauth.json` with To get the same oauth integrations as Windmill Cloud, mount `oauth.json` with
the following format: the following format:
@@ -245,13 +231,12 @@ the following format:
and mount it at `/usr/src/app/oauth.json`. and mount it at `/usr/src/app/oauth.json`.
The redirect url for the oauth clients is: The redirect url for the oauth clients is: `<instance_url>/user/login_callback/<client>`
`<instance_url>/user/login_callback/<client>`
[The list of all possible "connect an app" oauth clients](https://github.com/windmill-labs/windmill/blob/main/backend/oauth_connect.json) [The list of all possible "connect an app" oauth clients](https://github.com/windmill-labs/windmill/blob/main/backend/oauth_connect.json)
To add more "connect an app" OAuth clients to the Windmill project, read the To add more "connect an app" OAuth clients to the Windmill project, read the
[Contributor's guide](https://docs.windmill.dev/docs/misc/contributing). We [Contributor's guide](https://docs.windmill.dev/docs/contributors_guide). We
welcome contributions! welcome contributions!
You may also add your own custom OAuth2 IdP and OAuth2 Resource provider: You may also add your own custom OAuth2 IdP and OAuth2 Resource provider:
@@ -283,26 +268,22 @@ You may also add your own custom OAuth2 IdP and OAuth2 Resource provider:
### Resource types ### Resource types
You will also want to import all the approved resource types from You will also want to import all the approved resource types from
[WindmillHub](https://hub.windmill.dev). A setup script will prompt you to have [WindmillHub](https://hub.windmill.dev). There is no automatic way to do this
it being synced automatically everyday. automatically currently, but it will be possible using a command with the
upcoming CLI tool.
## Environment Variables ## Environment Variables
| Environment Variable name | Default | Description | Api Server/Worker/All | | Environment Variable name | Default | Description | Api Server/Worker/All |
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All | | DATABASE_URL | | The Postgres database url. | All |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker | | DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | |
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server | | NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker) | Worker |
| PORT | 8000 | Exposed port | Server |
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker, set to 0 for an API only instance) | Worker |
| DISABLE_SERVER | false | Binary would operate as a worker only instance | Worker |
| METRICS_ADDR | None | The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All | | METRICS_ADDR | None | The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
| JSON_FMT | false | Output the logs in json format instead of logfmt | All | | JSON_FMT | false | Output the logs in json format instead of logfmt | All |
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance | Server | | BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance | Server |
| BASE_INTERNAL_URL | http://localhost:8000 | The base url that is reachable by your workers to talk to the Servers. This help avoiding going through the external load balancer for VPC-internal requests. | Worker | | BASE_INTERNAL_URL | http://localhost:8000 | The base url that is reachable by your workers to talk to the Servers. This help avoiding going through the external load balancer for VPC-internal requests. | Worker |
| TIMEOUT | 300 | The maximum time of execution of a script. When reached, the job is failed as having timedout. | Worker | | TIMEOUT | 300 | The timeout in seconds for the execution of a script | Worker |
| ZOMBIE_JOB_TIMEOUT | 30 | The timeout after which a job is considered to be zombie if the worker did not send pings about processing the job (every server check for zombie jobs every 30s) | Server |
| RESTART_ZOMBIE_JOBS | true | If true then a zombie job is restarted (in-place with the same uuid and some logs), if false the zombie job is failed | Server |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker | | SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker | | MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker |
| DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker | | DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker |
@@ -312,11 +293,10 @@ it being synced automatically everyday.
| TAR_CACHE_RATE (EE only) | 100 | The rate at which to tar the cache of the workers. 100 means every 100th job in average (uniformly randomly distributed). | Worker | | TAR_CACHE_RATE (EE only) | 100 | The rate at which to tar the cache of the workers. 100 means every 100th job in average (uniformly randomly distributed). | Worker |
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server | | SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server | | COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| SERVE_CSP | None | The CSP directives to use when serving the frontend static assets | Server |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker | | DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
| PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker | | PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker |
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker | | GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
| GOPRIVATE | | The GOPRIVATE env variable to use private go modules | Worker |
| NETRC | | The netrc content to use a private go registry | Worker |
| PIP_INDEX_URL | None | The index url to pass for pip. | Worker | | PIP_INDEX_URL | None | The index url to pass for pip. | Worker |
| PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker | | PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker |
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker | | PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
@@ -328,13 +308,12 @@ it being synced automatically everyday.
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker | | QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker | | DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker | | DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| NPM_CONFIG_REGISTRY | None | Registry to use for NPM dependencies, set if you have a private repository you need to use instead of the default public NPM registry | Worker | | PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again |
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker | | ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server | | INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
| WHITELIST_WORKSPACES | None | Whitelist of workspaces this worker takes job from | Worker |
| BLACKLIST_WORKSPACES | None | Blacklist of workspaces this worker takes job from | Worker |
| INSTANCE_EVENTS_WEBHOOK | None | Webhook to notify of events such as new user added, signup/invite. Can hook back to windmill to send emails | Server |
## Run a local dev setup ## Run a local dev setup
@@ -381,4 +360,4 @@ running options.
## Copyright ## Copyright
Windmill Labs, Inc 2023 Windmill Labs, Inc 2022

View File

@@ -8,14 +8,3 @@ rustflags = [
] ]
incremental = true incremental = true
[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
[target.aarch64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]

1776
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "windmill" name = "windmill"
version = "1.87.0" version = "1.61.1"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
@@ -19,7 +19,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "1.87.0" version = "1.61.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"] authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021" edition = "2021"
@@ -48,16 +48,12 @@ git-version.workspace = true
rsa.workspace = true rsa.workspace = true
base64.workspace = true base64.workspace = true
sha2.workspace = true sha2.workspace = true
rsmq_async.workspace = true
url.workspace = true
[dev-dependencies] [dev-dependencies]
serde_json.workspace = true serde_json.workspace = true
reqwest.workspace = true reqwest.workspace = true
windmill-queue.workspace = true windmill-queue.workspace = true
axum.workspace = true axum.workspace = true
serde.workspace = true
[workspace.dependencies] [workspace.dependencies]
windmill-api = { path = "./windmill-api" } windmill-api = { path = "./windmill-api" }
@@ -76,7 +72,7 @@ headers = "^0"
hyper = { version = "^0", features = ["full"] } hyper = { version = "^0", features = ["full"] }
tokio = { version = "^1", features = ["full", "tracing"] } tokio = { version = "^1", features = ["full", "tracing"] }
tower = "^0" tower = "^0"
tower-http = { version = "^0", features = ["trace", "cors"] } tower-http = { version = "^0", features = ["trace"] }
tower-cookies = "^0" tower-cookies = "^0"
serde = "^1" serde = "^1"
serde_json = { version = "^1", features = ["preserve_order"] } serde_json = { version = "^1", features = ["preserve_order"] }
@@ -84,11 +80,10 @@ uuid = { version = "^1", features = ["serde", "v4"] }
thiserror = "^1" thiserror = "^1"
anyhow = "^1" anyhow = "^1"
chrono = { version = "^0", features = ["serde"] } chrono = { version = "^0", features = ["serde"] }
chrono-tz = "^0"
tracing = "^0" tracing = "^0"
tracing-subscriber = { version = "^0", features = ["env-filter", "json"] } tracing-subscriber = { version = "^0", features = ["env-filter", "json"] }
prometheus = { version = "^0", default-features = false } prometheus = { version = "^0", default-features = false }
cookie = { version = "0.17.0" } cookie = { version = "0.16.2" }
phf = { version = "0.11", features = ["macros"] } phf = { version = "0.11", features = ["macros"] }
rust-embed = "^6" rust-embed = "^6"
mime_guess = "^2" mime_guess = "^2"
@@ -123,10 +118,10 @@ itertools = "^0"
regex = "^1" regex = "^1"
deno_core = "^0" deno_core = "^0"
async-recursion = "^1" async-recursion = "^1"
swc_common = "0.29.39" swc_common = "^0"
swc_ecma_parser = "0.128.2" swc_ecma_parser = "^0"
swc_ecma_ast = "0.98.1" swc_ecma_ast = "^0"
base64 = "0.21.0" base64 = "^0"
unicode-general-category = "^0" unicode-general-category = "^0"
hmac = "0.12.1" hmac = "0.12.1"
sha2 = "0.10.6" sha2 = "0.10.6"
@@ -143,20 +138,10 @@ sqlx = { version = "^0", features = [
dotenv = "^0" dotenv = "^0"
ulid = { version = "^1", features = ["uuid"] } ulid = { version = "^1", features = ["uuid"] }
futures = "^0" futures = "^0"
futures-core = "^0"
tokio-metrics = "0.1.0" tokio-metrics = "0.1.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
serde_derive = "1.0.147" serde_derive = "1.0.147"
const_format = { version = "0.2", features = ["rust_1_64", "rust_1_51"] } const_format = { version = "0.2", features = ["rust_1_64", "rust_1_51"] }
dyn-iter = "0.2.0" dyn-iter = "0.2.0"
rsa = "0.7.2" rsa = "0.7.2"
async-stripe = { version = "0.14", features = [ async-stripe = { version = "0.14", features = ["runtime-tokio-hyper", "checkout"] }
"runtime-tokio-hyper",
"checkout",
] }
async_zip = { version = "0.0.11", features = ["full"] }
once_cell = "1.17.1"
rsmq_async = { version = "5.1.5" }
gosyn = "0.2.2"
[patch.crates-io]

View File

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

View File

@@ -1,12 +0,0 @@
-- Add up migration script here
ALTER TABLE
password
ADD
first_time_user boolean NOT NULL DEFAULT (false);
UPDATE
password
SET
first_time_user = true
WHERE
email = 'admin@windmill.dev';

View File

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

View File

@@ -1,5 +0,0 @@
-- Add up migration script here
ALTER TABLE
workspace_settings
ADD
COLUMN webhook text;

View File

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

View File

@@ -1,3 +0,0 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN mem_peak INTEGER;
ALTER TABLE completed_job ADD COLUMN mem_peak INTEGER;

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,16 +0,0 @@
-- Add up migration script here
-- Add up migration script here
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.63.1/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "admins", Deno.env.get("BASE_INTERNAL_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
await run("hub", "pull");
}
async function run(...cmd: string[]) {
console.log("Running \"" + cmd.join('' '') + "\"");
await wmill.parse(cmd);
}', summary = 'Synchronize Hub Resource types with admins workspace',
description = 'Basic administrative script to sync latest resource types from hub to share to every workspace. Recommended to run at least once. On a schedule by default.'
WHERE hash = -28028598712388162 AND workspace_id = 'admins';

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
-- Add up migration script here
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.70.1/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "admins", Deno.env.get("BASE_INTERNAL_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
await run("hub", "pull");
}
async function run(...cmd: string[]) {
console.log("Running \"" + cmd.join('' '') + "\"");
await wmill.parse(cmd);
}', summary = 'Synchronize Hub Resource types with admins workspace',
description = 'Basic administrative script to sync latest resource types from hub to share to every workspace. Recommended to run at least once. On a schedule by default.'
WHERE hash = -28028598712388162 AND workspace_id = 'admins';

View File

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

View File

@@ -1,3 +0,0 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN root_job uuid;
ALTER TABLE queue ADD COLUMN leaf_jobs jsonb;

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,11 +0,0 @@
-- Add up migration script here
CREATE POLICY see_extra_perms_user ON app FOR ALL
USING (extra_perms ? CONCAT('u/', current_setting('session.user')))
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_groups ON app FOR ALL
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
WITH CHECK (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));

View File

@@ -1,2 +0,0 @@
ALTER TABLE schedule DROP COLUMN timezone;
ALTER TABLE schedule ADD COLUMN offset_ INTEGER NOT NULL DEFAULT 0;

View File

@@ -1,26 +0,0 @@
ALTER TABLE schedule ADD COLUMN timezone VARCHAR(255) NOT NULL DEFAULT 'UTC';
-- INSERT the correct IANA timezone string for each offset value
UPDATE schedule SET timezone = 'Pacific/Honolulu' WHERE offset_ = 600;
UPDATE schedule SET timezone = 'America/Anchorage' WHERE offset_ = 540;
UPDATE schedule SET timezone = 'America/Los_Angeles' WHERE offset_ = 480;
UPDATE schedule SET timezone = 'America/Chicago' WHERE offset_ = 360;
UPDATE schedule SET timezone = 'America/New_York' WHERE offset_ = 300;
UPDATE schedule SET timezone = 'America/Halifax' WHERE offset_ = 240;
UPDATE schedule SET timezone = 'America/Sao_Paulo' WHERE offset_ = 180;
UPDATE schedule SET timezone = 'Atlantic/South_Georgia' WHERE offset_ = 120;
UPDATE schedule SET timezone = 'Atlantic/Cape_Verde' WHERE offset_ = 60;
UPDATE schedule SET timezone = 'Europe/London' WHERE offset_ = 0;
UPDATE schedule SET timezone = 'Europe/Berlin' WHERE offset_ = -60;
UPDATE schedule SET timezone = 'Europe/Athens' WHERE offset_ = -120;
UPDATE schedule SET timezone = 'Europe/Moscow' WHERE offset_ = -180;
UPDATE schedule SET timezone = 'Asia/Dubai' WHERE offset_ = -240;
UPDATE schedule SET timezone = 'Asia/Aqtau' WHERE offset_ = -300;
UPDATE schedule SET timezone = 'Asia/Almaty' WHERE offset_ = -360;
UPDATE schedule SET timezone = 'Asia/Bangkok' WHERE offset_ = -420;
UPDATE schedule SET timezone = 'Asia/Hong_Kong' WHERE offset_ = -480;
UPDATE schedule SET timezone = 'Asia/Tokyo' WHERE offset_ = -540;
UPDATE schedule SET timezone = 'Australia/Sydney' WHERE offset_ = -600;
ALTER TABLE schedule DROP COLUMN offset_;

View File

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

View File

@@ -1,3 +0,0 @@
-- Add up migration script here
ALTER TABLE password ALTER COLUMN company TYPE VARCHAR(255);
ALTER TABLE password ALTER COLUMN name TYPE VARCHAR(255);

View File

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

View File

@@ -1,6 +0,0 @@
-- Add up migration script here
CREATE TABLE pip_resolution_cache(
hash VARCHAR(255) PRIMARY KEY,
expiration TIMESTAMP NOT NULL,
lockfile TEXT NOT NULL
);

View File

@@ -1,2 +0,0 @@
DROP TABLE IF EXISTS input;
DROP TYPE RUNNABLE_TYPE;

View File

@@ -1,13 +0,0 @@
CREATE TYPE RUNNABLE_TYPE AS ENUM ('ScriptHash', 'ScriptPath', 'FlowPath');
CREATE TABLE IF NOT EXISTS input (
id UUID PRIMARY KEY,
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
runnable_id VARCHAR(255) NOT NULL,
runnable_type RUNNABLE_TYPE NOT NULL,
name TEXT NOT NULL,
args JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(50) NOT NULL,
is_public BOOLEAN NOT NULL DEFAULT FALSE
);

View File

@@ -16,5 +16,4 @@ unicode-general-category.workspace = true
itertools.workspace = true itertools.workspace = true
anyhow.workspace = true anyhow.workspace = true
regex.workspace = true regex.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
serde_json.workspace = true

View File

@@ -1,8 +1,6 @@
#![allow(non_snake_case)] // TODO: switch to parse_* function naming #![allow(non_snake_case)] // TODO: switch to parse_* function naming
use anyhow::anyhow;
use regex::Regex; use regex::Regex;
use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
use windmill_parser::{Arg, MainArgSignature, Typ}; use windmill_parser::{Arg, MainArgSignature, Typ};
@@ -19,32 +17,19 @@ pub fn parse_bash_sig(code: &str) -> windmill_common::error::Result<MainArgSigna
} }
} }
lazy_static::lazy_static! {
static ref RE: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+):-(.*)\})"$"#).unwrap();
}
fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> { fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut hm: HashMap<i32, (String, Option<String>)> = HashMap::new(); let mut hm = HashMap::new();
for cap in RE.captures_iter(code) { let re = Regex::new(r#"(?m)^(\w+)="\$(\d+)"$"#).unwrap();
hm.insert( for cap in re.captures_iter(code) {
cap.get(2) hm.insert(cap[2].parse::<i32>()?, cap[1].to_string());
.or(cap.get(3))
.and_then(|x| x.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?,
(
cap[1].to_string(),
cap.get(4).map(|x| x.as_str().to_string()),
),
);
} }
let mut args = vec![]; let mut args = vec![];
for i in 1..20 { for i in 1..20 {
if hm.contains_key(&i) { if hm.contains_key(&i) {
let (name, default) = hm.get(&i).unwrap();
args.push(Arg { args.push(Arg {
name: name.clone(), name: hm[&i].clone(),
typ: Typ::Str(None), typ: Typ::Str(None),
default: default.clone().map(|x| json!(x)), default: None,
otyp: None, otyp: None,
has_default: false, has_default: false,
}); });
@@ -58,8 +43,6 @@ fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde_json::json;
use super::*; use super::*;
#[test] #[test]
@@ -67,7 +50,8 @@ mod tests {
let code = r#" let code = r#"
token="$1" token="$1"
image="$2" image="$2"
digest="${3:-latest with spaces}" digest="${3:-latest}"
foo="$4"
"#; "#;
//println!("{}", serde_json::to_string()?); //println!("{}", serde_json::to_string()?);
@@ -90,13 +74,6 @@ digest="${3:-latest with spaces}"
typ: Typ::Str(None), typ: Typ::Str(None),
default: None, default: None,
has_default: false has_default: false
},
Arg {
otyp: None,
name: "digest".to_string(),
typ: Typ::Str(None),
default: Some(json!("latest with spaces")),
has_default: false
} }
] ]
} }

View File

@@ -15,4 +15,3 @@ phf.workspace = true
unicode-general-category.workspace = true unicode-general-category.workspace = true
itertools.workspace = true itertools.workspace = true
anyhow.workspace = true anyhow.workspace = true
gosyn.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,347 @@
#![allow(clippy::large_enum_variant)] // TODO: we allow large enum variant for now, let's profile properly to see if we want to box.
use crate::parser_go_token::{Position, Token};
use std::collections::BTreeMap;
// https://pkg.go.dev/go/ast#CommentGroup
#[derive(Debug)]
pub struct CommentGroup {
// List []*Comment // len(List) > 0
}
// https://pkg.go.dev/go/ast#FieldList
#[derive(Debug)]
pub struct FieldList<'a> {
pub opening: Option<Position<'a>>, // position of opening parenthesis/brace, if any
pub list: Vec<Field<'a>>, // field list; or nil
pub closing: Option<Position<'a>>, // position of closing parenthesis/brace, if any
}
// https://pkg.go.dev/go/ast#Field
#[derive(Debug)]
pub struct Field<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub names: Option<Vec<Ident<'a>>>, // field/method/(type) parameter names, or type "type"; or nil
pub type_: Option<Expr<'a>>, // field/method/parameter type, type list type; or nil
pub tag: Option<BasicLit<'a>>, // field tag; or nil
pub comment: Option<CommentGroup>, // line comments; or nil
}
// https://pkg.go.dev/go/ast#File
#[derive(Debug)]
pub struct File<'a> {
// package name
pub decls: Vec<Decl<'a>>, // top-level declarations; or nil // list of all comments in the source file
}
// https://pkg.go.dev/go/ast#FuncDecl
#[derive(Debug)]
pub struct FuncDecl<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub recv: Option<FieldList<'a>>, // receiver (methods); or nil (functions)
pub name: Ident<'a>, // function/method name
pub type_: FuncType<'a>, // function signature: type and value parameters, results, and position of "func" keyword
pub body: Option<BlockStmt<'a>>, // function body; or nil for external (non-Go) function
}
// https://pkg.go.dev/go/ast#BlockStmt
#[derive(Debug)]
pub struct BlockStmt<'a> {
pub lbrace: Position<'a>, // position of "{"
pub list: Vec<Stmt>,
pub rbrace: Position<'a>, // position of "}", if any (may be absent due to syntax error)
}
// https://pkg.go.dev/go/ast#FuncType
#[derive(Debug)]
pub struct FuncType<'a> {
pub func: Option<Position<'a>>, // position of "func" keyword (token.NoPos if there is no "func")
pub params: FieldList<'a>, // (incoming) parameters; non-nil
pub results: Option<FieldList<'a>>, // (outgoing) results; or nil
}
// https://pkg.go.dev/go/ast#Ident
#[derive(Debug)]
pub struct Ident<'a> {
pub name_pos: Position<'a>, // identifier position
pub name: &'a str, // identifier name
pub obj: Option<Box<Object<'a>>>, // denoted object; or nil
}
// https://pkg.go.dev/go/ast#ValueSpec
#[derive(Debug)]
pub struct ValueSpec<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub names: Vec<Ident<'a>>, // value names (len(Names) > 0)
pub type_: Option<Expr<'a>>, // value type; or nil
pub values: Option<Vec<Expr<'a>>>, // initial values; or nil
pub comment: Option<CommentGroup>, // line comments; or nil
}
// https://pkg.go.dev/go/ast#BasicLit
#[derive(Debug)]
pub struct BasicLit<'a> {
pub value_pos: Position<'a>, // literal position
pub kind: Token, // token.INT, token.FLOAT, token.IMAG, token.CHAR, or token.STRING
pub value: &'a str, // literal string; e.g. 42, 0x7f, 3.14, 1e-9, 2.4i, 'a', '\x7f', "foo" or `\m\n\o`
}
// https://pkg.go.dev/go/ast#Object
#[derive(Debug)]
pub struct Object<'a> {
pub kind: ObjKind,
pub name: &'a str, // declared name
pub decl: Option<ObjDecl>, // corresponding Field, XxxSpec, FuncDecl, LabeledStmt, AssignStmt, Scope; or nil
pub data: Option<usize>, // object-specific data; or nil
pub type_: Option<()>, // placeholder for type information; may be nil
}
// https://pkg.go.dev/go/ast#Ellipsis
#[derive(Debug)]
pub struct Ellipsis<'a> {
pub ellipsis: Position<'a>, // position of "..."
pub elt: Option<Box<Expr<'a>>>, // ellipsis element type (parameter lists only); or nil
}
// https://pkg.go.dev/go/ast#Ellipsis
#[derive(Debug)]
pub struct TypeAssertExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub lparen: Position<'a>, // position of "("
pub type_: Box<Expr<'a>>, // asserted type; nil means type switch X.(type)
pub rparen: Position<'a>, // position of ")"
}
// https://pkg.go.dev/go/ast#SliceExpr
#[derive(Debug)]
pub struct SliceExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub lbrack: Position<'a>, // position of "["
pub low: Option<Box<Expr<'a>>>, // begin of slice range; or nil
pub high: Option<Box<Expr<'a>>>, // end of slice range; or nil
pub max: Option<Box<Expr<'a>>>, // maximum capacity of slice; or nil
pub slice3: bool, // true if 3-index slice (2 colons present)
pub rbrack: Position<'a>, // position of "]"
}
// https://pkg.go.dev/go/ast#ObjKind
#[derive(Debug)]
pub enum ObjKind {}
#[derive(Debug)]
pub enum ObjDecl {}
// https://pkg.go.dev/go/ast#Decl
#[derive(Debug)]
pub enum Decl<'a> {
FuncDecl(FuncDecl<'a>),
}
// https://pkg.go.dev/go/ast#Scope
#[derive(Debug)]
pub struct Scope<'a> {
pub outer: Option<Box<Scope<'a>>>,
pub objects: BTreeMap<&'a str, Object<'a>>,
}
// https://pkg.go.dev/go/ast#GenDecl
#[derive(Debug)]
pub struct GenDecl<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub tok_pos: Position<'a>, // position of Tok
pub tok: Token, // IMPORT, CONST, TYPE, or VAR
pub lparen: Option<Position<'a>>, // position of '(', if any
pub specs: Vec<Spec>,
pub rparen: Option<Position<'a>>, // position of ')', if any
}
// https://pkg.go.dev/go/ast#AssignStmt
#[derive(Debug)]
pub struct AssignStmt<'a> {
pub lhs: Vec<Expr<'a>>,
pub tok_pos: Position<'a>, // position of Tok
pub tok: Token, // assignment token, DEFINE
pub rhs: Vec<Expr<'a>>,
}
// https://pkg.go.dev/go/ast#BinaryExpr
#[derive(Debug)]
pub struct BinaryExpr<'a> {
pub x: Box<Expr<'a>>, // left operand
pub op_pos: Position<'a>, // position of Op
pub op: Token, // operator
pub y: Box<Expr<'a>>, // right operand
}
// https://pkg.go.dev/go/ast#ReturnStmt
#[derive(Debug)]
pub struct ReturnStmt<'a> {
pub return_: Position<'a>, // position of "return" keyword
pub results: Vec<Expr<'a>>, // result expressions; or nil
}
// https://pkg.go.dev/go/ast#TypeSpec
#[derive(Debug)]
pub struct TypeSpec<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub name: Option<Ident<'a>>, // type name
pub assign: Option<Position<'a>>, // position of '=', if any
pub type_: Expr<'a>, // *Ident, *ParenExpr, *SelectorExpr, *StarExpr, or any of the *XxxTypes
pub comment: Option<CommentGroup>, // line comments; or nil
}
// https://pkg.go.dev/go/ast#StructType
#[derive(Debug)]
pub struct StructType<'a> {
pub struct_: Position<'a>, // position of "struct" keyword
pub fields: Option<FieldList<'a>>, // list of field declarations
pub incomplete: bool, // true if (source) fields are missing in the Fields list
}
// https://pkg.go.dev/go/ast#StarExpr
#[derive(Debug)]
pub struct StarExpr<'a> {
pub star: Position<'a>, // position of "*"
pub x: Box<Expr<'a>>, // operand
}
// https://pkg.go.dev/go/ast#InterfaceType
#[derive(Debug)]
pub struct InterfaceType<'a> {
pub interface: Position<'a>, // position of "interface" keyword
pub methods: Option<FieldList<'a>>, // list of embedded interfaces, methods, or types
pub incomplete: bool, // true if (source) methods or types are missing in the Methods list
}
// https://pkg.go.dev/go/ast#UnaryExpr
#[derive(Debug)]
pub struct UnaryExpr<'a> {
pub op_pos: Position<'a>, // position of Op
pub op: Token, // operator
pub x: Box<Expr<'a>>, // operand
}
// https://pkg.go.dev/go/ast#CallExpr
#[derive(Debug)]
pub struct CallExpr<'a> {
pub fun: Box<Expr<'a>>, // function expression
pub lparen: Position<'a>, // position of "("
pub args: Option<Vec<Expr<'a>>>, // function arguments; or nil
pub ellipsis: Option<Position<'a>>, // position of "..." (token.NoPos if there is no "...")
pub rparen: Position<'a>, // position of ")"
}
// https://pkg.go.dev/go/ast#SelectorExpr
#[derive(Debug)]
pub struct SelectorExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub sel: Ident<'a>, // field selector
}
// https://pkg.go.dev/go/ast#ParenExpr
#[derive(Debug)]
pub struct ParenExpr<'a> {
pub lparen: Position<'a>, // position of "("
pub x: Box<Expr<'a>>, // parenthesized expression
pub rparen: Position<'a>, // position of ")"
}
// https://pkg.go.dev/go/ast#FuncLit
#[derive(Debug)]
pub struct FuncLit<'a> {
pub type_: FuncType<'a>, // function type
pub body: BlockStmt<'a>, // function body
}
// https://pkg.go.dev/go/ast#ChanType
#[derive(Debug)]
pub struct ChanType<'a> {
pub begin: Position<'a>, // position of "chan" keyword or "<-" (whichever comes first)
pub arrow: Option<Position<'a>>, // position of "<-" (token.NoPos if there is no "<-")
pub dir: u8, // channel direction
pub value: Box<Expr<'a>>, // value type
}
// htt/opt/visual-studio-code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.htmlps://pkg.go.dev/go/ast#IndexExpr
#[derive(Debug)]
pub struct IndexExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub lbrack: Position<'a>, // position of "["
pub index: Box<Expr<'a>>, // index expression
pub rbrack: Position<'a>, // position of "]"
}
// https://pkg.go.dev/go/ast#MapType
#[derive(Debug)]
pub struct MapType<'a> {
pub map: Position<'a>,
pub key: Box<Expr<'a>>,
pub value: Box<Expr<'a>>,
}
// https://pkg.go.dev/go/ast#CompositeLit
#[derive(Debug)]
pub struct CompositeLit<'a> {
pub type_: Box<Expr<'a>>, // literal type; or nil
pub lbrace: Position<'a>, // position of "{"
pub elts: Option<Vec<Expr<'a>>>, // list of composite elements; or nil
pub rbrace: Position<'a>, // position of "}"
pub incomplete: bool, // true if (source) expressions are missing in the Elts list
}
// https://pkg.go.dev/go/ast#KeyValueExpr
#[derive(Debug)]
pub struct KeyValueExpr<'a> {
pub key: Box<Expr<'a>>,
pub colon: Position<'a>, // position of ":"
pub value: Box<Expr<'a>>,
}
// https://pkg.go.dev/go/ast#ArrayType
#[derive(Debug)]
pub struct ArrayType<'a> {
pub lbrack: Position<'a>, // position of "["
pub len: Option<Box<Expr<'a>>>, // Ellipsis node for [...]T array types, nil for slice types
pub elt: Box<Expr<'a>>, // element type
}
// https://pkg.go.dev/go/ast#ChanDir
#[derive(Debug)]
pub enum ChanDir {
SEND = 1 << 0,
RECV = 1 << 1,
}
// https://pkg.go.dev/go/ast#Spec
#[derive(Debug)]
pub enum Spec {}
// https://pkg.go.dev/go/ast#Expr
#[derive(Debug)]
pub enum Expr<'a> {
ArrayType(ArrayType<'a>),
BasicLit(BasicLit<'a>),
BinaryExpr(BinaryExpr<'a>),
CallExpr(CallExpr<'a>),
ChanType(ChanType<'a>),
CompositeLit(CompositeLit<'a>),
Ellipsis(Ellipsis<'a>),
FuncLit(FuncLit<'a>),
FuncType(FuncType<'a>),
Ident(Ident<'a>),
IndexExpr(IndexExpr<'a>),
InterfaceType(InterfaceType<'a>),
KeyValueExpr(KeyValueExpr<'a>),
MapType(MapType<'a>),
ParenExpr(ParenExpr<'a>),
SelectorExpr(SelectorExpr<'a>),
SliceExpr(SliceExpr<'a>),
StarExpr(StarExpr<'a>),
StructType(StructType<'a>),
TypeAssertExpr(TypeAssertExpr<'a>),
UnaryExpr(UnaryExpr<'a>),
}
// https://pkg.go.dev/go/ast#Stmt
#[derive(Debug)]
pub enum Stmt {}

View File

@@ -0,0 +1,948 @@
// https://golang.org/ref/spec#Lexical_elements
use crate::parser_go_token::{Position, Token};
use phf::{phf_map, Map};
use std::fmt;
use unicode_general_category::{get_general_category, GeneralCategory};
pub type Step<'a> = (Position<'a>, Token, &'a str);
#[derive(Debug)]
pub enum ScannerError {
HexadecimalNotFound,
OctalNotFound,
UnterminatedComment,
UnterminatedEscapedChar,
UnterminatedRune,
UnterminatedString,
InvalidDirective,
}
impl std::error::Error for ScannerError {}
impl fmt::Display for ScannerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "scanner error: {:?}", self)
}
}
pub type Result<T> = std::result::Result<T, ScannerError>;
#[derive(Debug)]
pub struct Scanner<'a> {
directory: &'a str,
file: &'a str,
buffer: &'a str,
//
chars: std::iter::Peekable<std::str::Chars<'a>>,
current_char: Option<char>,
current_char_len: usize,
//
offset: usize,
line: usize,
column: usize,
start_offset: usize,
start_line: usize,
start_column: usize,
//
hide_column: bool,
insert_semi: bool,
pending_line_info: Option<LineInfo<'a>>,
}
type LineInfo<'a> = (Option<&'a str>, usize, Option<usize>, bool);
impl<'a> Scanner<'a> {
pub fn new(filename: &'a str, buffer: &'a str) -> Self {
let (directory, file) = filename.rsplit_once('/').unwrap_or(("", filename));
let mut s = Scanner {
directory,
file,
buffer,
//
chars: buffer.chars().peekable(),
current_char: None,
current_char_len: 0,
//
offset: 0,
line: 1,
column: 1,
start_offset: 0,
start_line: 1,
start_column: 1,
//
hide_column: false,
insert_semi: false,
pending_line_info: None,
};
s.next(); // read the first character
s
}
#[allow(clippy::cognitive_complexity)] // Allow complex scan function
pub fn scan(&mut self) -> Result<Step<'a>> {
let insert_semi = self.insert_semi;
self.insert_semi = false;
while let Some(c) = self.current_char {
self.reset_start();
match c {
' ' | '\t' | '\r' => {
self.next();
}
'\n' => {
self.next();
if insert_semi {
return Ok((self.position(), Token::SEMICOLON, "\n"));
}
}
_ => break,
}
}
if let Some(c) = self.current_char {
match c {
'+' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::ADD_ASSIGN, ""));
}
Some('+') => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::INC, ""));
}
_ => return Ok((self.position(), Token::ADD, "")),
}
}
'-' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::SUB_ASSIGN, ""));
}
Some('-') => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::DEC, ""));
}
_ => return Ok((self.position(), Token::SUB, "")),
}
}
'*' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::MUL_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::MUL, "")),
}
}
'/' => match self.peek() {
Some('=') => {
self.next();
self.next();
return Ok((self.position(), Token::QUO_ASSIGN, ""));
}
Some('/') => {
if insert_semi {
return Ok((self.position(), Token::SEMICOLON, "\n"));
}
return self.scan_line_comment();
}
Some('*') => {
if insert_semi && self.find_line_end() {
return Ok((self.position(), Token::SEMICOLON, "\n"));
}
return self.scan_general_comment();
}
_ => {
self.next();
return Ok((self.position(), Token::QUO, ""));
}
},
'%' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::REM_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::REM, "")),
}
}
'&' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::AND_ASSIGN, ""));
}
Some('&') => {
self.next();
return Ok((self.position(), Token::LAND, ""));
}
Some('^') => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::AND_NOT_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::AND_NOT, "")),
}
}
_ => return Ok((self.position(), Token::AND, "")),
}
}
'|' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::OR_ASSIGN, ""));
}
Some('|') => {
self.next();
return Ok((self.position(), Token::LOR, ""));
}
_ => return Ok((self.position(), Token::OR, "")),
}
}
'^' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::XOR_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::XOR, "")),
}
}
'<' => {
self.next();
match self.current_char {
Some('<') => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::SHL_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::SHL, "")),
}
}
Some('=') => {
self.next();
return Ok((self.position(), Token::LEQ, ""));
}
Some('-') => {
self.next();
return Ok((self.position(), Token::ARROW, ""));
}
_ => return Ok((self.position(), Token::LSS, "")),
}
}
'>' => {
self.next();
match self.current_char {
Some('>') => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::SHR_ASSIGN, ""));
}
_ => {
return Ok((self.position(), Token::SHR, ""));
}
}
}
Some('=') => {
self.next();
return Ok((self.position(), Token::GEQ, ""));
}
_ => return Ok((self.position(), Token::GTR, "")),
}
}
':' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::DEFINE, ""));
}
_ => return Ok((self.position(), Token::COLON, "")),
}
}
'!' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::NEQ, ""));
}
_ => return Ok((self.position(), Token::NOT, "")),
}
}
',' => {
self.next();
return Ok((self.position(), Token::COMMA, ""));
}
'(' => {
self.next();
return Ok((self.position(), Token::LPAREN, ""));
}
')' => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::RPAREN, ""));
}
'[' => {
self.next();
return Ok((self.position(), Token::LBRACK, ""));
}
']' => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::RBRACK, ""));
}
'{' => {
self.next();
return Ok((self.position(), Token::LBRACE, ""));
}
'}' => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::RBRACE, ""));
}
';' => {
self.next();
return Ok((self.position(), Token::SEMICOLON, ";"));
}
'.' => {
self.next();
match self.current_char {
Some('0'..='9') => return self.scan_int_or_float_or_imag(true),
Some('.') => match self.peek() {
Some('.') => {
self.next();
self.next();
return Ok((self.position(), Token::ELLIPSIS, ""));
}
_ => return Ok((self.position(), Token::PERIOD, "")),
},
_ => return Ok((self.position(), Token::PERIOD, "")),
}
}
'=' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::EQL, ""));
}
_ => return Ok((self.position(), Token::ASSIGN, "")),
}
}
'0'..='9' => return self.scan_int_or_float_or_imag(false),
'\'' => return self.scan_rune(),
'"' => return self.scan_interpreted_string(),
'`' => return self.scan_raw_string(),
_ => return self.scan_pkg_or_keyword_or_ident(),
};
}
self.reset_start();
if insert_semi {
Ok((self.position(), Token::SEMICOLON, "\n"))
} else {
Ok((self.position(), Token::EOF, ""))
}
}
// https://golang.org/ref/spec#Keywords
// https://golang.org/ref/spec#Identifiers
fn scan_pkg_or_keyword_or_ident(&mut self) -> Result<Step<'a>> {
self.next();
while let Some(c) = self.current_char {
if !(is_letter(c) || is_unicode_digit(c)) {
break;
}
self.next()
}
let pos = self.position();
let literal = self.literal();
if literal.len() > 1 {
if let Some(&token) = KEYWORDS.get(literal) {
self.insert_semi = matches!(
token,
Token::BREAK | Token::CONTINUE | Token::FALLTHROUGH | Token::RETURN
);
return Ok((pos, token, literal));
}
}
self.insert_semi = true;
Ok((pos, Token::IDENT, literal))
}
// https://golang.org/ref/spec#Integer_literals
// https://golang.org/ref/spec#Floating-point_literals
// https://golang.org/ref/spec#Imaginary_literals
fn scan_int_or_float_or_imag(&mut self, preceding_dot: bool) -> Result<Step<'a>> {
self.insert_semi = true;
let mut token = Token::INT;
let mut digits = "_0123456789";
let mut exp = "eE";
if !preceding_dot {
if matches!(self.current_char, Some('0')) {
self.next();
match self.current_char {
Some('b' | 'B') => {
digits = "_01";
exp = "";
self.next();
}
Some('o' | 'O') => {
digits = "_01234567";
exp = "";
self.next();
}
Some('x' | 'X') => {
digits = "_0123456789abcdefABCDEF";
exp = "pP";
self.next();
}
_ => {}
};
}
while let Some(c) = self.current_char {
if !digits.contains(c) {
break;
}
self.next();
}
}
if preceding_dot || matches!(self.current_char, Some('.')) {
token = Token::FLOAT;
self.next();
while let Some(c) = self.current_char {
if !digits.contains(c) {
break;
}
self.next();
}
}
if !exp.is_empty() {
if let Some(c) = self.current_char {
if exp.contains(c) {
token = Token::FLOAT;
self.next();
if matches!(self.current_char, Some('-' | '+')) {
self.next();
}
while let Some(c) = self.current_char {
if !matches!(c, '_' | '0'..='9') {
break;
}
self.next();
}
}
}
}
if matches!(self.current_char, Some('i')) {
token = Token::IMAG;
self.next();
}
Ok((self.position(), token, self.literal()))
}
// https://golang.org/ref/spec#Rune_literals
fn scan_rune(&mut self) -> Result<Step<'a>> {
self.insert_semi = true;
self.next();
match self.current_char {
Some('\\') => self.require_escaped_char::<'\''>()?,
Some(_) => self.next(),
_ => return Err(ScannerError::UnterminatedRune),
}
if matches!(self.current_char, Some('\'')) {
self.next();
return Ok((self.position(), Token::CHAR, self.literal()));
}
Err(ScannerError::UnterminatedRune)
}
// https://golang.org/ref/spec#String_literals
fn scan_interpreted_string(&mut self) -> Result<Step<'a>> {
self.insert_semi = true;
self.next();
while let Some(c) = self.current_char {
match c {
'"' => {
self.next();
return Ok((self.position(), Token::STRING, self.literal()));
}
'\\' => self.require_escaped_char::<'"'>()?,
_ => self.next(),
}
}
Err(ScannerError::UnterminatedString)
}
// https://golang.org/ref/spec#String_literals
fn scan_raw_string(&mut self) -> Result<Step<'a>> {
self.insert_semi = true;
self.next();
while let Some(c) = self.current_char {
match c {
'`' => {
self.next();
return Ok((self.position(), Token::STRING, self.literal()));
}
_ => self.next(),
}
}
Err(ScannerError::UnterminatedString)
}
// https://golang.org/ref/spec#Comments
fn scan_general_comment(&mut self) -> Result<Step<'a>> {
self.next();
self.next();
while let Some(c) = self.current_char {
match c {
'*' => {
self.next();
if matches!(self.current_char, Some('/')) {
self.next();
let pos = self.position();
let lit = self.literal();
// look for compiler directives
self.directive(&lit["/*".len()..lit.len() - "*/".len()], true)?;
return Ok((pos, Token::COMMENT, lit));
}
}
_ => self.next(),
}
}
Err(ScannerError::UnterminatedComment)
}
// https://golang.org/ref/spec#Comments
fn scan_line_comment(&mut self) -> Result<Step<'a>> {
self.next();
self.next();
while let Some(c) = self.current_char {
if is_newline(c) {
break;
}
self.next();
}
let pos = self.position();
let lit = self.literal();
// look for compiler directives (at the beginning of line)
if self.start_column == 1 {
self.directive(lit["//".len()..].trim_end(), false)?;
}
Ok((pos, Token::COMMENT, self.literal()))
}
// https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives
fn directive(&mut self, input: &'a str, immediate: bool) -> Result<()> {
if let Some(line_directive) = input.strip_prefix("line ") {
self.pending_line_info = self.parse_line_directive(line_directive)?;
if immediate {
self.consume_pending_line_info();
}
}
Ok(())
}
fn parse_line_directive(&mut self, line_directive: &'a str) -> Result<Option<LineInfo<'a>>> {
if let Some((file, line)) = line_directive.rsplit_once(':') {
let line = line.parse().map_err(|_| ScannerError::InvalidDirective)?;
if let Some((file, l)) = file.rsplit_once(':') {
if let Ok(l) = l.parse() {
//line :line:col
//line filename:line:col
/*line :line:col*/
/*line filename:line:col*/
let file = if !file.is_empty() { Some(file) } else { None };
let col = Some(line);
let line = l;
let hide_column = false;
return Ok(Some((file, line, col, hide_column)));
}
}
//line :line
//line filename:line
/*line :line*/
/*line filename:line*/
Ok(Some((Some(file), line, None, true)))
} else {
Ok(None)
}
}
const fn find_line_end(&self) -> bool {
let buffer = self.buffer.as_bytes();
let mut in_comment = true;
let mut i = self.offset;
let max = self.buffer.len();
while i < max {
let c = buffer[i] as char;
if i < max - 1 {
let n = buffer[i + 1] as char;
if !in_comment && c == '/' && n == '/' {
return true;
}
if c == '/' && n == '*' {
i += 2;
in_comment = true;
continue;
}
if c == '*' && n == '/' {
i += 2;
in_comment = false;
continue;
}
}
if is_newline(c) {
return true;
}
if !in_comment && !matches!(c, ' ' | '\t' | '\r') {
return false;
}
i += 1;
}
!in_comment
}
fn consume_pending_line_info(&mut self) {
if let Some(line_info) = self.pending_line_info.take() {
if let Some(file) = line_info.0 {
self.file = file;
}
self.line = line_info.1;
if let Some(column) = line_info.2 {
self.column = column;
}
self.hide_column = line_info.3;
}
}
fn peek(&mut self) -> Option<char> {
self.chars.peek().copied()
}
fn next(&mut self) {
self.offset += self.current_char_len;
self.column += self.current_char_len;
let last_char = self.current_char;
self.current_char = self.chars.next();
if let Some(c) = self.current_char {
self.current_char_len = c.len_utf8();
if matches!(last_char, Some('\n')) {
self.line += 1;
self.column = 1;
self.consume_pending_line_info();
}
} else {
self.current_char_len = 0
}
}
const fn position(&self) -> Position<'a> {
Position {
directory: self.directory,
file: self.file,
offset: self.start_offset,
line: self.start_line,
column: if self.hide_column {
0
} else {
self.start_column
},
}
}
fn reset_start(&mut self) {
self.start_offset = self.offset;
self.start_line = self.line;
self.start_column = self.column;
}
fn literal(&self) -> &'a str {
&self.buffer[self.start_offset..self.offset]
}
fn require_escaped_char<const DELIM: char>(&mut self) -> Result<()> {
self.next();
let c = self
.current_char
.ok_or(ScannerError::UnterminatedEscapedChar)?;
// TODO: move this to the match when const generics can be referenced in patterns
if c == DELIM {
self.next();
return Ok(());
}
match c {
'a' | 'b' | 'f' | 'n' | 'r' | 't' | 'v' | '\\' => self.next(),
'x' => {
self.next();
self.require_hex_digits::<2>()?
}
'u' => {
self.next();
self.require_hex_digits::<4>()?;
}
'U' => {
self.next();
self.require_hex_digits::<8>()?;
}
'0'..='7' => self.require_octal_digits::<3>()?,
_ => return Err(ScannerError::UnterminatedEscapedChar),
}
Ok(())
}
fn require_octal_digits<const COUNT: usize>(&mut self) -> Result<()> {
for _ in 0..COUNT {
let c = self.current_char.ok_or(ScannerError::OctalNotFound)?;
if !is_octal_digit(c) {
return Err(ScannerError::OctalNotFound);
}
self.next();
}
Ok(())
}
fn require_hex_digits<const COUNT: usize>(&mut self) -> Result<()> {
for _ in 0..COUNT {
let c = self.current_char.ok_or(ScannerError::HexadecimalNotFound)?;
if !is_hex_digit(c) {
return Err(ScannerError::HexadecimalNotFound);
}
self.next();
}
Ok(())
}
}
impl<'a> IntoIterator for Scanner<'a> {
type Item = Result<Step<'a>>;
type IntoIter = IntoIter<'a>;
fn into_iter(self) -> Self::IntoIter {
Self::IntoIter::new(self)
}
}
pub struct IntoIter<'a> {
scanner: Scanner<'a>,
done: bool,
}
impl<'a> IntoIter<'a> {
const fn new(scanner: Scanner<'a>) -> Self {
Self { scanner, done: false }
}
}
impl<'a> Iterator for IntoIter<'a> {
type Item = Result<Step<'a>>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
match self.scanner.scan() {
Ok((pos, tok, lit)) => {
if tok == Token::EOF {
self.done = true;
}
Some(Ok((pos, tok, lit)))
}
Err(err) => {
self.done = true;
Some(Err(err))
}
}
}
}
// https://golang.org/ref/spec#Letters_and_digits
fn is_letter(c: char) -> bool {
c == '_' || is_unicode_letter(c)
}
//const fn is_decimal_digit(c: char) -> bool {
//matches!(c, '0'..='9')
//}
//const fn is_binary_digit(c: char) -> bool {
//matches!(c, '0'..='1')
//}
const fn is_octal_digit(c: char) -> bool {
matches!(c, '0'..='7')
}
const fn is_hex_digit(c: char) -> bool {
matches!(c, '0'..='9' | 'A'..='F' | 'a'..='f')
}
// https://golang.org/ref/spec#Characters
const fn is_newline(c: char) -> bool {
c == '\n'
}
//const fn is_unicode_char(c: char) -> bool {
//c != '\n'
//}
fn is_unicode_letter(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::UppercaseLetter
| GeneralCategory::LowercaseLetter
| GeneralCategory::TitlecaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
)
}
fn is_unicode_digit(c: char) -> bool {
get_general_category(c) == GeneralCategory::DecimalNumber
}
// https://golang.org/ref/spec#Keywords
static KEYWORDS: Map<&'static str, Token> = phf_map! {
"break" => Token::BREAK,
"case" => Token::CASE,
"chan" => Token::CHAN,
"const" => Token::CONST,
"continue" => Token::CONTINUE,
"default" => Token::DEFAULT,
"defer" => Token::DEFER,
"else" => Token::ELSE,
"fallthrough" => Token::FALLTHROUGH,
"for" => Token::FOR,
"func" => Token::FUNC,
"go" => Token::GO,
"goto" => Token::GOTO,
"if" => Token::IF,
"import" => Token::IMPORT,
"interface" => Token::INTERFACE,
"map" => Token::MAP,
"package" => Token::PACKAGE,
"range" => Token::RANGE,
"return" => Token::RETURN,
"select" => Token::SELECT,
"struct" => Token::STRUCT,
"switch" => Token::SWITCH,
"type" => Token::TYPE,
"var" => Token::VAR,
};
#[cfg(test)]
mod tests {
use super::Scanner;
#[test] // fuzz
fn it_should_return_an_error_on_missing_line_number() {
let input = "/*line :*/";
let mut out: Vec<_> = Scanner::new(file!(), input).into_iter().collect();
assert!(out.pop().unwrap().is_err());
}
}

View File

@@ -0,0 +1,273 @@
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/go/token/token.go
#![allow(non_camel_case_types)] // For consistency with the Go tokens
use std::fmt;
#[derive(Clone, Copy, Debug, Default)]
pub struct Position<'a> {
pub directory: &'a str,
pub file: &'a str,
pub offset: usize,
pub line: usize,
pub column: usize,
}
impl<'a> fmt::Display for Position<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.file.is_empty() {
write!(f, ":{}:{}", self.line, self.column)
} else if self.file.starts_with('/') {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
} else {
write!(
f,
"{}/{}:{}:{}",
self.directory, self.file, self.line, self.column
)
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Token {
EOF,
COMMENT,
IDENT, // main
INT, // 12345
FLOAT, // 123.45
IMAG, // 123.45i
CHAR, // 'a'
STRING, // "abc"
ADD, // +
SUB, // -
MUL, // *
QUO, // /
REM, // %
AND, // &
OR, // |
XOR, // ^
SHL, // <<
SHR, // >>
AND_NOT, // &^
ADD_ASSIGN, // +=
SUB_ASSIGN, // -=
MUL_ASSIGN, // *=
QUO_ASSIGN, // /=
REM_ASSIGN, // %=
AND_ASSIGN, // &=
OR_ASSIGN, // |=
XOR_ASSIGN, // ^=
SHL_ASSIGN, // <<=
SHR_ASSIGN, // >>=
AND_NOT_ASSIGN, // &^=
LAND, // &&
LOR, // ||
ARROW, // <-
INC, // ++
DEC, // --
EQL, // ==
LSS, // <
GTR, // >
ASSIGN, // =
NOT, // !
NEQ, // !=
LEQ, // <=
GEQ, // >=
DEFINE, // :=
ELLIPSIS, // ...
LPAREN, // (
LBRACK, // [
LBRACE, // {
COMMA, // ,
PERIOD, // .
RPAREN, // )
RBRACK, // ]
RBRACE, // }
SEMICOLON, // ;
COLON, // :
BREAK,
CASE,
CHAN,
CONST,
CONTINUE,
DEFAULT,
DEFER,
ELSE,
FALLTHROUGH,
FOR,
FUNC,
GO,
GOTO,
IF,
IMPORT,
INTERFACE,
MAP,
PACKAGE,
RANGE,
RETURN,
SELECT,
STRUCT,
SWITCH,
TYPE,
VAR,
}
impl Token {
pub const fn is_assign_op(&self) -> bool {
use Token::*;
matches!(
self,
ADD_ASSIGN
| SUB_ASSIGN
| MUL_ASSIGN
| QUO_ASSIGN
| REM_ASSIGN
| AND_ASSIGN
| OR_ASSIGN
| XOR_ASSIGN
| SHL_ASSIGN
| SHR_ASSIGN
| AND_NOT_ASSIGN
)
}
// https://go.dev/ref/spec#Operator_precedence
pub fn precedence(&self) -> u8 {
use Token::*;
match self {
MUL | QUO | REM | SHL | SHR | AND | AND_NOT => 5,
ADD | SUB | OR | XOR => 4,
EQL | NEQ | LSS | LEQ | GTR | GEQ => 3,
LAND => 2,
LOR => 1,
_ => unreachable!(
"precedence() is only supported for binary operators, called with: {:?}",
self
),
}
}
pub const fn lowest_precedence() -> u8 {
0
}
}
impl From<&Token> for &'static str {
fn from(token: &Token) -> Self {
use Token::*;
match token {
EOF => "EOF",
COMMENT => "COMMENT",
IDENT => "IDENT",
INT => "INT",
FLOAT => "FLOAT",
IMAG => "IMAG",
CHAR => "CHAR",
STRING => "STRING",
ADD => "+",
SUB => "-",
MUL => "*",
QUO => "/",
REM => "%",
AND => "&",
OR => "|",
XOR => "^",
SHL => "<<",
SHR => ">>",
AND_NOT => "&^",
ADD_ASSIGN => "+=",
SUB_ASSIGN => "-=",
MUL_ASSIGN => "*=",
QUO_ASSIGN => "/=",
REM_ASSIGN => "%=",
AND_ASSIGN => "&=",
OR_ASSIGN => "|=",
XOR_ASSIGN => "^=",
SHL_ASSIGN => "<<=",
SHR_ASSIGN => ">>=",
AND_NOT_ASSIGN => "&^=",
LAND => "&&",
LOR => "||",
ARROW => "<-",
INC => "++",
DEC => "--",
EQL => "==",
LSS => "<",
GTR => ">",
ASSIGN => "=",
NOT => "!",
NEQ => "!=",
LEQ => "<=",
GEQ => ">=",
DEFINE => ":=",
ELLIPSIS => "...",
LPAREN => "(",
LBRACK => "[",
LBRACE => "{",
COMMA => ",",
PERIOD => ".",
RPAREN => ")",
RBRACK => "]",
RBRACE => "}",
SEMICOLON => ";",
COLON => ":",
BREAK => "break",
CASE => "case",
CHAN => "chan",
CONST => "const",
CONTINUE => "continue",
DEFAULT => "default",
DEFER => "defer",
ELSE => "else",
FALLTHROUGH => "fallthrough",
FOR => "for",
FUNC => "func",
GO => "go",
GOTO => "goto",
IF => "if",
IMPORT => "import",
INTERFACE => "interface",
MAP => "map",
PACKAGE => "package",
RANGE => "range",
RETURN => "return",
SELECT => "select",
STRUCT => "struct",
SWITCH => "switch",
TYPE => "type",
VAR => "var",
}
}
}

View File

@@ -17,8 +17,10 @@ use serde_json::json;
use windmill_common::error; use windmill_common::error;
use windmill_parser::{json_to_typ, Arg, MainArgSignature, Typ}; use windmill_parser::{json_to_typ, Arg, MainArgSignature, Typ};
use rustpython_parser as parser; use rustpython_parser::{
use rustpython_parser::ast::{Constant, ExprKind, Located, StmtKind}; ast::{Constant, ExprKind, Located, StmtKind},
parser,
};
const DEF_MAIN: &str = "def main("; const DEF_MAIN: &str = "def main(";
const FUNCTION_CALL: &str = "<function call>"; const FUNCTION_CALL: &str = "<function call>";
@@ -181,7 +183,6 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
"git" => "GitPython", "git" => "GitPython",
"u" => "requests", "u" => "requests",
"f" => "requests", "f" => "requests",
"." => "requests",
"shopify" => "ShopifyAPI", "shopify" => "ShopifyAPI",
"seleniumwire" => "selenium-wire", "seleniumwire" => "selenium-wire",
"openbb-terminal" => "openbb[all]", "openbb-terminal" => "openbb[all]",
@@ -218,29 +219,20 @@ pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
let ast = parser::parse_program(code, "main.py").map_err(|e| { let ast = parser::parse_program(code, "main.py").map_err(|e| {
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string())) error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
})?; })?;
let mut imports: Vec<String> = ast let imports = ast
.into_iter() .into_iter()
.filter_map(|x| match x { .filter_map(|x| match x {
Located { node, .. } => match node { Located { node, .. } => match node {
StmtKind::Import { names } => Some( StmtKind::Import { names } => Some(
names names
.into_iter() .into_iter()
.map(|x| { .map(|x| x.node.name.split('.').next().unwrap_or("").to_string())
let name = x.node.name;
if name.starts_with('.') {
".".to_string()
} else {
name.split('.').next().unwrap_or("").to_string()
}
})
.map(replace_import) .map(replace_import)
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
), ),
StmtKind::ImportFrom { level: Some(i), .. } if i > 0 => {
Some(vec!["requests".to_string()])
}
StmtKind::ImportFrom { level: _, module: Some(mod_), names: _ } => { StmtKind::ImportFrom { level: _, module: Some(mod_), names: _ } => {
let imprt = mod_.split('.').next().unwrap_or("").replace("_", "-"); let imprt = mod_.split('.').next().unwrap_or("").replace("_", "-");
Some(vec![replace_import(imprt)]) Some(vec![replace_import(imprt)])
} }
_ => None, _ => None,
@@ -250,7 +242,7 @@ pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
.filter(|x| !STDIMPORTS.contains(&x.as_str())) .filter(|x| !STDIMPORTS.contains(&x.as_str()))
.unique() .unique()
.collect(); .collect();
imports.sort();
Ok(imports) Ok(imports)
} }
} }
@@ -452,7 +444,6 @@ import os
import wmill import wmill
from zanzibar.estonie import talin from zanzibar.estonie import talin
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from . import tests
def main(): def main():
pass pass
@@ -460,7 +451,7 @@ def main():
"; ";
let r = parse_python_imports(code)?; let r = parse_python_imports(code)?;
// println!("{}", serde_json::to_string(&r)?); // println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["matplotlib", "requests", "wmill", "zanzibar"]); assert_eq!(r, vec!["wmill", "zanzibar", "matplotlib"]);
Ok(()) Ok(())
} }

View File

@@ -6,22 +6,21 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use deno_core::{serde_v8, v8, JsRuntime, RuntimeOptions}; use deno_core::{serde_v8, v8, JsRuntime, RuntimeOptions};
use serde_json::Value;
use windmill_common::error; use windmill_common::error;
use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectProperty, Typ}; use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectProperty, Typ};
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Span, Spanned}; use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Spanned};
use swc_ecma_ast::{ use swc_ecma_ast::{
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident, Lit, ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident, Lit,
ModuleDecl, ModuleItem, Number, ObjectLit, Param, Pat, Str, TsArrayType, TsEntityName, ModuleDecl, ModuleItem, Number, ObjectLit, Pat, Str, TsArrayType, TsEntityName, TsKeywordType,
TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType, TsOptionalType, TsPropertySignature, TsKeywordTypeKind, TsLit, TsLitType, TsOptionalType, TsPropertySignature, TsType,
TsType, TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType, TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
}; };
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig}; use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
pub fn parse_deno_signature(code: &str, skip_dflt: bool) -> error::Result<MainArgSignature> { pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
let cm: Lrc<SourceMap> = Default::default(); let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("main.ts".into()), code.into()); let fm = cm.new_source_file(FileName::Custom("test.ts".into()), code.into());
let lexer = Lexer::new( let lexer = Lexer::new(
// We want to parse ecmascript // We want to parse ecmascript
Syntax::Typescript(TsConfig::default()), Syntax::Typescript(TsConfig::default()),
@@ -55,17 +54,65 @@ pub fn parse_deno_signature(code: &str, skip_dflt: bool) -> error::Result<MainAr
})) if &sym.to_string() == "main" => Some(function.params), })) if &sym.to_string() == "main" => Some(function.params),
_ => None, _ => None,
}); });
if let Some(params) = params { if let Some(params) = params {
let r = MainArgSignature { Ok(MainArgSignature {
star_args: false, star_args: false,
star_kwargs: false, star_kwargs: false,
args: params args: params
.into_iter() .into_iter()
.map(|x| parse_param(x, &cm, skip_dflt)) .map(|x| match x.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(&ident);
Ok(Arg {
otyp: None,
name,
typ,
default: None,
has_default: ident.id.optional || nullable,
})
}
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) =
left.as_ident().map(binding_ident_to_arg).ok_or_else(|| {
error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(left.span())
.unwrap_or_else(|_| cm.span_to_string(left.span()))
))
})?;
let span = match *right {
Expr::Lit(Lit::Str(Str { span, .. })) => Some(span),
Expr::Lit(Lit::Num(Number { span, .. })) => Some(span),
Expr::Lit(Lit::BigInt(BigInt { span, .. })) => Some(span),
Expr::Lit(Lit::Bool(Bool { span, .. })) => Some(span),
Expr::Object(ObjectLit { span, .. }) => Some(span),
Expr::Array(ArrayLit { span, .. }) => Some(span),
_ => None,
};
let expr = span
.and_then(|x| cm.span_to_snippet(x).ok())
.map(|x| serde_json::from_str(&x).map_err(|_| x));
let default = match expr.clone() {
Some(Ok(x)) => Some(x),
Some(Err(x)) => eval_sync(&x).ok(),
None => None,
};
if typ == Typ::Unknown && default.is_some() {
typ = json_to_typ(default.as_ref().unwrap());
}
Ok(Arg { otyp: None, name, typ, default, has_default: true })
}
_ => Err(error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(x.span())
.unwrap_or_else(|_| cm.span_to_string(x.span()))
))),
})
.collect::<Result<Vec<Arg>, error::Error>>()?, .collect::<Result<Vec<Arg>, error::Error>>()?,
}; })
Ok(r)
} else { } else {
Err(error::Error::ExecutionErr( Err(error::Error::ExecutionErr(
"main function was not findable (expected to find 'export function main(...)'" "main function was not findable (expected to find 'export function main(...)'"
@@ -74,75 +121,6 @@ pub fn parse_deno_signature(code: &str, skip_dflt: bool) -> error::Result<MainAr
} }
} }
fn parse_param(x: Param, cm: &Lrc<SourceMap>, skip_dflt: bool) -> error::Result<Arg> {
let r = match x.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(&ident);
Ok(Arg {
otyp: None,
name,
typ,
default: None,
has_default: ident.id.optional || nullable,
})
}
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) =
left.as_ident().map(binding_ident_to_arg).ok_or_else(|| {
error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(left.span())
.unwrap_or_else(|_| cm.span_to_string(left.span()))
))
})?;
let dflt = if skip_dflt {
None
} else {
match *right {
Expr::Lit(Lit::Str(Str { value, .. })) => {
Some(Value::String(value.to_string()))
}
Expr::Lit(Lit::Num(Number { value, .. }))
if (value == (value as u64) as f64) =>
{
Some(serde_json::json!(value as u64))
}
Expr::Lit(Lit::Num(Number { value, .. })) => Some(serde_json::json!(value)),
Expr::Lit(Lit::BigInt(BigInt { value, .. })) => Some(serde_json::json!(value)),
Expr::Lit(Lit::Bool(Bool { value, .. })) => Some(Value::Bool(value)),
Expr::Object(ObjectLit { span, .. }) => eval_span(span, cm),
Expr::Array(ArrayLit { span, .. }) => eval_span(span, cm),
_ => None,
}
};
if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap());
}
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true })
}
_ => Err(error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(x.span())
.unwrap_or_else(|_| cm.span_to_string(x.span()))
))),
};
r
}
fn eval_span(span: Span, cm: &Lrc<SourceMap>) -> Option<Value> {
let expr = cm
.span_to_snippet(span)
.ok()
.map(|x| serde_json::from_str(&x).map_err(|_| x));
match expr {
Some(Ok(x)) => Some(x),
Some(Err(x)) => eval_sync(&x).ok(),
None => None,
}
}
fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) { fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) {
let (typ, nullable) = type_ann let (typ, nullable) = type_ann
.as_ref() .as_ref()
@@ -268,7 +246,7 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
pub fn eval_sync(code: &str) -> Result<serde_json::Value, String> { pub fn eval_sync(code: &str) -> Result<serde_json::Value, String> {
let mut context = JsRuntime::new(RuntimeOptions::default()); let mut context = JsRuntime::new(RuntimeOptions::default());
let code = format!("let x = {}; x", code); let code = format!("let x = {}; x", code);
let res = context.execute_script("<anon>", code); let res = context.execute_script("<anon>", &code);
match res { match res {
Ok(global) => { Ok(global) => {
let scope = &mut context.handle_scope(); let scope = &mut context.handle_scope();
@@ -304,7 +282,7 @@ export function main(test1?: string, test2: string = \"burkina\",
} }
"; ";
assert_eq!( assert_eq!(
parse_deno_signature(code, false)?, parse_deno_signature(code)?,
MainArgSignature { MainArgSignature {
star_args: false, star_args: false,
star_kwargs: false, star_kwargs: false,
@@ -416,7 +394,7 @@ export function main(test2 = \"burkina\",
} }
"; ";
assert_eq!( assert_eq!(
parse_deno_signature(code, false)?, parse_deno_signature(code)?,
MainArgSignature { MainArgSignature {
star_args: false, star_args: false,
star_kwargs: false, star_kwargs: false,

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,4 @@
#[cfg(feature = "enterprise")] #[cfg(feature = "enterprise")]
use base64::Engine;
#[cfg(feature = "enterprise")]
use rsa::{pkcs8::DecodePublicKey, signature::Verifier}; use rsa::{pkcs8::DecodePublicKey, signature::Verifier};
#[cfg(feature = "enterprise")] #[cfg(feature = "enterprise")]
use sha2::Sha256; use sha2::Sha256;
@@ -13,9 +11,9 @@ pub fn verify_license_key(license_key: Option<String>) -> anyhow::Result<()> {
.expect("license_key can be splitted with a ."); .expect("license_key can be splitted with a .");
let pub_key = rsa::RsaPublicKey::from_public_key_der( let pub_key = rsa::RsaPublicKey::from_public_key_der(
&base64::engine::general_purpose::STANDARD.decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB")?)?; &base64::decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB")?)?;
let msg = base64::engine::general_purpose::STANDARD.decode(splitted_lk.0)?; let msg = base64::decode(splitted_lk.0)?;
let signature = base64::engine::general_purpose::STANDARD.decode(splitted_lk.1)?; let signature = base64::decode(splitted_lk.1)?;
rsa::pss::VerifyingKey::<Sha256>::new(pub_key) rsa::pss::VerifyingKey::<Sha256>::new(pub_key)
.verify(&msg, &rsa::pss::Signature::from(signature)) .verify(&msg, &rsa::pss::Signature::from(signature))
.map_err(|_| anyhow::anyhow!("Invalid license key".to_string()))?; .map_err(|_| anyhow::anyhow!("Invalid license key".to_string()))?;

View File

@@ -6,16 +6,14 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::net::SocketAddr;
use git_version::git_version; use git_version::git_version;
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use windmill_common::{utils::rd_string, METRICS_ADDR}; use windmill_common::utils::rd_string;
use windmill_worker::WorkerConfig;
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version"); const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
const DEFAULT_NUM_WORKERS: usize = 3;
const DEFAULT_PORT: u16 = 8000;
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
mod ee; mod ee;
@@ -28,61 +26,25 @@ async fn main() -> anyhow::Result<()> {
let num_workers = std::env::var("NUM_WORKERS") let num_workers = std::env::var("NUM_WORKERS")
.ok() .ok()
.and_then(|x| x.parse::<i32>().ok()) .and_then(|x| x.parse::<i32>().ok())
.unwrap_or(DEFAULT_NUM_WORKERS as i32); .unwrap_or(windmill_common::DEFAULT_NUM_WORKERS as i32);
let metrics_addr: Option<SocketAddr> = *METRICS_ADDR; let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
let server_bind_address: IpAddr = std::env::var("SERVER_BIND_ADDR")
.ok() .ok()
.and_then(|x| x.parse().ok()) .map(|s| {
.unwrap_or(IpAddr::from(DEFAULT_SERVER_BIND_ADDR)); s.parse::<bool>()
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
let port: u16 = std::env::var("PORT") .or_else(|_| s.parse::<SocketAddr>().map(Some))
.ok() })
.and_then(|x| x.parse::<u16>().ok()) .transpose()?
.unwrap_or(DEFAULT_PORT as u16); .flatten();
let base_internal_url: String = std::env::var("BASE_INTERNAL_URL")
.unwrap_or_else(|_| format!("http://localhost:{}", port.to_string()));
let server_mode = !std::env::var("DISABLE_SERVER") let server_mode = !std::env::var("DISABLE_SERVER")
.ok() .ok()
.and_then(|x| x.parse::<bool>().ok()) .and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false); .unwrap_or(false);
let rsmq_config = std::env::var("REDIS_URL").ok().map(|x| {
let url = x.parse::<url::Url>().unwrap();
let mut config = rsmq_async::RsmqOptions { ..Default::default() };
config.host = url.host_str().expect("redis host required").to_owned();
config.password = url.password().map(|s| s.to_owned());
config.db = url
.path_segments()
.and_then(|mut segments| segments.next())
.and_then(|segment| segment.parse().ok())
.unwrap_or(0);
config.ns = url
.query_pairs()
.find(|s| s.0 == "rsmq_namespace")
.map(|s| s.1)
.unwrap_or(std::borrow::Cow::Borrowed("rsmq"))
.into_owned();
config.port = url.port().unwrap_or(6379).to_string();
config
});
let db = windmill_common::connect_db(server_mode).await?; let db = windmill_common::connect_db(server_mode).await?;
let rsmq = if let Some(config) = rsmq_config {
let mut rsmq = rsmq_async::MultiplexedRsmq::new(config).await.unwrap();
let _ = rsmq_async::RsmqConnection::create_queue(&mut rsmq, "main_queue", None, None, None)
.await;
Some(rsmq)
} else {
None
};
if server_mode { if server_mode {
windmill_api::migrate_db(&db).await?; windmill_api::migrate_db(&db).await?;
} }
@@ -90,159 +52,145 @@ async fn main() -> anyhow::Result<()> {
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3); let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
let shutdown_signal = windmill_common::shutdown_signal(tx); let shutdown_signal = windmill_common::shutdown_signal(tx);
#[cfg(feature = "enterprise")] let base_url = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
tracing::info!(
"
##############################
Windmill Enterprise Edition {GIT_VERSION}
##############################"
);
#[cfg(not(feature = "enterprise"))] let base_internal_url =
tracing::info!( std::env::var("BASE_INTERNAL_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
" let timeout = std::env::var("TIMEOUT")
############################## .ok()
Windmill Community Edition {GIT_VERSION} .and_then(|x| x.parse::<i32>().ok())
##############################" .unwrap_or(windmill_common::DEFAULT_TIMEOUT);
);
display_config(vec![
"DISABLE_NSJAIL",
"DISABLE_SERVER",
"NUM_WORKERS",
"METRICS_ADDR",
"JSON_FMT",
"BASE_URL",
"BASE_INTERNAL_URL",
"TIMEOUT",
"ZOMBIE_JOB_TIMEOUT",
"RESTART_ZOMBIE_JOBS",
"SLEEP_QUEUE",
"MAX_LOG_SIZE",
"SERVER_BIND_ADDR",
"PORT",
"KEEP_JOB_DIR",
"S3_CACHE_BUCKET",
"TAR_CACHE_RATE",
"COOKIE_DOMAIN",
"PYTHON_PATH",
"DENO_PATH",
"GO_PATH",
"GOPRIVATE",
"NETRC",
"PIP_INDEX_URL",
"PIP_EXTRA_INDEX_URL",
"PIP_TRUSTED_HOST",
"PATH",
"HOME",
"DATABASE_CONNECTIONS",
"TIMEOUT_WAIT_RESULT",
"QUEUE_LIMIT_WAIT_RESULT",
"DENO_AUTH_TOKENS",
"DENO_FLAGS",
"NPM_CONFIG_REGISTRY",
"PIP_LOCAL_DEPENDENCIES",
"ADDITIONAL_PYTHON_PATHS",
"INCLUDE_HEADERS",
"WHITELIST_WORKSPACES",
"BLACKLIST_WORKSPACES",
"INSTANCE_EVENTS_WEBHOOK",
"CLOUD_HOSTED",
]);
if server_mode || num_workers > 0 { if server_mode || num_workers > 0 {
let addr = SocketAddr::from((server_bind_address, port)); let addr = SocketAddr::from(([0, 0, 0, 0], 8000));
let rsmq2 = rsmq.clone(); let base_url2 = base_url.clone();
let server_f = async { let server_f = async {
if server_mode { if server_mode {
windmill_api::run_server(db.clone(), rsmq2, addr, rx.resubscribe()).await?; windmill_api::run_server(db.clone(), addr, base_url, rx.resubscribe()).await?;
} }
Ok(()) as anyhow::Result<()> Ok(()) as anyhow::Result<()>
}; };
let base_url = base_url2.clone();
let workers_f = async { let workers_f = async {
if num_workers > 0 { if num_workers > 0 {
let sleep_queue = std::env::var("SLEEP_QUEUE")
.ok()
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or(windmill_common::DEFAULT_SLEEP_QUEUE);
let disable_nuser = std::env::var("DISABLE_NUSER")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
let disable_nsjail = std::env::var("DISABLE_NSJAIL")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(true);
let keep_job_dir = std::env::var("KEEP_JOB_DIR")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
let license_key = std::env::var("LICENSE_KEY").ok();
let sync_bucket = std::env::var("S3_CACHE_BUCKET")
.ok()
.map(|e| Some(e))
.unwrap_or(None);
#[cfg(feature = "enterprise")]
tracing::info!(
"
##############################
Windmill Enterprise Edition {GIT_VERSION} LICENSE_KEY: {license_key:?}, S3_CACHE_BUCKET: {sync_bucket:?}
##############################"
);
#[cfg(not(feature = "enterprise"))]
tracing::info!(
"
##############################
Windmill Community Edition {GIT_VERSION}
##############################"
);
tracing::info!(
"DISABLE_NSJAIL: {disable_nsjail}, DISABLE_NUSER: {disable_nuser}, BASE_URL: \
{base_url}, SLEEP_QUEUE: {sleep_queue}, NUM_WORKERS: {num_workers}, TIMEOUT: \
{timeout}, KEEP_JOB_DIR: {keep_job_dir}"
);
run_workers( run_workers(
db.clone(), db.clone(),
rx.resubscribe(), addr,
timeout,
num_workers, num_workers,
base_internal_url.clone(), sleep_queue,
rsmq.clone(), WorkerConfig {
disable_nsjail,
disable_nuser,
base_internal_url,
base_url,
keep_job_dir,
},
rx.resubscribe(),
sync_bucket,
license_key,
) )
.await?; .await?;
} }
Ok(()) as anyhow::Result<()> Ok(()) as anyhow::Result<()>
}; };
let rsmq2 = rsmq.clone(); let base_url = base_url2;
let monitor_f = async { let monitor_f = async {
if server_mode { if server_mode {
monitor_db(&db, rx.resubscribe(), &base_internal_url, rsmq2); monitor_db(&db, timeout, base_url, rx.resubscribe());
} }
Ok(()) as anyhow::Result<()> Ok(()) as anyhow::Result<()>
}; };
let metrics_f = async { let metrics_f = async {
match metrics_addr { match metrics_addr {
Some(addr) => { Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
windmill_common::serve_metrics(addr, rx.resubscribe(), num_workers > 0) .await
.await .map_err(anyhow::Error::from),
.map_err(anyhow::Error::from)
}
None => Ok(()), None => Ok(()),
} }
}; };
futures::try_join!(shutdown_signal, server_f, metrics_f, workers_f, monitor_f)?; futures::try_join!(shutdown_signal, server_f, metrics_f, workers_f, monitor_f)?;
} else {
tracing::info!("Nothing to do, exiting.");
} }
Ok(()) Ok(())
} }
fn display_config(envs: Vec<&str>) { pub fn monitor_db(
tracing::info!(
"config: {}",
envs.iter()
.filter(|env| std::env::var(env).is_ok())
.map(|env| {
format!(
"{}: {}",
env,
std::env::var(env).unwrap_or_else(|_| "not set".to_string())
)
})
.collect::<Vec<String>>()
.join(", ")
)
}
pub fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
db: &Pool<Postgres>, db: &Pool<Postgres>,
timeout: i32,
base_url: String,
rx: tokio::sync::broadcast::Receiver<()>, rx: tokio::sync::broadcast::Receiver<()>,
base_internal_url: &str,
rsmq: Option<R>,
) { ) {
let db1 = db.clone(); let db1 = db.clone();
let db2 = db.clone(); let db2 = db.clone();
let rx2 = rx.resubscribe(); let rx2 = rx.resubscribe();
let base_internal_url = base_internal_url.to_string();
tokio::spawn(async move { tokio::spawn(async move {
windmill_worker::handle_zombie_jobs_periodically(&db1, rx, &base_internal_url, rsmq).await windmill_worker::handle_zombie_jobs_periodically(&db1, timeout, &base_url, rx).await
}); });
tokio::spawn(async move { windmill_api::delete_expired_items_perdiodically(&db2, rx2).await }); tokio::spawn(async move { windmill_api::delete_expired_items_perdiodically(&db2, rx2).await });
} }
pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>( pub async fn run_workers(
db: Pool<Postgres>, db: Pool<Postgres>,
rx: tokio::sync::broadcast::Receiver<()>, addr: SocketAddr,
timeout: i32,
num_workers: i32, num_workers: i32,
base_internal_url: String, sleep_queue: u64,
rsmq: Option<R>, worker_config: WorkerConfig,
rx: tokio::sync::broadcast::Receiver<()>,
mut periodic_script: Option<String>,
license_key: Option<String>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let license_key = std::env::var("LICENSE_KEY").ok();
#[cfg(feature = "enterprise")] #[cfg(feature = "enterprise")]
ee::verify_license_key(license_key)?; ee::verify_license_key(license_key)?;
@@ -250,6 +198,12 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
if license_key.is_some() { if license_key.is_some() {
panic!("License key is required ONLY for the enterprise edition"); panic!("License key is required ONLY for the enterprise edition");
} }
#[cfg(not(feature = "enterprise"))]
if !worker_config.disable_nsjail {
tracing::warn!(
"NSJAIL to sandbox process in untrusted environments is an enterprise feature but allowed to be used for testing purposes"
);
}
let instance_name = rd_string(5); let instance_name = rd_string(5);
let monitor = tokio_metrics::TaskMonitor::new(); let monitor = tokio_metrics::TaskMonitor::new();
@@ -269,19 +223,22 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
let worker_name = format!("dt-worker-{}-{}", &instance_name, rd_string(5)); let worker_name = format!("dt-worker-{}-{}", &instance_name, rd_string(5));
let ip = ip.clone(); let ip = ip.clone();
let rx = rx.resubscribe(); let rx = rx.resubscribe();
let base_internal_url = base_internal_url.clone(); let worker_config = worker_config.clone();
let rsmq2 = rsmq.clone(); let wp = periodic_script.take();
handles.push(tokio::spawn(monitor.instrument(async move { handles.push(tokio::spawn(monitor.instrument(async move {
tracing::info!(worker = %worker_name, "starting worker"); tracing::info!(addr = %addr.to_string(), worker = %worker_name, "starting worker");
windmill_worker::run_worker( windmill_worker::run_worker(
&db1, &db1,
timeout,
&instance_name, &instance_name,
worker_name, worker_name,
i as u64, i as u64,
num_workers as u64,
&ip, &ip,
sleep_queue,
worker_config,
wp,
rx, rx,
&base_internal_url,
rsmq2,
) )
.await .await
}))); })));

View File

@@ -1,5 +1,4 @@
use futures::{stream, Stream}; use futures::{stream, Stream};
use serde::Deserialize;
use serde_json::json; use serde_json::json;
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres, Transaction}; use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres, Transaction};
use windmill_api::jobs::{CompletedJob, Job}; use windmill_api::jobs::{CompletedJob, Job};
@@ -7,8 +6,10 @@ use windmill_common::{
flow_status::{FlowStatus, FlowStatusModule}, flow_status::{FlowStatus, FlowStatusModule},
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform}, flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
scripts::ScriptLang, scripts::ScriptLang,
DEFAULT_SLEEP_QUEUE,
}; };
use windmill_queue::{get_queued_job, JobPayload, RawCode}; use windmill_queue::{get_queued_job, JobPayload, RawCode};
use windmill_worker::WorkerConfig;
async fn initialize_tracing() { async fn initialize_tracing() {
use std::sync::Once; use std::sync::Once;
@@ -88,7 +89,14 @@ impl ApiServer {
let addr = sock.local_addr().unwrap(); let addr = sock.local_addr().unwrap();
drop(sock); drop(sock);
let task = tokio::task::spawn(windmill_api::run_server(db.clone(), None, addr, rx)); let task = tokio::task::spawn({
windmill_api::run_server(
db.clone(),
addr,
format!("http://localhost:{}", addr.port()),
rx,
)
});
return Self { addr, tx, task }; return Self { addr, tx, task };
} }
@@ -155,12 +163,12 @@ mod suspend_resume {
serde_json::from_value(serde_json::json!({ serde_json::from_value(serde_json::json!({
"modules": [{ "modules": [{
"id": "a", "id": "a",
"input_transform": {
"n": { "type": "javascript", "expr": "flow_input.n", },
"port": { "type": "javascript", "expr": "flow_input.port", },
"op": { "type": "javascript", "expr": "flow_input.op ?? 'resume'", },
},
"value": { "value": {
"input_transforms": {
"n": { "type": "javascript", "expr": "flow_input.n", },
"port": { "type": "javascript", "expr": "flow_input.port", },
"op": { "type": "javascript", "expr": "flow_input.op ?? 'resume'", },
},
"type": "rawscript", "type": "rawscript",
"language": "deno", "language": "deno",
"content": "\ "content": "\
@@ -194,12 +202,12 @@ mod suspend_resume {
}, },
}, { }, {
"id": "b", "id": "b",
"input_transform": {
"n": { "type": "javascript", "expr": "results.a", },
"resume": { "type": "javascript", "expr": "resume", },
"resumes": { "type": "javascript", "expr": "resumes", },
},
"value": { "value": {
"input_transforms": {
"n": { "type": "javascript", "expr": "results.a", },
"resume": { "type": "javascript", "expr": "resume", },
"resumes": { "type": "javascript", "expr": "resumes", },
},
"type": "rawscript", "type": "rawscript",
"language": "deno", "language": "deno",
"content": "export function main(n, resume, resumes) { return { n: n + 1, resume, resumes } }" "content": "export function main(n, resume, resumes) { return { n: n + 1, resume, resumes } }"
@@ -208,12 +216,12 @@ mod suspend_resume {
"required_events": 1 "required_events": 1
}, },
}, { }, {
"input_transform": {
"last": { "type": "javascript", "expr": "results.b", },
"resume": { "type": "javascript", "expr": "resume", },
"resumes": { "type": "javascript", "expr": "resumes", },
},
"value": { "value": {
"input_transforms": {
"last": { "type": "javascript", "expr": "results.b", },
"resume": { "type": "javascript", "expr": "resume", },
"resumes": { "type": "javascript", "expr": "resumes", },
},
"type": "rawscript", "type": "rawscript",
"language": "deno", "language": "deno",
"content": "export function main(last, resume, resumes) { return { last, resume, resumes } }" "content": "export function main(last, resume, resumes) { return { last, resume, resumes } }"
@@ -253,7 +261,9 @@ mod suspend_resume {
let second = completed.next().await.unwrap(); let second = completed.next().await.unwrap();
// print_job(second, &db).await; // print_job(second, &db).await;
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap(); let tx = db.begin().await.unwrap();
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
tx.commit().await.unwrap();
let secret = reqwest::get(format!( let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben" "http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
)) ))
@@ -354,7 +364,9 @@ mod suspend_resume {
/* ... and send a request resume it. */ /* ... and send a request resume it. */
let second = completed.next().await.unwrap(); let second = completed.next().await.unwrap();
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap(); let tx = db.begin().await.unwrap();
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
tx.commit().await.unwrap();
let secret = reqwest::get(format!( let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}" "http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
)) ))
@@ -474,11 +486,11 @@ def main(last, port):
"iterator": { "type": "javascript", "expr": "flow_input.items" }, "iterator": { "type": "javascript", "expr": "flow_input.items" },
"skip_failures": false, "skip_failures": false,
"modules": [{ "modules": [{
"input_transform": {
"index": { "type": "javascript", "expr": "flow_input.iter.index" },
"port": { "type": "javascript", "expr": "flow_input.port" },
},
"value": { "value": {
"input_transforms": {
"index": { "type": "javascript", "expr": "flow_input.iter.index" },
"port": { "type": "javascript", "expr": "flow_input.port" },
},
"type": "rawscript", "type": "rawscript",
"language": "deno", "language": "deno",
"content": inner_step(), "content": inner_step(),
@@ -487,11 +499,11 @@ def main(last, port):
}, },
"retry": { "constant": { "attempts": 2, "seconds": 0 } }, "retry": { "constant": { "attempts": 2, "seconds": 0 } },
}, { }, {
"input_transform": {
"last": { "type": "javascript", "expr": "results.a" },
"port": { "type": "javascript", "expr": "flow_input.port" },
},
"value": { "value": {
"input_transforms": {
"last": { "type": "javascript", "expr": "results.a" },
"port": { "type": "javascript", "expr": "flow_input.port" },
},
"type": "rawscript", "type": "rawscript",
"language": "python3", "language": "python3",
"content": last_step(), "content": last_step(),
@@ -629,7 +641,7 @@ def main(last, port):
"modules": [{ "modules": [{
"id": "a", "id": "a",
"value": { "value": {
"input_transforms": { "port": { "type": "javascript", "expr": "flow_input.port" } }, "input_transform": { "port": { "type": "javascript", "expr": "flow_input.port" } },
"type": "rawscript", "type": "rawscript",
"language": "python3", "language": "python3",
"content": r#" "content": r#"
@@ -642,7 +654,7 @@ def main(port):
}], }],
"failure_module": { "failure_module": {
"value": { "value": {
"input_transforms": { "error": { "type": "javascript", "expr": "previous_result", }, "input_transform": { "error": { "type": "javascript", "expr": "previous_result", },
"port": { "type": "javascript", "expr": "flow_input.port" } }, "port": { "type": "javascript", "expr": "flow_input.port" } },
"type": "rawscript", "type": "rawscript",
"language": "python3", "language": "python3",
@@ -656,7 +668,7 @@ def main(error, port):
}, },
})) }))
.unwrap(); .unwrap();
let (_attempts, responses) = [ let (attempts, responses) = [
/* fail the first step twice */ /* fail the first step twice */
(0x00, None), (0x00, None),
(0x00, None), (0x00, None),
@@ -678,20 +690,14 @@ def main(error, port):
_ => panic!("expected failure module"), _ => panic!("expected failure module"),
} }
println!("result: {:#?}", result); assert_eq!(server.close().await, attempts);
assert_eq!( assert_eq!(
result result,
.get("from failure module") json!({
.unwrap() "recv": 42,
.get("error") "from failure module": {"error": {"name": "IndexError", "stack": " File \"/tmp/inner.py\", line 5, in main\n return sock.recv(1)[0]\n", "message": "index out of range"}},
.unwrap() })
.get("name")
.unwrap()
.clone(),
json!("IndexError")
); );
assert_eq!(result.get("recv").unwrap().clone(), json!(42));
} }
} }
@@ -708,13 +714,13 @@ async fn test_iteration(db: Pool<Postgres>) {
"iterator": { "type": "javascript", "expr": "result.items" }, "iterator": { "type": "javascript", "expr": "result.items" },
"skip_failures": false, "skip_failures": false,
"modules": [{ "modules": [{
"value": { "input_transform": {
"input_transforms": { "n": {
"n": { "type": "javascript",
"type": "javascript", "expr": "flow_input.iter.value",
"expr": "flow_input.iter.value",
},
}, },
},
"value": {
"type": "rawscript", "type": "rawscript",
"language": "python3", "language": "python3",
"content": "def main(n):\n if 1 < n:\n raise StopIteration(n)", "content": "def main(n):\n if 1 < n:\n raise StopIteration(n)",
@@ -765,13 +771,13 @@ async fn test_iteration_parallel(db: Pool<Postgres>) {
"skip_failures": false, "skip_failures": false,
"parallel": true, "parallel": true,
"modules": [{ "modules": [{
"value": { "input_transform": {
"input_transforms": { "n": {
"n": { "type": "javascript",
"type": "javascript", "expr": "flow_input.iter.value",
"expr": "flow_input.iter.value",
},
}, },
},
"value": {
"type": "rawscript", "type": "rawscript",
"language": "python3", "language": "python3",
"content": "def main(n):\n if 1 < n:\n raise StopIteration(n)", "content": "def main(n):\n if 1 < n:\n raise StopIteration(n)",
@@ -827,8 +833,9 @@ impl RunJob {
async fn push(self, db: &Pool<Postgres>) -> Uuid { async fn push(self, db: &Pool<Postgres>) -> Uuid {
let RunJob { payload, args } = self; let RunJob { payload, args } = self;
let (uuid, tx) = windmill_queue::push::<rsmq_async::MultiplexedRsmq>( let tx = db.begin().await.unwrap();
(None, db.begin().await.unwrap()).into(), let (uuid, tx) = windmill_queue::push(
tx,
"test-workspace", "test-workspace",
payload, payload,
args, args,
@@ -838,7 +845,6 @@ impl RunJob {
/* scheduled_for_o */ None, /* scheduled_for_o */ None,
/* schedule_path */ None, /* schedule_path */ None,
/* parent_job */ None, /* parent_job */ None,
/* root job */ None,
/* is_flow_step */ false, /* is_flow_step */ false,
/* running */ false, /* running */ false,
None, None,
@@ -846,7 +852,8 @@ impl RunJob {
) )
.await .await
.expect("push has to succeed"); .expect("push has to succeed");
tx.commit().await.unwrap();
tx.commit().await.expect("push has to commit");
uuid uuid
} }
@@ -910,21 +917,43 @@ fn spawn_test_worker(
) { ) {
let (tx, rx) = tokio::sync::broadcast::channel(1); let (tx, rx) = tokio::sync::broadcast::channel(1);
let db = db.to_owned(); let db = db.to_owned();
let timeout = 4_000;
let worker_instance: &str = "test worker instance"; let worker_instance: &str = "test worker instance";
let worker_name: String = next_worker_name(); let worker_name: String = next_worker_name();
let i_worker: u64 = Default::default(); let i_worker: u64 = Default::default();
let num_workers: u64 = 2;
let ip: &str = Default::default(); let ip: &str = Default::default();
let sleep_queue: u64 = DEFAULT_SLEEP_QUEUE / num_workers;
let port = port;
let worker_config = WorkerConfig {
base_internal_url: format!("http://localhost:{port}"),
base_url: format!("http://localhost:{port}"),
disable_nuser: std::env::var("DISABLE_NUSER")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false),
disable_nsjail: std::env::var("DISABLE_NSJAIL")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false),
keep_job_dir: std::env::var("KEEP_JOB_DIR")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false),
};
let future = async move { let future = async move {
let base_internal_url = format!("http://localhost:{}", port); windmill_worker::run_worker(
windmill_worker::run_worker::<rsmq_async::MultiplexedRsmq>(
&db, &db,
timeout,
worker_instance, worker_instance,
worker_name, worker_name,
i_worker, i_worker,
num_workers,
ip, ip,
rx, sleep_queue,
&base_internal_url, worker_config,
None, None,
rx,
) )
.await .await
}; };
@@ -1003,6 +1032,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
path: None, path: None,
lock: None, lock: None,
}, },
input_transforms: Default::default(),
stop_after_if: Default::default(), stop_after_if: Default::default(),
summary: Default::default(), summary: Default::default(),
suspend: Default::default(), suspend: Default::default(),
@@ -1030,6 +1060,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
path: None, path: None,
lock: None, lock: None,
}, },
input_transforms: Default::default(),
stop_after_if: Default::default(), stop_after_if: Default::default(),
summary: Default::default(), summary: Default::default(),
suspend: Default::default(), suspend: Default::default(),
@@ -1037,6 +1068,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
sleep: None, sleep: None,
}], }],
}, },
input_transforms: Default::default(),
stop_after_if: Default::default(), stop_after_if: Default::default(),
summary: Default::default(), summary: Default::default(),
suspend: Default::default(), suspend: Default::default(),
@@ -1130,6 +1162,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
path: None, path: None,
lock: None, lock: None,
}, },
input_transforms: Default::default(),
stop_after_if: Default::default(), stop_after_if: Default::default(),
summary: Default::default(), summary: Default::default(),
suspend: Default::default(), suspend: Default::default(),
@@ -1145,24 +1178,25 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
modules: vec![ modules: vec![
FlowModule { FlowModule {
id: "d".to_string(), id: "d".to_string(),
input_transforms: [
(
"i".to_string(),
InputTransform::Javascript {
expr: "flow_input.iter.value".to_string(),
},
),
(
"loop".to_string(),
InputTransform::Static { value: json!(true) },
),
(
"path".to_string(),
InputTransform::Static { value: json!("inner.txt") },
),
]
.into(),
value: FlowModuleValue::RawScript { value: FlowModuleValue::RawScript {
input_transforms: [ input_transforms: [].into(),
(
"i".to_string(),
InputTransform::Javascript {
expr: "flow_input.iter.value".to_string(),
},
),
(
"loop".to_string(),
InputTransform::Static { value: json!(true) },
),
(
"path".to_string(),
InputTransform::Static { value: json!("inner.txt") },
),
]
.into(),
language: ScriptLang::Deno, language: ScriptLang::Deno,
content: write_file, content: write_file,
path: None, path: None,
@@ -1193,6 +1227,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
path: None, path: None,
lock: None, lock: None,
}, },
input_transforms: [].into(),
stop_after_if: Default::default(), stop_after_if: Default::default(),
summary: Default::default(), summary: Default::default(),
suspend: Default::default(), suspend: Default::default(),
@@ -1201,6 +1236,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
}, },
], ],
}, },
input_transforms: Default::default(),
stop_after_if: Default::default(), stop_after_if: Default::default(),
summary: Default::default(), summary: Default::default(),
suspend: Default::default(), suspend: Default::default(),
@@ -1235,6 +1271,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
path: None, path: None,
lock: None, lock: None,
}, },
input_transforms: [].into(),
stop_after_if: Default::default(), stop_after_if: Default::default(),
summary: Default::default(), summary: Default::default(),
suspend: Default::default(), suspend: Default::default(),
@@ -1431,6 +1468,7 @@ async fn test_python_flow(db: Pool<Postgres>) {
let doubles = "def main(n): return n * 2"; let doubles = "def main(n): return n * 2";
let flow: FlowValue = serde_json::from_value(serde_json::json!( { let flow: FlowValue = serde_json::from_value(serde_json::json!( {
"input_transform": {},
"modules": [ "modules": [
{ {
"value": { "value": {
@@ -1446,16 +1484,16 @@ async fn test_python_flow(db: Pool<Postgres>) {
"skip_failures": false, "skip_failures": false,
"modules": [{ "modules": [{
"value": { "value": {
"input_transforms": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
"type": "rawscript", "type": "rawscript",
"language": "python3", "language": "python3",
"content": doubles, "content": doubles,
}, },
"input_transform": {
"n": {
"type": "javascript",
"expr": "flow_input.iter.value",
},
},
}], }],
}, },
}, },
@@ -1488,11 +1526,11 @@ async fn test_python_flow_2(db: Pool<Postgres>) {
"modules": [ "modules": [
{ {
"value": { "value": {
"input_transforms": {},
"type": "rawscript", "type": "rawscript",
"content": "import wmill\ndef main(): return \"Hello\"", "content": "import wmill\ndef main(): return \"Hello\"",
"language": "python3" "language": "python3"
}, },
"input_transform": {}
} }
] ]
})) }))
@@ -1520,8 +1558,6 @@ async fn test_go_job(db: Pool<Postgres>) {
let port = server.addr.port(); let port = server.addr.port();
let content = r#" let content = r#"
package inner
import "fmt" import "fmt"
func main(derp string) (string, error) { func main(derp string) (string, error) {
@@ -1673,7 +1709,7 @@ async fn test_empty_loop(db: Pool<Postgres>) {
"modules": [ "modules": [
{ {
"value": { "value": {
"input_transforms": { "input_transform": {
"n": { "n": {
"type": "javascript", "type": "javascript",
"expr": "flow_input.iter.value", "expr": "flow_input.iter.value",
@@ -1689,7 +1725,7 @@ async fn test_empty_loop(db: Pool<Postgres>) {
}, },
{ {
"value": { "value": {
"input_transforms": { "input_transform": {
"items": { "items": {
"type": "javascript", "type": "javascript",
"expr": "results.a", "expr": "results.a",
@@ -1766,13 +1802,13 @@ async fn test_empty_loop_2(db: Pool<Postgres>) {
"iterator": { "type": "static", "value": [] }, "iterator": { "type": "static", "value": [] },
"modules": [ "modules": [
{ {
"value": { "input_transform": {
"input_transforms": { "n": {
"n": { "type": "javascript",
"type": "javascript", "expr": "flow_input.iter.value",
"expr": "flow_input.iter.value",
},
}, },
},
"value": {
"type": "rawscript", "type": "rawscript",
"language": "python3", "language": "python3",
"content": "def main(n): return n", "content": "def main(n): return n",
@@ -1808,13 +1844,13 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
"iterator": { "type": "static", "value": [2,3,4] }, "iterator": { "type": "static", "value": [2,3,4] },
"modules": [ "modules": [
{ {
"value": { "input_transform": {
"input_transforms": { "n": {
"n": { "type": "javascript",
"type": "javascript", "expr": "flow_input.iter.value",
"expr": "flow_input.iter.value",
},
}, },
},
"value": {
"type": "rawscript", "type": "rawscript",
"language": "python3", "language": "python3",
"content": "def main(n): return n", "content": "def main(n): return n",
@@ -1824,13 +1860,13 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
}, },
}, },
{ {
"value": { "input_transform": {
"input_transforms": { "items": {
"items": { "type": "javascript",
"type": "javascript", "expr": "results.a",
"expr": "results.a",
},
}, },
},
"value": {
"type": "rawscript", "type": "rawscript",
"language": "python3", "language": "python3",
"content": "def main(items): return sum(items)", "content": "def main(items): return sum(items)",
@@ -1852,17 +1888,17 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
fn module_add_item_to_list(i: i32, id: &str) -> serde_json::Value { fn module_add_item_to_list(i: i32, id: &str) -> serde_json::Value {
json!({ json!({
"id": format!("id_{}", i.to_string().replace("-", "_")), "id": format!("id_{}", i.to_string().replace("-", "_")),
"value": { "input_transform": {
"input_transforms": { "array": {
"array": { "type": "javascript",
"type": "javascript", "expr": format!("results.{id}"),
"expr": format!("results.{id}"),
},
"i": {
"type": "static",
"value": json!(i),
}
}, },
"i": {
"type": "static",
"value": json!(i),
}
},
"value": {
"type": "rawscript", "type": "rawscript",
"language": "deno", "language": "deno",
"content": "export function main(array, i){ array.push(i); return array }", "content": "export function main(array, i){ array.push(i); return array }",
@@ -1872,8 +1908,8 @@ fn module_add_item_to_list(i: i32, id: &str) -> serde_json::Value {
fn module_failure() -> serde_json::Value { fn module_failure() -> serde_json::Value {
json!({ json!({
"input_transform": {},
"value": { "value": {
"input_transforms": {},
"type": "rawscript", "type": "rawscript",
"language": "deno", "language": "deno",
"content": "export function main(){ throw Error('failure') }", "content": "export function main(){ throw Error('failure') }",
@@ -2028,16 +2064,6 @@ async fn test_branchall_simple(db: Pool<Postgres>) {
assert_eq!(result, serde_json::json!([[1, 2], [1, 3]])); assert_eq!(result, serde_json::json!([[1, 2], [1, 3]]));
} }
#[derive(Deserialize)]
struct ErrorResult {
error: NamedError,
}
#[derive(Deserialize)]
struct NamedError {
name: String,
}
#[sqlx::test(fixtures("base"))] #[sqlx::test(fixtures("base"))]
async fn test_branchall_skip_failure(db: Pool<Postgres>) { async fn test_branchall_skip_failure(db: Pool<Postgres>) {
initialize_tracing().await; initialize_tracing().await;
@@ -2073,11 +2099,8 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
serde_json::from_value::<ErrorResult>(result.get(0).unwrap().clone()) result,
.unwrap() serde_json::json!([{"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1", "message": "failure"}}, [1,3]])
.error
.name,
"Error"
); );
let flow: FlowValue = serde_json::from_value(json!({ let flow: FlowValue = serde_json::from_value(json!({
@@ -2110,11 +2133,8 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
serde_json::from_value::<ErrorResult>(result.get(0).unwrap().clone()) result,
.unwrap() serde_json::json!([ {"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1", "message": "failure"}}, [1, 2]])
.error
.name,
"Error"
); );
} }
@@ -2247,7 +2267,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
"modules": [{ "modules": [{
"id": "a", "id": "a",
"value": { "value": {
"input_transforms": { "input_transform": {
"l": { "type": "javascript", "expr": "[]", }, "l": { "type": "javascript", "expr": "[]", },
"n": { "type": "javascript", "expr": "flow_input.n", }, "n": { "type": "javascript", "expr": "flow_input.n", },
}, },
@@ -2258,7 +2278,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
}, { }, {
"id": "b", "id": "b",
"value": { "value": {
"input_transforms": { "input_transform": {
"l": { "type": "javascript", "expr": "results.a.l", }, "l": { "type": "javascript", "expr": "results.a.l", },
"n": { "type": "javascript", "expr": "flow_input.n", }, "n": { "type": "javascript", "expr": "flow_input.n", },
}, },
@@ -2268,7 +2288,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
}, },
}, { }, {
"value": { "value": {
"input_transforms": { "input_transform": {
"l": { "type": "javascript", "expr": "results.b.l", }, "l": { "type": "javascript", "expr": "results.b.l", },
"n": { "type": "javascript", "expr": "flow_input.n", }, "n": { "type": "javascript", "expr": "flow_input.n", },
}, },
@@ -2278,8 +2298,8 @@ async fn test_failure_module(db: Pool<Postgres>) {
}, },
}], }],
"failure_module": { "failure_module": {
"input_transform": { "error": { "type": "javascript", "expr": "previous_result", } },
"value": { "value": {
"input_transforms": { "error": { "type": "javascript", "expr": "previous_result", } },
"type": "rawscript", "type": "rawscript",
"language": "deno", "language": "deno",
"content": "export function main(error) { return { 'from failure module': error } }", "content": "export function main(error) { return { 'from failure module': error } }",

View File

@@ -8,8 +8,12 @@ edition.workspace = true
name = "windmill_api" name = "windmill_api"
path = "src/lib.rs" path = "src/lib.rs"
[[bin]]
name = "windmill_api"
path = "src/main.rs"
[features] [features]
enterprise = ["windmill-queue/enterprise", "async-stripe"] enterprise = ["windmill-queue/enterprise"]
[dependencies] [dependencies]
windmill-queue.workspace = true windmill-queue.workspace = true
@@ -47,7 +51,6 @@ tracing.workspace = true
sql-builder.workspace = true sql-builder.workspace = true
serde_json.workspace = true serde_json.workspace = true
chrono.workspace = true chrono.workspace = true
chrono-tz.workspace = true
hex.workspace = true hex.workspace = true
base64.workspace = true base64.workspace = true
serde_urlencoded.workspace = true serde_urlencoded.workspace = true
@@ -66,9 +69,5 @@ hmac.workspace = true
cookie.workspace = true cookie.workspace = true
sha2.workspace = true sha2.workspace = true
urlencoding.workspace = true urlencoding.workspace = true
async-stripe = { workspace = true, optional = true } async-stripe.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
prometheus.workspace = true
async_zip.workspace = true
rsmq_async.workspace = true
regex.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3" openapi: "3.0.3"
info: info:
version: 1.87.0 version: 1.61.1
title: Windmill API title: Windmill API
contact: contact:
@@ -117,10 +117,9 @@ paths:
responses: responses:
"200": "200":
description: > description: >
Successfully authenticated. The session ID is returned in a cookie Successfully authenticated.
named `token` and as plaintext response. Preferred method of The session ID is returned in a cookie named `token` and as plaintext response.
authorization is through the bearer token. The cookie is only for Preferred method of authorization is through the bearer token. The cookie is only for browser convenience.
browser convenience.
headers: headers:
Set-Cookie: Set-Cookie:
@@ -205,6 +204,7 @@ paths:
schema: schema:
type: string type: string
/w/{workspace}/users/is_owner/{path}: /w/{workspace}/users/is_owner/{path}:
get: get:
summary: is owner of path summary: is owner of path
@@ -883,8 +883,6 @@ paths:
type: string type: string
customer_id: customer_id:
type: string type: string
webhook:
type: string
/w/{workspace}/workspaces/premium_info: /w/{workspace}/workspaces/premium_info:
get: get:
@@ -964,33 +962,6 @@ paths:
schema: schema:
type: string type: string
/w/{workspace}/workspaces/edit_webhook:
post:
summary: edit webhook
operationId: editWebhook
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: WorkspaceWebhook
required: true
content:
application/json:
schema:
type: object
properties:
webhook:
type: string
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/users/list: /w/{workspace}/users/list:
get: get:
summary: list users summary: list users
@@ -1048,27 +1019,6 @@ paths:
schema: schema:
type: string type: string
/users/tokens/impersonate:
post:
summary: create token to impersonate a user (require superadmin)
operationId: createTokenImpersonate
tags:
- user
requestBody:
description: new token
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewTokenImpersonate"
responses:
"201":
description: token created
content:
text/plain:
schema:
type: string
/users/tokens/delete/{token_prefix}: /users/tokens/delete/{token_prefix}:
delete: delete:
summary: delete token summary: delete token
@@ -1113,10 +1063,6 @@ paths:
- variable - variable
parameters: parameters:
- $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/WorkspaceId"
- name: already_encrypted
in: query
schema:
type: boolean
requestBody: requestBody:
description: new variable description: new variable
required: true required: true
@@ -1158,10 +1104,6 @@ paths:
parameters: parameters:
- $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path" - $ref: "#/components/parameters/Path"
- name: already_encrypted
in: query
schema:
type: boolean
requestBody: requestBody:
description: updated variable description: updated variable
required: true required: true
@@ -1279,10 +1221,9 @@ paths:
responses: responses:
"200": "200":
description: > description: >
Successfully authenticated. The session ID is returned in a cookie Successfully authenticated.
named `token` and as plaintext response. Preferred method of The session ID is returned in a cookie named `token` and as plaintext response.
authorization is through the bearer token. The cookie is only for Preferred method of authorization is through the bearer token. The cookie is only for browser convenience.
browser convenience.
headers: headers:
Set-Cookie: Set-Cookie:
@@ -1571,7 +1512,7 @@ paths:
required: true required: true
content: content:
application/json: application/json:
schema: schema:
type: object type: object
properties: properties:
value: {} value: {}
@@ -2125,6 +2066,7 @@ paths:
items: items:
type: string type: string
/w/{workspace}/scripts/create: /w/{workspace}/scripts/create:
post: post:
summary: create script summary: create script
@@ -2299,7 +2241,7 @@ paths:
/w/{workspace}/scripts/delete/h/{hash}: /w/{workspace}/scripts/delete/h/{hash}:
post: post:
summary: delete script by hash (erase content but keep hash, require admin) summary: delete script by hash (erase content but keep hash)
operationId: deleteScriptByHash operationId: deleteScriptByHash
tags: tags:
- script - script
@@ -2314,23 +2256,6 @@ paths:
schema: schema:
$ref: "#/components/schemas/Script" $ref: "#/components/schemas/Script"
/w/{workspace}/scripts/delete/p/{path}:
post:
summary: delete all scripts at a given path (require admin)
operationId: deleteScriptByPath
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: script path
content:
application/json:
schema:
type: string
/w/{workspace}/scripts/get/p/{path}: /w/{workspace}/scripts/get/p/{path}:
get: get:
summary: get script by path summary: get script by path
@@ -2365,24 +2290,6 @@ paths:
schema: schema:
type: string type: string
/scripts_u/tokened_raw/{workspace}/{token}/{path}:
get:
summary: raw script by path with a token (mostly used by lsp to be used with import maps to resolve scripts)
operationId: rawScriptByPathTokened
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Token"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: script content
content:
text/plain:
schema:
type: string
/w/{workspace}/scripts/exists/p/{path}: /w/{workspace}/scripts/exists/p/{path}:
get: get:
summary: exists script by path summary: exists script by path
@@ -2508,6 +2415,17 @@ paths:
parameters: parameters:
- $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath" - $ref: "#/components/parameters/ScriptPath"
- name: scheduled_for
description: when to schedule this job (leave empty for immediate run)
in: query
schema:
type: string
format: date-time
- name: scheduled_in_secs
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/ParentJob" - $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/IncludeHeader" - $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit" - $ref: "#/components/parameters/QueueLimit"
@@ -2527,26 +2445,6 @@ paths:
application/json: application/json:
schema: {} schema: {}
get:
summary: run script by path with get
operationId: runWaitResultScriptByPathGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
responses:
"200":
description: job result
content:
application/json:
schema: {}
/w/{workspace}/jobs/run_wait_result/f/{path}: /w/{workspace}/jobs/run_wait_result/f/{path}:
post: post:
summary: run flow by path and wait until completion summary: run flow by path and wait until completion
@@ -2556,6 +2454,17 @@ paths:
parameters: parameters:
- $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath" - $ref: "#/components/parameters/ScriptPath"
- name: scheduled_for
description: when to schedule this job (leave empty for immediate run)
in: query
schema:
type: string
format: date-time
- name: scheduled_in_secs
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/IncludeHeader" - $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit" - $ref: "#/components/parameters/QueueLimit"
@@ -2592,6 +2501,11 @@ paths:
required: true required: true
schema: schema:
type: string type: string
- name: skip_direct
description: Skip checking that the node is part of the given flow.
in: query
schema:
type: boolean
responses: responses:
"200": "200":
description: job result description: job result
@@ -2765,44 +2679,6 @@ paths:
schema: schema:
type: string type: string
/w/{workspace}/flows/delete/{path}:
delete:
summary: delete flow by path
operationId: deleteFlowByPath
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: flow delete
content:
text/plain:
schema:
type: string
/w/{workspace}/flows/input_history/p/{path}:
get:
summary: list inputs for previous completed flow jobs
operationId: getFlowInputHistoryByPath
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: input history for completed jobs with this flow path
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Input"
/w/{workspace}/apps/list: /w/{workspace}/apps/list:
get: get:
summary: list all available apps summary: list all available apps
@@ -2893,6 +2769,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/AppWithLastVersion" $ref: "#/components/schemas/AppWithLastVersion"
/w/{workspace}/apps/secret_of/{path}: /w/{workspace}/apps/secret_of/{path}:
get: get:
summary: get public secret of app summary: get public secret of app
@@ -3225,14 +3102,12 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath" - $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath" - $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash" - $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/StartedBefore" - $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/StartedAfter" - $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/Success" - $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/JobKinds" - $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/Suspended" - $ref: "#/components/parameters/Suspended"
- $ref: "#/components/parameters/Running" - $ref: "#/components/parameters/Running"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
responses: responses:
"200": "200":
description: All available queued jobs description: All available queued jobs
@@ -3257,12 +3132,10 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath" - $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath" - $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash" - $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/StartedBefore" - $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/StartedAfter" - $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/Success" - $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/JobKinds" - $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
- name: is_skipped - name: is_skipped
description: is the job skipped description: is the job skipped
in: query in: query
@@ -3296,11 +3169,9 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath" - $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath" - $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash" - $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/StartedBefore" - $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/StartedAfter" - $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/JobKinds" - $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
- name: is_skipped - name: is_skipped
description: is the job skipped description: is the job skipped
in: query in: query
@@ -3343,6 +3214,23 @@ paths:
schema: schema:
$ref: "#/components/schemas/Job" $ref: "#/components/schemas/Job"
# /w/{workspace}/jobs/flow/current_state/{id}:
# get:
# summary: get flow current step state
# operationId: getJob
# tags:
# - job
# parameters:
# - $ref: "#/components/parameters/WorkspaceId"
# - $ref: "#/components/parameters/JobId"
# responses:
# "200":
# description: state details
# content:
# application/json:
# schema:
# type: string
/w/{workspace}/jobs_u/getupdate/{id}: /w/{workspace}/jobs_u/getupdate/{id}:
get: get:
summary: get job updates summary: get job updates
@@ -3375,8 +3263,6 @@ paths:
type: boolean type: boolean
new_logs: new_logs:
type: string type: string
mem_peak:
type: integer
/w/{workspace}/jobs/completed/get/{id}: /w/{workspace}/jobs/completed/get/{id}:
get: get:
@@ -3395,22 +3281,6 @@ paths:
schema: schema:
$ref: "#/components/schemas/CompletedJob" $ref: "#/components/schemas/CompletedJob"
/w/{workspace}/jobs/completed/get_result/{id}:
get:
summary: get completed job result
operationId: getCompletedJobResult
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
responses:
"200":
description: result
content:
application/json:
schema: {}
/w/{workspace}/jobs/completed/delete/{id}: /w/{workspace}/jobs/completed/delete/{id}:
post: post:
summary: delete completed job (erase content but keep run id) summary: delete completed job (erase content but keep run id)
@@ -3428,7 +3298,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/CompletedJob" $ref: "#/components/schemas/CompletedJob"
/w/{workspace}/jobs_u/queue/cancel/{id}: /w/{workspace}/jobs/queue/cancel/{id}:
post: post:
summary: cancel queued job summary: cancel queued job
operationId: cancelQueuedJob operationId: cancelQueuedJob
@@ -3456,34 +3326,6 @@ paths:
schema: schema:
type: string type: string
/w/{workspace}/jobs_u/queue/force_cancel/{id}:
post:
summary: force cancel queued job
operationId: forceCancelQueuedJob
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
requestBody:
description: reason
required: true
content:
application/json:
schema:
type: object
properties:
reason:
type: string
responses:
"200":
description: job canceled
content:
text/plain:
schema:
type: string
/w/{workspace}/jobs/job_signature/{id}/{resume_id}: /w/{workspace}/jobs/job_signature/{id}/{resume_id}:
get: get:
summary: create an HMac signature given a job id and a resume id summary: create an HMac signature given a job id and a resume id
@@ -3512,7 +3354,7 @@ paths:
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}: /w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
get: get:
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
operationId: getResumeUrls operationId: getResumeUrls
tags: tags:
- job - job
@@ -3538,7 +3380,7 @@ paths:
properties: properties:
approvalPage: approvalPage:
type: string type: string
resume: resume:
type: string type: string
cancel: cancel:
type: string type: string
@@ -3556,7 +3398,6 @@ paths:
parameters: parameters:
- $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId" - $ref: "#/components/parameters/JobId"
- $ref: "#/components/parameters/Payload"
- name: resume_id - name: resume_id
in: path in: path
required: true required: true
@@ -3770,14 +3611,13 @@ paths:
properties: properties:
schedule: schedule:
type: string type: string
timezone: offset:
type: string type: integer
required: required:
- schedule - schedule
- timezone
responses: responses:
"200": "200":
description: List of 5 estimated upcoming execution events (in UTC) description: the preview of the next 10 time this schedule would apply to
content: content:
application/json: application/json:
schema: schema:
@@ -4185,7 +4025,7 @@ paths:
type: string type: string
owners: owners:
type: array type: array
items: items:
type: string type: string
extra_perms: extra_perms:
additionalProperties: additionalProperties:
@@ -4219,7 +4059,7 @@ paths:
properties: properties:
owners: owners:
type: array type: array
items: items:
type: string type: string
extra_perms: extra_perms:
additionalProperties: additionalProperties:
@@ -4390,8 +4230,7 @@ paths:
required: true required: true
schema: schema:
type: string type: string
enum: enum: [script, group_, resource, schedule, variable, flow, folder, app]
[script, group_, resource, schedule, variable, flow, folder, app]
responses: responses:
"200": "200":
description: acls description: acls
@@ -4416,8 +4255,7 @@ paths:
required: true required: true
schema: schema:
type: string type: string
enum: enum: [script, group_, resource, schedule, variable, flow, folder, app]
[script, group_, resource, schedule, variable, flow, folder, app]
requestBody: requestBody:
description: acl to add description: acl to add
required: true required: true
@@ -4453,8 +4291,7 @@ paths:
required: true required: true
schema: schema:
type: string type: string
enum: enum: [script, group_, resource, schedule, variable, flow, folder, app]
[script, group_, resource, schedule, variable, flow, folder, app]
requestBody: requestBody:
description: acl to add description: acl to add
required: true required: true
@@ -4474,18 +4311,19 @@ paths:
schema: schema:
type: string type: string
/w/{workspace}/capture_u/{path}: /w/{workspace}/capture_u/{path}:
post: post:
summary: update flow preview capture summary: update flow preview capture
operationId: updateCapture operationId: updateCapture
tags: tags:
- capture - capture
parameters: parameters:
- $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path" - $ref: "#/components/parameters/Path"
responses: responses:
"204": "204":
description: flow preview captured description: flow preview captured
/w/{workspace}/capture/{path}: /w/{workspace}/capture/{path}:
put: put:
@@ -4562,118 +4400,6 @@ paths:
"200": "200":
description: unstar item description: unstar item
/w/{workspace}/inputs/history:
get:
summary: List Inputs used in previously completed jobs
operationId: getInputHistory
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/RunnableId"
- $ref: "#/components/parameters/RunnableType"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: Input history for completed jobs
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Input"
/w/{workspace}/inputs/list:
get:
summary: List saved Inputs for a Runnable
operationId: listInputs
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/RunnableId"
- $ref: "#/components/parameters/RunnableType"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: Saved Inputs for a Runnable
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Input"
/w/{workspace}/inputs/create:
post:
summary: Create an Input for future use in a script or flow
operationId: createInput
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/RunnableId"
- $ref: "#/components/parameters/RunnableType"
requestBody:
description: Input
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateInput"
responses:
"201":
description: Input created
content:
text/plain:
schema:
type: string
format: uuid
/w/{workspace}/inputs/update:
post:
summary: Update an Input
operationId: updateInput
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: UpdateInput
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateInput"
responses:
"201":
description: Input updated
content:
text/plain:
schema:
type: string
format: uuid
/w/{workspace}/inputs/delete/{input}:
post:
summary: Delete a Saved Input
operationId: deleteInput
tags:
- input
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/InputId"
responses:
"200":
description: Input deleted
content:
text/plain:
schema:
type: string
format: uuid
components: components:
securitySchemes: securitySchemes:
bearerAuth: bearerAuth:
@@ -4683,7 +4409,6 @@ components:
type: apiKey type: apiKey
in: cookie in: cookie
name: token name: token
parameters: parameters:
WorkspaceId: WorkspaceId:
name: workspace name: workspace
@@ -4691,12 +4416,6 @@ components:
required: true required: true
schema: schema:
type: string type: string
Token:
name: token
in: path
required: true
schema:
type: string
AccountId: AccountId:
name: id name: id
in: path in: path
@@ -4772,9 +4491,7 @@ components:
type: string type: string
ParentJob: ParentJob:
name: parent_job name: parent_job
description: description: The parent job that is at the origin and responsible for the execution of this script if any
The parent job that is at the origin and responsible for the execution
of this script if any
in: query in: query
schema: schema:
type: string type: string
@@ -4794,14 +4511,7 @@ components:
in: query in: query
schema: schema:
type: string type: string
Payload:
name: payload
description: |
The base64 encoded payload that has been encoded as a JSON. e.g how to encode such payload encodeURIComponent
`encodeURIComponent(btoa(JSON.stringify({a: 2})))`
in: query
schema:
type: string
ScriptStartPath: ScriptStartPath:
name: script_path_start name: script_path_start
description: mask to filter matching starting path description: mask to filter matching starting path
@@ -4820,15 +4530,15 @@ components:
in: query in: query
schema: schema:
type: string type: string
StartedBefore: CreatedBefore:
name: started_before name: created_before
description: filter on created before (inclusive) timestamp description: filter on created before (inclusive) timestamp
in: query in: query
schema: schema:
type: string type: string
format: date-time format: date-time
StartedAfter: CreatedAfter:
name: started_after name: created_after
description: filter on created after (exclusive) timestamp description: filter on created after (exclusive) timestamp
in: query in: query
schema: schema:
@@ -4852,18 +4562,6 @@ components:
in: query in: query
schema: schema:
type: boolean type: boolean
ArgsFilter:
name: args
description: filter on jobs containing those args as a json subset (@> in postgres)
in: query
schema:
type: string
ResultFilter:
name: result
description: filter on jobs containing those result as a json subset (@> in postgres)
in: query
schema:
type: string
After: After:
name: after name: after
description: filter on created after (exclusive) timestamp description: filter on created after (exclusive) timestamp
@@ -4905,9 +4603,7 @@ components:
enum: [Create, Update, Delete, Execute] enum: [Create, Update, Delete, Execute]
JobKinds: JobKinds:
name: job_kinds name: job_kinds
description: description: filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by,
filter on job kind (values 'preview', 'script', 'dependencies', 'flow')
separated by,
in: query in: query
schema: schema:
type: string type: string
@@ -4917,26 +4613,9 @@ components:
# type: string # type: string
# enum: ["preview", "script", "dependencies"] # enum: ["preview", "script", "dependencies"]
# explode: false # explode: false
RunnableId:
name: runnable_id
in: query
schema:
type: string
RunnableType:
name: runnable_type
in: query
schema:
$ref: "#/components/schemas/RunnableType"
InputId:
name: input
in: path
required: true
schema:
type: string
schemas: schemas:
$ref: "../../openflow.openapi.yaml#/components/schemas" $ref: "../../openflow.openapi.yaml#/components/schemas"
Script: Script:
type: object type: object
properties: properties:
@@ -5007,60 +4686,6 @@ components:
type: object type: object
additionalProperties: {} additionalProperties: {}
Input:
type: object
properties:
id:
type: string
name:
type: string
args:
type: object
created_by:
type: string
created_at:
type: string
format: date-time
is_public:
type: boolean
required:
- id
- name
- args
- created_by
- created_at
- is_public
CreateInput:
type: object
properties:
name:
type: string
args:
type: object
required:
- name
- args
- created_by
UpdateInput:
type: object
properties:
id:
type: string
name:
type: string
is_public:
type: boolean
required:
- id
- name
- is_public
RunnableType:
type: string
enum: ["ScriptHash", "ScriptPath", "FlowPath"]
QueuedJob: QueuedJob:
type: object type: object
properties: properties:
@@ -5136,8 +4761,6 @@ components:
type: string type: string
visible_to_owner: visible_to_owner:
type: boolean type: boolean
mem_peak:
type: integer
required: required:
- id - id
- running - running
@@ -5224,8 +4847,6 @@ components:
type: string type: string
visible_to_owner: visible_to_owner:
type: boolean type: boolean
mem_peak:
type: integer
required: required:
- id - id
- created_by - created_by
@@ -5363,19 +4984,6 @@ components:
type: string type: string
format: date-time format: date-time
NewTokenImpersonate:
type: object
properties:
label:
type: string
expiration:
type: string
format: date-time
impersonate_email:
type: string
required:
- impersonate_email
ListableVariable: ListableVariable:
type: object type: object
properties: properties:
@@ -5501,11 +5109,6 @@ components:
MainArgSignature: MainArgSignature:
type: object type: object
properties: properties:
type:
type: string
enum: ["Valid", "Invalid"]
error:
type: string
star_args: star_args:
type: boolean type: boolean
star_kwargs: star_kwargs:
@@ -5615,8 +5218,6 @@ components:
- star_args - star_args
- start_kwargs - start_kwargs
- args - args
- type
- error
Preview: Preview:
type: object type: object
@@ -5751,8 +5352,8 @@ components:
format: date-time format: date-time
schedule: schedule:
type: string type: string
timezone: offset_:
type: string type: integer
enabled: enabled:
type: boolean type: boolean
script_path: script_path:
@@ -5775,7 +5376,7 @@ components:
- edited_at - edited_at
- schedule - schedule
- script_path - script_path
- timezone - offset_
- extra_perms - extra_perms
- is_flow - is_flow
- enabled - enabled
@@ -5788,8 +5389,8 @@ components:
type: string type: string
schedule: schedule:
type: string type: string
timezone: offset:
type: string type: integer
script_path: script_path:
type: string type: string
is_flow: is_flow:
@@ -5801,7 +5402,6 @@ components:
required: required:
- path - path
- schedule - schedule
- timezone
- script_path - script_path
- is_flow - is_flow
- args - args
@@ -5811,13 +5411,10 @@ components:
properties: properties:
schedule: schedule:
type: string type: string
timezone:
type: string
args: args:
$ref: "#/components/schemas/ScriptArgs" $ref: "#/components/schemas/ScriptArgs"
required: required:
- schedule - schedule
- timezone
- script_path - script_path
- is_flow - is_flow
- args - args
@@ -5865,8 +5462,9 @@ components:
type: string type: string
worker_instance: worker_instance:
type: string type: string
last_ping: ping_at:
type: number type: string
format: date-time
started_at: started_at:
type: string type: string
format: date-time format: date-time
@@ -6082,7 +5680,6 @@ components:
- extra_perms - extra_perms
- edited_at - edited_at
- execution_mode - execution_mode
AppWithLastVersion: AppWithLastVersion:
type: object type: object
properties: properties:

View File

@@ -12,8 +12,6 @@ use crate::{
jobs::script_path_to_payload, jobs::script_path_to_payload,
users::{require_owner_of_path, Authed, OptAuthed}, users::{require_owner_of_path, Authed, OptAuthed},
variables::build_crypt, variables::build_crypt,
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
}; };
use axum::{ use axum::{
extract::{Extension, Json, Path, Query}, extract::{Extension, Json, Path, Query},
@@ -22,6 +20,7 @@ use axum::{
}; };
use hyper::StatusCode; use hyper::StatusCode;
use magic_crypt::MagicCryptTrait; use magic_crypt::MagicCryptTrait;
use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value}; use serde_json::{json, Map, Value};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -37,7 +36,7 @@ use windmill_common::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath, http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
}, },
}; };
use windmill_queue::{push, JobPayload, QueueTransaction, RawCode}; use windmill_queue::{push, JobPayload, RawCode};
pub fn workspaced_service() -> Router { pub fn workspaced_service() -> Router {
Router::new() Router::new()
@@ -170,7 +169,7 @@ async fn list_apps(
) )
.order_desc("favorite.path IS NOT NULL") .order_desc("favorite.path IS NOT NULL")
.order_by("app_version.created_at", true) .order_by("app_version.created_at", true)
.and_where("app.workspace_id = ?".bind(&w_id)) .and_where("app.workspace_id = ? OR app.workspace_id = 'starter'".bind(&w_id))
.offset(offset) .offset(offset)
.limit(per_page) .limit(per_page)
.clone(); .clone();
@@ -311,19 +310,11 @@ async fn get_secret_id(
async fn create_app( async fn create_app(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Json(mut app): Json<CreateApp>, Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
app.policy.on_behalf_of_email = Some(authed.email);
if &app.path == "" {
return Err(Error::BadRequest("App path cannot be empty".to_string()));
}
let id = sqlx::query_scalar!( let id = sqlx::query_scalar!(
"INSERT INTO app "INSERT INTO app
(workspace_id, path, summary, policy, versions) (workspace_id, path, summary, policy, versions)
@@ -365,19 +356,17 @@ async fn create_app(
None, None,
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: app.path.clone() },
);
Ok((StatusCode::CREATED, app.path)) Ok((StatusCode::CREATED, app.path))
} }
async fn list_hub_apps(Authed { email, .. }: Authed) -> JsonResult<serde_json::Value> { async fn list_hub_apps(
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
let flows = list_elems_from_hub( let flows = list_elems_from_hub(
&HTTP_CLIENT, http_client,
"https://hub.windmill.dev/searchUiData?approved=true", "https://hub.windmill.dev/searchUiData?approved=true",
&email, &email,
) )
@@ -388,9 +377,10 @@ async fn list_hub_apps(Authed { email, .. }: Authed) -> JsonResult<serde_json::V
pub async fn get_hub_app_by_id( pub async fn get_hub_app_by_id(
Authed { email, .. }: Authed, Authed { email, .. }: Authed,
Path(id): Path<i32>, Path(id): Path<i32>,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> { ) -> JsonResult<serde_json::Value> {
let value = http_get_from_hub( let value = http_get_from_hub(
&HTTP_CLIENT, http_client,
&format!("https://hub.windmill.dev/apps/{id}/json"), &format!("https://hub.windmill.dev/apps/{id}/json"),
&email, &email,
false, false,
@@ -405,7 +395,6 @@ pub async fn get_hub_app_by_id(
async fn delete_app( async fn delete_app(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> { ) -> Result<String> {
let path = path.to_path(); let path = path.to_path();
@@ -429,10 +418,6 @@ async fn delete_app(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() },
);
Ok(format!("app {} deleted", path)) Ok(format!("app {} deleted", path))
} }
@@ -440,7 +425,6 @@ async fn delete_app(
async fn update_app( async fn update_app(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>, Json(ns): Json<EditApp>,
@@ -470,9 +454,7 @@ async fn update_app(
sqlb.set_str("summary", nsummary); sqlb.set_str("summary", nsummary);
} }
if let Some(mut npolicy) = ns.policy { if let Some(npolicy) = ns.policy {
npolicy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
npolicy.on_behalf_of_email = Some(authed.email);
sqlb.set( sqlb.set(
"policy", "policy",
&format!( &format!(
@@ -532,14 +514,6 @@ async fn update_app(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("app {} updated (npath: {:?})", path, npath)) Ok(format!("app {} updated (npath: {:?})", path, npath))
} }
@@ -565,7 +539,6 @@ fn digest(code: &str) -> String {
async fn execute_component( async fn execute_component(
OptAuthed(opt_authed): OptAuthed, OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<ExecuteApp>, Json(payload): Json<ExecuteApp>,
) -> Result<String> { ) -> Result<String> {
@@ -584,7 +557,7 @@ async fn execute_component(
}; };
let path = path.to_path(); let path = path.to_path();
let mut tx: QueueTransaction<'_, _> = (rsmq, db.begin().await?).into(); let mut tx = db.begin().await?;
let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields { let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields {
let mut hm = HashMap::new(); let mut hm = HashMap::new();
@@ -653,12 +626,8 @@ async fn execute_component(
} }
ExecuteApp { args, raw_code: None, path: Some(path), .. } => { ExecuteApp { args, raw_code: None, path: Some(path), .. } => {
let payload = if path.starts_with("script/") { let payload = if path.starts_with("script/") {
script_path_to_payload( script_path_to_payload(path.strip_prefix("script/").unwrap(), &mut tx, &w_id)
path.strip_prefix("script/").unwrap(), .await?
tx.transaction_mut(),
&w_id,
)
.await?
} else if path.starts_with("flow/") { } else if path.starts_with("flow/") {
JobPayload::Flow(path.strip_prefix("flow/").unwrap().to_string()) JobPayload::Flow(path.strip_prefix("flow/").unwrap().to_string())
} else { } else {
@@ -684,15 +653,14 @@ async fn execute_component(
None, None,
None, None,
None, None,
None,
false, false,
false, false,
None, None,
true, true,
) )
.await?; .await?;
tx.commit().await?;
tx.commit().await?;
Ok(uuid.to_string()) Ok(uuid.to_string())
} }
@@ -742,9 +710,6 @@ fn build_args(
path: String, path: String,
args: &Map<String, Value>, args: &Map<String, Value>,
) -> Result<Map<String, Value>> { ) -> Result<Map<String, Value>> {
// disallow var and res access in args coming from the user for security reasons
args.into_iter()
.try_for_each(|x| disallow_var_res_access(x.1))?;
let static_args = policy let static_args = policy
.triggerables .triggerables
.get(&path) .get(&path)
@@ -765,20 +730,3 @@ fn build_args(
} }
Ok(args) Ok(args)
} }
fn disallow_var_res_access(args: &serde_json::Value) -> Result<()> {
match args {
Value::Object(v) => v.into_iter().try_for_each(|x| disallow_var_res_access(x.1)),
Value::Array(arr) => arr.into_iter().try_for_each(|v| disallow_var_res_access(v)),
Value::String(s) => {
if s.starts_with("$var:") || s.starts_with("$res:") {
Err(Error::BadRequest(format!(
"For security reasons, variable or resource access is not allowed as dynamic argument"
)))
} else {
Ok(())
}
}
_ => Ok(()),
}
}

View File

@@ -7,12 +7,11 @@
*/ */
use axum::{ use axum::{
extract::{Extension, Path, Query}, extract::{Extension, Path},
routing::{get, post, put}, routing::{get, post, put},
Json, Router, Json, Router,
}; };
use hyper::{HeaderMap, StatusCode}; use hyper::StatusCode;
use serde::Deserialize;
use windmill_common::{ use windmill_common::{
error::{JsonResult, Result}, error::{JsonResult, Result},
utils::{not_found_if_none, StripPath}, utils::{not_found_if_none, StripPath},
@@ -20,7 +19,6 @@ use windmill_common::{
use crate::{ use crate::{
db::{UserDB, DB}, db::{UserDB, DB},
jobs::add_include_headers,
users::Authed, users::Authed,
}; };
@@ -85,21 +83,13 @@ pub async fn new_payload(
Ok(StatusCode::CREATED) Ok(StatusCode::CREATED)
} }
#[derive(Deserialize, Clone)]
pub struct IncludeHeaderQuery {
include_header: Option<String>,
}
pub async fn update_payload( pub async fn update_payload(
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
Query(run_query): Query<IncludeHeaderQuery>, Json(payload): Json<serde_json::Value>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> Result<StatusCode> { ) -> Result<StatusCode> {
let mut tx = db.begin().await?; let mut tx = db.begin().await?;
let args = add_include_headers(&run_query.include_header, headers, args.unwrap_or_default());
sqlx::query!( sqlx::query!(
" "
UPDATE capture UPDATE capture
@@ -109,7 +99,7 @@ pub async fn update_payload(
", ",
&w_id, &w_id,
&path.to_path(), &path.to_path(),
serde_json::json!(args), &payload,
) )
.execute(&mut tx) .execute(&mut tx)
.await?; .await?;

View File

@@ -6,20 +6,15 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use crate::{ use hyper::StatusCode;
db::{UserDB, DB}, use reqwest::Client;
schedule::clear_schedule, use sql_builder::prelude::*;
users::{maybe_refresh_folders, require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
use axum::{ use axum::{
extract::{Extension, Path, Query}, extract::{Extension, Path, Query},
routing::{delete, get, post}, routing::{get, post},
Json, Router, Json, Router,
}; };
use hyper::StatusCode;
use sql_builder::prelude::*;
use sql_builder::SqlBuilder; use sql_builder::SqlBuilder;
use sqlx::{Postgres, Transaction}; use sqlx::{Postgres, Transaction};
use windmill_audit::{audit_log, ActionKind}; use windmill_audit::{audit_log, ActionKind};
@@ -31,7 +26,13 @@ use windmill_common::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath, http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
}, },
}; };
use windmill_queue::{push, schedule::push_scheduled_job, JobPayload, QueueTransaction}; use windmill_queue::{push, schedule::push_scheduled_job, JobPayload};
use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
};
pub fn workspaced_service() -> Router { pub fn workspaced_service() -> Router {
Router::new() Router::new()
@@ -39,7 +40,6 @@ pub fn workspaced_service() -> Router {
.route("/create", post(create_flow)) .route("/create", post(create_flow))
.route("/update/*path", post(update_flow)) .route("/update/*path", post(update_flow))
.route("/archive/*path", post(archive_flow_by_path)) .route("/archive/*path", post(archive_flow_by_path))
.route("/delete/*path", delete(delete_flow_by_path))
.route("/get/*path", get(get_flow_by_path)) .route("/get/*path", get(get_flow_by_path))
.route("/exists/*path", get(exists_flow_by_path)) .route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths)) .route("/list_paths", get(list_paths))
@@ -80,7 +80,7 @@ async fn list_flows(
) )
.order_desc("favorite.path IS NOT NULL") .order_desc("favorite.path IS NOT NULL")
.order_by("edited_at", lq.order_desc.unwrap_or(true)) .order_by("edited_at", lq.order_desc.unwrap_or(true))
.and_where("o.workspace_id = ?".bind(&w_id)) .and_where("o.workspace_id = ? OR o.workspace_id = 'starter'".bind(&w_id))
.offset(offset) .offset(offset)
.limit(per_page) .limit(per_page)
.clone(); .clone();
@@ -110,9 +110,12 @@ async fn list_flows(
Ok(Json(rows)) Ok(Json(rows))
} }
async fn list_hub_flows(Authed { email, .. }: Authed) -> JsonResult<serde_json::Value> { async fn list_hub_flows(
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
let flows = list_elems_from_hub( let flows = list_elems_from_hub(
&HTTP_CLIENT, http_client,
"https://hub.windmill.dev/searchFlowData?approved=true", "https://hub.windmill.dev/searchFlowData?approved=true",
&email, &email,
) )
@@ -141,9 +144,10 @@ async fn list_paths(
pub async fn get_hub_flow_by_id( pub async fn get_hub_flow_by_id(
Authed { email, .. }: Authed, Authed { email, .. }: Authed,
Path(id): Path<i32>, Path(id): Path<i32>,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> { ) -> JsonResult<serde_json::Value> {
let value = http_get_from_hub( let value = http_get_from_hub(
&HTTP_CLIENT, http_client,
&format!("https://hub.windmill.dev/flows/{id}/json"), &format!("https://hub.windmill.dev/flows/{id}/json"),
&email, &email,
false, false,
@@ -176,20 +180,15 @@ async fn check_path_conflict<'c>(
async fn create_flow( async fn create_flow(
authed: Authed, authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Json(nf): Json<NewFlow>, Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
// cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; // cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
let authed = maybe_refresh_folders(&nf.path, &w_id, authed, &db).await; let mut tx = user_db.clone().begin(&authed).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into(); check_path_conflict(&mut tx, &w_id, &nf.path).await?;
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
check_path_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
check_schedule_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
sqlx::query!( sqlx::query!(
"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, \ "INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, \
@@ -221,11 +220,9 @@ async fn create_flow(
) )
.await?; .await?;
webhook.send_message( tx.commit().await?;
w_id.clone(),
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
);
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push( let (dependency_job_uuid, mut tx) = push(
tx, tx,
&w_id, &w_id,
@@ -237,14 +234,12 @@ async fn create_flow(
None, None,
None, None,
None, None,
None,
false, false,
false, false,
None, None,
true, true,
) )
.await?; .await?;
sqlx::query!( sqlx::query!(
"UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3", "UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3",
dependency_job_uuid, dependency_job_uuid,
@@ -284,18 +279,14 @@ async fn check_schedule_conflict<'c>(
async fn update_flow( async fn update_flow(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, flow_path)): Path<(String, StripPath)>, Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>, Json(nf): Json<NewFlow>,
) -> Result<String> { ) -> Result<String> {
let mut tx = user_db.clone().begin(&authed).await?;
let flow_path = flow_path.to_path(); let flow_path = flow_path.to_path();
let authed = maybe_refresh_folders(&flow_path, &w_id, authed, &db).await; check_schedule_conflict(&mut tx, &w_id, flow_path).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
check_schedule_conflict(tx.transaction_mut(), &w_id, flow_path).await?;
let schema = nf.schema.map(|x| x.0); let schema = nf.schema.map(|x| x.0);
let old_dep_job = sqlx::query_scalar!( let old_dep_job = sqlx::query_scalar!(
@@ -322,13 +313,13 @@ async fn update_flow(
.await?; .await?;
if nf.path != flow_path { if nf.path != flow_path {
check_schedule_conflict(tx.transaction_mut(), &w_id, &nf.path).await?; check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
if !authed.is_admin { if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &flow_path, &db).await?; require_owner_of_path(&w_id, &authed.username, &authed.groups, &flow_path, &db).await?;
} }
let mut schedulables: Vec<Schedule> = sqlx::query_as!( let mut schedulables = sqlx::query_as!(
Schedule, Schedule,
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *", "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
nf.path, nf.path,
@@ -351,41 +342,34 @@ async fn update_flow(
schedulables.push(schedule); schedulables.push(schedule);
} }
for schedule in schedulables.into_iter() { for schedule in schedulables {
// TODO: Why is this in the loop in the first place? Seems like it's just doing nothing after the first iteration? Should this use schedule.path? clear_schedule(&mut tx, flow_path, true).await?;
clear_schedule(tx.transaction_mut(), flow_path, true).await?;
if schedule.enabled { if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?; tx = push_scheduled_job(tx, schedule).await?;
} }
} }
audit_log(
&mut tx,
&authed.username,
"flows.update",
ActionKind::Create,
&w_id,
Some(&nf.path.to_string()),
Some(
[Some(("flow", nf.path.as_str()))]
.into_iter()
.flatten()
.collect(),
),
)
.await?;
} }
webhook.send_message( audit_log(
w_id.clone(), &mut tx,
WebhookMessage::UpdateFlow { &authed.username,
workspace: w_id.clone(), "flows.update",
old_path: flow_path.to_owned(), ActionKind::Create,
new_path: nf.path.clone(), &w_id,
}, Some(&nf.path.to_string()),
); Some(
[Some(("flow", nf.path.as_str()))]
.into_iter()
.flatten()
.collect(),
),
)
.await?;
tx.commit().await?;
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push( let (dependency_job_uuid, mut tx) = push(
tx, tx,
&w_id, &w_id,
@@ -397,7 +381,6 @@ async fn update_flow(
None, None,
None, None,
None, None,
None,
false, false,
false, false,
None, None,
@@ -432,12 +415,13 @@ async fn get_flow_by_path(
let path = path.to_path(); let path = path.to_path();
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
let flow_o = let flow_o = sqlx::query_as::<_, Flow>(
sqlx::query_as::<_, Flow>("SELECT * FROM flow WHERE path = $1 AND workspace_id = $2") "SELECT * FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
.bind(path) )
.bind(w_id) .bind(path)
.fetch_optional(&mut tx) .bind(w_id)
.await?; .fetch_optional(&mut tx)
.await?;
tx.commit().await?; tx.commit().await?;
let flow = not_found_if_none(flow_o, "Flow", path)?; let flow = not_found_if_none(flow_o, "Flow", path)?;
@@ -451,7 +435,8 @@ async fn exists_flow_by_path(
let path = path.to_path(); let path = path.to_path();
let exists = sqlx::query_scalar!( let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)", "SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id \
= 'starter'))",
path, path,
w_id w_id
) )
@@ -465,7 +450,6 @@ async fn exists_flow_by_path(
async fn archive_flow_by_path( async fn archive_flow_by_path(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> { ) -> Result<String> {
let path = path.to_path(); let path = path.to_path();
@@ -490,50 +474,10 @@ async fn archive_flow_by_path(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::ArchiveFlow { workspace: w_id, path: path.to_owned() },
);
Ok(format!("Flow {path} archived")) Ok(format!("Flow {path} archived"))
} }
async fn delete_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM flow WHERE path = $1 AND workspace_id = $2",
path,
&w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"flows.delete",
ActionKind::Delete,
&w_id,
Some(path),
Some([("workspace", w_id.as_str())].into()),
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteFlow { workspace: w_id, path: path.to_owned() },
);
Ok(format!("Flow {path} deleted"))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -555,6 +499,7 @@ mod tests {
modules: vec![ modules: vec![
FlowModule { FlowModule {
id: "a".to_string(), id: "a".to_string(),
input_transforms: [].into(),
value: FlowModuleValue::Script { value: FlowModuleValue::Script {
path: "test".to_string(), path: "test".to_string(),
input_transforms: [( input_transforms: [(
@@ -572,6 +517,7 @@ mod tests {
}, },
FlowModule { FlowModule {
id: "b".to_string(), id: "b".to_string(),
input_transforms: HashMap::new(),
value: FlowModuleValue::RawScript { value: FlowModuleValue::RawScript {
input_transforms: HashMap::new(), input_transforms: HashMap::new(),
content: "test".to_string(), content: "test".to_string(),
@@ -590,6 +536,7 @@ mod tests {
}, },
FlowModule { FlowModule {
id: "c".to_string(), id: "c".to_string(),
input_transforms: HashMap::new(),
value: FlowModuleValue::ForloopFlow { value: FlowModuleValue::ForloopFlow {
iterator: InputTransform::Static { value: serde_json::json!([1, 2, 3]) }, iterator: InputTransform::Static { value: serde_json::json!([1, 2, 3]) },
modules: vec![], modules: vec![],
@@ -608,6 +555,7 @@ mod tests {
], ],
failure_module: Some(FlowModule { failure_module: Some(FlowModule {
id: "d".to_string(), id: "d".to_string(),
input_transforms: HashMap::new(),
value: FlowModuleValue::Script { value: FlowModuleValue::Script {
path: "test".to_string(), path: "test".to_string(),
input_transforms: HashMap::new(), input_transforms: HashMap::new(),
@@ -628,6 +576,7 @@ mod tests {
"modules": [ "modules": [
{ {
"id": "a", "id": "a",
"input_transforms": {},
"value": { "value": {
"input_transforms": { "input_transforms": {
"test": { "test": {
@@ -638,22 +587,29 @@ mod tests {
"type": "script", "type": "script",
"path": "test" "path": "test"
}, },
"stop_after_if": null,
"summary": null
}, },
{ {
"id": "b", "id": "b",
"input_transforms": {},
"value": { "value": {
"input_transforms": {}, "input_transforms": {},
"type": "rawscript", "type": "rawscript",
"content": "test", "content": "test",
"lock": null,
"path": null,
"language": "deno" "language": "deno"
}, },
"stop_after_if": { "stop_after_if": {
"expr": "foo = 'bar'", "expr": "foo = 'bar'",
"skip_if_stopped": false "skip_if_stopped": false
} },
"summary": null
}, },
{ {
"id": "c", "id": "c",
"input_transforms": {},
"value": { "value": {
"type": "forloopflow", "type": "forloopflow",
"iterator": { "iterator": {
@@ -671,11 +627,13 @@ mod tests {
"stop_after_if": { "stop_after_if": {
"expr": "previous.isEmpty()", "expr": "previous.isEmpty()",
"skip_if_stopped": false, "skip_if_stopped": false,
} },
"summary": null
} }
], ],
"failure_module": { "failure_module": {
"id": "d", "id": "d",
"input_transforms": {},
"value": { "value": {
"input_transforms": {}, "input_transforms": {},
"type": "script", "type": "script",
@@ -684,12 +642,38 @@ mod tests {
"stop_after_if": { "stop_after_if": {
"expr": "previous.isEmpty()", "expr": "previous.isEmpty()",
"skip_if_stopped": false "skip_if_stopped": false
} },
"summary": null
} }
}); });
assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect)); assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect));
} }
#[test]
fn test_back_compat() {
/* renamed input_transform -> input_transforms but should deserialize old name */
let s = r#"
{
"value": {
"type": "rawscript",
"content": "def main(n): return",
"language": "python3"
},
"input_transform": {
"n": {
"expr": "flow_input.iter.value",
"type": "javascript"
}
}
}
"#;
let module: FlowModule = serde_json::from_str(s).unwrap();
assert_eq!(
module.input_transforms["n"],
InputTransform::Javascript { expr: "flow_input.iter.value".to_string() }
);
}
#[test] #[test]
fn retry_serde() { fn retry_serde() {
assert_eq!(Retry::default(), serde_json::from_str(r#"{}"#).unwrap()); assert_eq!(Retry::default(), serde_json::from_str(r#"{}"#).unwrap());

View File

@@ -6,23 +6,19 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use std::sync::Arc;
use crate::{ use crate::{
db::{UserDB, DB}, db::{UserDB, DB},
users::{AuthCache, Authed, Tokened}, users::Authed,
webhook_util::{WebhookMessage, WebhookShared},
}; };
use axum::{ use axum::{
extract::{Extension, Path, Query}, extract::{Extension, Path, Query},
routing::{delete, get, post}, routing::{delete, get, post},
Json, Router, Json, Router,
}; };
use lazy_static::lazy_static; use itertools::Itertools;
use regex::Regex;
use windmill_audit::{audit_log, ActionKind}; use windmill_audit::{audit_log, ActionKind};
use windmill_common::{ use windmill_common::{
error::{self, to_anyhow, Error, JsonResult, Result}, error::{self, Error, JsonResult, Result},
users::username_to_permissioned_as, users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, Pagination}, utils::{not_found_if_none, paginate, Pagination},
}; };
@@ -140,28 +136,15 @@ async fn check_name_conflict<'c>(
return Ok(()); return Ok(());
} }
lazy_static! {
static ref VALID_FOLDER_NAME: Regex = Regex::new(r#"^[a-zA-Z_0-9]+$"#).unwrap();
}
async fn create_folder( async fn create_folder(
authed: Authed, authed: Authed,
Tokened { token }: Tokened,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(cache): Extension<Arc<AuthCache>>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Json(ng): Json<NewFolder>, Json(ng): Json<NewFolder>,
) -> Result<String> { ) -> Result<String> {
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
if !VALID_FOLDER_NAME.is_match(&ng.name) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Folder name can only contain alphanumeric characters, underscores"
)));
}
check_name_conflict(&mut tx, &w_id, &ng.name).await?; check_name_conflict(&mut tx, &w_id, &ng.name).await?;
cache.invalidate(&w_id, token).await;
let owner = username_to_permissioned_as(&authed.username); let owner = username_to_permissioned_as(&authed.username);
let owners = &ng.owners.unwrap_or(vec![owner.clone()]); let owners = &ng.owners.unwrap_or(vec![owner.clone()]);
@@ -210,12 +193,8 @@ async fn create_folder(
None, None,
) )
.await?; .await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFolder { workspace: w_id, name: ng.name.clone() },
);
tx.commit().await?;
Ok(format!("Created folder {}", ng.name)) Ok(format!("Created folder {}", ng.name))
} }
@@ -266,7 +245,6 @@ pub async fn require_is_owner(
async fn update_folder( async fn update_folder(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>, Path((w_id, name)): Path<(String, String)>,
Json(ng): Json<UpdateFolder>, Json(ng): Json<UpdateFolder>,
) -> Result<String> { ) -> Result<String> {
@@ -277,26 +255,13 @@ async fn update_folder(
sqlb.and_where_eq("workspace_id", "?".bind(&w_id)); sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
if let Some(display_name) = ng.display_name { if let Some(display_name) = ng.display_name {
sqlb.set("display_name", "?".bind(&display_name)); sqlb.set("display_name", display_name);
} }
if let Some(owners) = ng.owners { if let Some(owners) = ng.owners {
sqlb.set( sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
"owners",
"?".bind(&format!(
"{{{}}}",
owners
.iter()
.map(|x| format!("\"{x}\""))
.collect::<Vec<_>>()
.join(","),
)),
);
} }
if let Some(extra_perms) = ng.extra_perms { if let Some(extra_perms) = ng.extra_perms {
sqlb.set( sqlb.set_str("extra_perms", extra_perms.to_string());
"extra_perms",
"?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?),
);
} }
sqlb.returning("*"); sqlb.returning("*");
@@ -333,12 +298,8 @@ async fn update_folder(
None, None,
) )
.await?; .await?;
tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.to_owned() },
);
tx.commit().await?;
Ok(format!("Updated folder {}", name)) Ok(format!("Updated folder {}", name))
} }
@@ -455,7 +416,6 @@ async fn get_folder_usage(
async fn delete_folder( async fn delete_folder(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>, Path((w_id, name)): Path<(String, String)>,
) -> Result<String> { ) -> Result<String> {
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
@@ -480,12 +440,6 @@ async fn delete_folder(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("delete folder at name {}", name)) Ok(format!("delete folder at name {}", name))
} }
@@ -493,7 +447,6 @@ async fn add_owner(
authed: Authed, authed: Authed,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>, Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>, Json(Owner { owner }): Json<Owner>,
) -> Result<String> { ) -> Result<String> {
@@ -524,12 +477,6 @@ async fn add_owner(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("Added {} to folder {}", owner, name)) Ok(format!("Added {} to folder {}", owner, name))
} }
@@ -563,7 +510,6 @@ async fn remove_owner(
authed: Authed, authed: Authed,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>, Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>, Json(Owner { owner }): Json<Owner>,
) -> Result<String> { ) -> Result<String> {
@@ -594,11 +540,5 @@ async fn remove_owner(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("Removed {} to folder {}", owner, name)) Ok(format!("Removed {} to folder {}", owner, name))
} }

View File

@@ -1,282 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use crate::{db::UserDB, jobs::CompletedJob, users::Authed};
use axum::{
extract::{Path, Query},
routing::{get, post},
Extension, Json, Router,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sqlx::types::Uuid;
use std::{
fmt::{Display, Formatter},
vec,
};
use windmill_common::{
error::JsonResult,
scripts::to_i64,
utils::{paginate, Pagination},
};
use windmill_queue::JobKind;
pub fn workspaced_service() -> Router {
Router::new()
.route("/history", get(get_input_history))
.route("/list", get(list_saved_inputs))
.route("/create", post(create_input))
.route("/update", post(update_input))
.route("/delete/:id", post(delete_input))
}
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
pub struct InputRow {
pub id: Uuid,
pub workspace_id: String,
pub runnable_id: String,
pub runnable_type: RunnableType,
pub name: String,
pub args: Value,
pub created_at: DateTime<Utc>,
pub created_by: String,
pub is_public: bool,
}
#[derive(Debug, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "runnable_type")]
pub enum RunnableType {
ScriptHash,
ScriptPath,
FlowPath,
}
impl Display for RunnableType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RunnableType::ScriptHash => write!(f, "ScriptHash"),
RunnableType::ScriptPath => write!(f, "ScriptPath"),
RunnableType::FlowPath => write!(f, "FlowPath"),
}
}
}
impl RunnableType {
fn job_kind(&self) -> JobKind {
match self {
RunnableType::ScriptHash => JobKind::Script,
RunnableType::ScriptPath => JobKind::Script,
RunnableType::FlowPath => JobKind::Flow,
}
}
fn column_name(&self) -> &'static str {
match self {
RunnableType::ScriptHash => "script_hash",
RunnableType::ScriptPath => "script_path",
RunnableType::FlowPath => "script_path",
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RunnableParams {
pub runnable_id: String,
pub runnable_type: RunnableType,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Input {
id: Uuid,
name: String,
created_at: chrono::DateTime<chrono::Utc>,
args: serde_json::Value,
created_by: String,
is_public: bool,
}
async fn get_input_history(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(r): Query<RunnableParams>,
) -> JsonResult<Vec<Input>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.begin(&authed).await?;
let sql = &format!(
"select distinct on (args) * from completed_job \
where {} = $1 and job_kind = $2 and workspace_id = $3 \
order by args, started_at desc limit $4 offset $5",
r.runnable_type.column_name()
);
let query = sqlx::query_as::<_, CompletedJob>(sql);
let query = match r.runnable_type {
RunnableType::ScriptHash => query.bind(to_i64(&r.runnable_id)?),
_ => query.bind(&r.runnable_id),
};
let rows = query
.bind(r.runnable_type.job_kind())
.bind(&w_id)
.bind(per_page as i32)
.bind(offset as i32)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
let mut inputs = vec![];
for row in rows {
inputs.push(Input {
id: row.id,
name: format!(
"{} {}",
row.created_at.format("%H:%M %-d/%-m"),
row.created_by
),
created_at: row.created_at,
args: row.args.unwrap_or(serde_json::json!({})),
created_by: row.created_by,
is_public: true,
});
}
Ok(Json(inputs))
}
async fn list_saved_inputs(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(r): Query<RunnableParams>,
) -> JsonResult<Vec<Input>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, InputRow>(
"select * from input \
where runnable_id = $1 and runnable_type = $2 and workspace_id = $3 \
and is_public IS true OR created_by = $4 \
order by created_at desc limit $5 offset $6",
)
.bind(&r.runnable_id)
.bind(&r.runnable_type)
.bind(&w_id)
.bind(&authed.username)
.bind(per_page as i32)
.bind(offset as i32)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
let mut inputs: Vec<Input> = Vec::new();
for row in rows {
inputs.push(Input {
id: row.id,
name: row.name,
args: row.args,
created_by: row.created_by,
created_at: row.created_at,
is_public: row.is_public,
})
}
Ok(Json(inputs))
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateInput {
name: String,
args: serde_json::Value,
}
async fn create_input(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(r): Query<RunnableParams>,
Json(input): Json<CreateInput>,
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
let id = Uuid::new_v4();
sqlx::query(
"INSERT INTO input (id, workspace_id, runnable_id, runnable_type, name, args, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7)",
)
.bind(&id)
.bind(&w_id)
.bind(&r.runnable_id)
.bind(&r.runnable_type)
.bind(&input.name)
.bind(&input.args)
.bind(&authed.username)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(id.to_string()))
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateInput {
id: Uuid,
name: String,
is_public: bool,
}
async fn update_input(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(input): Json<UpdateInput>,
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
sqlx::query("UPDATE input SET name = $1, is_public = $2 WHERE id = $3 and workspace_id = $4")
.bind(&input.name)
.bind(&input.is_public)
.bind(&input.id)
.bind(&w_id)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(input.id.to_string()))
}
async fn delete_input(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, i_id)): Path<(String, Uuid)>,
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
sqlx::query("DELETE FROM input WHERE id = $1 and workspace_id = $2")
.bind(&i_id)
.bind(&w_id)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(i_id.to_string()))
}

View File

@@ -6,12 +6,8 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use crate::{ use std::sync::Arc;
db::{UserDB, DB},
users::{require_owner_of_path, Authed, OptAuthed},
variables::get_workspace_key,
BASE_URL,
};
use anyhow::Context; use anyhow::Context;
use axum::{ use axum::{
extract::{FromRequest, Json, Path, Query}, extract::{FromRequest, Json, Path, Query},
@@ -36,8 +32,13 @@ use windmill_common::{
users::username_to_permissioned_as, users::username_to_permissioned_as,
utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath}, utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath},
}; };
use windmill_queue::{ use windmill_queue::{get_queued_job, push, JobKind, JobPayload, QueuedJob, RawCode};
get_queued_job, push, JobKind, JobPayload, QueueTransaction, QueuedJob, RawCode,
use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed},
variables::get_workspace_key,
BaseUrl, QueueLimitWaitResult, TimeoutWaitResult,
}; };
pub fn workspaced_service() -> Router { pub fn workspaced_service() -> Router {
@@ -48,10 +49,6 @@ pub fn workspaced_service() -> Router {
"/run_wait_result/p/*script_path", "/run_wait_result/p/*script_path",
post(run_wait_result_job_by_path), post(run_wait_result_job_by_path),
) )
.route(
"/run_wait_result/p/*script_path",
get(run_wait_result_job_by_path_get),
)
.route( .route(
"/run_wait_result/h/:hash", "/run_wait_result/h/:hash",
post(run_wait_result_job_by_hash), post(run_wait_result_job_by_hash),
@@ -65,7 +62,7 @@ pub fn workspaced_service() -> Router {
.route("/run/preview_flow", post(run_preview_flow_job)) .route("/run/preview_flow", post(run_preview_flow_job))
.route("/list", get(list_jobs)) .route("/list", get(list_jobs))
.route("/queue/list", get(list_queue_jobs)) .route("/queue/list", get(list_queue_jobs))
.route("/queue/count", get(count_queue_jobs)) .route("/queue/cancel/:id", post(cancel_job_api))
.route("/completed/list", get(list_completed_jobs)) .route("/completed/list", get(list_completed_jobs))
.route("/completed/get/:id", get(get_completed_job)) .route("/completed/get/:id", get(get_completed_job))
.route("/completed/get_result/:id", get(get_completed_job_result)) .route("/completed/get_result/:id", get(get_completed_job_result))
@@ -103,39 +100,32 @@ pub fn global_service() -> Router {
) )
.route("/get/:id", get(get_job)) .route("/get/:id", get(get_job))
.route("/getupdate/:id", get(get_job_update)) .route("/getupdate/:id", get(get_job_update))
.route("/queue/cancel/:id", post(cancel_job_api))
.route("/queue/force_cancel/:id", post(force_cancel))
} }
async fn get_result_by_id( async fn get_result_by_id(
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>, Query(ResultByIdQuery { skip_direct }): Query<ResultByIdQuery>,
Path((w_id, flow_id, node_id)): Path<(String, String, String)>,
) -> windmill_common::error::JsonResult<serde_json::Value> { ) -> windmill_common::error::JsonResult<serde_json::Value> {
let res = windmill_queue::get_result_by_id(db, w_id, flow_id, node_id).await?; let res = windmill_queue::get_result_by_id(db, skip_direct, w_id, flow_id, node_id).await?;
Ok(Json(res)) Ok(Json(res))
} }
async fn cancel_job_api( async fn cancel_job_api(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>, authed: Authed,
OptAuthed(opt_authed): OptAuthed, Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>, Path((w_id, id)): Path<(String, Uuid)>,
Json(CancelJob { reason }): Json<CancelJob>, Json(CancelJob { reason }): Json<CancelJob>,
) -> error::Result<String> { ) -> error::Result<String> {
let tx = db.begin().await?; let tx = user_db.begin(&authed).await?;
let username = match opt_authed {
Some(authed) => authed.username,
None => "anonymous".to_string(),
};
let (mut tx, job_option) = let (mut tx, job_option) =
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, rsmq, false).await?; windmill_queue::cancel_job(&authed.username, reason, id, &w_id, tx).await?;
if let Some(id) = job_option { if let Some(id) = job_option {
audit_log( audit_log(
&mut tx, &mut tx,
&username, &authed.username,
"jobs.cancel", "jobs.cancel",
ActionKind::Delete, ActionKind::Delete,
&w_id, &w_id,
@@ -158,56 +148,14 @@ async fn cancel_job_api(
} }
} }
async fn force_cancel(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Json(CancelJob { reason }): Json<CancelJob>,
) -> error::Result<String> {
let tx = db.begin().await?;
let username = match opt_authed {
Some(authed) => authed.username,
None => "anonymous".to_string(),
};
let (mut tx, job_option) =
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, rsmq, true).await?;
if let Some(id) = job_option {
audit_log(
&mut tx,
&username,
"jobs.force_cancel",
ActionKind::Delete,
&w_id,
Some(&id.to_string()),
None,
)
.await?;
tx.commit().await?;
Ok(id.to_string())
} else {
let (job_o, tx) = get_job_by_id(tx, &w_id, id).await?;
tx.commit().await?;
let err = match job_o {
Some(Job::CompletedJob(_)) => {
return Ok(format!("queued job id {} is already completed", id))
}
_ => error::Error::NotFound(format!("queued job id {} does not exist", id)),
};
Err(err)
}
}
pub async fn get_path_for_hash<'c>( pub async fn get_path_for_hash<'c>(
db: &mut Transaction<'c, Postgres>, db: &mut Transaction<'c, Postgres>,
w_id: &str, w_id: &str,
hash: i64, hash: i64,
) -> error::Result<String> { ) -> error::Result<String> {
let path = sqlx::query_scalar!( let path = sqlx::query_scalar!(
"select path from script where hash = $1 AND workspace_id = $2", "select path from script where hash = $1 AND (workspace_id = $2 OR workspace_id = \
'starter')",
hash, hash,
w_id w_id
) )
@@ -232,6 +180,11 @@ async fn get_job(
Ok(Json(job)) Ok(Json(job))
} }
#[derive(Deserialize)]
pub struct ResultByIdQuery {
pub skip_direct: bool,
}
pub async fn get_job_by_id<'c>( pub async fn get_job_by_id<'c>(
mut tx: Transaction<'c, Postgres>, mut tx: Transaction<'c, Postgres>,
w_id: &str, w_id: &str,
@@ -305,8 +258,6 @@ pub struct CompletedJob {
pub is_skipped: bool, pub is_skipped: bool,
pub email: String, pub email: String,
pub visible_to_owner: bool, pub visible_to_owner: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
@@ -317,7 +268,6 @@ pub struct RunJobQuery {
include_header: Option<String>, include_header: Option<String>,
invisible_to_owner: Option<bool>, invisible_to_owner: Option<bool>,
queue_limit: Option<i64>, queue_limit: Option<i64>,
payload: Option<String>,
} }
lazy_static::lazy_static! { lazy_static::lazy_static! {
@@ -346,51 +296,41 @@ impl RunJobQuery {
fn add_include_headers( fn add_include_headers(
&self, &self,
headers: HeaderMap, headers: HeaderMap,
args: serde_json::Map<String, serde_json::Value>, mut args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> { ) -> serde_json::Map<String, serde_json::Value> {
return add_include_headers(&self.include_header, headers, args); let whitelist = self
.include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
serde_json::Value::String(v.to_str().unwrap().to_string()),
);
}
});
args
} }
} }
pub fn add_include_headers(
include_header: &Option<String>,
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let whitelist = include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
serde_json::Value::String(v.to_str().unwrap().to_string()),
);
}
});
args
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ListQueueQuery { pub struct ListQueueQuery {
pub script_path_start: Option<String>, pub script_path_start: Option<String>,
pub script_path_exact: Option<String>, pub script_path_exact: Option<String>,
pub script_hash: Option<String>, pub script_hash: Option<String>,
pub created_by: Option<String>, pub created_by: Option<String>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>, pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>, pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub running: Option<bool>, pub running: Option<bool>,
pub parent_job: Option<String>, pub parent_job: Option<String>,
pub order_desc: Option<bool>, pub order_desc: Option<bool>,
pub job_kinds: Option<String>, pub job_kinds: Option<String>,
pub suspended: Option<bool>, pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
} }
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder { fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
@@ -419,11 +359,11 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
if let Some(pj) = &lq.parent_job { if let Some(pj) = &lq.parent_job {
sqlb.and_where_eq("parent_job", "?".bind(pj)); sqlb.and_where_eq("parent_job", "?".bind(pj));
} }
if let Some(dt) = &lq.started_before { if let Some(dt) = &lq.created_before {
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp())); sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
} }
if let Some(dt) = &lq.started_after { if let Some(dt) = &lq.created_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp())); sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
} }
if let Some(s) = &lq.suspended { if let Some(s) = &lq.suspended {
@@ -433,7 +373,6 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
sqlb.and_where_eq("suspend", 0); sqlb.and_where_eq("suspend", 0);
} }
} }
if let Some(jk) = &lq.job_kinds { if let Some(jk) = &lq.job_kinds {
sqlb.and_where_in( sqlb.and_where_in(
"job_kind", "job_kind",
@@ -441,10 +380,6 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
); );
} }
if let Some(args) = &lq.args {
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
}
sqlb sqlb
} }
@@ -500,26 +435,6 @@ async fn list_queue_jobs(
Ok(Json(jobs)) Ok(Json(jobs))
} }
#[derive(Serialize, Debug, FromRow)]
struct QueueStats {
database_length: i64,
}
async fn count_queue_jobs(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> error::JsonResult<QueueStats> {
Ok(Json(
sqlx::query_as!(
QueueStats,
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE workspace_id = $1",
w_id
)
.fetch_one(&db)
.await?,
))
}
async fn list_jobs( async fn list_jobs(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
@@ -537,14 +452,13 @@ async fn list_jobs(
script_path_exact: lq.script_path_exact, script_path_exact: lq.script_path_exact,
script_hash: lq.script_hash, script_hash: lq.script_hash,
created_by: lq.created_by, created_by: lq.created_by,
started_before: lq.started_before, created_before: lq.created_before,
started_after: lq.started_after, created_after: lq.created_after,
running: None, running: None,
parent_job: lq.parent_job, parent_job: lq.parent_job,
order_desc: Some(true), order_desc: Some(true),
job_kinds: lq.job_kinds, job_kinds: lq.job_kinds,
suspended: lq.suspended, suspended: lq.suspended,
args: lq.args,
}, },
&[ &[
"'QueuedJob' as typ", "'QueuedJob' as typ",
@@ -558,7 +472,7 @@ async fn list_jobs(
"running", "running",
"script_hash", "script_hash",
"script_path", "script_path",
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END", "args",
"null as duration_ms", "null as duration_ms",
"null as success", "null as success",
"false as deleted", "false as deleted",
@@ -573,7 +487,6 @@ async fn list_jobs(
"email", "email",
"visible_to_owner", "visible_to_owner",
"suspend", "suspend",
"mem_peak",
], ],
); );
let sqlc = list_completed_jobs_query( let sqlc = list_completed_jobs_query(
@@ -593,7 +506,7 @@ async fn list_jobs(
"null as running", "null as running",
"script_hash", "script_hash",
"script_path", "script_path",
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END", "args",
"duration_ms", "duration_ms",
"success", "success",
"deleted", "deleted",
@@ -608,7 +521,6 @@ async fn list_jobs(
"email", "email",
"visible_to_owner", "visible_to_owner",
"null as suspend", "null as suspend",
"mem_peak",
], ],
); );
let sql = format!( let sql = format!(
@@ -817,7 +729,6 @@ async fn get_suspended_flow_info<'c>(
pub async fn cancel_suspended_job( pub async fn cancel_suspended_job(
/* unauthed */ /* unauthed */
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>, Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>, Query(approver): Query<QueryApprover>,
) -> error::Result<String> { ) -> error::Result<String> {
@@ -840,8 +751,6 @@ pub async fn cancel_suspended_job(
parent_flow, parent_flow,
&w_id, &w_id,
tx, tx,
rsmq,
false,
) )
.await?; .await?;
if job.is_some() { if job.is_some() {
@@ -1012,16 +921,16 @@ pub async fn get_resume_urls(
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Path((w_id, job_id, resume_id)): Path<(String, Uuid, u32)>, Path((w_id, job_id, resume_id)): Path<(String, Uuid, u32)>,
Query(approver): Query<QueryApprover>, Query(approver): Query<QueryApprover>,
Extension(base_url): Extension<Arc<BaseUrl>>,
) -> error::JsonResult<ResumeUrls> { ) -> error::JsonResult<ResumeUrls> {
let key = get_workspace_key(&w_id, &mut user_db.begin(&authed).await?).await?; let key = get_workspace_key(&w_id, &mut user_db.begin(&authed).await?).await?;
let signature = create_signature(key, job_id, resume_id, approver.approver.clone())?; let signature = create_signature(key, job_id, resume_id, approver.approver.clone())?;
let base_url = base_url.0.clone();
let approver = approver let approver = approver
.approver .approver
.as_ref() .as_ref()
.map(|x| format!("?approver={}", encode(x))) .map(|x| format!("?approver={}", encode(x)))
.unwrap_or_else(String::new); .unwrap_or_else(String::new);
let base_url = BASE_URL.as_str();
let res = ResumeUrls { let res = ResumeUrls {
approvalPage: format!( approvalPage: format!(
"{base_url}/approve/{w_id}/{job_id}/{resume_id}/{signature}{approver}" "{base_url}/approve/{w_id}/{job_id}/{resume_id}/{signature}{approver}"
@@ -1089,7 +998,6 @@ struct UnifiedJob {
email: String, email: String,
visible_to_owner: bool, visible_to_owner: bool,
suspend: Option<i32>, suspend: Option<i32>,
mem_peak: Option<i32>,
} }
impl From<UnifiedJob> for Job { impl From<UnifiedJob> for Job {
@@ -1124,7 +1032,6 @@ impl From<UnifiedJob> for Job {
is_skipped: uj.is_skipped, is_skipped: uj.is_skipped,
email: uj.email, email: uj.email,
visible_to_owner: uj.visible_to_owner, visible_to_owner: uj.visible_to_owner,
mem_peak: uj.mem_peak,
}), }),
"QueuedJob" => Job::QueuedJob(QueuedJob { "QueuedJob" => Job::QueuedJob(QueuedJob {
workspace_id: uj.workspace_id, workspace_id: uj.workspace_id,
@@ -1157,9 +1064,6 @@ impl From<UnifiedJob> for Job {
email: uj.email, email: uj.email,
visible_to_owner: uj.visible_to_owner, visible_to_owner: uj.visible_to_owner,
suspend: uj.suspend, suspend: uj.suspend,
mem_peak: uj.mem_peak,
root_job: None,
leaf_jobs: None,
}), }),
t => panic!("job type {} not valid", t), t => panic!("job type {} not valid", t),
} }
@@ -1222,27 +1126,26 @@ where
struct InPayload { struct InPayload {
payload: Option<String>, payload: Option<String>,
} }
}
}
fn decode_payload<D: DeserializeOwned>(t: String) -> anyhow::Result<D> { fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
let vec = base64::engine::general_purpose::URL_SAFE let vec = base64::engine::general_purpose::URL_SAFE
.decode(t) .decode(t)
.context("invalid base64")?; .context("invalid base64")?;
serde_json::from_slice(vec.as_slice()).context("invalid json") serde_json::from_slice(vec.as_slice()).context("invalid json")
}
}
} }
pub async fn run_flow_by_path( pub async fn run_flow_by_path(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, flow_path)): Path<(String, StripPath)>, Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>, Query(run_query): Query<RunJobQuery>,
headers: HeaderMap, headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>, Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::Result<(StatusCode, String)> { ) -> error::Result<(StatusCode, String)> {
let flow_path = flow_path.to_path(); let flow_path = flow_path.to_path();
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into(); let mut tx = user_db.begin(&authed).await?;
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?; let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default()); let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push( let (uuid, tx) = push(
@@ -1256,7 +1159,6 @@ pub async fn run_flow_by_path(
scheduled_for, scheduled_for,
None, None,
run_query.parent_job, run_query.parent_job,
run_query.parent_job,
false, false,
false, false,
None, None,
@@ -1270,16 +1172,15 @@ pub async fn run_flow_by_path(
pub async fn run_job_by_path( pub async fn run_job_by_path(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, script_path)): Path<(String, StripPath)>, Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>, Query(run_query): Query<RunJobQuery>,
headers: HeaderMap, headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>, Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::Result<(StatusCode, String)> { ) -> error::Result<(StatusCode, String)> {
let script_path = script_path.to_path(); let script_path = script_path.to_path();
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into(); let mut tx = user_db.begin(&authed).await?;
let job_payload = script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?; let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?; let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default()); let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push( let (uuid, tx) = push(
@@ -1293,7 +1194,6 @@ pub async fn run_job_by_path(
scheduled_for, scheduled_for,
None, None,
run_query.parent_job, run_query.parent_job,
run_query.parent_job,
false, false,
false, false,
None, None,
@@ -1408,85 +1308,22 @@ pub async fn check_queue_too_long(db: DB, queue_limit: Option<i64>) -> error::Re
} }
Ok(()) Ok(())
} }
lazy_static::lazy_static! {
pub static ref QUEUE_LIMIT_WAIT_RESULT: Option<i64> = std::env::var("QUEUE_LIMIT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok());
pub static ref TIMEOUT_WAIT_RESULT: i32 = std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(20);
}
pub async fn run_wait_result_job_by_path_get(
authed: Authed,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
) -> error::JsonResult<serde_json::Value> {
let payload_r = run_query
.payload
.map(decode_payload)
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
let args = if let Some(payload) = payload_r {
payload?
} else {
serde_json::Map::new()
};
check_queue_too_long(db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
let script_path = script_path.to_path();
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
let job_payload = script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?;
let (uuid, tx) = push(
tx,
&w_id,
job_payload,
args,
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
None,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
)
.await?;
tx.commit().await?;
run_wait_result(
authed,
Extension(user_db),
*TIMEOUT_WAIT_RESULT,
uuid,
Path((w_id, script_path)),
)
.await
}
pub async fn run_wait_result_job_by_path( pub async fn run_wait_result_job_by_path(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(timeout): Extension<Arc<TimeoutWaitResult>>,
Extension(queue_limit): Extension<Arc<QueueLimitWaitResult>>,
Path((w_id, script_path)): Path<(String, StripPath)>, Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>, Query(run_query): Query<RunJobQuery>,
headers: HeaderMap, headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>, Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::JsonResult<serde_json::Value> { ) -> error::JsonResult<serde_json::Value> {
check_queue_too_long(db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?; check_queue_too_long(db, queue_limit.0.or(run_query.queue_limit)).await?;
let script_path = script_path.to_path(); let script_path = script_path.to_path();
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into(); let mut tx = user_db.clone().begin(&authed).await?;
let job_payload = script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?; let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default()); let args = run_query.add_include_headers(headers, args.unwrap_or_default());
@@ -1498,9 +1335,8 @@ pub async fn run_wait_result_job_by_path(
&authed.username, &authed.username,
&authed.email, &authed.email,
username_to_permissioned_as(&authed.username), username_to_permissioned_as(&authed.username),
scheduled_for,
None, None,
None,
run_query.parent_job,
run_query.parent_job, run_query.parent_job,
false, false,
false, false,
@@ -1513,7 +1349,7 @@ pub async fn run_wait_result_job_by_path(
run_wait_result( run_wait_result(
authed, authed,
Extension(user_db), Extension(user_db),
*TIMEOUT_WAIT_RESULT, timeout.0,
uuid, uuid,
Path((w_id, script_path)), Path((w_id, script_path)),
) )
@@ -1523,8 +1359,8 @@ pub async fn run_wait_result_job_by_path(
pub async fn run_wait_result_job_by_hash( pub async fn run_wait_result_job_by_hash(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(timeout): Extension<Arc<TimeoutWaitResult>>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>, Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>, Query(run_query): Query<RunJobQuery>,
headers: HeaderMap, headers: HeaderMap,
@@ -1533,9 +1369,9 @@ pub async fn run_wait_result_job_by_hash(
check_queue_too_long(db, run_query.queue_limit).await?; check_queue_too_long(db, run_query.queue_limit).await?;
let hash = script_hash.0; let hash = script_hash.0;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into(); let mut tx = user_db.clone().begin(&authed).await?;
let path = get_path_for_hash(tx.transaction_mut(), &w_id, hash).await?; let path = get_path_for_hash(&mut tx, &w_id, hash).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default()); let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push( let (uuid, tx) = push(
@@ -1546,9 +1382,8 @@ pub async fn run_wait_result_job_by_hash(
&authed.username, &authed.username,
&authed.email, &authed.email,
username_to_permissioned_as(&authed.username), username_to_permissioned_as(&authed.username),
scheduled_for,
None, None,
None,
run_query.parent_job,
run_query.parent_job, run_query.parent_job,
false, false,
false, false,
@@ -1561,7 +1396,7 @@ pub async fn run_wait_result_job_by_hash(
run_wait_result( run_wait_result(
authed, authed,
Extension(user_db), Extension(user_db),
*TIMEOUT_WAIT_RESULT, timeout.0,
uuid, uuid,
Path((w_id, script_hash)), Path((w_id, script_hash)),
) )
@@ -1571,8 +1406,8 @@ pub async fn run_wait_result_job_by_hash(
pub async fn run_wait_result_flow_by_path( pub async fn run_wait_result_flow_by_path(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(timeout): Extension<Arc<TimeoutWaitResult>>,
Path((w_id, flow_path)): Path<(String, StripPath)>, Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>, Query(run_query): Query<RunJobQuery>,
headers: HeaderMap, headers: HeaderMap,
@@ -1581,8 +1416,8 @@ pub async fn run_wait_result_flow_by_path(
check_queue_too_long(db, run_query.queue_limit).await?; check_queue_too_long(db, run_query.queue_limit).await?;
let flow_path = flow_path.to_path(); let flow_path = flow_path.to_path();
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into(); let mut tx = user_db.clone().begin(&authed).await?;
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?; let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default()); let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push( let (uuid, tx) = push(
@@ -1596,19 +1431,19 @@ pub async fn run_wait_result_flow_by_path(
scheduled_for, scheduled_for,
None, None,
run_query.parent_job, run_query.parent_job,
run_query.parent_job,
false, false,
false, false,
None, None,
!run_query.invisible_to_owner.unwrap_or(false), !run_query.invisible_to_owner.unwrap_or(false),
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
run_wait_result( run_wait_result(
authed, authed,
Extension(user_db), Extension(user_db),
*TIMEOUT_WAIT_RESULT, timeout.0,
uuid, uuid,
Path((w_id, flow_path)), Path((w_id, flow_path)),
) )
@@ -1633,14 +1468,13 @@ pub async fn script_path_to_payload<'c>(
async fn run_preview_job( async fn run_preview_job(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>, Query(run_query): Query<RunJobQuery>,
headers: HeaderMap, headers: HeaderMap,
Json(preview): Json<Preview>, Json(preview): Json<Preview>,
) -> error::Result<(StatusCode, String)> { ) -> error::Result<(StatusCode, String)> {
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into(); let mut tx = user_db.begin(&authed).await?;
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?; let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, preview.args.unwrap_or_default()); let args = run_query.add_include_headers(headers, preview.args.unwrap_or_default());
let (uuid, tx) = push( let (uuid, tx) = push(
@@ -1659,7 +1493,6 @@ async fn run_preview_job(
scheduled_for, scheduled_for,
None, None,
None, None,
None,
false, false,
false, false,
None, None,
@@ -1667,21 +1500,19 @@ async fn run_preview_job(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
Ok((StatusCode::CREATED, uuid.to_string())) Ok((StatusCode::CREATED, uuid.to_string()))
} }
async fn run_preview_flow_job( async fn run_preview_flow_job(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>, Query(run_query): Query<RunJobQuery>,
headers: HeaderMap, headers: HeaderMap,
Json(raw_flow): Json<PreviewFlow>, Json(raw_flow): Json<PreviewFlow>,
) -> error::Result<(StatusCode, String)> { ) -> error::Result<(StatusCode, String)> {
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into(); let mut tx = user_db.begin(&authed).await?;
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?; let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, raw_flow.args.unwrap_or_default()); let args = run_query.add_include_headers(headers, raw_flow.args.unwrap_or_default());
let (uuid, tx) = push( let (uuid, tx) = push(
@@ -1695,7 +1526,6 @@ async fn run_preview_flow_job(
scheduled_for, scheduled_for,
None, None,
None, None,
None,
false, false,
false, false,
None, None,
@@ -1703,23 +1533,21 @@ async fn run_preview_flow_job(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
Ok((StatusCode::CREATED, uuid.to_string())) Ok((StatusCode::CREATED, uuid.to_string()))
} }
pub async fn run_job_by_hash( pub async fn run_job_by_hash(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>, Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>, Query(run_query): Query<RunJobQuery>,
headers: HeaderMap, headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>, Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::Result<(StatusCode, String)> { ) -> error::Result<(StatusCode, String)> {
let hash = script_hash.0; let hash = script_hash.0;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into(); let mut tx = user_db.begin(&authed).await?;
let path = get_path_for_hash(tx.transaction_mut(), &w_id, hash).await?; let path = get_path_for_hash(&mut tx, &w_id, hash).await?;
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?; let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default()); let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push( let (uuid, tx) = push(
@@ -1733,7 +1561,6 @@ pub async fn run_job_by_hash(
scheduled_for, scheduled_for,
None, None,
run_query.parent_job, run_query.parent_job,
run_query.parent_job,
false, false,
false, false,
None, None,
@@ -1741,7 +1568,6 @@ pub async fn run_job_by_hash(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
Ok((StatusCode::CREATED, uuid.to_string())) Ok((StatusCode::CREATED, uuid.to_string()))
} }
@@ -1756,7 +1582,6 @@ pub struct JobUpdate {
pub running: Option<bool>, pub running: Option<bool>,
pub completed: Option<bool>, pub completed: Option<bool>,
pub new_logs: Option<String>, pub new_logs: Option<String>,
pub mem_peak: Option<i32>,
} }
async fn get_job_update( async fn get_job_update(
@@ -1766,8 +1591,8 @@ async fn get_job_update(
) -> error::JsonResult<JobUpdate> { ) -> error::JsonResult<JobUpdate> {
let mut tx = db.begin().await?; let mut tx = db.begin().await?;
let record = sqlx::query!( let logs = query_scalar!(
"SELECT substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3", "SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3",
log_offset, log_offset,
&w_id, &w_id,
&id &id
@@ -1775,13 +1600,12 @@ async fn get_job_update(
.fetch_optional(&mut tx) .fetch_optional(&mut tx)
.await?; .await?;
if let Some(record) = record { if let Some(logs) = logs {
tx.commit().await?; tx.commit().await?;
Ok(Json(JobUpdate { Ok(Json(JobUpdate {
running: if !running { Some(true) } else { None }, running: if !running { Some(true) } else { None },
completed: None, completed: None,
new_logs: record.logs, new_logs: logs,
mem_peak: record.mem_peak,
})) }))
} else { } else {
let logs = query_scalar!( let logs = query_scalar!(
@@ -1799,7 +1623,6 @@ async fn get_job_update(
running: Some(false), running: Some(false),
completed: Some(true), completed: Some(true),
new_logs: logs, new_logs: logs,
mem_peak: record.map(|r| r.mem_peak).flatten(),
})) }))
} }
} }
@@ -1837,11 +1660,11 @@ fn list_completed_jobs_query(
if let Some(pj) = &lq.parent_job { if let Some(pj) = &lq.parent_job {
sqlb.and_where_eq("parent_job", "?".bind(pj)); sqlb.and_where_eq("parent_job", "?".bind(pj));
} }
if let Some(dt) = &lq.started_before { if let Some(dt) = &lq.created_before {
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp())); sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
} }
if let Some(dt) = &lq.started_after { if let Some(dt) = &lq.created_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp())); sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
} }
if let Some(sk) = &lq.is_skipped { if let Some(sk) = &lq.is_skipped {
sqlb.and_where_eq("is_skipped", sk); sqlb.and_where_eq("is_skipped", sk);
@@ -1856,14 +1679,6 @@ fn list_completed_jobs_query(
); );
} }
if let Some(args) = &lq.args {
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
}
if let Some(result) = &lq.result {
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
}
sqlb sqlb
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
@@ -1872,8 +1687,8 @@ pub struct ListCompletedQuery {
pub script_path_exact: Option<String>, pub script_path_exact: Option<String>,
pub script_hash: Option<String>, pub script_hash: Option<String>,
pub created_by: Option<String>, pub created_by: Option<String>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>, pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>, pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub success: Option<bool>, pub success: Option<bool>,
pub parent_job: Option<String>, pub parent_job: Option<String>,
pub order_desc: Option<bool>, pub order_desc: Option<bool>,
@@ -1881,10 +1696,6 @@ pub struct ListCompletedQuery {
pub is_skipped: Option<bool>, pub is_skipped: Option<bool>,
pub is_flow_step: Option<bool>, pub is_flow_step: Option<bool>,
pub suspended: Option<bool>, pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
// filter by matching a subset of the result using base64 encoded json subset
pub result: Option<String>,
} }
async fn list_completed_jobs( async fn list_completed_jobs(
@@ -1929,7 +1740,6 @@ async fn list_completed_jobs(
"is_skipped", "is_skipped",
"email", "email",
"visible_to_owner", "visible_to_owner",
"mem_peak",
], ],
) )
.sql()?; .sql()?;
@@ -1951,7 +1761,6 @@ async fn get_completed_job(
.fetch_optional(&db) .fetch_optional(&db)
.await?; .await?;
tracing::info!("job_o: {:?}", job_o);
let job = not_found_if_none(job_o, "Completed Job", id.to_string())?; let job = not_found_if_none(job_o, "Completed Job", id.to_string())?;
Ok(Json(job)) Ok(Json(job))
} }

View File

@@ -6,28 +6,22 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use crate::oauth2::AllClients; use argon2::Argon2;
use axum::{middleware::from_extractor, routing::get, Extension, Router};
use db::DB;
use git_version::git_version;
use std::{net::SocketAddr, sync::Arc};
use tower::ServiceBuilder;
use tower_cookies::CookieManagerLayer;
use tower_http::trace::TraceLayer;
use windmill_common::{error::to_anyhow, utils::rd_string};
use crate::{ use crate::{
db::UserDB, db::UserDB,
oauth2::{build_oauth_clients, SlackVerifier}, oauth2::{build_oauth_clients, SlackVerifier},
tracing_init::{MyMakeSpan, MyOnResponse}, tracing_init::{MyMakeSpan, MyOnResponse},
users::{Authed, OptAuthed}, users::{Authed, OptAuthed},
webhook_util::WebhookShared,
}; };
use argon2::Argon2;
use axum::{middleware::from_extractor, routing::get, Extension, Router};
use db::DB;
use git_version::git_version;
use hyper::Method;
use reqwest::Client;
use std::{net::SocketAddr, sync::Arc};
use tower::ServiceBuilder;
use tower_cookies::CookieManagerLayer;
use tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use windmill_common::utils::rd_string;
mod apps; mod apps;
mod audit; mod audit;
@@ -38,7 +32,6 @@ mod flows;
mod folders; mod folders;
mod granular_acls; mod granular_acls;
mod groups; mod groups;
mod inputs;
pub mod jobs; pub mod jobs;
mod oauth2; mod oauth2;
mod resources; mod resources;
@@ -49,40 +42,26 @@ mod tracing_init;
mod users; mod users;
mod utils; mod utils;
mod variables; mod variables;
mod webhook_util;
mod worker_ping; mod worker_ping;
mod workspaces; mod workspaces;
pub const GIT_VERSION: &str = pub const GIT_VERSION: &str =
git_version!(args = ["--tag", "--always"], fallback = "unknown-version"); git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
pub struct BaseUrl(String);
pub struct IsSecure(bool);
pub struct CookieDomain(Option<String>);
pub struct CloudHosted(bool);
pub struct ContentSecurityPolicy(String);
pub struct TimeoutWaitResult(i32);
pub struct QueueLimitWaitResult(Option<i64>);
pub use users::delete_expired_items_perdiodically; pub use users::delete_expired_items_perdiodically;
lazy_static::lazy_static! {
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
pub static ref COOKIE_DOMAIN: Option<String> = std::env::var("COOKIE_DOMAIN").ok();
pub static ref SLACK_SIGNING_SECRET: Option<SlackVerifier> = std::env::var("SLACK_SIGNING_SECRET")
.ok()
.map(|x| SlackVerifier::new(x).unwrap());
static ref IS_SECURE: bool = BASE_URL.starts_with("https://");
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.build().unwrap();
pub static ref OAUTH_CLIENTS: AllClients = build_oauth_clients(&BASE_URL)
.map_err(|e| tracing::error!("Error building oauth clients: {}", e))
.unwrap();
}
pub async fn run_server( pub async fn run_server(
db: DB, db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
addr: SocketAddr, addr: SocketAddr,
base_url: String,
mut rx: tokio::sync::broadcast::Receiver<()>, mut rx: tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let user_db = UserDB::new(db.clone()); let user_db = UserDB::new(db.clone());
@@ -92,7 +71,16 @@ pub async fn run_server(
std::env::var("SUPERADMIN_SECRET").ok(), std::env::var("SUPERADMIN_SECRET").ok(),
)); ));
let argon2 = Arc::new(Argon2::default()); let argon2 = Arc::new(Argon2::default());
let basic_clients = Arc::new(build_oauth_clients(&base_url).await?);
let slack_verifier = Arc::new(
std::env::var("SLACK_SIGNING_SECRET")
.ok()
.map(|x| SlackVerifier::new(x).unwrap()),
);
let http_client = reqwest::ClientBuilder::new()
.user_agent("windmill/beta")
.build()
.map_err(to_anyhow)?;
let middleware_stack = ServiceBuilder::new() let middleware_stack = ServiceBuilder::new()
.layer( .layer(
TraceLayer::new_for_http() TraceLayer::new_for_http()
@@ -101,16 +89,24 @@ pub async fn run_server(
.on_request(()), .on_request(()),
) )
.layer(Extension(db.clone())) .layer(Extension(db.clone()))
.layer(Extension(rsmq))
.layer(Extension(user_db)) .layer(Extension(user_db))
.layer(Extension(auth_cache.clone())) .layer(Extension(auth_cache.clone()))
.layer(CookieManagerLayer::new()) .layer(Extension(basic_clients))
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone()))); .layer(Extension(Arc::new(BaseUrl(base_url.to_string()))))
.layer(Extension(Arc::new(ContentSecurityPolicy(
let cors = CorsLayer::new() std::env::var("SERVE_CSP").unwrap_or("".to_owned()),
.allow_methods([Method::GET, Method::POST]) ))))
.allow_origin(Any); .layer(Extension(Arc::new(CloudHosted(
std::env::var("CLOUD_HOSTED").is_ok(),
))))
.layer(Extension(Arc::new(IsSecure(
base_url.starts_with("https://"),
))))
.layer(Extension(Arc::new(CookieDomain(
std::env::var("COOKIE_DOMAIN").ok(),
))))
.layer(Extension(http_client))
.layer(CookieManagerLayer::new());
// build our application with a route // build our application with a route
let app = Router::new() let app = Router::new()
.nest( .nest(
@@ -119,27 +115,39 @@ pub async fn run_server(
.nest( .nest(
"/w/:workspace_id", "/w/:workspace_id",
Router::new() Router::new()
// Reordered alphabetically
.nest("/acls", granular_acls::workspaced_service())
.nest("/apps", apps::workspaced_service())
.nest("/audit", audit::workspaced_service())
.nest("/capture", capture::workspaced_service())
.nest("/favorites", favorite::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest("/folders", folders::workspaced_service())
.nest("/groups", groups::workspaced_service())
.nest("/inputs", inputs::workspaced_service())
.nest("/jobs", jobs::workspaced_service().layer(cors.clone()))
.nest("/oauth", oauth2::workspaced_service())
.nest("/resources", resources::workspaced_service())
.nest("/schedules", schedule::workspaced_service())
.nest("/scripts", scripts::workspaced_service()) .nest("/scripts", scripts::workspaced_service())
.nest(
"/jobs",
jobs::workspaced_service()
.layer(Extension(Arc::new(TimeoutWaitResult(
std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(20),
))))
.layer(Extension(Arc::new(QueueLimitWaitResult(
std::env::var("QUEUE_LIMIT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok()),
)))),
)
.nest( .nest(
"/users", "/users",
users::workspaced_service().layer(Extension(argon2.clone())), users::workspaced_service().layer(Extension(argon2.clone())),
) )
.nest("/variables", variables::workspaced_service()) .nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service()), .nest("/oauth", oauth2::workspaced_service())
.nest("/resources", resources::workspaced_service())
.nest("/schedules", schedule::workspaced_service())
.nest("/groups", groups::workspaced_service())
.nest("/audit", audit::workspaced_service())
.nest("/acls", granular_acls::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/apps", apps::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest("/capture", capture::workspaced_service())
.nest("/favorites", favorite::workspaced_service())
.nest("/folders", folders::workspaced_service()),
) )
.nest("/workspaces", workspaces::global_service()) .nest("/workspaces", workspaces::global_service())
.nest( .nest(
@@ -149,30 +157,24 @@ pub async fn run_server(
.nest("/workers", worker_ping::global_service()) .nest("/workers", worker_ping::global_service())
.nest("/scripts", scripts::global_service()) .nest("/scripts", scripts::global_service())
.nest("/flows", flows::global_service()) .nest("/flows", flows::global_service())
.nest("/apps", apps::global_service().layer(cors.clone())) .nest("/apps", apps::global_service())
.nest("/schedules", schedule::global_service()) .nest("/schedules", schedule::global_service())
.route_layer(from_extractor::<Authed>()) .route_layer(from_extractor::<Authed>())
.route_layer(from_extractor::<users::Tokened>()) .route_layer(from_extractor::<users::Tokened>())
.nest("/scripts_u", scripts::global_unauthed_service())
.nest( .nest(
"/w/:workspace_id/apps_u", "/w/:workspace_id/apps_u",
apps::unauthed_service() apps::unauthed_service().layer(from_extractor::<OptAuthed>()),
.layer(from_extractor::<OptAuthed>())
.layer(cors.clone()),
)
.nest(
"/w/:workspace_id/jobs_u",
jobs::global_service().layer(cors.clone()),
)
.nest(
"/w/:workspace_id/capture_u",
capture::global_service().layer(cors),
) )
.nest("/w/:workspace_id/jobs_u", jobs::global_service())
.nest("/w/:workspace_id/capture_u", capture::global_service())
.nest( .nest(
"/auth", "/auth",
users::make_unauthed_service().layer(Extension(argon2)), users::make_unauthed_service().layer(Extension(argon2)),
) )
.nest("/oauth", oauth2::global_service()) .nest(
"/oauth",
oauth2::global_service().layer(Extension(slack_verifier)),
)
.route("/version", get(git_v)) .route("/version", get(git_v))
.route("/openapi.yaml", get(openapi)), .route("/openapi.yaml", get(openapi)),
) )

View File

@@ -0,0 +1,70 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use std::net::SocketAddr;
use anyhow::Ok;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
windmill_common::tracing_init::initialize_tracing();
let db = windmill_common::connect_db(true).await?;
let num_workers = std::env::var("NUM_WORKERS")
.ok()
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(windmill_common::DEFAULT_NUM_WORKERS as i32);
let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
.ok()
.map(|s| {
s.parse::<bool>()
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
.or_else(|_| s.parse::<SocketAddr>().map(Some))
})
.transpose()?
.flatten();
let server_mode = !std::env::var("DISABLE_SERVER")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
if server_mode {
windmill_api::migrate_db(&db).await?;
}
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
let shutdown_signal = windmill_common::shutdown_signal(tx);
let base_url = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
if server_mode || num_workers > 0 {
let addr = SocketAddr::from(([0, 0, 0, 0], 8000));
let server_f = async {
if server_mode {
windmill_api::run_server(db.clone(), addr, base_url, rx.resubscribe()).await?;
}
Ok(()) as anyhow::Result<()>
};
let metrics_f = async {
match metrics_addr {
Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
.await
.map_err(anyhow::Error::from),
None => Ok(()),
}
};
futures::try_join!(shutdown_signal, server_f, metrics_f)?;
}
Ok(())
}

View File

@@ -1,69 +0,0 @@
// /*
// * Author: Ruben Fiszel
// * Copyright: Windmill Labs, Inc 2022
// * This file and its contents are licensed under the AGPLv3 License.
// * Please see the included NOTICE for copyright information and
// * LICENSE-AGPL for a copy of the license.
// */
// use std::net::SocketAddr;
// use anyhow::Ok;
// pub const DEFAULT_NUM_WORKERS: usize = 3;
// #[tokio::main]
// async fn main() -> anyhow::Result<()> {
// windmill_common::tracing_init::initialize_tracing();
// let db = windmill_common::connect_db(true).await?;
// let num_workers = std::env::var("NUM_WORKERS")
// .ok()
// .and_then(|x| x.parse::<i32>().ok())
// .unwrap_or(DEFAULT_NUM_WORKERS as i32);
// let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
// .ok()
// .map(|s| {
// s.parse::<bool>()
// .map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
// .or_else(|_| s.parse::<SocketAddr>().map(Some))
// })
// .transpose()?
// .flatten();
// let server_mode = !std::env::var("DISABLE_SERVER")
// .ok()
// .and_then(|x| x.parse::<bool>().ok())
// .unwrap_or(false);
// if server_mode {
// windmill_api::migrate_db(&db).await?;
// }
// let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
// let shutdown_signal = windmill_common::shutdown_signal(tx);
// if server_mode || num_workers > 0 {
// let addr = SocketAddr::from(([0, 0, 0, 0], 8000));
// let server_f = async {
// if server_mode {
// windmill_api::run_server(db.clone(), addr, rx.resubscribe()).await?;
// }
// Ok(()) as anyhow::Result<()>
// };
// let metrics_f = async {
// match metrics_addr {
// Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
// .await
// .map_err(anyhow::Error::from),
// None => Ok(()),
// }
// };
// futures::try_join!(shutdown_signal, server_f, metrics_f)?;
// }
// Ok(())
// }

View File

@@ -8,6 +8,8 @@
use std::{collections::HashMap, fmt::Debug}; use std::{collections::HashMap, fmt::Debug};
use std::sync::Arc;
use anyhow::Context; use anyhow::Context;
use axum::extract::FromRequestParts; use axum::extract::FromRequestParts;
use axum::http::request::Parts; use axum::http::request::Parts;
@@ -27,26 +29,26 @@ use oauth2::{Client as OClient, *};
use reqwest::Client; use reqwest::Client;
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sqlx::{Postgres, Transaction}; use sqlx::{Postgres, Transaction};
use tokio::{fs::File, io::AsyncReadExt};
use tower_cookies::{Cookie, Cookies}; use tower_cookies::{Cookie, Cookies};
use windmill_audit::{audit_log, ActionKind}; use windmill_audit::{audit_log, ActionKind};
use windmill_common::users::username_to_permissioned_as;
use windmill_common::utils::{not_found_if_none, now_from_db}; use windmill_common::utils::{not_found_if_none, now_from_db};
use crate::users::{truncate_token, Authed}; use crate::users::{truncate_token, Authed};
use crate::webhook_util::{InstanceEvent, WebhookShared};
use crate::workspaces::invite_user_to_all_auto_invite_worspaces; use crate::workspaces::invite_user_to_all_auto_invite_worspaces;
use crate::{ use crate::{
db::{UserDB, DB}, db::{UserDB, DB},
variables::{build_crypt, encrypt}, variables::{build_crypt, encrypt},
workspaces::WorkspaceSettings, workspaces::WorkspaceSettings,
BaseUrl,
}; };
use crate::{BASE_URL, HTTP_CLIENT, IS_SECURE, OAUTH_CLIENTS, SLACK_SIGNING_SECRET}; use crate::{CookieDomain, IsSecure};
use windmill_common::error::{self, to_anyhow, Error}; use windmill_common::error::{self, to_anyhow, Error};
use windmill_common::oauth2::*; use windmill_common::oauth2::*;
use windmill_queue::{JobPayload, QueueTransaction}; use windmill_queue::JobPayload;
use std::{fs, str}; use std::str;
pub fn global_service() -> Router { pub fn global_service() -> Router {
Router::new() Router::new()
@@ -109,7 +111,7 @@ pub struct AllClients {
pub slack: Option<OClient>, pub slack: Option<OClient>,
} }
pub fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> { pub async fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
let connect_configs = serde_json::from_str::<HashMap<String, OAuthConfig>>(include_str!( let connect_configs = serde_json::from_str::<HashMap<String, OAuthConfig>>(include_str!(
"../../oauth_connect.json" "../../oauth_connect.json"
))?; ))?;
@@ -117,12 +119,14 @@ pub fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
"../../oauth_login.json" "../../oauth_login.json"
))?; ))?;
let mut content = String::new();
let path = "./oauth.json"; let path = "./oauth.json";
let content = if std::path::Path::new(path).exists() { if std::path::Path::new(path).exists() {
fs::read_to_string(path).map_err(to_anyhow)? let mut file = File::open(path).await?;
file.read_to_string(&mut content).await?;
} else { } else {
"{}".to_string() content.push_str("{}");
}; }
let oauths: HashMap<String, OAuthClient> = let oauths: HashMap<String, OAuthClient> =
match serde_json::from_str::<HashMap<String, OAuthClient>>(&content) { match serde_json::from_str::<HashMap<String, OAuthClient>>(&content) {
@@ -284,10 +288,12 @@ pub struct SlackBotToken {
async fn connect( async fn connect(
Path(client_name): Path<String>, Path(client_name): Path<String>,
Query(query): Query<HashMap<String, String>>, Query(query): Query<HashMap<String, String>>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(is_secure): Extension<Arc<IsSecure>>,
cookies: Cookies, cookies: Cookies,
) -> error::Result<Redirect> { ) -> error::Result<Redirect> {
let mut query = query.clone(); let mut query = query.clone();
let connects = &OAUTH_CLIENTS.connects; let connects = &clients.connects;
let scopes = query let scopes = query
.get("scopes") .get("scopes")
.map(|x| x.split('+').map(|x| x.to_owned()).collect()); .map(|x| x.split('+').map(|x| x.to_owned()).collect());
@@ -303,7 +309,7 @@ async fn connect(
cookies, cookies,
scopes, scopes,
extra_params, extra_params,
*IS_SECURE, is_secure.0,
) )
} }
@@ -370,9 +376,11 @@ async fn delete_account(
Ok(format!("Deleted account id {id}")) Ok(format!("Deleted account id {id}"))
} }
async fn list_logins() -> error::JsonResult<Vec<String>> { async fn list_logins(
Extension(clients): Extension<Arc<AllClients>>,
) -> error::JsonResult<Vec<String>> {
Ok(Json( Ok(Json(
OAUTH_CLIENTS clients
.logins .logins
.keys() .keys()
.map(|x| x.to_owned()) .map(|x| x.to_owned())
@@ -385,9 +393,11 @@ struct ScopesAndParams {
scopes: Vec<String>, scopes: Vec<String>,
extra_params: Option<HashMap<String, String>>, extra_params: Option<HashMap<String, String>>,
} }
async fn list_connects() -> error::JsonResult<HashMap<String, ScopesAndParams>> { async fn list_connects(
Extension(clients): Extension<Arc<AllClients>>,
) -> error::JsonResult<HashMap<String, ScopesAndParams>> {
Ok(Json( Ok(Json(
(&OAUTH_CLIENTS.connects) (&clients.connects)
.into_iter() .into_iter()
.map(|(k, v)| { .map(|(k, v)| {
( (
@@ -402,8 +412,12 @@ async fn list_connects() -> error::JsonResult<HashMap<String, ScopesAndParams>>
)) ))
} }
async fn connect_slack(cookies: Cookies) -> error::Result<Redirect> { async fn connect_slack(
let mut client = OAUTH_CLIENTS Extension(clients): Extension<Arc<AllClients>>,
Extension(is_secure): Extension<Arc<IsSecure>>,
cookies: Cookies,
) -> error::Result<Redirect> {
let mut client = clients
.slack .slack
.as_ref() .as_ref()
.ok_or_else(|| error::Error::BadRequest("slack client not setup".to_string()))? .ok_or_else(|| error::Error::BadRequest("slack client not setup".to_string()))?
@@ -414,7 +428,7 @@ async fn connect_slack(cookies: Cookies) -> error::Result<Redirect> {
client.add_scope("commands"); client.add_scope("commands");
let url = client.authorize_url(&state); let url = client.authorize_url(&state);
set_cookie(&state, cookies, *IS_SECURE); set_cookie(&state, cookies, is_secure.0);
Ok(Redirect::to(url.as_str())) Ok(Redirect::to(url.as_str()))
} }
@@ -456,9 +470,14 @@ async fn disconnect_slack(
Ok(format!("slack disconnected")) Ok(format!("slack disconnected"))
} }
async fn login(Path(client_name): Path<String>, cookies: Cookies) -> error::Result<Redirect> { async fn login(
let clients = &OAUTH_CLIENTS.logins; Extension(clients): Extension<Arc<AllClients>>,
oauth_redirect(clients, client_name, cookies, None, None, *IS_SECURE) Extension(is_secure): Extension<Arc<IsSecure>>,
Path(client_name): Path<String>,
cookies: Cookies,
) -> error::Result<Redirect> {
let clients = &clients.logins;
oauth_redirect(clients, client_name, cookies, None, None, is_secure.0)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -469,11 +488,13 @@ async fn refresh_token(
authed: Authed, authed: Authed,
Path((w_id, id)): Path<(String, i32)>, Path((w_id, id)): Path<(String, i32)>,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
Json(VariablePath { path }): Json<VariablePath>, Json(VariablePath { path }): Json<VariablePath>,
) -> error::Result<String> { ) -> error::Result<String> {
let tx = user_db.begin(&authed).await?; let tx = user_db.begin(&authed).await?;
_refresh_token(tx, &path, w_id, id).await?; _refresh_token(tx, &path, w_id, id, clients, http_client).await?;
Ok(format!("Token at path {path} refreshed")) Ok(format!("Token at path {path} refreshed"))
} }
@@ -483,6 +504,8 @@ pub async fn _refresh_token<'c>(
path: &str, path: &str,
w_id: String, w_id: String,
id: i32, id: i32,
clients: Arc<AllClients>,
http_client: Client,
) -> error::Result<String> { ) -> error::Result<String> {
let account = sqlx::query!( let account = sqlx::query!(
"SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2", "SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2",
@@ -492,14 +515,14 @@ pub async fn _refresh_token<'c>(
.fetch_optional(&mut tx) .fetch_optional(&mut tx)
.await?; .await?;
let account = not_found_if_none(account, "Account", &id.to_string())?; let account = not_found_if_none(account, "Account", &id.to_string())?;
let client = (&OAUTH_CLIENTS let client = (&clients
.connects .connects
.get(&account.client) .get(&account.client)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))? .ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?
.client) .client)
.to_owned(); .to_owned();
let token = _exchange_token(client, &account.refresh_token).await; let token = _exchange_token(client, &account.refresh_token, http_client).await;
if let Err(token_err) = token { if let Err(token_err) = token {
sqlx::query!( sqlx::query!(
@@ -557,10 +580,14 @@ pub async fn _refresh_token<'c>(
Ok(token_str) Ok(token_str)
} }
async fn _exchange_token(client: OClient, refresh_token: &str) -> Result<TokenResponse, Error> { async fn _exchange_token(
client: OClient,
refresh_token: &str,
http_client: Client,
) -> Result<TokenResponse, Error> {
let token_json = client let token_json = client
.exchange_refresh_token(&RefreshToken::from(refresh_token.clone())) .exchange_refresh_token(&RefreshToken::from(refresh_token.clone()))
.with_client(&HTTP_CLIENT) .with_client(&http_client)
.execute::<serde_json::Value>() .execute::<serde_json::Value>()
.await .await
.map_err(to_anyhow)?; .map_err(to_anyhow)?;
@@ -581,9 +608,11 @@ pub struct OAuthCallback {
async fn connect_callback( async fn connect_callback(
cookies: Cookies, cookies: Cookies,
Path(client_name): Path<String>, Path(client_name): Path<String>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
Json(callback): Json<OAuthCallback>, Json(callback): Json<OAuthCallback>,
) -> error::JsonResult<TokenResponse> { ) -> error::JsonResult<TokenResponse> {
let client_w_scopes = OAUTH_CLIENTS let client_w_scopes = &clients
.connects .connects
.get(&client_name) .get(&client_name)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?; .ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?;
@@ -591,7 +620,7 @@ async fn connect_callback(
let client = client_w_scopes.client.to_owned(); let client = client_w_scopes.client.to_owned();
let extra_params = client_w_scopes.extra_params_callback.clone(); let extra_params = client_w_scopes.extra_params_callback.clone();
let token_response = let token_response =
exchange_code::<TokenResponse>(callback, &cookies, client, &HTTP_CLIENT, extra_params) exchange_code::<TokenResponse>(callback, &cookies, client, &http_client, extra_params)
.await?; .await?;
Ok(Json(token_response)) Ok(Json(token_response))
@@ -602,15 +631,17 @@ async fn connect_slack_callback(
authed: Authed, authed: Authed,
cookies: Cookies, cookies: Cookies,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
Json(callback): Json<OAuthCallback>, Json(callback): Json<OAuthCallback>,
) -> error::Result<String> { ) -> error::Result<String> {
let client = OAUTH_CLIENTS let client = clients
.slack .slack
.as_ref() .as_ref()
.ok_or_else(|| error::Error::BadRequest("slack client not setup".to_string()))? .ok_or_else(|| error::Error::BadRequest("slack client not setup".to_string()))?
.to_owned(); .to_owned();
let token = let token =
exchange_code::<SlackTokenResponse>(callback, &cookies, client, &HTTP_CLIENT, None).await?; exchange_code::<SlackTokenResponse>(callback, &cookies, client, &http_client, None).await?;
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
@@ -626,26 +657,14 @@ async fn connect_slack_callback(
) )
.execute(&mut tx) .execute(&mut tx)
.await?; .await?;
sqlx::query_as!(
Group,
"INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
w_id,
"slack",
"The group slack commands act on belhalf of",
serde_json::json!({username_to_permissioned_as(&authed.username): true})
)
.execute(&mut tx)
.await?;
sqlx::query!( sqlx::query!(
"INSERT INTO folder "INSERT INTO folder
(workspace_id, name, display_name, owners, extra_perms) (workspace_id, name, owners, extra_perms)
VALUES ($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING", VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
&w_id, &w_id,
"slack_bot", "slack_bot",
"Slack bot", &[],
&["g/slack".to_string()], serde_json::json!({})
serde_json::json!({"g/slack": true})
) )
.execute(&mut tx) .execute(&mut tx)
.await?; .await?;
@@ -728,21 +747,26 @@ where
async fn slack_command( async fn slack_command(
SlackSig { sig, ts }: SlackSig, SlackSig { sig, ts }: SlackSig,
Extension(slack_verifier): Extension<Arc<Option<SlackVerifier>>>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>, Extension(base_url): Extension<Arc<BaseUrl>>,
body: Bytes, body: Bytes,
) -> error::Result<String> { ) -> error::Result<String> {
let form: SlackCommand = serde_urlencoded::from_bytes(&body) let form: SlackCommand = serde_urlencoded::from_bytes(&body)
.map_err(|_| error::Error::BadRequest("invalid payload".to_string()))?; .map_err(|_| error::Error::BadRequest("invalid payload".to_string()))?;
let body = String::from_utf8_lossy(&body); let body = String::from_utf8_lossy(&body);
if let Some(sv) = SLACK_SIGNING_SECRET.as_ref() { if slack_verifier
if sv.verify(&ts, &body, &sig).ok().is_none() { .as_ref()
return Err(error::Error::BadRequest("verification failed".to_owned())); .as_ref()
} .map(|sv| sv.verify(&ts, &body, &sig).ok())
.flatten()
.is_none()
{
return Err(error::Error::BadRequest("verification failed".to_owned()));
} }
let mut tx: QueueTransaction<'_, _> = (rsmq, db.begin().await?).into(); let mut tx = db.begin().await?;
let settings = sqlx::query_as!( let settings = sqlx::query_as!(
WorkspaceSettings, WorkspaceSettings,
"SELECT * FROM workspace_settings WHERE slack_team_id = $1", "SELECT * FROM workspace_settings WHERE slack_team_id = $1",
@@ -758,7 +782,7 @@ async fn slack_command(
} else { } else {
let path = path.strip_prefix("script/").unwrap_or_else(|| path); let path = path.strip_prefix("script/").unwrap_or_else(|| path);
let script_hash = windmill_common::get_latest_hash_for_path( let script_hash = windmill_common::get_latest_hash_for_path(
tx.transaction_mut(), &mut tx,
&settings.workspace_id, &settings.workspace_id,
path, path,
) )
@@ -783,22 +807,20 @@ async fn slack_command(
None, None,
None, None,
None, None,
None,
false, false,
false, false,
None, None,
true, true,
) )
.await?; .await?;
let url = BASE_URL.to_owned();
tx.commit().await?; tx.commit().await?;
let url = base_url.0.to_owned();
return Ok(format!( return Ok(format!(
"Job launched. See details at {url}/run/{uuid}?workspace={}", "Job launched. See details at {url}/run/{uuid}?workspace={}",
&settings.workspace_id &settings.workspace_id
)); ));
} }
} }
tx.commit().await?;
return Ok(format!( return Ok(format!(
"workspace not properly configured (did you set the script to trigger in the settings?)" "workspace not properly configured (did you set the script to trigger in the settings?)"
@@ -817,28 +839,31 @@ pub struct UserInfo {
async fn login_callback( async fn login_callback(
Path(client_name): Path<String>, Path(client_name): Path<String>,
cookies: Cookies, cookies: Cookies,
Extension(clients): Extension<Arc<AllClients>>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>, Extension(http_client): Extension<Client>,
Extension(is_secure): Extension<Arc<IsSecure>>,
Extension(cookie_domain): Extension<Arc<CookieDomain>>,
Json(callback): Json<OAuthCallback>, Json(callback): Json<OAuthCallback>,
) -> error::Result<String> { ) -> error::Result<String> {
let client_w_config = &OAUTH_CLIENTS let client_w_config = &clients
.logins .logins
.get(&client_name) .get(&client_name)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?; .ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?;
let client = client_w_config.client.to_owned(); let client = client_w_config.client.to_owned();
let token_res = let token_res =
exchange_code::<TokenResponse>(callback, &cookies, client, &HTTP_CLIENT, None).await; exchange_code::<TokenResponse>(callback, &cookies, client, &http_client, None).await;
if let Ok(token) = token_res { if let Ok(token) = token_res {
let token = &token.access_token.to_string(); let token = &token.access_token.to_string();
let userinfo_url = client_w_config.userinfo_url.as_ref().ok_or_else(|| { let userinfo_url = client_w_config.userinfo_url.as_ref().ok_or_else(|| {
Error::BadConfig(format!("Missing userinfo_url in client {client_name}")) Error::BadConfig(format!("Missing userinfo_url in client {client_name}"))
})?; })?;
let user = http_get_user_info::<UserInfo>(&HTTP_CLIENT, userinfo_url, token).await?; let user = http_get_user_info::<UserInfo>(&http_client, userinfo_url, token).await?;
let email = match client_name.as_str() { let email = match client_name.as_str() {
"github" => http_get_user_info::<Vec<GHEmailInfo>>( "github" => http_get_user_info::<Vec<GHEmailInfo>>(
&HTTP_CLIENT, &http_client,
"https://api.github.com/user/emails", "https://api.github.com/user/emails",
token, token,
) )
@@ -853,8 +878,7 @@ async fn login_callback(
_ => user.email.ok_or_else(|| { _ => user.email.ok_or_else(|| {
error::Error::BadRequest("email address not fetchable from user info".to_string()) error::Error::BadRequest("email address not fetchable from user info".to_string())
})?, })?,
} };
.to_lowercase();
if let Some(domains) = &client_w_config.allowed_domains { if let Some(domains) = &client_w_config.allowed_domains {
if !domains.iter().any(|d| email.ends_with(d)) { if !domains.iter().any(|d| email.ends_with(d)) {
@@ -875,7 +899,15 @@ async fn login_callback(
if let Some((email, login_type, super_admin)) = login { if let Some((email, login_type, super_admin)) = login {
let login_type = serde_json::json!(login_type); let login_type = serde_json::json!(login_type);
if login_type == client_name { if login_type == client_name {
crate::users::create_session_token(&email, super_admin, &mut tx, cookies).await?; crate::users::create_session_token(
&email,
super_admin,
&mut tx,
cookies,
is_secure.0,
&cookie_domain.as_ref().0,
)
.await?;
} else { } else {
return Err(error::Error::BadRequest(format!( return Err(error::Error::BadRequest(format!(
"an user with the email associated to this login exists but with a different \ "an user with the email associated to this login exists but with a different \
@@ -910,7 +942,15 @@ async fn login_callback(
tx.commit().await?; tx.commit().await?;
invite_user_to_all_auto_invite_worspaces(&db, &email).await?; invite_user_to_all_auto_invite_worspaces(&db, &email).await?;
tx = db.begin().await?; tx = db.begin().await?;
crate::users::create_session_token(&email, false, &mut tx, cookies).await?; crate::users::create_session_token(
&email,
false,
&mut tx,
cookies,
is_secure.0,
&cookie_domain.as_ref().0,
)
.await?;
audit_log( audit_log(
&mut tx, &mut tx,
&email, &email,
@@ -921,7 +961,6 @@ async fn login_callback(
Some([("method", &client_name[..])].into()), Some([("method", &client_name[..])].into()),
) )
.await?; .await?;
let demo_exists = let demo_exists =
sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'demo')") sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'demo')")
.fetch_one(&mut tx) .fetch_one(&mut tx)
@@ -943,9 +982,6 @@ async fn login_callback(
} }
} }
tx.commit().await?; tx.commit().await?;
webhook.send_instance_event(InstanceEvent::UserSignupOAuth { email: email.clone() });
Ok("Successfully logged in".to_string()) Ok("Successfully logged in".to_string())
} else { } else {
Err(error::Error::BadRequest(format!( Err(error::Error::BadRequest(format!(
@@ -1067,7 +1103,7 @@ fn set_cookie(state: &State, cookies: Cookies, is_secure: bool) {
let csrf = state.to_base64(); let csrf = state.to_base64();
let mut cookie = Cookie::new("csrf", csrf); let mut cookie = Cookie::new("csrf", csrf);
cookie.set_secure(is_secure); cookie.set_secure(is_secure);
cookie.set_same_site(Some(cookie::SameSite::Lax)); cookie.set_same_site(cookie::SameSite::Lax);
cookie.set_http_only(true); cookie.set_http_only(true);
cookie.set_path("/"); cookie.set_path("/");
cookies.add(cookie); cookies.add(cookie);

View File

@@ -8,8 +8,7 @@
use crate::{ use crate::{
db::{UserDB, DB}, db::{UserDB, DB},
users::{maybe_refresh_folders, require_owner_of_path, Authed}, users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
}; };
use axum::{ use axum::{
extract::{Extension, Path, Query}, extract::{Extension, Path, Query},
@@ -142,7 +141,7 @@ async fn list_resources(
.join("account") .join("account")
.on("variable.account = account.id AND account.workspace_id = variable.workspace_id") .on("variable.account = account.id AND account.workspace_id = variable.workspace_id")
.order_by("path", true) .order_by("path", true)
.and_where("resource.workspace_id = ?".bind(&w_id)) .and_where("resource.workspace_id = ? OR resource.workspace_id = 'starter'".bind(&w_id))
.offset(offset) .offset(offset)
.limit(per_page) .limit(per_page)
.clone(); .clone();
@@ -187,7 +186,7 @@ async fn get_resource(
FROM resource FROM resource
LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id
WHERE resource.path = $1 AND resource.workspace_id = $2", WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')",
path.to_owned(), path.to_owned(),
&w_id &w_id
) )
@@ -226,7 +225,8 @@ async fn get_resource_value(
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
let value_o = sqlx::query_scalar!( let value_o = sqlx::query_scalar!(
"SELECT value from resource WHERE path = $1 AND workspace_id = $2", "SELECT value from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = \
'starter')",
path.to_owned(), path.to_owned(),
&w_id &w_id
) )
@@ -263,13 +263,9 @@ async fn check_path_conflict<'c>(
async fn create_resource( async fn create_resource(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Json(resource): Json<CreateResource>, Json(resource): Json<CreateResource>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
let authed = maybe_refresh_folders(&resource.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &resource.path).await?; check_path_conflict(&mut tx, &w_id, &resource.path).await?;
@@ -297,11 +293,6 @@ async fn create_resource(
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateResource { workspace: w_id, path: resource.path.clone() },
);
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
format!("resource {} created", resource.path), format!("resource {} created", resource.path),
@@ -311,7 +302,6 @@ async fn create_resource(
async fn delete_resource( async fn delete_resource(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> { ) -> Result<String> {
let path = path.to_path(); let path = path.to_path();
@@ -343,18 +333,12 @@ async fn delete_resource(
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResource { workspace: w_id, path: path.to_owned() },
);
Ok(format!("resource {} deleted", path)) Ok(format!("resource {} deleted", path))
} }
async fn update_resource( async fn update_resource(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditResource>, Json(ns): Json<EditResource>,
@@ -378,7 +362,6 @@ async fn update_resource(
} }
sqlb.returning("path"); sqlb.returning("path");
let authed = maybe_refresh_folders(path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
@@ -417,15 +400,6 @@ async fn update_resource(
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResource {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("resource {} updated (npath: {:?})", path, npath)) Ok(format!("resource {} updated (npath: {:?})", path, npath))
} }
@@ -437,7 +411,6 @@ struct UpdateResource {
async fn update_resource_value( async fn update_resource_value(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
Json(nv): Json<UpdateResource>, Json(nv): Json<UpdateResource>,
) -> Result<String> { ) -> Result<String> {
@@ -463,14 +436,6 @@ async fn update_resource_value(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResource {
workspace: w_id,
old_path: path.to_owned(),
new_path: path.to_owned(),
},
);
Ok(format!("value of resource {} updated", path)) Ok(format!("value of resource {} updated", path))
} }
@@ -481,7 +446,7 @@ async fn list_resource_types(
) -> JsonResult<Vec<ResourceType>> { ) -> JsonResult<Vec<ResourceType>> {
let rows = sqlx::query_as!( let rows = sqlx::query_as!(
ResourceType, ResourceType,
"SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER \ "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter' OR workspace_id = 'admins') ORDER \
BY name", BY name",
&w_id &w_id
) )
@@ -496,7 +461,7 @@ async fn list_resource_types_names(
Path(w_id): Path<String>, Path(w_id): Path<String>,
) -> JsonResult<Vec<String>> { ) -> JsonResult<Vec<String>> {
let rows = sqlx::query_scalar!( let rows = sqlx::query_scalar!(
"SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') \ "SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter' OR workspace_id = 'admins') \
ORDER BY name", ORDER BY name",
&w_id &w_id
) )
@@ -515,7 +480,8 @@ async fn get_resource_type(
let resource_type_o = sqlx::query_as!( let resource_type_o = sqlx::query_as!(
ResourceType, ResourceType,
"SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')", "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = \
'starter' OR workspace_id = 'admins')",
&name, &name,
&w_id &w_id
) )
@@ -532,7 +498,8 @@ async fn exists_resource_type(
Path((w_id, name)): Path<(String, String)>, Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<bool> { ) -> JsonResult<bool> {
let exists = sqlx::query_scalar!( let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins'))", "SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = \
'starter' OR workspace_id = 'admins'))",
name, name,
w_id w_id
) )
@@ -546,7 +513,6 @@ async fn exists_resource_type(
async fn create_resource_type( async fn create_resource_type(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Json(resource_type): Json<CreateResourceType>, Json(resource_type): Json<CreateResourceType>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
@@ -577,11 +543,6 @@ async fn create_resource_type(
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateResourceType { name: resource_type.name.clone() },
);
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
format!("resource_type {} created", resource_type.name), format!("resource_type {} created", resource_type.name),
@@ -613,7 +574,6 @@ async fn check_rt_path_conflict<'c>(
async fn delete_resource_type( async fn delete_resource_type(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>, Path((w_id, name)): Path<(String, String)>,
) -> Result<String> { ) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?; require_admin(authed.is_admin, &authed.username)?;
@@ -638,10 +598,6 @@ async fn delete_resource_type(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResourceType { name: name.clone() },
);
Ok(format!("resource_type {} deleted", name)) Ok(format!("resource_type {} deleted", name))
} }
@@ -649,7 +605,6 @@ async fn delete_resource_type(
async fn update_resource_type( async fn update_resource_type(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>, Path((w_id, name)): Path<(String, String)>,
Json(ns): Json<EditResourceType>, Json(ns): Json<EditResourceType>,
) -> Result<String> { ) -> Result<String> {
@@ -679,10 +634,6 @@ async fn update_resource_type(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResourceType { name: name.clone() },
);
Ok(format!("resource_type {} updated", name)) Ok(format!("resource_type {} updated", name))
} }

View File

@@ -6,26 +6,27 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use std::str::FromStr;
use crate::{ use crate::{
db::{UserDB, DB}, db::{UserDB, DB},
users::{maybe_refresh_folders, Authed}, users::Authed,
}; };
use axum::{ use axum::{
extract::{Extension, Path, Query}, extract::{Extension, Path, Query},
routing::{delete, get, post}, routing::{delete, get, post},
Json, Router, Json, Router,
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, FixedOffset};
use serde::Deserialize; use serde::Deserialize;
use sqlx::{Postgres, Transaction}; use sqlx::{Postgres, Transaction};
use std::str::FromStr;
use windmill_audit::{audit_log, ActionKind}; use windmill_audit::{audit_log, ActionKind};
use windmill_common::{ use windmill_common::{
error::{Error, JsonResult, Result}, error::{Error, JsonResult, Result},
schedule::Schedule, schedule::Schedule,
utils::{not_found_if_none, paginate, Pagination, StripPath}, utils::{not_found_if_none, paginate, Pagination, StripPath},
}; };
use windmill_queue::{self, schedule::push_scheduled_job, JobKind, QueueTransaction}; use windmill_queue::{self, schedule::push_scheduled_job, JobKind};
pub fn workspaced_service() -> Router { pub fn workspaced_service() -> Router {
Router::new() Router::new()
@@ -46,7 +47,7 @@ pub fn global_service() -> Router {
pub struct NewSchedule { pub struct NewSchedule {
pub path: String, pub path: String,
pub schedule: String, pub schedule: String,
pub timezone: String, pub offset: i32,
pub script_path: String, pub script_path: String,
pub is_flow: bool, pub is_flow: bool,
pub args: Option<serde_json::Value>, pub args: Option<serde_json::Value>,
@@ -77,34 +78,23 @@ async fn check_path_conflict<'c>(
async fn create_schedule( async fn create_schedule(
authed: Authed, authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Json(ns): Json<NewSchedule>, Json(ns): Json<NewSchedule>,
) -> Result<String> { ) -> Result<String> {
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await; let mut tx = user_db.begin(&authed).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?; cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
check_path_conflict(tx.transaction_mut(), &w_id, &ns.path).await?; check_path_conflict(&mut tx, &w_id, &ns.path).await?;
check_flow_conflict( check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
tx.transaction_mut(),
&w_id,
&ns.path,
ns.is_flow,
&ns.script_path,
)
.await?;
let schedule = sqlx::query_as!( let schedule = sqlx::query_as!(
Schedule, Schedule,
"INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \ "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, \
is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *", is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *",
w_id, w_id,
ns.path, ns.path,
ns.schedule, ns.schedule,
ns.timezone, ns.offset,
&authed.username, &authed.username,
ns.script_path, ns.script_path,
ns.is_flow, ns.is_flow,
@@ -145,18 +135,13 @@ async fn create_schedule(
async fn edit_schedule( async fn edit_schedule(
authed: Authed, authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
Json(es): Json<EditSchedule>, Json(es): Json<EditSchedule>,
) -> Result<String> { ) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path(); let path = path.to_path();
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
(rsmq, user_db.begin(&authed).await?).into();
cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?; cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
let is_flow = sqlx::query_scalar!( let is_flow = sqlx::query_scalar!(
@@ -167,13 +152,12 @@ async fn edit_schedule(
.fetch_one(&mut tx) .fetch_one(&mut tx)
.await?; .await?;
clear_schedule(tx.transaction_mut(), path, is_flow).await?; clear_schedule(&mut tx, path, is_flow).await?;
let schedule = sqlx::query_as!( let schedule = sqlx::query_as!(
Schedule, Schedule,
"UPDATE schedule SET schedule = $1, timezone = $2, args = $3 WHERE path \ "UPDATE schedule SET schedule = $1, args = $2 WHERE path \
= $4 AND workspace_id = $5 RETURNING *", = $3 AND workspace_id = $4 RETURNING *",
es.schedule, es.schedule,
es.timezone,
es.args, es.args,
path, path,
w_id, w_id,
@@ -182,6 +166,10 @@ async fn edit_schedule(
.await .await
.map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?; .map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
audit_log( audit_log(
&mut tx, &mut tx,
&authed.username, &authed.username,
@@ -197,10 +185,6 @@ async fn edit_schedule(
), ),
) )
.await?; .await?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
tx.commit().await?; tx.commit().await?;
Ok(path.to_string()) Ok(path.to_string())
@@ -251,26 +235,15 @@ async fn exists_schedule(
Ok(Json(res)) Ok(Json(res))
} }
#[derive(Deserialize)]
pub struct PreviewPayload {
pub schedule: String,
pub timezone: String,
}
pub async fn preview_schedule( pub async fn preview_schedule(
Json(payload): Json<PreviewPayload>, Json(payload): Json<PreviewPayload>,
) -> JsonResult<Vec<DateTime<Utc>>> { ) -> JsonResult<Vec<DateTime<chrono::Utc>>> {
let schedule = cron::Schedule::from_str(&payload.schedule) let schedule = cron::Schedule::from_str(&payload.schedule)
.map_err(|e| Error::BadRequest(e.to_string()))?; .map_err(|e| Error::BadRequest(e.to_string()))?;
let upcoming: Vec<DateTime<chrono::Utc>> = schedule
let tz = .upcoming(get_offset(payload.offset))
chrono_tz::Tz::from_str(&payload.timezone).map_err(|e| Error::BadRequest(e.to_string()))?; .take(10)
.map(|x| x.into())
let upcoming: Vec<DateTime<Utc>> = schedule
.upcoming(tz)
.take(5)
// Convert back to UTC for a standardised API response. The client will convert to the local timezone.
.map(|x| x.with_timezone(&Utc))
.collect(); .collect();
Ok(Json(upcoming)) Ok(Json(upcoming))
@@ -279,12 +252,10 @@ pub async fn preview_schedule(
pub async fn set_enabled( pub async fn set_enabled(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<SetEnabled>, Json(payload): Json<SetEnabled>,
) -> Result<String> { ) -> Result<String> {
let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> = let mut tx = user_db.begin(&authed).await?;
(rsmq, user_db.begin(&authed).await?).into();
let path = path.to_path(); let path = path.to_path();
let schedule_o = sqlx::query_as!( let schedule_o = sqlx::query_as!(
Schedule, Schedule,
@@ -299,8 +270,11 @@ pub async fn set_enabled(
let schedule = not_found_if_none(schedule_o, "Schedule", path)?; let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
clear_schedule(tx.transaction_mut(), path, schedule.is_flow).await?; clear_schedule(&mut tx, path, schedule.is_flow).await?;
if payload.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
audit_log( audit_log(
&mut tx, &mut tx,
&authed.username, &authed.username,
@@ -311,12 +285,7 @@ pub async fn set_enabled(
Some([("enabled", payload.enabled.to_string().as_ref())].into()), Some([("enabled", payload.enabled.to_string().as_ref())].into()),
) )
.await?; .await?;
if payload.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
tx.commit().await?; tx.commit().await?;
Ok(format!( Ok(format!(
"succesfully updated schedule at path {} to status {}", "succesfully updated schedule at path {} to status {}",
path, payload.enabled path, payload.enabled
@@ -384,7 +353,6 @@ async fn check_flow_conflict<'c>(
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct EditSchedule { pub struct EditSchedule {
pub schedule: String, pub schedule: String,
pub timezone: String,
pub args: Option<serde_json::Value>, pub args: Option<serde_json::Value>,
} }
@@ -408,6 +376,16 @@ pub async fn clear_schedule<'c>(
Ok(()) Ok(())
} }
#[derive(Deserialize)]
pub struct PreviewPayload {
pub schedule: String,
pub offset: Option<i32>,
}
fn get_offset(offset: Option<i32>) -> FixedOffset {
FixedOffset::west_opt(offset.unwrap_or(0) * 60).expect("Invalid offset")
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct SetEnabled { pub struct SetEnabled {
pub enabled: bool, pub enabled: bool,

View File

@@ -6,12 +6,14 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use reqwest::Client;
use sql_builder::prelude::*;
use windmill_audit::{audit_log, ActionKind};
use crate::{ use crate::{
db::{UserDB, DB}, db::{UserDB, DB},
schedule::clear_schedule, schedule::clear_schedule,
users::{maybe_refresh_folders, require_owner_of_path, AuthCache, Authed}, users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
}; };
use axum::{ use axum::{
extract::{Extension, Path, Query}, extract::{Extension, Path, Query},
@@ -21,15 +23,12 @@ use axum::{
use hyper::StatusCode; use hyper::StatusCode;
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
use sql_builder::prelude::*;
use sql_builder::SqlBuilder; use sql_builder::SqlBuilder;
use sqlx::{FromRow, Postgres, Transaction}; use sqlx::{FromRow, Postgres, Transaction};
use std::{ use std::{
collections::hash_map::DefaultHasher, collections::hash_map::DefaultHasher,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
sync::Arc,
}; };
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{ use windmill_common::{
error::{Error, JsonResult, Result}, error::{Error, JsonResult, Result},
schedule::Schedule, schedule::Schedule,
@@ -42,8 +41,7 @@ use windmill_common::{
list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath, list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath,
}, },
}; };
use windmill_parser::MainArgSignature; use windmill_queue::{self, schedule::push_scheduled_job};
use windmill_queue::{self, schedule::push_scheduled_job, QueueTransaction};
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20; const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
@@ -61,13 +59,6 @@ pub fn global_service() -> Router {
.route("/hub/get_full/*path", get(get_full_hub_script_by_path)) .route("/hub/get_full/*path", get(get_full_hub_script_by_path))
} }
pub fn global_unauthed_service() -> Router {
Router::new().route(
"/tokened_raw/:workspace/:token/*path",
get(get_tokened_raw_script_by_path),
)
}
pub fn workspaced_service() -> Router { pub fn workspaced_service() -> Router {
Router::new() Router::new()
.route("/list", get(list_scripts)) .route("/list", get(list_scripts))
@@ -78,13 +69,11 @@ pub fn workspaced_service() -> Router {
.route("/exists/p/*path", get(exists_script_by_path)) .route("/exists/p/*path", get(exists_script_by_path))
.route("/archive/h/:hash", post(archive_script_by_hash)) .route("/archive/h/:hash", post(archive_script_by_hash))
.route("/delete/h/:hash", post(delete_script_by_hash)) .route("/delete/h/:hash", post(delete_script_by_hash))
.route("/delete/p/*path", post(delete_script_by_path))
.route("/get/h/:hash", get(get_script_by_hash)) .route("/get/h/:hash", get(get_script_by_hash))
.route("/raw/h/:hash", get(raw_script_by_hash)) .route("/raw/h/:hash", get(raw_script_by_hash))
.route("/deployment_status/h/:hash", get(get_deployment_status)) .route("/deployment_status/h/:hash", get(get_deployment_status))
.route("/list_paths", get(list_paths)) .route("/list_paths", get(list_paths))
} }
async fn list_scripts( async fn list_scripts(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
@@ -121,7 +110,7 @@ async fn list_scripts(
) )
.order_desc("favorite.path IS NOT NULL") .order_desc("favorite.path IS NOT NULL")
.order_by("created_at", lq.order_desc.unwrap_or(true)) .order_by("created_at", lq.order_desc.unwrap_or(true))
.and_where("o.workspace_id = ?".bind(&w_id)) .and_where("o.workspace_id = ? OR o.workspace_id = 'starter'".bind(&w_id))
.offset(offset) .offset(offset)
.limit(per_page) .limit(per_page)
.clone(); .clone();
@@ -130,10 +119,9 @@ async fn list_scripts(
sqlb.and_where_eq( sqlb.and_where_eq(
"created_at", "created_at",
"(select max(created_at) from script where o.path = path "(select max(created_at) from script where o.path = path
AND workspace_id = ?)" AND (workspace_id = ? OR workspace_id = 'starter'))"
.bind(&w_id), .bind(&w_id),
); );
sqlb.and_where_eq("archived", true);
} else { } else {
sqlb.and_where_eq("archived", false); sqlb.and_where_eq("archived", false);
} }
@@ -174,9 +162,12 @@ async fn list_scripts(
Ok(Json(rows)) Ok(Json(rows))
} }
async fn list_hub_scripts(Authed { email, .. }: Authed) -> JsonResult<serde_json::Value> { async fn list_hub_scripts(
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
let asks = list_elems_from_hub( let asks = list_elems_from_hub(
&HTTP_CLIENT, http_client,
"https://hub.windmill.dev/searchData?approved=true", "https://hub.windmill.dev/searchData?approved=true",
&email, &email,
) )
@@ -193,22 +184,19 @@ fn hash_script(ns: &NewScript) -> i64 {
async fn create_script( async fn create_script(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Json(ns): Json<NewScript>, Json(ns): Json<NewScript>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
let hash = ScriptHash(hash_script(&ns)); let hash = ScriptHash(hash_script(&ns));
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await; let mut tx = user_db.begin(&authed).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
if sqlx::query_scalar!( if sqlx::query_scalar!(
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2", "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
hash.0, hash.0,
&w_id &w_id
) )
.fetch_optional(tx.transaction_mut()) .fetch_optional(&mut tx)
.await? .await?
.is_some() .is_some()
{ {
@@ -269,7 +257,7 @@ async fn create_script(
))); )));
}; };
let ps = get_script_by_hash_internal(tx.transaction_mut(), &w_id, p_hash).await?; let ps = get_script_by_hash_internal(&mut tx, &w_id, p_hash).await?;
if ps.path != ns.path { if ps.path != ns.path {
if !authed.is_admin { if !authed.is_admin {
@@ -364,7 +352,7 @@ async fn create_script(
.await?; .await?;
for schedule in schedulables { for schedule in schedulables {
clear_schedule(tx.transaction_mut(), &schedule.path, false).await?; clear_schedule(&mut tx, &schedule.path, false).await?;
if schedule.enabled { if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?; tx = push_scheduled_job(tx, schedule).await?;
@@ -372,6 +360,35 @@ async fn create_script(
} }
} }
let mut tx = if needs_lock_gen {
let dependencies = match ns.language {
ScriptLang::Python3 => {
windmill_parser_py::parse_python_imports(&ns.content)?.join("\n")
}
_ => ns.content,
};
let (_, tx) = windmill_queue::push(
tx,
&w_id,
windmill_queue::JobPayload::Dependencies { hash, dependencies, language: ns.language },
serde_json::Map::new(),
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
None,
None,
None,
false,
false,
None,
true,
)
.await?;
tx
} else {
tx
};
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() { if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {
audit_log( audit_log(
&mut tx, &mut tx,
@@ -383,14 +400,6 @@ async fn create_script(
Some([("hash", hash.to_string().as_str())].into()), Some([("hash", hash.to_string().as_str())].into()),
) )
.await?; .await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateScript {
workspace: w_id.clone(),
path: ns.path.clone(),
hash: hash.to_string(),
},
);
} else { } else {
audit_log( audit_log(
&mut tx, &mut tx,
@@ -408,42 +417,6 @@ async fn create_script(
), ),
) )
.await?; .await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateScript {
workspace: w_id.clone(),
path: ns.path.clone(),
hash: hash.to_string(),
},
);
}
if needs_lock_gen {
let dependencies = match ns.language {
ScriptLang::Python3 => {
windmill_parser_py::parse_python_imports(&ns.content)?.join("\n")
}
_ => ns.content,
};
let (_, new_tx) = windmill_queue::push(
tx,
&w_id,
windmill_queue::JobPayload::Dependencies { hash, dependencies, language: ns.language },
serde_json::Map::new(),
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
None,
None,
None,
None,
false,
false,
None,
true,
)
.await?;
tx = new_tx;
} }
tx.commit().await?; tx.commit().await?;
@@ -451,16 +424,21 @@ async fn create_script(
Ok((StatusCode::CREATED, format!("{}", hash))) Ok((StatusCode::CREATED, format!("{}", hash)))
} }
pub async fn get_hub_script_by_path(authed: Authed, Path(path): Path<StripPath>) -> Result<String> { pub async fn get_hub_script_by_path(
windmill_common::scripts::get_hub_script_by_path(&authed.email, path, &HTTP_CLIENT).await authed: Authed,
Path(path): Path<StripPath>,
Extension(http_client): Extension<Client>,
) -> Result<String> {
windmill_common::scripts::get_hub_script_by_path(&authed.email, path, http_client).await
} }
pub async fn get_full_hub_script_by_path( pub async fn get_full_hub_script_by_path(
Authed { email, .. }: Authed, Authed { email, .. }: Authed,
Path(path): Path<StripPath>, Path(path): Path<StripPath>,
Extension(http_client): Extension<Client>,
) -> JsonResult<HubScript> { ) -> JsonResult<HubScript> {
Ok(Json( Ok(Json(
windmill_common::scripts::get_full_hub_script_by_path(&email, path, &HTTP_CLIENT).await?, windmill_common::scripts::get_full_hub_script_by_path(&email, path, http_client).await?,
)) ))
} }
@@ -473,9 +451,9 @@ async fn get_script_by_path(
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, Script>( let script_o = sqlx::query_as::<_, Script>(
"SELECT * FROM script WHERE path = $1 AND workspace_id = $2 \ "SELECT * FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') \
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)", (workspace_id = $2 OR workspace_id = 'starter'))",
) )
.bind(path) .bind(path)
.bind(w_id) .bind(w_id)
@@ -505,18 +483,6 @@ async fn list_paths(
Ok(Json(scripts)) Ok(Json(scripts))
} }
async fn get_tokened_raw_script_by_path(
Extension(user_db): Extension<UserDB>,
Path((w_id, token, path)): Path<(String, String, StripPath)>,
Extension(cache): Extension<Arc<AuthCache>>,
) -> Result<String> {
let authed = cache
.get_authed(Some(w_id.clone()), &token)
.await
.ok_or_else(|| Error::NotAuthorized("Invalid token".to_string()))?;
return raw_script_by_path(authed, Extension(user_db), Path((w_id, path))).await;
}
async fn raw_script_by_path( async fn raw_script_by_path(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
@@ -526,12 +492,11 @@ async fn raw_script_by_path(
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
let content_o = sqlx::query_scalar!( let content_o = sqlx::query_scalar!(
"SELECT content FROM script WHERE path = $1 AND workspace_id = $2 \ "SELECT content FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') \
AND AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND \ created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND \
workspace_id = $2)", (workspace_id = $2 OR workspace_id = 'starter'))",
path, path, w_id
w_id
) )
.fetch_optional(&mut tx) .fetch_optional(&mut tx)
.await?; .await?;
@@ -548,8 +513,10 @@ async fn exists_script_by_path(
let path = path.to_path(); let path = path.to_path();
let exists = sqlx::query_scalar!( let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND workspace_id = $2 AND "SELECT EXISTS(SELECT 1 FROM script WHERE path = $1 AND (workspace_id = $2 OR \
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2))", workspace_id = 'starter') AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 \
OR workspace_id = 'starter')))",
path, path,
w_id w_id
) )
@@ -565,22 +532,24 @@ async fn get_script_by_hash_internal<'c>(
workspace_id: &str, workspace_id: &str,
hash: &ScriptHash, hash: &ScriptHash,
) -> Result<Script> { ) -> Result<Script> {
let script_o = let script_o = sqlx::query_as::<_, Script>(
sqlx::query_as::<_, Script>("SELECT * FROM script WHERE hash = $1 AND workspace_id = $2") "SELECT * FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')",
.bind(hash) )
.bind(workspace_id) .bind(hash)
.fetch_optional(db) .bind(workspace_id)
.await?; .fetch_optional(db)
.await?;
let script = not_found_if_none(script_o, "Script", hash.to_string())?; let script = not_found_if_none(script_o, "Script", hash.to_string())?;
Ok(script) Ok(script)
} }
async fn get_script_by_hash( async fn get_script_by_hash(
Extension(db): Extension<DB>, authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, hash)): Path<(String, ScriptHash)>, Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> { ) -> JsonResult<Script> {
let mut tx = db.begin().await?; let mut tx = user_db.begin(&authed).await?;
let r = get_script_by_hash_internal(&mut tx, &w_id, &hash).await?; let r = get_script_by_hash_internal(&mut tx, &w_id, &hash).await?;
tx.commit().await?; tx.commit().await?;
@@ -588,10 +557,11 @@ async fn get_script_by_hash(
} }
async fn raw_script_by_hash( async fn raw_script_by_hash(
Extension(db): Extension<DB>, authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, hash_str)): Path<(String, String)>, Path((w_id, hash_str)): Path<(String, String)>,
) -> Result<String> { ) -> Result<String> {
let mut tx = db.begin().await?; let mut tx = user_db.begin(&authed).await?;
let hash = ScriptHash(to_i64(hash_str.strip_suffix(".ts").ok_or_else(|| { let hash = ScriptHash(to_i64(hash_str.strip_suffix(".ts").ok_or_else(|| {
Error::BadRequest("Raw script path must end with .ts".to_string()) Error::BadRequest("Raw script path must end with .ts".to_string())
})?)?); })?)?);
@@ -607,13 +577,15 @@ struct DeploymentStatus {
lock_error_logs: Option<String>, lock_error_logs: Option<String>,
} }
async fn get_deployment_status( async fn get_deployment_status(
Extension(db): Extension<DB>, authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, hash)): Path<(String, ScriptHash)>, Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<DeploymentStatus> { ) -> JsonResult<DeploymentStatus> {
let mut tx = db.begin().await?; let mut tx = user_db.begin(&authed).await?;
let status_o: Option<DeploymentStatus> = sqlx::query_as!( let status_o: Option<DeploymentStatus> = sqlx::query_as!(
DeploymentStatus, DeploymentStatus,
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND workspace_id = $2", "SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND (workspace_id = $2 OR \
workspace_id = 'starter')",
hash.0, hash.0,
w_id, w_id,
) )
@@ -628,7 +600,6 @@ async fn get_deployment_status(
async fn archive_script_by_path( async fn archive_script_by_path(
authed: Authed, authed: Authed,
Extension(webhook): Extension<WebhookShared>,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
@@ -655,10 +626,6 @@ async fn archive_script_by_path(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(()) Ok(())
} }
@@ -666,7 +633,6 @@ async fn archive_script_by_path(
async fn archive_script_by_hash( async fn archive_script_by_hash(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, hash)): Path<(String, ScriptHash)>, Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> { ) -> JsonResult<Script> {
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
@@ -691,18 +657,12 @@ async fn archive_script_by_hash(
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(Json(script)) Ok(Json(script))
} }
async fn delete_script_by_hash( async fn delete_script_by_hash(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>, Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> { ) -> JsonResult<Script> {
@@ -731,79 +691,28 @@ async fn delete_script_by_hash(
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(Json(script)) Ok(Json(script))
} }
async fn delete_script_by_path( async fn parse_python_code_to_jsonschema(
authed: Authed, Json(code): Json<String>,
Extension(user_db): Extension<UserDB>, ) -> JsonResult<windmill_parser::MainArgSignature> {
Extension(webhook): Extension<WebhookShared>, windmill_parser_py::parse_python_signature(&code).map(Json)
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
require_admin(authed.is_admin, &authed.username)?;
let script = sqlx::query_scalar!(
"DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
w_id
)
.fetch_one(&db)
.await
.map_err(|e| Error::InternalErr(format!("deleting script by path {w_id}: {e}")))?;
audit_log(
&mut tx,
&authed.username,
"scripts.delete",
ActionKind::Delete,
&w_id,
Some(&path),
Some([("workspace", w_id.as_str())].into()),
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScriptPath { workspace: w_id, path: path.to_string() },
);
Ok(Json(script))
} }
#[derive(Debug, Serialize)] async fn parse_deno_code_to_jsonschema(
#[serde(tag = "type")] Json(code): Json<String>,
enum SigParsing { ) -> JsonResult<windmill_parser::MainArgSignature> {
Valid(MainArgSignature), windmill_parser_ts::parse_deno_signature(&code).map(Json)
Invalid { error: String }, }
async fn parse_go_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_go::parse_go_sig(&code).map(Json)
} }
fn result_to_sig_parsing(result: Result<MainArgSignature>) -> Json<SigParsing> { async fn parse_bash_code_to_jsonschema(
match result { Json(code): Json<String>,
Ok(sig) => Json(SigParsing::Valid(sig)), ) -> JsonResult<windmill_parser::MainArgSignature> {
Err(e) => Json(SigParsing::Invalid { error: e.to_string() }), windmill_parser_bash::parse_bash_sig(&code).map(Json)
}
}
async fn parse_python_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_py::parse_python_signature(&code))
}
async fn parse_deno_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_ts::parse_deno_signature(&code, false))
}
async fn parse_go_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_go::parse_go_sig(&code))
}
async fn parse_bash_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_bash::parse_bash_sig(&code))
} }

View File

@@ -9,41 +9,58 @@
use axum::{ use axum::{
body::{self, BoxBody}, body::{self, BoxBody},
extract::OriginalUri, extract::OriginalUri,
http::{header, Response}, http::{header, response::Builder, Response},
response::IntoResponse, response::IntoResponse,
Extension,
}; };
use hyper::Uri; use crate::{CloudHosted, ContentSecurityPolicy, IsSecure};
use mime_guess::mime; use mime_guess::mime;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use std::sync::Arc;
// static_handler is a handler that serves static files from the // static_handler is a handler that serves static files from the
pub async fn static_handler(OriginalUri(original_uri): OriginalUri) -> StaticFile { pub async fn static_handler(
StaticFile(original_uri) Extension(is_secure): Extension<Arc<IsSecure>>,
Extension(is_cloud_hosted): Extension<Arc<CloudHosted>>,
Extension(csp): Extension<Arc<ContentSecurityPolicy>>,
OriginalUri(original_uri): OriginalUri,
) -> StaticFile {
let path = original_uri.path().trim_start_matches('/').to_string();
StaticFile(path, is_secure.0, is_cloud_hosted.0, csp)
} }
#[derive(RustEmbed)] #[derive(RustEmbed)]
#[folder = "../../frontend/build/"] #[folder = "../../frontend/build/"]
struct Asset; struct Asset;
pub struct StaticFile(Uri); pub struct StaticFile(
pub String,
pub bool,
pub bool,
pub Arc<ContentSecurityPolicy>,
);
impl IntoResponse for StaticFile { impl IntoResponse for StaticFile {
fn into_response(self) -> Response<BoxBody> { fn into_response(self) -> Response<BoxBody> {
let path = self.0.path().trim_start_matches('/'); let path = self.0;
serve_path(path) let can_set_security_headers = self.1 && self.2;
let csp = self.3;
serve_path(path, can_set_security_headers, csp)
} }
} }
const TWO_HUNDRED: &str = "200.html"; fn serve_path(
path: String,
fn serve_path(path: &str) -> Response<BoxBody> { can_set_security_headers: bool,
csp: Arc<ContentSecurityPolicy>,
) -> Response<BoxBody> {
if path.starts_with("api/") { if path.starts_with("api/") {
return Response::builder() return Response::builder()
.status(404) .status(404)
.body(body::boxed(body::Empty::new())) .body(body::boxed(body::Empty::new()))
.unwrap(); .unwrap();
} }
match Asset::get(path) { match Asset::get(path.as_str()) {
Some(content) => { Some(content) => {
let body = body::boxed(body::Full::from(content.data)); let body = body::boxed(body::Full::from(content.data));
let mime = mime_guess::from_path(path).first_or_octet_stream(); let mime = mime_guess::from_path(path).first_or_octet_stream();
@@ -58,12 +75,26 @@ fn serve_path(path: &str) -> Response<BoxBody> {
res = res.header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate"); res = res.header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate");
} }
if can_set_security_headers {
res = set_security_headers(res, csp);
}
res.body(body).unwrap() res.body(body).unwrap()
} }
None if path.starts_with("_app/") => Response::builder() None if path.as_str().starts_with("_app/") => Response::builder()
.status(404) .status(404)
.body(body::boxed(body::Empty::new())) .body(body::boxed(body::Empty::new()))
.unwrap(), .unwrap(),
None => serve_path(TWO_HUNDRED), None => serve_path("200.html".to_owned(), can_set_security_headers, csp),
} }
} }
fn set_security_headers(mut res: Builder, csp: Arc<ContentSecurityPolicy>) -> Builder {
res = res.header("X-Frame-Options", "DENY");
res = res.header("X-Content-Type-Options", "nosniff");
if !csp.0.is_empty() {
res = res.header("Content-Security-Policy", &csp.0);
}
res
}

View File

@@ -12,9 +12,8 @@ use crate::{
db::{UserDB, DB}, db::{UserDB, DB},
folders::get_folders_for_user, folders::get_folders_for_user,
utils::require_super_admin, utils::require_super_admin,
webhook_util::{InstanceEvent, WebhookShared},
workspaces::invite_user_to_all_auto_invite_worspaces, workspaces::invite_user_to_all_auto_invite_worspaces,
COOKIE_DOMAIN, IS_SECURE, CookieDomain, IsSecure,
}; };
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use axum::{ use axum::{
@@ -26,9 +25,7 @@ use axum::{
Json, Router, Json, Router,
}; };
use hyper::{header::LOCATION, StatusCode}; use hyper::{header::LOCATION, StatusCode};
use lazy_static::lazy_static;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use regex::Regex;
use retainer::Cache; use retainer::Cache;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::FromRow; use sqlx::FromRow;
@@ -38,7 +35,6 @@ use tracing::{Instrument, Span};
use windmill_audit::{audit_log, ActionKind}; use windmill_audit::{audit_log, ActionKind};
use windmill_common::{ use windmill_common::{
error::{self, Error, JsonResult, Result}, error::{self, Error, JsonResult, Result},
users::SUPERADMIN_SECRET_EMAIL,
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath}, utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
}; };
use windmill_queue::CLOUD_HOSTED; use windmill_queue::CLOUD_HOSTED;
@@ -77,7 +73,6 @@ pub fn global_service() -> Router {
.route("/tokens/create", post(create_token)) .route("/tokens/create", post(create_token))
.route("/tokens/delete/:token_prefix", delete(delete_token)) .route("/tokens/delete/:token_prefix", delete(delete_token))
.route("/tokens/list", get(list_tokens)) .route("/tokens/list", get(list_tokens))
.route("/tokens/impersonate", post(impersonate))
.route("/usage", get(get_usage)) .route("/usage", get(get_usage))
// .route("/list_invite_codes", get(list_invite_codes)) // .route("/list_invite_codes", get(list_invite_codes))
// .route("/create_invite_code", post(create_invite_code)) // .route("/create_invite_code", post(create_invite_code))
@@ -104,10 +99,6 @@ impl AuthCache {
AuthCache { cache: Cache::new(), db, superadmin_secret } AuthCache { cache: Cache::new(), db, superadmin_secret }
} }
pub async fn invalidate(&self, w_id: &str, token: String) {
self.cache.remove(&(w_id.to_string(), token)).await;
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<Authed> { pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<Authed> {
let key = ( let key = (
w_id.as_ref().unwrap_or(&"".to_string()).to_string(), w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
@@ -275,7 +266,7 @@ impl AuthCache {
.unwrap_or(false) .unwrap_or(false)
{ {
Some(Authed { Some(Authed {
email: SUPERADMIN_SECRET_EMAIL.to_string(), email: "superadmin_secret@windmill.dev".to_string(),
username: "superadmin_secret".to_string(), username: "superadmin_secret".to_string(),
is_admin: true, is_admin: true,
groups: Vec::new(), groups: Vec::new(),
@@ -362,31 +353,6 @@ pub struct Authed {
pub folders: Vec<(String, bool)>, pub folders: Vec<(String, bool)>,
} }
pub async fn maybe_refresh_folders(path: &str, w_id: &str, authed: Authed, db: &DB) -> Authed {
if authed.is_admin {
return authed;
}
let splitted = path.split('/').collect::<Vec<_>>();
if splitted.len() >= 2
&& splitted[0] == "f"
&& !authed.folders.iter().any(|(f, _)| f == splitted[1])
{
let name = &authed.username;
let groups = get_groups_for_user(w_id, name, db)
.await
.ok()
.unwrap_or_default();
let folders = get_folders_for_user(w_id, name, &groups, db)
.await
.ok()
.unwrap_or_default();
Authed { folders, ..authed }
} else {
authed
}
}
#[async_trait] #[async_trait]
impl<S> FromRequestParts<S> for Authed impl<S> FromRequestParts<S> for Authed
where where
@@ -595,7 +561,6 @@ pub struct TruncatedToken {
pub struct NewToken { pub struct NewToken {
pub label: Option<String>, pub label: Option<String>,
pub expiration: Option<chrono::DateTime<chrono::Utc>>, pub expiration: Option<chrono::DateTime<chrono::Utc>>,
pub impersonate_email: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -755,12 +720,14 @@ async fn logout(
Tokened { token }: Tokened, Tokened { token }: Tokened,
cookies: Cookies, cookies: Cookies,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(cookie_domain): Extension<Arc<CookieDomain>>,
Query(LogoutQuery { rd }): Query<LogoutQuery>, Query(LogoutQuery { rd }): Query<LogoutQuery>,
) -> Result<Response> { ) -> Result<Response> {
let mut cookie = Cookie::new(COOKIE_NAME, ""); let mut cookie = Cookie::new(COOKIE_NAME, "");
cookie.set_path(COOKIE_PATH); cookie.set_path(COOKIE_PATH);
if COOKIE_DOMAIN.is_some() { let domain = cookie_domain.0.clone();
cookie.set_domain(COOKIE_DOMAIN.clone().unwrap()); if domain.is_some() {
cookie.set_domain(domain.clone().unwrap());
} }
cookies.remove(cookie); cookies.remove(cookie);
let mut tx = db.begin().await?; let mut tx = db.begin().await?;
@@ -1032,22 +999,14 @@ async fn decline_invite(
} }
} }
lazy_static! {
pub static ref VALID_USERNAME: Regex = Regex::new(r#"^[a-zA-Z_0-9]+$"#).unwrap();
}
async fn accept_invite( async fn accept_invite(
Authed { email, .. }: Authed, Authed { email, .. }: Authed,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Json(nu): Json<AcceptInvite>, Json(nu): Json<AcceptInvite>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
if !VALID_USERNAME.is_match(&nu.username) { if &nu.username == "bot" {
return Err(windmill_common::error::Error::BadRequest(format!( return Err(Error::BadRequest("bot is a reserved username".to_string()));
"Usermame can only contain alphanumeric characters and underscores"
)));
} }
let mut tx = db.begin().await?; let mut tx = db.begin().await?;
let r = sqlx::query!( let r = sqlx::query!(
@@ -1084,11 +1043,6 @@ async fn accept_invite(
} }
if is_some { if is_some {
webhook.send_instance_event(InstanceEvent::UserJoinedWorkspace {
email: email.clone(),
workspace: nu.workspace_id.clone(),
username: nu.username.clone(),
});
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
format!( format!(
@@ -1125,12 +1079,6 @@ async fn add_user_to_workspace<'c>(
))); )));
} }
if !VALID_USERNAME.is_match(username) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Usermame can only contain alphanumeric characters and underscores"
)));
}
let already_exists_email = sqlx::query_scalar!( let already_exists_email = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)", "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
&w_id, &w_id,
@@ -1322,27 +1270,15 @@ async fn delete_user(
Ok(format!("email {} deleted", &email_to_delete)) Ok(format!("email {} deleted", &email_to_delete))
} }
lazy_static::lazy_static! {
pub static ref NEW_USER_WEBHOOK: Option<String> = std::env::var("NEW_USER_WEBHOOK").ok();
}
async fn create_user( async fn create_user(
Authed { email, .. }: Authed, Authed { email, .. }: Authed,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Extension(argon2): Extension<Arc<Argon2<'_>>>, Extension(argon2): Extension<Arc<Argon2<'_>>>,
Json(mut nu): Json<NewUser>, Json(nu): Json<NewUser>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?; let mut tx = db.begin().await?;
require_super_admin(&mut tx, &email).await?; require_super_admin(&mut tx, &email).await?;
nu.email = nu.email.to_lowercase();
if nu.email == SUPERADMIN_SECRET_EMAIL {
return Err(Error::BadRequest(
"The superadmin email is a reserved email".into(),
));
}
sqlx::query!( sqlx::query!(
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, \ "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, \
@@ -1369,10 +1305,8 @@ async fn create_user(
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?; invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?;
webhook.send_instance_event(InstanceEvent::UserAdded { email: nu.email.clone() });
Ok((StatusCode::CREATED, format!("email {} created", nu.email))) Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
} }
@@ -1615,19 +1549,21 @@ async fn login(
cookies: Cookies, cookies: Cookies,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(argon2): Extension<Arc<Argon2<'_>>>, Extension(argon2): Extension<Arc<Argon2<'_>>>,
Extension(is_secure): Extension<Arc<IsSecure>>,
Extension(cookie_domain): Extension<Arc<CookieDomain>>,
Json(Login { email, password }): Json<Login>, Json(Login { email, password }): Json<Login>,
) -> Result<String> { ) -> Result<String> {
let mut tx = db.begin().await?; let mut tx = db.begin().await?;
let email = email.to_lowercase();
let email_w_h: Option<(String, String, bool, bool)> = sqlx::query_as( let email_w_h: Option<(String, String, bool)> = sqlx::query_as(
"SELECT email, password_hash, super_admin, first_time_user FROM password WHERE email = $1 AND login_type = \ "SELECT email, password_hash, super_admin FROM password WHERE email = $1 AND login_type = \
'password'", 'password'",
) )
.bind(&email) .bind(&email)
.fetch_optional(&mut tx) .fetch_optional(&mut tx)
.await?; .await?;
if let Some((email, hash, super_admin, first_time_user)) = email_w_h { if let Some((email, hash, super_admin)) = email_w_h {
let parsed_hash = let parsed_hash =
PasswordHash::new(&hash).map_err(|e| Error::InternalErr(e.to_string()))?; PasswordHash::new(&hash).map_err(|e| Error::InternalErr(e.to_string()))?;
if argon2 if argon2
@@ -1636,27 +1572,15 @@ async fn login(
{ {
Err(Error::BadRequest("Invalid login".to_string())) Err(Error::BadRequest("Invalid login".to_string()))
} else { } else {
if first_time_user { let token = create_session_token(
sqlx::query_scalar!( &email,
"UPDATE password SET first_time_user = false WHERE email = $1", super_admin,
&email &mut tx,
) cookies,
.execute(&mut tx) is_secure.0,
.await?; &cookie_domain.as_ref().0,
let mut c = Cookie::new("first_time", "1"); )
if let Some(domain) = COOKIE_DOMAIN.as_ref() { .await?;
c.set_domain(domain);
}
c.set_secure(false);
c.set_expires(time::OffsetDateTime::now_utc() + time::Duration::minutes(15));
c.set_http_only(false);
c.set_path("/");
cookies.add(c);
}
let token = create_session_token(&email, super_admin, &mut tx, cookies).await?;
tx.commit().await?; tx.commit().await?;
Ok(token) Ok(token)
} }
@@ -1670,6 +1594,8 @@ pub async fn create_session_token<'c>(
super_admin: bool, super_admin: bool,
tx: &mut sqlx::Transaction<'c, sqlx::Postgres>, tx: &mut sqlx::Transaction<'c, sqlx::Postgres>,
cookies: Cookies, cookies: Cookies,
is_secure: bool,
domain: &Option<String>,
) -> Result<String> { ) -> Result<String> {
let token = rd_string(30); let token = rd_string(30);
sqlx::query!( sqlx::query!(
@@ -1685,12 +1611,12 @@ pub async fn create_session_token<'c>(
.execute(tx) .execute(tx)
.await?; .await?;
let mut cookie = Cookie::new(COOKIE_NAME, token.clone()); let mut cookie = Cookie::new(COOKIE_NAME, token.clone());
cookie.set_secure(*IS_SECURE); cookie.set_secure(is_secure);
cookie.set_same_site(Some(cookie::SameSite::Lax)); cookie.set_same_site(cookie::SameSite::Lax);
cookie.set_http_only(true); cookie.set_http_only(true);
cookie.set_path(COOKIE_PATH); cookie.set_path(COOKIE_PATH);
if COOKIE_DOMAIN.is_some() { if domain.is_some() {
cookie.set_domain(COOKIE_DOMAIN.clone().unwrap()); cookie.set_domain(domain.clone().unwrap());
} }
let mut expire: OffsetDateTime = time::OffsetDateTime::now_utc(); let mut expire: OffsetDateTime = time::OffsetDateTime::now_utc();
expire += time::Duration::days(3); expire += time::Duration::days(3);
@@ -1740,58 +1666,6 @@ async fn create_token(
Ok((StatusCode::CREATED, token)) Ok((StatusCode::CREATED, token))
} }
async fn impersonate(
Extension(db): Extension<DB>,
Authed { email, username, .. }: Authed,
Json(new_token): Json<NewToken>,
) -> Result<(StatusCode, String)> {
let token = rd_string(30);
let mut tx = db.begin().await?;
require_super_admin(&mut tx, &email).await?;
if new_token.impersonate_email.is_none() {
return Err(Error::BadRequest(
"impersonate_username is required".to_string(),
));
}
let impersonated = new_token.impersonate_email.unwrap();
let is_super_admin = sqlx::query_scalar!(
"SELECT super_admin FROM password WHERE email = $1",
impersonated
)
.fetch_optional(&mut tx)
.await?
.unwrap_or(false);
sqlx::query!(
"INSERT INTO token
(token, email, label, expiration, super_admin)
VALUES ($1, $2, $3, $4, $5)",
token,
impersonated,
new_token.label,
new_token.expiration,
is_super_admin
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&username,
"users.impersonate",
ActionKind::Delete,
&"global",
Some(&token[0..10]),
Some([("impersonated", &format!("{impersonated}")[..])].into()),
)
.instrument(tracing::info_span!("token", email = &impersonated))
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, token))
}
async fn list_tokens( async fn list_tokens(
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Authed { email, .. }: Authed, Authed { email, .. }: Authed,
@@ -1892,17 +1766,6 @@ pub async fn delete_expired_items_perdiodically(
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()), Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
} }
let pip_resolution_r = sqlx::query_scalar!(
"DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
)
.fetch_all(db)
.await;
match pip_resolution_r {
Ok(res) => tracing::debug!("deleted {} pip_resolution: {:?}", res.len(), res),
Err(e) => tracing::error!("Error deleting pip_resolution: {}", e.to_string()),
}
let magic_links_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar( let magic_links_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
"DELETE FROM magic_link WHERE expiration <= now() "DELETE FROM magic_link WHERE expiration <= now()
RETURNING concat(substring(token for 10), '*****')", RETURNING concat(substring(token for 10), '*****')",

View File

@@ -7,24 +7,16 @@
*/ */
use sqlx::{Postgres, Transaction}; use sqlx::{Postgres, Transaction};
use windmill_common::{ use windmill_common::error::{self, Error};
error::{self, Error},
users::SUPERADMIN_SECRET_EMAIL,
};
pub async fn require_super_admin<'c>( pub async fn require_super_admin<'c>(
db: &mut Transaction<'c, Postgres>, db: &mut Transaction<'c, Postgres>,
email: &str, email: &str,
) -> error::Result<()> { ) -> error::Result<()> {
if email == SUPERADMIN_SECRET_EMAIL {
return Ok(());
}
let is_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email) let is_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
.fetch_optional(db) .fetch_one(db)
.await .await
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))? .map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))?;
.unwrap_or(false);
if !is_admin { if !is_admin {
Err(Error::NotAuthorized( Err(Error::NotAuthorized(
"This endpoint require caller to be a super admin".to_owned(), "This endpoint require caller to be a super admin".to_owned(),

View File

@@ -6,11 +6,13 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use std::sync::Arc;
use crate::{ use crate::{
db::{UserDB, DB}, db::{UserDB, DB},
oauth2::_refresh_token, oauth2::{AllClients, _refresh_token},
users::{maybe_refresh_folders, require_owner_of_path, Authed}, users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared}, BaseUrl,
}; };
/* /*
* Author: Ruben Fiszel * Author: Ruben Fiszel
@@ -26,7 +28,6 @@ use axum::{
Json, Router, Json, Router,
}; };
use hyper::StatusCode; use hyper::StatusCode;
use serde_json::Value;
use windmill_audit::{audit_log, ActionKind}; use windmill_audit::{audit_log, ActionKind};
use windmill_common::{ use windmill_common::{
error::{Error, JsonResult, Result}, error::{Error, JsonResult, Result},
@@ -35,6 +36,7 @@ use windmill_common::{
}; };
use magic_crypt::{MagicCrypt256, MagicCryptTrait}; use magic_crypt::{MagicCrypt256, MagicCryptTrait};
use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use sqlx::{Postgres, Transaction}; use sqlx::{Postgres, Transaction};
@@ -51,6 +53,7 @@ pub fn workspaced_service() -> Router {
async fn list_contextual_variables( async fn list_contextual_variables(
Path(w_id): Path<String>, Path(w_id): Path<String>,
Extension(base_url): Extension<Arc<BaseUrl>>,
Authed { username, email, .. }: Authed, Authed { username, email, .. }: Authed,
) -> JsonResult<Vec<ContextualVariable>> { ) -> JsonResult<Vec<ContextualVariable>> {
Ok(Json( Ok(Json(
@@ -61,6 +64,7 @@ async fn list_contextual_variables(
&username, &username,
"017e0ad5-f499-73b6-5488-92a61c5196dd", "017e0ad5-f499-73b6-5488-92a61c5196dd",
format!("u/{username}").as_str(), format!("u/{username}").as_str(),
&base_url.0,
Some("u/user/script_path".to_string()), Some("u/user/script_path".to_string()),
Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()), Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()),
Some("u/user/encapsulating_flow_path".to_string()), Some("u/user/encapsulating_flow_path".to_string()),
@@ -86,7 +90,7 @@ async fn list_variables(
from variable from variable
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = variable.workspace_id LEFT JOIN account ON variable.account = account.id AND account.workspace_id = variable.workspace_id
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
WHERE variable.workspace_id = $1 ORDER BY path", WHERE variable.workspace_id = $1 OR (is_secret IS NOT TRUE AND variable.workspace_id = 'starter') ORDER BY path",
) )
.bind(&w_id) .bind(&w_id)
.fetch_all(&mut tx) .fetch_all(&mut tx)
@@ -106,6 +110,8 @@ async fn get_variable(
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Query(q): Query<GetVariableQuery>, Query(q): Query<GetVariableQuery>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
) -> JsonResult<ListableVariable> { ) -> JsonResult<ListableVariable> {
let path = path.to_path(); let path = path.to_path();
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
@@ -117,7 +123,8 @@ async fn get_variable(
from variable from variable
LEFT JOIN account ON variable.account = account.id LEFT JOIN account ON variable.account = account.id
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
WHERE variable.path = $1 AND variable.workspace_id = $2 WHERE variable.path = $1 AND (variable.workspace_id = $2 OR (is_secret IS NOT TRUE AND \
variable.workspace_id = 'starter'))
LIMIT 1", LIMIT 1",
) )
.bind(&path) .bind(&path)
@@ -143,7 +150,17 @@ async fn get_variable(
let value = variable.value.unwrap_or_else(|| "".to_string()); let value = variable.value.unwrap_or_else(|| "".to_string());
ListableVariable { ListableVariable {
value: if variable.is_expired.unwrap_or(false) && variable.account.is_some() { value: if variable.is_expired.unwrap_or(false) && variable.account.is_some() {
Some(_refresh_token(tx, &variable.path, w_id, variable.account.unwrap()).await?) Some(
_refresh_token(
tx,
&variable.path,
w_id,
variable.account.unwrap(),
clients,
http_client,
)
.await?,
)
} else if !value.is_empty() && decrypt_secret { } else if !value.is_empty() && decrypt_secret {
let mc = build_crypt(&mut tx, &w_id).await?; let mc = build_crypt(&mut tx, &w_id).await?;
tx.commit().await?; tx.commit().await?;
@@ -206,19 +223,14 @@ async fn check_path_conflict<'c>(
async fn create_variable( async fn create_variable(
authed: Authed, authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
Json(variable): Json<CreateVariable>, Json(variable): Json<CreateVariable>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
let authed = maybe_refresh_folders(&variable.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &variable.path).await?; check_path_conflict(&mut tx, &w_id, &variable.path).await?;
let value = if variable.is_secret && !already_encrypted.unwrap_or(false) { let value = if variable.is_secret {
let mc = build_crypt(&mut tx, &w_id).await?; let mc = build_crypt(&mut tx, &w_id).await?;
encrypt(&mc, &variable.value) encrypt(&mc, &variable.value)
} else { } else {
@@ -253,11 +265,6 @@ async fn create_variable(
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateVariable { workspace: w_id, path: variable.path.clone() },
);
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
format!("variable {} created", variable.path), format!("variable {} created", variable.path),
@@ -267,7 +274,6 @@ async fn create_variable(
async fn delete_variable( async fn delete_variable(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> { ) -> Result<String> {
let path = path.to_path(); let path = path.to_path();
@@ -300,11 +306,6 @@ async fn delete_variable(
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteVariable { workspace: w_id, path: path.to_owned() },
);
Ok(format!("variable {} deleted", path)) Ok(format!("variable {} deleted", path))
} }
@@ -316,24 +317,16 @@ struct EditVariable {
description: Option<String>, description: Option<String>,
} }
#[derive(Deserialize)]
struct AlreadyEncrypted {
already_encrypted: Option<bool>,
}
async fn update_variable( async fn update_variable(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>, Path((w_id, path)): Path<(String, StripPath)>,
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
Json(ns): Json<EditVariable>, Json(ns): Json<EditVariable>,
) -> Result<String> { ) -> Result<String> {
use sql_builder::prelude::*; use sql_builder::prelude::*;
let path = path.to_path(); let path = path.to_path();
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?; let mut tx = user_db.begin(&authed).await?;
@@ -354,7 +347,7 @@ async fn update_variable(
.await? .await?
.unwrap_or(false); .unwrap_or(false);
let value = if is_secret && !already_encrypted.unwrap_or(false) { let value = if is_secret {
let mc = build_crypt(&mut tx, &w_id).await?; let mc = build_crypt(&mut tx, &w_id).await?;
encrypt(&mc, &nvalue) encrypt(&mc, &nvalue)
} else { } else {
@@ -383,27 +376,9 @@ async fn update_variable(
if !authed.is_admin { if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?; require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
} }
let mut v = sqlx::query_scalar!(
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.fetch_optional(&mut tx)
.await?
.flatten();
if let Some(old_v) = v {
v = Some(replace_path(
old_v,
&format!("$var:{path}"),
&format!("$var:{npath}"),
))
}
sqlx::query!( sqlx::query!(
"UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4", "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath, npath,
v,
path, path,
w_id w_id
) )
@@ -430,35 +405,9 @@ async fn update_variable(
.await?; .await?;
tx.commit().await?; tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateVariable {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("variable {} updated (npath: {:?})", path, npath)) Ok(format!("variable {} updated (npath: {:?})", path, npath))
} }
fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value {
match v {
Value::Object(v) => Value::Object(
v.into_iter()
.map(|(k, v)| (k, replace_path(v, path, npath)))
.collect(),
),
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(|v| replace_path(v, path, npath))
.collect(),
),
Value::String(s) if s == path => Value::String(npath.to_owned()),
_ => v,
}
}
pub async fn build_crypt<'c>( pub async fn build_crypt<'c>(
db: &mut Transaction<'c, Postgres>, db: &mut Transaction<'c, Postgres>,
w_id: &str, w_id: &str,

View File

@@ -1,152 +0,0 @@
use std::time::Duration;
use serde::Serialize;
use tokio::{select, sync::mpsc, time::interval};
use windmill_common::METRICS_ENABLED;
use crate::db::DB;
lazy_static::lazy_static! {
// TODO: these aren't synced, they should be moved into the queue abstraction once/if that happens.
static ref WEBHOOK_REQUEST_COUNT: prometheus::Histogram = prometheus::register_histogram!(
"webhook_request",
"Histogram of webhook requests made"
)
.unwrap();
pub static ref INSTANCE_EVENTS_WEBHOOK: Option<String> = std::env::var("INSTANCE_EVENTS_WEBHOOK").ok();
}
pub enum WebhookPayload {
WorkspaceEvent(String, WebhookMessage),
InstanceEvent(InstanceEvent),
}
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum InstanceEvent {
UserSignupOAuth { email: String },
UserAdded { email: String },
// UserDeleted { email: String },
// UserDeletedWorkspace { workspace: String, email: String },
UserAddedWorkspace { workspace: String, email: String },
UserInvitedWorkspace { workspace: String, email: String },
UserJoinedWorkspace { workspace: String, email: String, username: String },
}
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum WebhookMessage {
// See https://serde.rs/enum-representations.html#internally-tagged for how this looks in JSON
CreateApp { workspace: String, path: String },
DeleteApp { workspace: String, path: String },
UpdateApp { workspace: String, old_path: String, new_path: String },
CreateFlow { workspace: String, path: String },
UpdateFlow { workspace: String, old_path: String, new_path: String },
ArchiveFlow { workspace: String, path: String },
DeleteFlow { workspace: String, path: String },
CreateFolder { workspace: String, name: String },
UpdateFolder { workspace: String, name: String },
DeleteFolder { workspace: String, name: String },
DeleteResource { workspace: String, path: String },
CreateResource { workspace: String, path: String },
UpdateResource { workspace: String, old_path: String, new_path: String },
CreateResourceType { name: String },
DeleteResourceType { name: String },
UpdateResourceType { name: String },
CreateScript { workspace: String, path: String, hash: String },
UpdateScript { workspace: String, path: String, hash: String },
DeleteScript { workspace: String, hash: String },
DeleteScriptPath { workspace: String, path: String },
CreateVariable { workspace: String, path: String },
UpdateVariable { workspace: String, old_path: String, new_path: String },
DeleteVariable { workspace: String, path: String },
}
#[derive(Clone)]
pub struct WebhookShared {
pub channel: mpsc::UnboundedSender<WebhookPayload>,
}
impl WebhookShared {
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, db: DB) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<WebhookPayload>();
let _process = tokio::spawn(async move {
let client = reqwest::Client::builder()
// TODO: investigate pool timeouts and such if TCP load is high
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let cache = retainer::Cache::new();
let mut cache_purge_interval = interval(Duration::from_secs(30));
loop {
select! {
biased;
_ = shutdown_rx.recv() => break,
r = rx.recv() => match r {
Some(WebhookPayload::WorkspaceEvent(workspace_id, message)) => {
let url_guard = match cache.get(&workspace_id).await {
Some(guard) => {
guard
},
None => {
let Ok(webook_opt) =
sqlx::query_scalar!(
"SELECT webhook FROM workspace_settings WHERE workspace_id = $1",
workspace_id
)
.fetch_one(
&db,
)
.await else {
tracing::error!("Webhook Message to send - but cannot get workspace settings! Workspace: {workspace_id}");
continue;
};
cache.insert(workspace_id.clone(), webook_opt, Duration::from_secs(30)).await;
cache.get(&workspace_id).await.unwrap()
}
};
let webook_opt = url_guard.value();
if let Some(url) = webook_opt {
let timer = if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
let _ = client.post(url).json(&message).send().await;
timer.map(|x| x.stop_and_record());
drop(url_guard);
}
},
Some(WebhookPayload::InstanceEvent(event)) => {
if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
let r = client.post(INSTANCE_EVENTS_WEBHOOK.as_ref().unwrap()).json(&event).send().await;
if let Err(e) = r {
tracing::error!("Error sending instance event: {}", e);
}
},
None => break,
},
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
tracing::trace!("Purging Webhook Cache");
cache.purge(10, 0.50).await;
},
}
}
});
Self { channel: tx }
}
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
let _ = self.channel.send(WebhookPayload::WorkspaceEvent(
workspace_id.clone(),
message,
));
}
pub fn send_instance_event(&self, event: InstanceEvent) {
if INSTANCE_EVENTS_WEBHOOK.is_none() {
return;
}
let _ = self.channel.send(WebhookPayload::InstanceEvent(event));
}
}

View File

@@ -28,7 +28,7 @@ pub fn global_service() -> Router {
struct WorkerPing { struct WorkerPing {
worker: String, worker: String,
worker_instance: String, worker_instance: String,
last_ping: Option<i32>, ping_at: chrono::DateTime<chrono::Utc>,
started_at: chrono::DateTime<chrono::Utc>, started_at: chrono::DateTime<chrono::Utc>,
ip: String, ip: String,
jobs_executed: i32, jobs_executed: i32,
@@ -45,7 +45,7 @@ async fn list_worker_pings(
let rows = sqlx::query_as!( let rows = sqlx::query_as!(
WorkerPing, WorkerPing,
"SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2", "SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
per_page as i64, per_page as i64,
offset as i64 offset as i64
) )

View File

@@ -6,33 +6,24 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
#[cfg(feature = "enterprise")] use std::{str::FromStr, sync::Arc};
use std::str::FromStr;
#[cfg(feature = "enterprise")]
use crate::BASE_URL;
use crate::{ use crate::{
apps::AppWithLastVersion,
db::{UserDB, DB}, db::{UserDB, DB},
folders::Folder, folders::Folder,
resources::{Resource, ResourceType}, resources::{Resource, ResourceType},
users::{Authed, WorkspaceInvite, VALID_USERNAME}, users::{Authed, WorkspaceInvite},
utils::require_super_admin, utils::require_super_admin,
variables::build_crypt, BaseUrl,
webhook_util::{InstanceEvent, WebhookShared},
}; };
#[cfg(feature = "enterprise")]
use axum::response::Redirect;
use axum::{ use axum::{
body::StreamBody, body::StreamBody,
extract::{Extension, Path, Query}, extract::{Extension, Path, Query},
headers, headers,
response::IntoResponse, response::{IntoResponse, Redirect},
routing::{delete, get, post}, routing::{delete, get, post},
Json, Router, Json, Router,
}; };
use magic_crypt::MagicCryptTrait;
#[cfg(feature = "enterprise")]
use stripe::CustomerId; use stripe::CustomerId;
use windmill_audit::{audit_log, ActionKind}; use windmill_audit::{audit_log, ActionKind};
use windmill_common::{ use windmill_common::{
@@ -51,7 +42,7 @@ use tokio::fs::File;
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
pub fn workspaced_service() -> Router { pub fn workspaced_service() -> Router {
let router = Router::new() Router::new()
.route("/list_pending_invites", get(list_pending_invites)) .route("/list_pending_invites", get(list_pending_invites))
.route("/update", post(edit_workspace)) .route("/update", post(edit_workspace))
.route("/archive", post(archive_workspace)) .route("/archive", post(archive_workspace))
@@ -60,20 +51,11 @@ pub fn workspaced_service() -> Router {
.route("/delete_invite", post(delete_invite)) .route("/delete_invite", post(delete_invite))
.route("/get_settings", get(get_settings)) .route("/get_settings", get(get_settings))
.route("/edit_slack_command", post(edit_slack_command)) .route("/edit_slack_command", post(edit_slack_command))
.route("/edit_webhook", post(edit_webhook))
.route("/edit_auto_invite", post(edit_auto_invite)) .route("/edit_auto_invite", post(edit_auto_invite))
.route("/tarball", get(tarball_workspace)) .route("/tarball", get(tarball_workspace))
.route("/premium_info", get(premium_info)); .route("/premium_info", get(premium_info))
#[cfg(feature = "enterprise")]
tracing::info!("stripe enabled");
#[cfg(feature = "enterprise")]
let router = router
.route("/checkout", get(stripe_checkout)) .route("/checkout", get(stripe_checkout))
.route("/billing_portal", get(stripe_portal)); .route("/billing_portal", get(stripe_portal))
router
} }
pub fn global_service() -> Router { pub fn global_service() -> Router {
Router::new() Router::new()
@@ -108,7 +90,6 @@ pub struct WorkspaceSettings {
pub auto_invite_operator: Option<bool>, pub auto_invite_operator: Option<bool>,
pub customer_id: Option<String>, pub customer_id: Option<String>,
pub plan: Option<String>, pub plan: Option<String>,
pub webhook: Option<String>,
} }
#[derive(FromRow, Serialize, Debug)] #[derive(FromRow, Serialize, Debug)]
@@ -136,11 +117,6 @@ struct EditAutoInvite {
operator: Option<bool>, operator: Option<bool>,
} }
#[derive(Deserialize)]
struct EditWebhook {
webhook: Option<String>,
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct CreateWorkspace { struct CreateWorkspace {
id: String, id: String,
@@ -233,25 +209,24 @@ async fn premium_info(
Ok(Json(row)) Ok(Json(row))
} }
#[cfg(feature = "enterprise")]
#[derive(Deserialize)] #[derive(Deserialize)]
struct PlanQuery { struct PlanQuery {
plan: String, plan: String,
} }
#[cfg(feature = "enterprise")]
async fn stripe_checkout( async fn stripe_checkout(
authed: Authed, authed: Authed,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Query(plan): Query<PlanQuery>, Query(plan): Query<PlanQuery>,
Extension(base_url): Extension<Arc<BaseUrl>>,
) -> Result<Redirect> { ) -> Result<Redirect> {
// #[cfg(feature = "enterprise")] // #[cfg(feature = "enterprise")]
{ {
require_admin(authed.is_admin, &authed.username)?; require_admin(authed.is_admin, &authed.username)?;
let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY")); let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
let success_rd = format!("{}/workspace_settings/checkout?success=true", *BASE_URL); let success_rd = format!("{}/workspace_settings/checkout?success=true", base_url.0);
let failure_rd = format!("{}/workspace_settings/checkout?success=false", *BASE_URL); let failure_rd = format!("{}/workspace_settings/checkout?success=false", base_url.0);
let checkout_session = { let checkout_session = {
let mut params = stripe::CreateCheckoutSession::new(&failure_rd, &success_rd); let mut params = stripe::CreateCheckoutSession::new(&failure_rd, &success_rd);
params.mode = Some(stripe::CheckoutSessionMode::Subscription); params.mode = Some(stripe::CheckoutSessionMode::Subscription);
@@ -305,11 +280,11 @@ async fn stripe_checkout(
} }
} }
#[cfg(feature = "enterprise")]
async fn stripe_portal( async fn stripe_portal(
authed: Authed, authed: Authed,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(base_url): Extension<Arc<BaseUrl>>,
) -> Result<Redirect> { ) -> Result<Redirect> {
require_admin(authed.is_admin, &authed.username)?; require_admin(authed.is_admin, &authed.username)?;
let customer_id = sqlx::query_scalar!( let customer_id = sqlx::query_scalar!(
@@ -320,7 +295,7 @@ async fn stripe_portal(
.await? .await?
.ok_or_else(|| Error::InternalErr(format!("no customer id for workspace {}", w_id)))?; .ok_or_else(|| Error::InternalErr(format!("no customer id for workspace {}", w_id)))?;
let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY")); let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
let success_rd = format!("{}/workspace_settings?tab=premium", *BASE_URL); let success_rd = format!("{}/workspace_settings?tab=premium", base_url.0);
let portal_session = { let portal_session = {
let customer_id = CustomerId::from_str(&customer_id).unwrap(); let customer_id = CustomerId::from_str(&customer_id).unwrap();
let mut params = stripe::CreateBillingPortalSession::new(customer_id); let mut params = stripe::CreateBillingPortalSession::new(customer_id);
@@ -544,48 +519,6 @@ async fn edit_auto_invite(
)) ))
} }
async fn edit_webhook(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, username, .. }: Authed,
Json(ew): Json<EditWebhook>,
) -> Result<String> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
if let Some(webhook) = &ew.webhook {
sqlx::query!(
"UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2",
webhook,
&w_id
)
.execute(&mut tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&authed.username,
"workspaces.edit_webhook",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("webhook", &format!("{:?}", ew.webhook)[..])].into()),
)
.await?;
tx.commit().await?;
Ok(format!("Edit webhook for workspace {}", &w_id))
}
async fn list_workspaces_as_super_admin( async fn list_workspaces_as_super_admin(
authed: Authed, authed: Authed,
Extension(user_db): Extension<UserDB>, Extension(user_db): Extension<UserDB>,
@@ -679,19 +612,19 @@ async fn create_workspace(
.execute(&mut tx) .execute(&mut tx)
.await?; .await?;
// let mc = magic_crypt::new_magic_crypt!(key, 256); let mc = magic_crypt::new_magic_crypt!(key, 256);
// sqlx::query!( sqlx::query!(
// "INSERT INTO variable "INSERT INTO variable
// (workspace_id, path, value, is_secret, description) (workspace_id, path, value, is_secret, description)
// VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'), VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'),
// ($3, 'g/all/not_secret', $4, false, 'This item is not secret')", ($3, 'g/all/not_secret', $4, false, 'This item is not secret')",
// nw.id, nw.id,
// crate::variables::encrypt(&mc, "pretty secret value"), crate::variables::encrypt(&mc, "pretty secret value"),
// nw.id, nw.id,
// "finland does not actually exist", "finland does not actually exist",
// ) )
// .execute(&mut tx) .execute(&mut tx)
// .await?; .await?;
sqlx::query!( sqlx::query!(
"INSERT INTO usr "INSERT INTO usr
@@ -943,14 +876,11 @@ pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> R
async fn invite_user( async fn invite_user(
Authed { username, is_admin, .. }: Authed, Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Json(mut nu): Json<NewWorkspaceInvite>, Json(nu): Json<NewWorkspaceInvite>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
require_admin(is_admin, &username)?; require_admin(is_admin, &username)?;
nu.email = nu.email.to_lowercase();
let mut tx = db.begin().await?; let mut tx = db.begin().await?;
sqlx::query!( sqlx::query!(
@@ -967,11 +897,6 @@ async fn invite_user(
tx.commit().await?; tx.commit().await?;
webhook.send_instance_event(InstanceEvent::UserInvitedWorkspace {
email: nu.email.clone(),
workspace: w_id,
});
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
format!("user with email {} invited", nu.email), format!("user with email {} invited", nu.email),
@@ -981,19 +906,12 @@ async fn invite_user(
async fn add_user( async fn add_user(
Authed { username, is_admin, .. }: Authed, Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Json(mut nu): Json<NewWorkspaceUser>, Json(nu): Json<NewWorkspaceUser>,
) -> Result<(StatusCode, String)> { ) -> Result<(StatusCode, String)> {
require_admin(is_admin, &username)?; require_admin(is_admin, &username)?;
nu.email = nu.email.to_lowercase();
let mut tx = db.begin().await?; let mut tx = db.begin().await?;
if !VALID_USERNAME.is_match(&nu.username) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Usermame can only contain alphanumeric characters and underscores"
)));
}
sqlx::query!( sqlx::query!(
"INSERT INTO usr "INSERT INTO usr
@@ -1008,23 +926,8 @@ async fn add_user(
.execute(&mut tx) .execute(&mut tx)
.await?; .await?;
sqlx::query_as!(
Group,
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
&w_id,
nu.username,
"all",
)
.execute(&mut tx)
.await?;
tx.commit().await?; tx.commit().await?;
webhook.send_instance_event(InstanceEvent::UserAddedWorkspace {
workspace: w_id.clone(),
email: nu.email.clone(),
});
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
format!("user with email {} added", nu.email), format!("user with email {} added", nu.email),
@@ -1087,122 +990,22 @@ struct ScriptMetadata {
schema: Option<Schema>, schema: Option<Schema>,
is_template: bool, is_template: bool,
lock: Vec<String>, lock: Vec<String>,
kind: String,
}
enum ArchiveImpl {
Zip(async_zip::write::ZipFileWriter<File>),
Tar(tokio_tar::Builder<File>),
}
impl ArchiveImpl {
async fn write_to_archive(&mut self, content: &str, path: &str) -> Result<()> {
match self {
ArchiveImpl::Tar(t) => {
let bytes = content.as_bytes();
let mut header = tokio_tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mtime(0);
header.set_uid(0);
header.set_gid(0);
header.set_mode(0o777);
header.set_cksum();
t.append_data(&mut header, path, bytes).await?;
}
ArchiveImpl::Zip(z) => {
let header = async_zip::ZipEntryBuilder::new(
path.to_owned(),
async_zip::Compression::Deflate,
)
.last_modification_date(Default::default())
.unix_permissions(0o777)
.build();
z.write_entry_whole(header, content.as_bytes())
.await
.map_err(to_anyhow)?;
}
}
Ok(())
}
async fn finish(self) -> Result<()> {
match self {
ArchiveImpl::Tar(t) => t.into_inner().await?,
ArchiveImpl::Zip(z) => z.close().await.map_err(to_anyhow)?,
}
.sync_all()
.await?;
Ok(())
}
}
#[derive(Deserialize)]
struct ArchiveQueryParams {
archive_type: Option<String>,
plain_secret: Option<bool>,
}
#[inline]
pub fn to_string_without_metadata<T>(value: &T, preserve_extra_perms: bool) -> Result<String>
where
T: ?Sized + Serialize,
{
let value = serde_json::to_value(value).map_err(to_anyhow)?;
value
.as_object()
.map(|obj| {
let mut obj = obj.clone();
for key in [
"workspace_id",
"path",
"name",
"versions",
"id",
"created_at",
"updated_at",
"created_by",
"updated_by",
"edited_at",
"edited_by",
"archived",
] {
if obj.contains_key(key) {
obj.remove(key);
}
}
if !preserve_extra_perms && obj.contains_key("extra_perms") {
obj.remove("extra_perms");
}
serde_json::to_string_pretty(&obj).ok()
})
.flatten()
.ok_or_else(|| Error::BadRequest("Impossible to serialize value".to_string()))
} }
async fn tarball_workspace( async fn tarball_workspace(
authed: Authed, authed: Authed,
Extension(db): Extension<DB>, Extension(db): Extension<DB>,
Path(w_id): Path<String>, Path(w_id): Path<String>,
Query(ArchiveQueryParams { archive_type, plain_secret }): Query<ArchiveQueryParams>,
) -> Result<([(headers::HeaderName, String); 2], impl IntoResponse)> { ) -> Result<([(headers::HeaderName, String); 2], impl IntoResponse)> {
require_admin(authed.is_admin, &authed.username)?; require_admin(authed.is_admin, &authed.username)?;
let tmp_dir = TempDir::new_in(".")?; let tmp_dir = TempDir::new_in(".")?;
let name = match archive_type.as_deref() { let name = format!("windmill-{w_id}.tar");
Some("tar") | None => Ok(format!("windmill-{w_id}.tar")),
Some("zip") => Ok(format!("windmill-{w_id}.zip")),
Some(t) => Err(Error::BadRequest(format!("Invalid Archive Type {t}"))),
}?;
let file_path = tmp_dir.path().join(&name); let file_path = tmp_dir.path().join(&name);
let file = File::create(&file_path).await?; let file = File::create(&file_path).await?;
let mut archive = match archive_type.as_deref() { let mut a = tokio_tar::Builder::new(file);
Some("tar") | None => Ok(ArchiveImpl::Tar(tokio_tar::Builder::new(file))),
Some("zip") => Ok(ArchiveImpl::Zip(async_zip::write::ZipFileWriter::new(file))),
Some(t) => Err(Error::BadRequest(format!("Invalid Archive Type {t}"))),
}?;
{ {
let folders = sqlx::query_as::<_, Folder>("SELECT * FROM folder WHERE workspace_id = $1") let folders = sqlx::query_as::<_, Folder>("SELECT * FROM folder WHERE workspace_id = $1")
.bind(&w_id) .bind(&w_id)
@@ -1210,12 +1013,12 @@ async fn tarball_workspace(
.await?; .await?;
for folder in folders { for folder in folders {
archive write_to_archive(
.write_to_archive( serde_json::to_string_pretty(&folder).unwrap(),
&to_string_without_metadata(&folder, true).unwrap(), format!("f/{}/folder.meta.json", folder.name),
&format!("f/{}/folder.meta.json", folder.name), &mut a,
) )
.await?; .await?;
} }
} }
@@ -1236,9 +1039,7 @@ async fn tarball_workspace(
ScriptLang::Go => "go", ScriptLang::Go => "go",
ScriptLang::Bash => "sh", ScriptLang::Bash => "sh",
}; };
archive write_to_archive(script.content, format!("{}.{}", script.path, ext), &mut a).await?;
.write_to_archive(&script.content, &format!("{}.{}", script.path, ext))
.await?;
let lock = script let lock = script
.lock .lock
@@ -1251,13 +1052,10 @@ async fn tarball_workspace(
description: script.description, description: script.description,
schema: script.schema, schema: script.schema,
is_template: script.is_template, is_template: script.is_template,
kind: script.kind.to_string(),
lock, lock,
}; };
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap(); let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
archive write_to_archive(metadata_str, format!("{}.script.json", script.path), &mut a).await?;
.write_to_archive(&metadata_str, &format!("{}.script.json", script.path))
.await?;
} }
} }
@@ -1271,10 +1069,13 @@ async fn tarball_workspace(
.await?; .await?;
for resource in resources { for resource in resources {
let resource_str = &to_string_without_metadata(&resource, false).unwrap(); let resource_str = serde_json::to_string_pretty(&resource).unwrap();
archive write_to_archive(
.write_to_archive(&resource_str, &format!("{}.resource.json", resource.path)) resource_str,
.await?; format!("{}.resource.json", resource.path),
&mut a,
)
.await?;
} }
} }
@@ -1288,13 +1089,13 @@ async fn tarball_workspace(
.await?; .await?;
for resource_type in resource_types { for resource_type in resource_types {
let resource_str = &to_string_without_metadata(&resource_type, false).unwrap(); let resource_str = serde_json::to_string_pretty(&resource_type).unwrap();
archive write_to_archive(
.write_to_archive( resource_str,
&resource_str, format!("{}.resource-type.json", resource_type.name),
&format!("{}.resource-type.json", resource_type.name), &mut a,
) )
.await?; .await?;
} }
} }
@@ -1307,57 +1108,25 @@ async fn tarball_workspace(
.await?; .await?;
for flow in flows { for flow in flows {
let flow_str = &to_string_without_metadata(&flow, false).unwrap(); let flow_str = serde_json::to_string_pretty(&flow).unwrap();
archive write_to_archive(flow_str, format!("{}.flow.json", flow.path), &mut a).await?;
.write_to_archive(&flow_str, &format!("{}.flow.json", flow.path))
.await?;
} }
} }
{ {
let variables = sqlx::query_as::<_, ExportableListableVariable>( let variables = sqlx::query_as::<_, ExportableListableVariable>(
"SELECT *, false as is_expired FROM variable WHERE workspace_id = $1", "SELECT *, false as is_expired FROM variable WHERE workspace_id = $1 AND is_secret = false",
) )
.bind(&w_id) .bind(&w_id)
.fetch_all(&db) .fetch_all(&db)
.await?; .await?;
let mc = build_crypt(&mut db.begin().await?, &w_id).await?; for var in variables {
let flow_str = serde_json::to_string_pretty(&var).unwrap();
for mut var in variables { write_to_archive(flow_str, format!("{}.variable.json", var.path), &mut a).await?;
if plain_secret.unwrap_or(false) && var.value.is_some() && var.is_secret {
var.value = Some(
mc.decrypt_base64_to_string(var.value.unwrap())
.map_err(|e| Error::InternalErr(e.to_string()))?,
);
}
let var_str = &to_string_without_metadata(&var, false).unwrap();
archive
.write_to_archive(&var_str, &format!("{}.variable.json", var.path))
.await?;
} }
} }
a.into_inner().await?;
{
let apps = sqlx::query_as!(
AppWithLastVersion,
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by from app, app_version
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
&w_id
)
.fetch_all(&db)
.await?;
for app in apps {
let app_str = &to_string_without_metadata(&app, false).unwrap();
archive
.write_to_archive(&app_str, &format!("{}.app.json", app.path))
.await?;
}
}
archive.finish().await?;
let file = tokio::fs::File::open(file_path).await?; let file = tokio::fs::File::open(file_path).await?;
@@ -1374,3 +1143,20 @@ async fn tarball_workspace(
Ok((headers, body)) Ok((headers, body))
} }
async fn write_to_archive(
content: String,
path: String,
a: &mut tokio_tar::Builder<File>,
) -> Result<()> {
let bytes = content.as_bytes();
let mut header = tokio_tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mtime(0);
header.set_uid(0);
header.set_gid(0);
header.set_mode(0o777);
header.set_cksum();
a.append_data(&mut header, path, bytes).await?;
Ok(())
}

View File

@@ -41,8 +41,8 @@ pub struct AuditLog {
} }
#[tracing::instrument(level = "trace", skip_all)] #[tracing::instrument(level = "trace", skip_all)]
pub async fn audit_log<'c, E: sqlx::Executor<'c, Database = Postgres>>( pub async fn audit_log<'c>(
db: E, db: &mut Transaction<'c, Postgres>,
username: &str, username: &str,
operation: &str, operation: &str,
action_kind: ActionKind, action_kind: ActionKind,

View File

@@ -41,4 +41,3 @@ hyper = { workspace = true, optional = true }
tokio = { workspace = true, optional = true } tokio = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true } reqwest = { workspace = true, optional = true }
tracing-subscriber = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true }
lazy_static.workspace = true

View File

@@ -125,7 +125,7 @@ pub enum FlowStatusModule {
}, },
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone)]
pub enum JobResult { pub enum JobResult {
SingleJob(Uuid), SingleJob(Uuid),
ListJob(Vec<Uuid>), ListJob(Vec<Uuid>),

View File

@@ -6,12 +6,9 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use std::{ use std::{collections::HashMap, time::Duration};
collections::{BTreeMap, HashMap},
time::Duration,
};
use serde::{self, Deserialize, Serialize, Serializer}; use serde::{self, Deserialize, Serialize};
use crate::{ use crate::{
more_serde::{ more_serde::{
@@ -62,7 +59,6 @@ pub struct NewFlow {
#[derive(Deserialize, Serialize, Debug, Clone, Default)] #[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct FlowValue { pub struct FlowValue {
pub modules: Vec<FlowModule>, pub modules: Vec<FlowModule>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)] #[serde(default)]
pub failure_module: Option<FlowModule>, pub failure_module: Option<FlowModule>,
#[serde(default)] #[serde(default)]
@@ -152,10 +148,11 @@ pub struct Suspend {
pub struct FlowModule { pub struct FlowModule {
#[serde(default = "default_id")] #[serde(default = "default_id")]
pub id: String, pub id: String,
#[serde(default)]
#[serde(alias = "input_transform")]
pub input_transforms: HashMap<String, InputTransform>,
pub value: FlowModuleValue, pub value: FlowModuleValue,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_after_if: Option<StopAfterIf>, pub stop_after_if: Option<StopAfterIf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>, pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub suspend: Option<Suspend>, pub suspend: Option<Suspend>,
@@ -245,26 +242,16 @@ pub enum FlowModuleValue {
}, },
RawScript { RawScript {
#[serde(default)] #[serde(default)]
#[serde(alias = "input_transform", serialize_with = "ordered_map")] #[serde(alias = "input_transform")]
input_transforms: HashMap<String, InputTransform>, input_transforms: HashMap<String, InputTransform>,
content: String, content: String,
#[serde(skip_serializing_if = "Option::is_none")]
lock: Option<String>, lock: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>, path: Option<String>,
language: ScriptLang, language: ScriptLang,
}, },
Identity, Identity,
} }
fn ordered_map<S>(value: &HashMap<String, InputTransform>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let ordered: BTreeMap<_, _> = value.iter().collect();
ordered.serialize(serializer)
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ListFlowQuery { pub struct ListFlowQuery {
pub path_start: Option<String>, pub path_start: Option<String>,

View File

@@ -1 +0,0 @@

View File

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use std::{net::SocketAddr, sync::Arc}; use std::net::SocketAddr;
use error::Error; use error::Error;
@@ -26,25 +26,12 @@ pub mod variables;
#[cfg(feature = "tracing_init")] #[cfg(feature = "tracing_init")]
pub mod tracing_init; pub mod tracing_init;
pub const DEFAULT_NUM_WORKERS: usize = 3;
pub const DEFAULT_TIMEOUT: i32 = 300;
pub const DEFAULT_SLEEP_QUEUE: u64 = 50;
pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50; pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 3; pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 3;
lazy_static::lazy_static! {
pub static ref METRICS_ADDR: Option<SocketAddr> = std::env::var("METRICS_ADDR")
.ok()
.map(|s| {
s.parse::<bool>()
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
.or_else(|_| s.parse::<SocketAddr>().map(Some))
})
.transpose().ok()
.flatten()
.flatten();
pub static ref METRICS_ENABLED: bool = METRICS_ADDR.is_some();
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
pub static ref IS_READY: Arc<std::sync::atomic::AtomicBool> = Arc::new(std::sync::atomic::AtomicBool::new(false));
}
#[cfg(feature = "tokio")] #[cfg(feature = "tokio")]
pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::Result<()> { pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::Result<()> {
use std::io; use std::io;
@@ -70,31 +57,14 @@ pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::
pub async fn serve_metrics( pub async fn serve_metrics(
addr: SocketAddr, addr: SocketAddr,
mut rx: tokio::sync::broadcast::Receiver<()>, mut rx: tokio::sync::broadcast::Receiver<()>,
ready_worker_endpoint: bool,
) -> Result<(), hyper::Error> { ) -> Result<(), hyper::Error> {
use std::sync::atomic::Ordering;
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use hyper::StatusCode;
let router = Router::new().route("/metrics", get(metrics));
let router = if ready_worker_endpoint {
router.route(
"/ready",
get(|| async {
if IS_READY.load(Ordering::Relaxed) {
(StatusCode::OK, "ready")
} else {
(StatusCode::INTERNAL_SERVER_ERROR, "not ready")
}
}),
)
} else {
router
};
axum::Server::bind(&addr) axum::Server::bind(&addr)
.serve(router.into_make_service()) .serve(
Router::new()
.route("/metrics", get(metrics))
.into_make_service(),
)
.with_graceful_shutdown(async { .with_graceful_shutdown(async {
rx.recv().await.ok(); rx.recv().await.ok();
println!("Graceful shutdown of metrics"); println!("Graceful shutdown of metrics");
@@ -153,8 +123,10 @@ pub async fn get_latest_hash_for_path<'c>(
script_path: &str, script_path: &str,
) -> error::Result<scripts::ScriptHash> { ) -> error::Result<scripts::ScriptHash> {
let script_hash_o = sqlx::query_scalar!( let script_hash_o = sqlx::query_scalar!(
"select hash from script where path = $1 AND workspace_id = $2 AND "select hash from script where path = $1 AND (workspace_id = $2 OR workspace_id = \
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2) AND 'starter') AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR \
workspace_id = 'starter')) AND
deleted = false", deleted = false",
script_path, script_path,
w_id w_id

View File

@@ -17,7 +17,7 @@ pub struct Schedule {
pub edited_by: String, pub edited_by: String,
pub edited_at: DateTime<chrono::Utc>, pub edited_at: DateTime<chrono::Utc>,
pub schedule: String, pub schedule: String,
pub timezone: String, pub offset_: i32,
pub enabled: bool, pub enabled: bool,
pub script_path: String, pub script_path: String,
pub is_flow: bool, pub is_flow: bool,

View File

@@ -7,7 +7,7 @@
*/ */
use std::{ use std::{
fmt::{self, Display}, fmt::Display,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
}; };
@@ -17,7 +17,7 @@ use serde_json::to_string_pretty;
use crate::utils::StripPath; use crate::utils::StripPath;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Hash, Eq)] #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Hash)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr( #[cfg_attr(
feature = "sqlx", feature = "sqlx",
@@ -103,18 +103,6 @@ pub enum ScriptKind {
Approval, Approval,
} }
impl Display for ScriptKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(match self {
ScriptKind::Trigger => "trigger",
ScriptKind::Failure => "failure",
ScriptKind::Script => "script",
ScriptKind::Approval => "approval",
})?;
Ok(())
}
}
#[derive(Serialize)] #[derive(Serialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct Script { pub struct Script {
@@ -222,7 +210,7 @@ pub fn to_hex_string(i: &i64) -> String {
pub async fn get_hub_script_by_path( pub async fn get_hub_script_by_path(
email: &str, email: &str,
path: StripPath, path: StripPath,
http_client: &reqwest::Client, http_client: reqwest::Client,
) -> crate::error::Result<String> { ) -> crate::error::Result<String> {
use crate::{ use crate::{
error::{to_anyhow, Error}, error::{to_anyhow, Error},
@@ -251,7 +239,7 @@ pub async fn get_hub_script_by_path(
pub async fn get_full_hub_script_by_path( pub async fn get_full_hub_script_by_path(
email: &str, email: &str,
path: StripPath, path: StripPath,
http_client: &reqwest::Client, http_client: reqwest::Client,
) -> crate::error::Result<HubScript> { ) -> crate::error::Result<HubScript> {
use crate::{ use crate::{
error::{to_anyhow, Error}, error::{to_anyhow, Error},

View File

@@ -6,8 +6,6 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
pub const SUPERADMIN_SECRET_EMAIL: &str = "superadmin_secret@windmill.dev";
pub fn username_to_permissioned_as(user: &str) -> String { pub fn username_to_permissioned_as(user: &str) -> String {
if user.contains('@') { if user.contains('@') {
user.to_string() user.to_string()

View File

@@ -6,10 +6,10 @@
* LICENSE-AGPL for a copy of the license. * LICENSE-AGPL for a copy of the license.
*/ */
use crate::error::{Error, Result};
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
pub const MAX_PER_PAGE: usize = 10000; pub const MAX_PER_PAGE: usize = 10000;
pub const DEFAULT_PER_PAGE: usize = 1000; pub const DEFAULT_PER_PAGE: usize = 1000;
@@ -19,8 +19,7 @@ pub struct Pagination {
pub page: Option<usize>, pub page: Option<usize>,
pub per_page: Option<usize>, pub per_page: Option<usize>,
} }
#[derive(Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct StripPath(pub String); pub struct StripPath(pub String);
impl StripPath { impl StripPath {
@@ -52,8 +51,8 @@ pub fn paginate(pagination: Pagination) -> (usize, usize) {
} }
#[cfg(feature = "sqlx")] #[cfg(feature = "sqlx")]
pub async fn now_from_db<'c, E: sqlx::PgExecutor<'c>>( pub async fn now_from_db<'c>(
db: E, db: &mut sqlx::Transaction<'c, sqlx::Postgres>,
) -> Result<chrono::DateTime<chrono::Utc>> { ) -> Result<chrono::DateTime<chrono::Utc>> {
Ok(sqlx::query_scalar!("SELECT now()") Ok(sqlx::query_scalar!("SELECT now()")
.fetch_one(db) .fetch_one(db)
@@ -75,7 +74,7 @@ pub fn not_found_if_none<T, U: AsRef<str>>(opt: Option<T>, kind: &str, name: U)
#[cfg(feature = "reqwest")] #[cfg(feature = "reqwest")]
pub async fn list_elems_from_hub( pub async fn list_elems_from_hub(
http_client: &reqwest::Client, http_client: reqwest::Client,
url: &str, url: &str,
email: &str, email: &str,
) -> Result<serde_json::Value> { ) -> Result<serde_json::Value> {
@@ -89,7 +88,7 @@ pub async fn list_elems_from_hub(
#[cfg(feature = "reqwest")] #[cfg(feature = "reqwest")]
pub async fn http_get_from_hub( pub async fn http_get_from_hub(
http_client: &reqwest::Client, http_client: reqwest::Client,
url: &str, url: &str,
email: &str, email: &str,
plain: bool, plain: bool,
@@ -119,9 +118,3 @@ pub fn rd_string(len: usize) -> String {
.map(char::from) .map(char::from)
.collect() .collect()
} }
pub fn calculate_hash(s: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(s);
format!("{:x}", hasher.finalize())
}

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