Compare commits
15 Commits
dev-docker
...
rf/diff3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020e5f7b32 | ||
|
|
1414030afe | ||
|
|
92bf928b78 | ||
|
|
0bcc036a84 | ||
|
|
6277188c1b | ||
|
|
65c9d43419 | ||
|
|
cc5744ee2d | ||
|
|
1e796881b3 | ||
|
|
a55d99a8b7 | ||
|
|
07477f9e35 | ||
|
|
9409d5e266 | ||
|
|
2d71ebbe09 | ||
|
|
c1673ac036 | ||
|
|
8ca54e02c0 | ||
|
|
53ddf013df |
7
.env
7
.env
@@ -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
|
|
||||||
|
|||||||
2
.github/change-versions.sh
vendored
2
.github/change-versions.sh
vendored
@@ -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
|
||||||
|
|||||||
20
.github/workflows/deploy_to_windmill.yml
vendored
Normal file
20
.github/workflows/deploy_to_windmill.yml
vendored
Normal 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 }}
|
||||||
105
.github/workflows/docker-image.yml
vendored
105
.github/workflows/docker-image.yml
vendored
@@ -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
|
|
||||||
|
|||||||
2
.github/workflows/go_on_release.yml
vendored
2
.github/workflows/go_on_release.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.github/workflows/pypi_on_release.yml
vendored
2
.github/workflows/pypi_on_release.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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: ./
|
||||||
385
CHANGELOG.md
385
CHANGELOG.md
@@ -1,391 +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)
|
## [1.70.1](https://github.com/windmill-labs/windmill/compare/v1.70.0...v1.70.1) (2023-02-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
14
Caddyfile
14
Caddyfile
@@ -1,5 +1,15 @@
|
|||||||
{$BASE_URL} {
|
{
|
||||||
|
auto_https off
|
||||||
|
}
|
||||||
|
|
||||||
|
http://{$BASE_URL} {
|
||||||
|
bind {$ADDRESS}
|
||||||
reverse_proxy /ws/* http://lsp:3001
|
reverse_proxy /ws/* http://lsp:3001
|
||||||
reverse_proxy /* http://windmill_server:8000
|
reverse_proxy /* http://windmill_server:8000
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
https://{$BASE_URL} {
|
||||||
|
bind {$ADDRESS}
|
||||||
|
reverse_proxy /ws/* http://localhost:3001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -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.2-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}
|
||||||
|
|||||||
8
LICENSE
8
LICENSE
@@ -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.
|
|
||||||
|
|||||||
94
README.md
94
README.md
@@ -26,8 +26,7 @@ Open-source developer infrastructure for internal tools. Self-hostable alternati
|
|||||||
|
|
||||||
# Windmill - Turn scripts into workflows and UIs that you can share and run at scale
|
# Windmill - Turn scripts into workflows and UIs that you can share and run at scale
|
||||||
|
|
||||||
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers
|
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers dedicated instance and commercial support and licenses.
|
||||||
dedicated instance and commercial support and licenses.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -70,12 +69,12 @@ 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).
|
||||||

|

|
||||||
|
|
||||||
4. Build complex UI on top of your scripts and flows.
|
4. Build complex UI on top of your scripts and flows.
|
||||||

|

|
||||||
|
|
||||||
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!
|
||||||
@@ -83,49 +82,46 @@ You can build your entire infra on top of Windmill!
|
|||||||
## Show me some actual script code
|
## Show me some actual script code
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import * as wmill from "https://deno.land/x/windmill@v1.62.0/mod.ts";
|
import * as wmill from "https://deno.land/x/windmill@v1.62.0/mod.ts"
|
||||||
//import any dependency from npm
|
//import any dependency from npm
|
||||||
|
|
||||||
import cowsay from "npm:cowsay@1.5.0";
|
import cowsay from 'npm:cowsay@1.5.0'
|
||||||
|
|
||||||
export async function main(
|
export async function main(
|
||||||
a: number,
|
a: number,
|
||||||
// unions generate enums
|
// unions generate enums
|
||||||
b: "my" | "enum",
|
b: "my" | "enum",
|
||||||
// default parameters prefill the field
|
// default parameters prefill the field
|
||||||
d = "default arg",
|
d = "default arg",
|
||||||
// nested objects work c = { nested: "object" },
|
// nested objects work c = { nested: "object" },
|
||||||
// permissioned and typed json
|
// permissioned and typed json
|
||||||
db: wmill.Resource<"postgresql">
|
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
|
const email = Deno.env.get('WM_EMAIL')
|
||||||
return { foo: d, variable };
|
// 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 local files, github repos and to run scripts and flows on the instance from local commands. 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)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
### Running scripts locally
|
### Running scripts locally
|
||||||
|
|
||||||
You can run your script locally easily, you simply need to pass the right
|
You can run your script locally easily, you simply need to pass the right environment variables for the `wmill` client library to fetch resource and variables from your instance if necessary. See more: <https://docs.windmill.dev/docs/advanced/local_development/>
|
||||||
environment variables for the `wmill` client library to fetch resources and
|
|
||||||
variables from your instance if necessary. See more:
|
|
||||||
<https://docs.windmill.dev/docs/advanced/local_development/>
|
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
@@ -186,19 +182,15 @@ compiling from source or using without a postgres super user, see
|
|||||||
|
|
||||||
### 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 http://localhost et voilà :)
|
Go to http://localhost et voilà :)
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -207,9 +199,9 @@ We publish helm charts at:
|
|||||||
|
|
||||||
### Postgres without superuser
|
### Postgres without superuser
|
||||||
|
|
||||||
If you do not want, or cannot (for instance, in AWS Aurora or Cloud sql) use a
|
If you do not want, or cannot (for instance, in AWS Aurora or Cloud sql) use a postgres superuser,
|
||||||
postgres superuser, you can run `./init-db-as-superuser.sql` to init the
|
you can run `./init-db-as-superuser.sql` to init the required users for windmill.
|
||||||
required users for windmill.
|
|
||||||
|
|
||||||
### Commercial license
|
### Commercial license
|
||||||
|
|
||||||
@@ -283,8 +275,8 @@ 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). A setup script will prompt
|
||||||
it being synced automatically everyday.
|
you to have it being synced automatically everyday.
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
@@ -292,17 +284,14 @@ it being synced automatically everyday.
|
|||||||
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||||
| 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 | Worker |
|
||||||
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server |
|
| PORT | 8000 | Exposed port | Server | |
|
||||||
| 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 |
|
| 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 |
|
| 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 |
|
||||||
@@ -311,12 +300,10 @@ it being synced automatically everyday.
|
|||||||
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | Worker |
|
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | 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 |
|
| 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 | |
|
||||||
| 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 +315,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 |
|
| WHITELIST_WORKSPACES | None | Whitelist of workspaces this worker takes job from | Worker |
|
||||||
| BLACKLIST_WORKSPACES | None | Blacklist 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 |
|
| NEW_USER_WEBHOOK | None | Webhook to notify of a new user added, signup/invite. Can hook back to windmill to send emails | Server |
|
||||||
|
|
||||||
## Run a local dev setup
|
## Run a local dev setup
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
|
||||||
]
|
|
||||||
1416
backend/Cargo.lock
generated
1416
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "windmill"
|
name = "windmill"
|
||||||
version = "1.87.0"
|
version = "1.70.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.70.1"
|
||||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -28,7 +28,11 @@ name = "windmill"
|
|||||||
path = "./src/main.rs"
|
path = "./src/main.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise"]
|
enterprise = [
|
||||||
|
"windmill-worker/enterprise",
|
||||||
|
"windmill-queue/enterprise",
|
||||||
|
"windmill-api/enterprise",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
@@ -48,16 +52,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 +76,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,7 +84,6 @@ 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 }
|
||||||
@@ -123,9 +122,9 @@ 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.21.0"
|
||||||
unicode-general-category = "^0"
|
unicode-general-category = "^0"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
@@ -143,7 +142,6 @@ 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"
|
||||||
@@ -155,8 +153,3 @@ async-stripe = { version = "0.14", features = [
|
|||||||
"checkout",
|
"checkout",
|
||||||
] }
|
] }
|
||||||
async_zip = { version = "0.0.11", features = ["full"] }
|
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]
|
|
||||||
|
|||||||
@@ -1165,3 +1165,18 @@ VALUES
|
|||||||
}') RETURNING id)
|
}') RETURNING id)
|
||||||
UPDATE app SET versions = ARRAY((select id from _insert)), policy = '{ "execution_mode": "viewer", "triggerables": {} }'
|
UPDATE app SET versions = ARRAY((select id from _insert)), policy = '{ "execution_mode": "viewer", "triggerables": {} }'
|
||||||
WHERE workspace_id = 'admins' AND path = 'g/all/setup_app';
|
WHERE workspace_id = 'admins' AND path = 'g/all/setup_app';
|
||||||
|
|
||||||
|
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.69.3/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';
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- Add down migration script here
|
|
||||||
@@ -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';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- Add down migration script here
|
|
||||||
@@ -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;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- Add down migration script here
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
-- Add down migration script here
|
|
||||||
@@ -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));
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE schedule DROP COLUMN timezone;
|
|
||||||
ALTER TABLE schedule ADD COLUMN offset_ INTEGER NOT NULL DEFAULT 0;
|
|
||||||
@@ -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_;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- Add down migration script here
|
|
||||||
@@ -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);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- Add down migration script here
|
|
||||||
DROP TABLE pip_resolution_cache;
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS input;
|
|
||||||
DROP TYPE RUNNABLE_TYPE;
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
347
backend/parsers/windmill-parser-go/src/parser_go_ast.rs
Normal file
347
backend/parsers/windmill-parser-go/src/parser_go_ast.rs
Normal 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 {}
|
||||||
948
backend/parsers/windmill-parser-go/src/parser_go_scanner.rs
Normal file
948
backend/parsers/windmill-parser-go/src/parser_go_scanner.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
273
backend/parsers/windmill-parser-go/src/parser_go_token.rs
Normal file
273
backend/parsers/windmill-parser-go/src/parser_go_token.rs
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -181,7 +181,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 +217,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 +240,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 +442,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 +449,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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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()))?;
|
||||||
|
|||||||
@@ -6,16 +6,15 @@
|
|||||||
* 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;
|
||||||
|
|
||||||
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_NUM_WORKERS: usize = 3;
|
||||||
const DEFAULT_PORT: u16 = 8000;
|
const DEFAULT_PORT: u16 = 8000;
|
||||||
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
|
|
||||||
|
|
||||||
mod ee;
|
mod ee;
|
||||||
|
|
||||||
@@ -30,12 +29,15 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.and_then(|x| x.parse::<i32>().ok())
|
.and_then(|x| x.parse::<i32>().ok())
|
||||||
.unwrap_or(DEFAULT_NUM_WORKERS as i32);
|
.unwrap_or(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))))
|
||||||
|
.or_else(|_| s.parse::<SocketAddr>().map(Some))
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.flatten();
|
||||||
|
|
||||||
let port: u16 = std::env::var("PORT")
|
let port: u16 = std::env::var("PORT")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -49,40 +51,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.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,113 +60,100 @@ 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")]
|
|
||||||
tracing::info!(
|
|
||||||
"
|
|
||||||
##############################
|
|
||||||
Windmill Enterprise Edition {GIT_VERSION}
|
|
||||||
##############################"
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(not(feature = "enterprise"))]
|
|
||||||
tracing::info!(
|
|
||||||
"
|
|
||||||
##############################
|
|
||||||
Windmill Community Edition {GIT_VERSION}
|
|
||||||
##############################"
|
|
||||||
);
|
|
||||||
|
|
||||||
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], port));
|
||||||
|
|
||||||
let rsmq2 = rsmq.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, rx.resubscribe()).await?;
|
||||||
}
|
}
|
||||||
Ok(()) as anyhow::Result<()>
|
Ok(()) as anyhow::Result<()>
|
||||||
};
|
};
|
||||||
|
|
||||||
let workers_f = async {
|
let workers_f = async {
|
||||||
if num_workers > 0 {
|
if num_workers > 0 {
|
||||||
|
#[cfg(feature = "enterprise")]
|
||||||
|
tracing::info!(
|
||||||
|
"
|
||||||
|
##############################
|
||||||
|
Windmill Enterprise Edition {GIT_VERSION}
|
||||||
|
##############################"
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "enterprise"))]
|
||||||
|
tracing::info!(
|
||||||
|
"
|
||||||
|
##############################
|
||||||
|
Windmill Community Edition {GIT_VERSION}
|
||||||
|
##############################"
|
||||||
|
);
|
||||||
|
|
||||||
|
display_config(vec![
|
||||||
|
"DISABLE_NSJAIL",
|
||||||
|
"DISABLE_SERVER",
|
||||||
|
"NUM_WORKERS",
|
||||||
|
"METRICS_ADDR",
|
||||||
|
"JSON_FMT",
|
||||||
|
"BASE_URL",
|
||||||
|
"BASE_INTERNAL_URL",
|
||||||
|
"TIMEOUT",
|
||||||
|
"SLEEP_QUEUE",
|
||||||
|
"MAX_LOG_SIZE",
|
||||||
|
"PORT",
|
||||||
|
"KEEP_JOB_DIR",
|
||||||
|
"S3_CACHE_BUCKET",
|
||||||
|
"TAR_CACHE_RATE",
|
||||||
|
"COOKIE_DOMAIN",
|
||||||
|
"PYTHON_PATH",
|
||||||
|
"DENO_PATH",
|
||||||
|
"GO_PATH",
|
||||||
|
"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",
|
||||||
|
"PIP_LOCAL_DEPENDENCIES",
|
||||||
|
"ADDITIONAL_PYTHON_PATHS",
|
||||||
|
"INCLUDE_HEADERS",
|
||||||
|
"WHITELIST_WORKSPACES",
|
||||||
|
"BLACKLIST_WORKSPACES",
|
||||||
|
"NEW_USER_WEBHOOK",
|
||||||
|
"CLOUD_HOSTED",
|
||||||
|
]);
|
||||||
|
|
||||||
run_workers(
|
run_workers(
|
||||||
db.clone(),
|
db.clone(),
|
||||||
rx.resubscribe(),
|
rx.resubscribe(),
|
||||||
num_workers,
|
num_workers,
|
||||||
base_internal_url.clone(),
|
base_internal_url.clone(),
|
||||||
rsmq.clone(),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(()) as anyhow::Result<()>
|
Ok(()) as anyhow::Result<()>
|
||||||
};
|
};
|
||||||
|
|
||||||
let rsmq2 = rsmq.clone();
|
|
||||||
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, rx.resubscribe(), &base_internal_url);
|
||||||
}
|
}
|
||||||
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(())
|
||||||
}
|
}
|
||||||
@@ -218,11 +175,10 @@ fn display_config(envs: Vec<&str>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
|
pub fn monitor_db(
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
rx: tokio::sync::broadcast::Receiver<()>,
|
rx: tokio::sync::broadcast::Receiver<()>,
|
||||||
base_internal_url: &str,
|
base_internal_url: &str,
|
||||||
rsmq: Option<R>,
|
|
||||||
) {
|
) {
|
||||||
let db1 = db.clone();
|
let db1 = db.clone();
|
||||||
let db2 = db.clone();
|
let db2 = db.clone();
|
||||||
@@ -230,17 +186,16 @@ pub fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>
|
|||||||
let rx2 = rx.resubscribe();
|
let rx2 = rx.resubscribe();
|
||||||
let base_internal_url = base_internal_url.to_string();
|
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, rx, &base_internal_url).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<()>,
|
rx: tokio::sync::broadcast::Receiver<()>,
|
||||||
num_workers: i32,
|
num_workers: i32,
|
||||||
base_internal_url: String,
|
base_internal_url: String,
|
||||||
rsmq: Option<R>,
|
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let license_key = std::env::var("LICENSE_KEY").ok();
|
let license_key = std::env::var("LICENSE_KEY").ok();
|
||||||
#[cfg(feature = "enterprise")]
|
#[cfg(feature = "enterprise")]
|
||||||
@@ -270,7 +225,6 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
|
|||||||
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 base_internal_url = base_internal_url.clone();
|
||||||
let rsmq2 = rsmq.clone();
|
|
||||||
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!(worker = %worker_name, "starting worker");
|
||||||
windmill_worker::run_worker(
|
windmill_worker::run_worker(
|
||||||
@@ -281,7 +235,6 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
|
|||||||
&ip,
|
&ip,
|
||||||
rx,
|
rx,
|
||||||
&base_internal_url,
|
&base_internal_url,
|
||||||
rsmq2,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
})));
|
})));
|
||||||
|
|||||||
@@ -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};
|
||||||
@@ -88,7 +87,7 @@ 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, rx));
|
||||||
|
|
||||||
return Self { addr, tx, task };
|
return Self { addr, tx, task };
|
||||||
}
|
}
|
||||||
@@ -155,12 +154,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 +193,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 +207,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 +252,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 +355,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 +477,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 +490,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 +632,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 +645,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 +659,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 +681,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 +705,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 +762,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 +824,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 +836,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 +843,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
|
||||||
}
|
}
|
||||||
@@ -916,7 +914,7 @@ fn spawn_test_worker(
|
|||||||
let ip: &str = Default::default();
|
let ip: &str = Default::default();
|
||||||
let future = async move {
|
let future = async move {
|
||||||
let base_internal_url = format!("http://localhost:{}", port);
|
let base_internal_url = format!("http://localhost:{}", port);
|
||||||
windmill_worker::run_worker::<rsmq_async::MultiplexedRsmq>(
|
windmill_worker::run_worker(
|
||||||
&db,
|
&db,
|
||||||
worker_instance,
|
worker_instance,
|
||||||
worker_name,
|
worker_name,
|
||||||
@@ -924,7 +922,6 @@ fn spawn_test_worker(
|
|||||||
ip,
|
ip,
|
||||||
rx,
|
rx,
|
||||||
&base_internal_url,
|
&base_internal_url,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
};
|
};
|
||||||
@@ -1003,6 +1000,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 +1028,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 +1036,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 +1130,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 +1146,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 +1195,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 +1204,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 +1239,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 +1436,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 +1452,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 +1494,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 +1526,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 +1677,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 +1693,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 +1770,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 +1812,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 +1828,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 +1856,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 +1876,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 +2032,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 +2067,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 +2101,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 +2235,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 +2246,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 +2256,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 +2266,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 } }",
|
||||||
|
|||||||
@@ -47,7 +47,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
|
||||||
@@ -70,5 +69,3 @@ async-stripe = { workspace = true, optional = true }
|
|||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
prometheus.workspace = true
|
prometheus.workspace = true
|
||||||
async_zip.workspace = true
|
async_zip.workspace = true
|
||||||
rsmq_async.workspace = true
|
|
||||||
regex.workspace = true
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
openapi: "3.0.3"
|
openapi: "3.0.3"
|
||||||
|
|
||||||
info:
|
info:
|
||||||
version: 1.87.0
|
version: 1.70.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
|
||||||
@@ -1048,27 +1048,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
|
||||||
@@ -1279,10 +1258,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 +1549,7 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
value: {}
|
value: {}
|
||||||
@@ -2125,6 +2103,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 +2278,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 +2293,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 +2327,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 +2452,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 +2482,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 +2491,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 +2538,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
|
||||||
@@ -2782,27 +2733,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
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 +2823,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 +3156,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 +3186,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 +3223,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 +3268,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
|
||||||
@@ -3395,22 +3337,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 +3354,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 +3382,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 +3410,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 +3436,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
approvalPage:
|
approvalPage:
|
||||||
type: string
|
type: string
|
||||||
resume:
|
resume:
|
||||||
type: string
|
type: string
|
||||||
cancel:
|
cancel:
|
||||||
type: string
|
type: string
|
||||||
@@ -3556,7 +3454,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 +3667,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 +4081,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 +4115,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
owners:
|
owners:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
extra_perms:
|
extra_perms:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@@ -4390,8 +4286,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 +4311,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 +4347,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 +4367,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 +4456,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 +4465,6 @@ components:
|
|||||||
type: apiKey
|
type: apiKey
|
||||||
in: cookie
|
in: cookie
|
||||||
name: token
|
name: token
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
WorkspaceId:
|
WorkspaceId:
|
||||||
name: workspace
|
name: workspace
|
||||||
@@ -4691,12 +4472,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 +4547,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 +4567,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 +4586,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 +4618,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 +4659,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 +4669,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 +4742,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:
|
||||||
@@ -5363,19 +5044,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 +5169,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 +5278,6 @@ components:
|
|||||||
- star_args
|
- star_args
|
||||||
- start_kwargs
|
- start_kwargs
|
||||||
- args
|
- args
|
||||||
- type
|
|
||||||
- error
|
|
||||||
|
|
||||||
Preview:
|
Preview:
|
||||||
type: object
|
type: object
|
||||||
@@ -5751,8 +5412,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 +5436,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 +5449,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 +5462,6 @@ components:
|
|||||||
required:
|
required:
|
||||||
- path
|
- path
|
||||||
- schedule
|
- schedule
|
||||||
- timezone
|
|
||||||
- script_path
|
- script_path
|
||||||
- is_flow
|
- is_flow
|
||||||
- args
|
- args
|
||||||
@@ -5811,13 +5471,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 +5522,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 +5740,6 @@ components:
|
|||||||
- extra_perms
|
- extra_perms
|
||||||
- edited_at
|
- edited_at
|
||||||
- execution_mode
|
- execution_mode
|
||||||
|
|
||||||
AppWithLastVersion:
|
AppWithLastVersion:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -37,7 +37,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()
|
||||||
@@ -313,17 +313,10 @@ async fn create_app(
|
|||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
Extension(webhook): Extension<WebhookShared>,
|
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)
|
||||||
@@ -470,9 +463,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!(
|
||||||
@@ -565,7 +556,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 +574,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 +643,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 +670,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 +727,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 +747,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(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -6,20 +6,14 @@
|
|||||||
* 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 sql_builder::prelude::*;
|
||||||
schedule::clear_schedule,
|
|
||||||
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::{delete, 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 +25,15 @@ 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},
|
||||||
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
|
HTTP_CLIENT,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn workspaced_service() -> Router {
|
pub fn workspaced_service() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -176,20 +178,16 @@ 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>,
|
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 +219,13 @@ async fn create_flow(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
webhook.send_message(
|
webhook.send_message(
|
||||||
w_id.clone(),
|
w_id.clone(),
|
||||||
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.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 +237,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 +282,15 @@ 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>,
|
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 +317,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,32 +346,32 @@ 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?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
webhook.send_message(
|
webhook.send_message(
|
||||||
w_id.clone(),
|
w_id.clone(),
|
||||||
WebhookMessage::UpdateFlow {
|
WebhookMessage::UpdateFlow {
|
||||||
@@ -386,6 +381,7 @@ async fn update_flow(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 +393,6 @@ async fn update_flow(
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
@@ -555,6 +550,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 +568,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 +587,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 +606,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 +627,7 @@ mod tests {
|
|||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
"id": "a",
|
"id": "a",
|
||||||
|
"input_transforms": {},
|
||||||
"value": {
|
"value": {
|
||||||
"input_transforms": {
|
"input_transforms": {
|
||||||
"test": {
|
"test": {
|
||||||
@@ -638,22 +638,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 +678,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 +693,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());
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
* 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},
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -18,11 +16,10 @@ use axum::{
|
|||||||
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 +137,16 @@ 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(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()]);
|
||||||
|
|
||||||
@@ -277,26 +262,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("*");
|
||||||
|
|||||||
@@ -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()))
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,6 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{
|
|
||||||
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 +30,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,
|
||||||
|
BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn workspaced_service() -> Router {
|
pub fn workspaced_service() -> Router {
|
||||||
@@ -48,10 +47,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 +60,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 +98,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,49 +146,6 @@ 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,
|
||||||
@@ -232,6 +177,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,
|
||||||
@@ -317,7 +267,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 +295,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 +358,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 +372,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 +379,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 +434,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 +451,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",
|
||||||
@@ -817,7 +730,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 +752,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() {
|
||||||
@@ -1158,8 +1068,6 @@ impl From<UnifiedJob> for Job {
|
|||||||
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,
|
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 +1130,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 +1163,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 +1176,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 +1198,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,
|
||||||
@@ -1418,65 +1322,9 @@ lazy_static::lazy_static! {
|
|||||||
.and_then(|x| x.parse().ok())
|
.and_then(|x| x.parse().ok())
|
||||||
.unwrap_or(20);
|
.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>,
|
||||||
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>,
|
||||||
@@ -1485,8 +1333,9 @@ pub async fn run_wait_result_job_by_path(
|
|||||||
) -> 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_WAIT_RESULT.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 +1347,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,
|
||||||
@@ -1523,7 +1371,6 @@ 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>,
|
||||||
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>,
|
||||||
@@ -1533,9 +1380,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 +1393,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,
|
||||||
@@ -1571,7 +1417,6 @@ 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>,
|
||||||
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>,
|
||||||
@@ -1581,8 +1426,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,13 +1441,13 @@ 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(
|
||||||
@@ -1633,14 +1478,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 +1503,6 @@ async fn run_preview_job(
|
|||||||
scheduled_for,
|
scheduled_for,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
@@ -1667,21 +1510,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 +1536,6 @@ async fn run_preview_flow_job(
|
|||||||
scheduled_for,
|
scheduled_for,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
@@ -1703,23 +1543,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 +1571,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 +1578,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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1837,11 +1673,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 +1692,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 +1700,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 +1709,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(
|
||||||
@@ -1951,7 +1775,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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::oauth2::AllClients;
|
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 reqwest::Client;
|
||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_cookies::CookieManagerLayer;
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
use windmill_common::utils::rd_string;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::UserDB,
|
db::UserDB,
|
||||||
oauth2::{build_oauth_clients, SlackVerifier},
|
oauth2::{build_oauth_clients, SlackVerifier},
|
||||||
@@ -14,20 +25,6 @@ use crate::{
|
|||||||
users::{Authed, OptAuthed},
|
users::{Authed, OptAuthed},
|
||||||
webhook_util::WebhookShared,
|
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 +35,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;
|
||||||
@@ -81,7 +77,6 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
pub async fn run_server(
|
pub async fn run_server(
|
||||||
db: DB,
|
db: DB,
|
||||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
@@ -101,16 +96,10 @@ 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(CookieManagerLayer::new())
|
||||||
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
|
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
|
||||||
.allow_methods([Method::GET, Method::POST])
|
|
||||||
.allow_origin(Any);
|
|
||||||
|
|
||||||
// build our application with a route
|
// build our application with a route
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest(
|
.nest(
|
||||||
@@ -119,27 +108,25 @@ 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())
|
||||||
.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,25 +136,16 @@ 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)),
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ use windmill_audit::{audit_log, ActionKind};
|
|||||||
use windmill_common::users::username_to_permissioned_as;
|
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, NEW_USER_WEBHOOK};
|
||||||
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},
|
||||||
@@ -44,7 +43,7 @@ use crate::{BASE_URL, HTTP_CLIENT, IS_SECURE, OAUTH_CLIENTS, SLACK_SIGNING_SECRE
|
|||||||
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::{fs, str};
|
||||||
|
|
||||||
@@ -729,20 +728,23 @@ where
|
|||||||
async fn slack_command(
|
async fn slack_command(
|
||||||
SlackSig { sig, ts }: SlackSig,
|
SlackSig { sig, ts }: SlackSig,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
|
||||||
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_SIGNING_SECRET
|
||||||
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 +760,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 +785,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.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?)"
|
||||||
@@ -818,7 +818,6 @@ async fn login_callback(
|
|||||||
Path(client_name): Path<String>,
|
Path(client_name): Path<String>,
|
||||||
cookies: Cookies,
|
cookies: Cookies,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Extension(webhook): Extension<WebhookShared>,
|
|
||||||
Json(callback): Json<OAuthCallback>,
|
Json(callback): Json<OAuthCallback>,
|
||||||
) -> error::Result<String> {
|
) -> error::Result<String> {
|
||||||
let client_w_config = &OAUTH_CLIENTS
|
let client_w_config = &OAUTH_CLIENTS
|
||||||
@@ -853,8 +852,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)) {
|
||||||
@@ -944,7 +942,14 @@ async fn login_callback(
|
|||||||
}
|
}
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
webhook.send_instance_event(InstanceEvent::UserSignupOAuth { email: email.clone() });
|
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
||||||
|
let _ = HTTP_CLIENT
|
||||||
|
.post(&new_user_webhook)
|
||||||
|
.json(&serde_json::json!({"email" : &email, "event": "oauth_signup"}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
Ok("Successfully logged in".to_string())
|
Ok("Successfully logged in".to_string())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,7 +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},
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -264,12 +264,9 @@ async fn create_resource(
|
|||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
Extension(webhook): Extension<WebhookShared>,
|
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?;
|
||||||
@@ -378,7 +375,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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
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},
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
HTTP_CLIENT,
|
HTTP_CLIENT,
|
||||||
};
|
};
|
||||||
@@ -21,15 +24,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 +42,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 +60,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 +70,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>,
|
||||||
@@ -133,7 +123,6 @@ async fn list_scripts(
|
|||||||
AND workspace_id = ?)"
|
AND workspace_id = ?)"
|
||||||
.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);
|
||||||
}
|
}
|
||||||
@@ -193,22 +182,20 @@ 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(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 +256,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 +351,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 +359,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,
|
||||||
@@ -386,7 +402,7 @@ async fn create_script(
|
|||||||
webhook.send_message(
|
webhook.send_message(
|
||||||
w_id.clone(),
|
w_id.clone(),
|
||||||
WebhookMessage::UpdateScript {
|
WebhookMessage::UpdateScript {
|
||||||
workspace: w_id.clone(),
|
workspace: w_id,
|
||||||
path: ns.path.clone(),
|
path: ns.path.clone(),
|
||||||
hash: hash.to_string(),
|
hash: hash.to_string(),
|
||||||
},
|
},
|
||||||
@@ -411,41 +427,13 @@ async fn create_script(
|
|||||||
webhook.send_message(
|
webhook.send_message(
|
||||||
w_id.clone(),
|
w_id.clone(),
|
||||||
WebhookMessage::CreateScript {
|
WebhookMessage::CreateScript {
|
||||||
workspace: w_id.clone(),
|
workspace: w_id,
|
||||||
path: ns.path.clone(),
|
path: ns.path.clone(),
|
||||||
hash: hash.to_string(),
|
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?;
|
||||||
|
|
||||||
Ok((StatusCode::CREATED, format!("{}", hash)))
|
Ok((StatusCode::CREATED, format!("{}", hash)))
|
||||||
@@ -505,18 +493,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>,
|
||||||
@@ -577,10 +553,11 @@ async fn get_script_by_hash_internal<'c>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +565,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,10 +585,11 @@ 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",
|
||||||
@@ -739,71 +718,25 @@ async fn delete_script_by_hash(
|
|||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
COOKIE_DOMAIN, HTTP_CLIENT, IS_SECURE,
|
||||||
};
|
};
|
||||||
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)]
|
||||||
@@ -1032,22 +997,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 +1041,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 +1077,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,
|
||||||
@@ -1329,20 +1275,12 @@ lazy_static::lazy_static! {
|
|||||||
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, \
|
||||||
@@ -1370,9 +1308,17 @@ async fn create_user(
|
|||||||
.await?;
|
.await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
|
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
||||||
|
let _ = HTTP_CLIENT
|
||||||
|
.post(&new_user_webhook)
|
||||||
|
.json(&serde_json::json!({"email" : &nu.email, "name": &nu.name, "event": "new_user"}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1618,7 +1564,7 @@ async fn login(
|
|||||||
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, 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, first_time_user FROM password WHERE email = $1 AND login_type = \
|
||||||
'password'",
|
'password'",
|
||||||
@@ -1740,58 +1686,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 +1786,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), '*****')",
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
oauth2::_refresh_token,
|
oauth2::_refresh_token,
|
||||||
users::{maybe_refresh_folders, require_owner_of_path, Authed},
|
users::{require_owner_of_path, Authed},
|
||||||
webhook_util::{WebhookMessage, WebhookShared},
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
@@ -26,7 +26,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},
|
||||||
@@ -206,15 +205,12 @@ 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>,
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
|
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?;
|
||||||
@@ -333,7 +329,6 @@ async fn update_variable(
|
|||||||
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?;
|
||||||
|
|
||||||
@@ -383,27 +378,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
|
||||||
)
|
)
|
||||||
@@ -442,23 +419,6 @@ async fn update_variable(
|
|||||||
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,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::{select, sync::mpsc, time::interval};
|
use tokio::{select, sync::mpsc, time::interval};
|
||||||
use windmill_common::METRICS_ENABLED;
|
|
||||||
|
|
||||||
use crate::db::DB;
|
use crate::db::DB;
|
||||||
|
|
||||||
@@ -13,26 +12,6 @@ lazy_static::lazy_static! {
|
|||||||
"Histogram of webhook requests made"
|
"Histogram of webhook requests made"
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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)]
|
#[derive(Serialize)]
|
||||||
@@ -58,7 +37,6 @@ pub enum WebhookMessage {
|
|||||||
CreateScript { workspace: String, path: String, hash: String },
|
CreateScript { workspace: String, path: String, hash: String },
|
||||||
UpdateScript { workspace: String, path: String, hash: String },
|
UpdateScript { workspace: String, path: String, hash: String },
|
||||||
DeleteScript { workspace: String, hash: String },
|
DeleteScript { workspace: String, hash: String },
|
||||||
DeleteScriptPath { workspace: String, path: String },
|
|
||||||
CreateVariable { workspace: String, path: String },
|
CreateVariable { workspace: String, path: String },
|
||||||
UpdateVariable { workspace: String, old_path: String, new_path: String },
|
UpdateVariable { workspace: String, old_path: String, new_path: String },
|
||||||
DeleteVariable { workspace: String, path: String },
|
DeleteVariable { workspace: String, path: String },
|
||||||
@@ -66,12 +44,12 @@ pub enum WebhookMessage {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WebhookShared {
|
pub struct WebhookShared {
|
||||||
pub channel: mpsc::UnboundedSender<WebhookPayload>,
|
pub channel: mpsc::UnboundedSender<(String, WebhookMessage)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebhookShared {
|
impl WebhookShared {
|
||||||
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, db: DB) -> Self {
|
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, db: DB) -> Self {
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel::<WebhookPayload>();
|
let (tx, mut rx) = mpsc::unbounded_channel::<(String, WebhookMessage)>();
|
||||||
let _process = tokio::spawn(async move {
|
let _process = tokio::spawn(async move {
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
// TODO: investigate pool timeouts and such if TCP load is high
|
// TODO: investigate pool timeouts and such if TCP load is high
|
||||||
@@ -86,7 +64,7 @@ impl WebhookShared {
|
|||||||
biased;
|
biased;
|
||||||
_ = shutdown_rx.recv() => break,
|
_ = shutdown_rx.recv() => break,
|
||||||
r = rx.recv() => match r {
|
r = rx.recv() => match r {
|
||||||
Some(WebhookPayload::WorkspaceEvent(workspace_id, message)) => {
|
Some((workspace_id, message)) => {
|
||||||
let url_guard = match cache.get(&workspace_id).await {
|
let url_guard = match cache.get(&workspace_id).await {
|
||||||
Some(guard) => {
|
Some(guard) => {
|
||||||
guard
|
guard
|
||||||
@@ -110,19 +88,12 @@ impl WebhookShared {
|
|||||||
};
|
};
|
||||||
let webook_opt = url_guard.value();
|
let webook_opt = url_guard.value();
|
||||||
if let Some(url) = webook_opt {
|
if let Some(url) = webook_opt {
|
||||||
let timer = if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
|
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
|
||||||
let _ = client.post(url).json(&message).send().await;
|
let _ = client.post(url).json(&message).send().await;
|
||||||
timer.map(|x| x.stop_and_record());
|
timer.stop_and_record();
|
||||||
drop(url_guard);
|
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,
|
None => break,
|
||||||
},
|
},
|
||||||
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
|
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
|
||||||
@@ -137,16 +108,6 @@ impl WebhookShared {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
|
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
|
||||||
let _ = self.channel.send(WebhookPayload::WorkspaceEvent(
|
let _ = self.channel.send((workspace_id.clone(), message));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ use crate::{
|
|||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
folders::Folder,
|
folders::Folder,
|
||||||
resources::{Resource, ResourceType},
|
resources::{Resource, ResourceType},
|
||||||
users::{Authed, WorkspaceInvite, VALID_USERNAME},
|
users::{Authed, WorkspaceInvite, NEW_USER_WEBHOOK},
|
||||||
utils::require_super_admin,
|
utils::require_super_admin,
|
||||||
variables::build_crypt,
|
HTTP_CLIENT,
|
||||||
webhook_util::{InstanceEvent, WebhookShared},
|
|
||||||
};
|
};
|
||||||
#[cfg(feature = "enterprise")]
|
#[cfg(feature = "enterprise")]
|
||||||
use axum::response::Redirect;
|
use axum::response::Redirect;
|
||||||
@@ -31,7 +30,6 @@ use axum::{
|
|||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use magic_crypt::MagicCryptTrait;
|
|
||||||
#[cfg(feature = "enterprise")]
|
#[cfg(feature = "enterprise")]
|
||||||
use stripe::CustomerId;
|
use stripe::CustomerId;
|
||||||
use windmill_audit::{audit_log, ActionKind};
|
use windmill_audit::{audit_log, ActionKind};
|
||||||
@@ -943,14 +941,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,10 +962,14 @@ async fn invite_user(
|
|||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
webhook.send_instance_event(InstanceEvent::UserInvitedWorkspace {
|
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
||||||
email: nu.email.clone(),
|
let _ = &HTTP_CLIENT
|
||||||
workspace: w_id,
|
.post(&new_user_webhook)
|
||||||
});
|
.json(&serde_json::json!({"email" : &nu.email, "event": "new_invite"}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
@@ -981,19 +980,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 +1000,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,7 +1064,6 @@ struct ScriptMetadata {
|
|||||||
schema: Option<Schema>,
|
schema: Option<Schema>,
|
||||||
is_template: bool,
|
is_template: bool,
|
||||||
lock: Vec<String>,
|
lock: Vec<String>,
|
||||||
kind: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ArchiveImpl {
|
enum ArchiveImpl {
|
||||||
@@ -1139,7 +1115,6 @@ impl ArchiveImpl {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ArchiveQueryParams {
|
struct ArchiveQueryParams {
|
||||||
archive_type: Option<String>,
|
archive_type: Option<String>,
|
||||||
plain_secret: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -1185,7 +1160,7 @@ 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>,
|
Query(ArchiveQueryParams { archive_type }): 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)?;
|
||||||
|
|
||||||
@@ -1251,7 +1226,6 @@ 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();
|
||||||
@@ -1322,15 +1296,7 @@ async fn tarball_workspace(
|
|||||||
.fetch_all(&db)
|
.fetch_all(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mc = build_crypt(&mut db.begin().await?, &w_id).await?;
|
for var in variables {
|
||||||
|
|
||||||
for mut var in variables {
|
|
||||||
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();
|
let var_str = &to_string_without_metadata(&var, false).unwrap();
|
||||||
archive
|
archive
|
||||||
.write_to_archive(&var_str, &format!("{}.variable.json", var.path))
|
.write_to_archive(&var_str, &format!("{}.variable.json", var.path))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>),
|
||||||
|
|||||||
@@ -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::{
|
||||||
@@ -152,6 +149,9 @@ 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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub stop_after_if: Option<StopAfterIf>,
|
pub stop_after_if: Option<StopAfterIf>,
|
||||||
@@ -245,7 +245,7 @@ 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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -257,14 +257,6 @@ pub enum FlowModuleValue {
|
|||||||
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>,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -30,19 +30,7 @@ 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! {
|
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 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")]
|
||||||
@@ -70,31 +58,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");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,12 +25,8 @@ serde_json.workspace = true
|
|||||||
ulid.workspace = true
|
ulid.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
chrono-tz.workspace = true
|
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
prometheus.workspace = true
|
prometheus.workspace = true
|
||||||
cron.workspace = true
|
cron.workspace = true
|
||||||
rsmq_async.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
futures-core.workspace = true
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
@@ -22,11 +22,8 @@ use windmill_common::{
|
|||||||
flows::{FlowModule, FlowModuleValue, FlowValue},
|
flows::{FlowModule, FlowModuleValue, FlowValue},
|
||||||
scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang},
|
scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang},
|
||||||
utils::StripPath,
|
utils::StripPath,
|
||||||
METRICS_ENABLED,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::QueueTransaction;
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||||
.user_agent("windmill/beta")
|
.user_agent("windmill/beta")
|
||||||
@@ -54,43 +51,32 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
const MAX_FREE_EXECS: i32 = 1000;
|
const MAX_FREE_EXECS: i32 = 1000;
|
||||||
const MAX_FREE_CONCURRENT_RUNS: i32 = 15;
|
const MAX_FREE_CONCURRENT_RUNS: i32 = 15;
|
||||||
const RSMQ_MAIN_QUEUE: &'static str = "main_queue";
|
|
||||||
|
|
||||||
pub async fn cancel_job<'c, R: rsmq_async::RsmqConnection + Clone>(
|
pub async fn cancel_job<'c>(
|
||||||
username: &str,
|
username: &str,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
w_id: &str,
|
w_id: &str,
|
||||||
mut tx: Transaction<'c, Postgres>,
|
mut tx: Transaction<'c, Postgres>,
|
||||||
rsmq: Option<R>,
|
|
||||||
force_rerun: bool,
|
|
||||||
) -> error::Result<(Transaction<'c, Postgres>, Option<Uuid>)> {
|
) -> error::Result<(Transaction<'c, Postgres>, Option<Uuid>)> {
|
||||||
let job_option = sqlx::query_scalar!(
|
let job_option = sqlx::query_scalar!(
|
||||||
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0, running = CASE WHEN $3 THEN false ELSE running END WHERE id = $4 \
|
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0 WHERE id = $3 \
|
||||||
AND workspace_id = $5 RETURNING id",
|
AND workspace_id = $4 RETURNING id",
|
||||||
username,
|
username,
|
||||||
reason,
|
reason,
|
||||||
force_rerun,
|
|
||||||
id,
|
id,
|
||||||
w_id
|
w_id
|
||||||
)
|
)
|
||||||
.fetch_optional(&mut tx)
|
.fetch_optional(&mut tx)
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(mut rsmq) = rsmq {
|
|
||||||
rsmq.change_message_visibility(RSMQ_MAIN_QUEUE, &id.to_string(), 0)
|
|
||||||
.await
|
|
||||||
.map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut jobs = job_option.map(|j| vec![j]).unwrap_or_default();
|
let mut jobs = job_option.map(|j| vec![j]).unwrap_or_default();
|
||||||
while !jobs.is_empty() {
|
while !jobs.is_empty() {
|
||||||
let p_job = jobs.pop();
|
let p_job = jobs.pop();
|
||||||
let new_jobs = sqlx::query_scalar!(
|
let new_jobs = sqlx::query_scalar!(
|
||||||
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, running = CASE WHEN $3 THEN false ELSE running END WHERE parent_job = $4 \
|
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 WHERE parent_job = $3 \
|
||||||
AND workspace_id = $5 RETURNING id",
|
AND workspace_id = $4 RETURNING id",
|
||||||
username,
|
username,
|
||||||
reason,
|
reason,
|
||||||
force_rerun,
|
|
||||||
p_job,
|
p_job,
|
||||||
w_id
|
w_id
|
||||||
)
|
)
|
||||||
@@ -101,11 +87,10 @@ pub async fn cancel_job<'c, R: rsmq_async::RsmqConnection + Clone>(
|
|||||||
Ok((tx, job_option))
|
Ok((tx, job_option))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pull<R: rsmq_async::RsmqConnection + Clone>(
|
pub async fn pull(
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
whitelist_workspaces: Option<Vec<String>>,
|
whitelist_workspaces: Option<Vec<String>>,
|
||||||
blacklist_workspaces: Option<Vec<String>>,
|
blacklist_workspaces: Option<Vec<String>>,
|
||||||
rsmq: Option<R>,
|
|
||||||
) -> windmill_common::error::Result<Option<QueuedJob>> {
|
) -> windmill_common::error::Result<Option<QueuedJob>> {
|
||||||
let mut workspaces_filter = String::new();
|
let mut workspaces_filter = String::new();
|
||||||
if let Some(whitelist) = whitelist_workspaces {
|
if let Some(whitelist) = whitelist_workspaces {
|
||||||
@@ -117,9 +102,6 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Clone>(
|
|||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(",")
|
.join(",")
|
||||||
));
|
));
|
||||||
if let Some(_rsmq) = rsmq {
|
|
||||||
todo!("REDIS: Implement workspace filters for redis");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Some(blacklist) = blacklist_workspaces {
|
if let Some(blacklist) = blacklist_workspaces {
|
||||||
workspaces_filter.push_str(&format!(
|
workspaces_filter.push_str(&format!(
|
||||||
@@ -130,50 +112,16 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Clone>(
|
|||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(",")
|
.join(",")
|
||||||
));
|
));
|
||||||
if let Some(_rsmq) = rsmq {
|
|
||||||
todo!("REDIS: Implement workspace filters for redis");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
/* Jobs can be started if they:
|
||||||
let job: Option<QueuedJob> = if let Some(mut rsmq) = rsmq {
|
* - haven't been started before,
|
||||||
// TODO: REDIS: Race conditions / replace last_ping
|
* running = false
|
||||||
let msg = rsmq
|
* - are flows with a step that needed resume,
|
||||||
.pop_message::<Vec<u8>>(RSMQ_MAIN_QUEUE)
|
* suspend_until is non-null
|
||||||
.await
|
* and suspend = 0 when the resume messages are received
|
||||||
.map_err(|e| anyhow::anyhow!(e))?;
|
* or suspend_until <= now() if it has timed out */
|
||||||
|
let job: Option<QueuedJob> = sqlx::query_as::<_, QueuedJob>(&format!(
|
||||||
if let Some(msg) = msg {
|
"UPDATE queue
|
||||||
let uuid = Uuid::from_bytes_le(
|
|
||||||
msg.message
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to parsed Redis message"))?,
|
|
||||||
);
|
|
||||||
|
|
||||||
sqlx::query_as::<_, QueuedJob>(
|
|
||||||
"UPDATE queue
|
|
||||||
SET running = true
|
|
||||||
, started_at = coalesce(started_at, now())
|
|
||||||
, last_ping = now()
|
|
||||||
, suspend_until = null
|
|
||||||
WHERE id = $1
|
|
||||||
RETURNING *",
|
|
||||||
)
|
|
||||||
.bind(uuid)
|
|
||||||
.fetch_optional(db)
|
|
||||||
.await?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/* Jobs can be started if they:
|
|
||||||
* - haven't been started before,
|
|
||||||
* running = false
|
|
||||||
* - are flows with a step that needed resume,
|
|
||||||
* suspend_until is non-null
|
|
||||||
* and suspend = 0 when the resume messages are received
|
|
||||||
* or suspend_until <= now() if it has timed out */
|
|
||||||
sqlx::query_as::<_, QueuedJob>(&format!(
|
|
||||||
"UPDATE queue
|
|
||||||
SET running = true
|
SET running = true
|
||||||
, started_at = coalesce(started_at, now())
|
, started_at = coalesce(started_at, now())
|
||||||
, last_ping = now()
|
, last_ping = now()
|
||||||
@@ -191,12 +139,11 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Clone>(
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
RETURNING *"
|
RETURNING *"
|
||||||
))
|
))
|
||||||
.fetch_optional(db)
|
.fetch_optional(db)
|
||||||
.await?
|
.await?;
|
||||||
};
|
|
||||||
|
|
||||||
if job.is_some() && *METRICS_ENABLED {
|
if job.is_some() {
|
||||||
QUEUE_PULL_COUNT.inc();
|
QUEUE_PULL_COUNT.inc();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,24 +152,55 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Clone>(
|
|||||||
|
|
||||||
pub async fn get_result_by_id(
|
pub async fn get_result_by_id(
|
||||||
db: Pool<Postgres>,
|
db: Pool<Postgres>,
|
||||||
|
mut skip_direct: bool,
|
||||||
w_id: String,
|
w_id: String,
|
||||||
flow_id: Uuid,
|
flow_id: String,
|
||||||
node_id: String,
|
node_id: String,
|
||||||
) -> error::Result<serde_json::Value> {
|
) -> error::Result<serde_json::Value> {
|
||||||
let job_result: Option<JobResult> = sqlx::query_scalar!(
|
let mut result_id: Option<JobResult> = None;
|
||||||
"SELECT leaf_jobs->$1::text FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3",
|
let mut parent_id = Uuid::from_str(&flow_id).ok();
|
||||||
node_id,
|
while result_id.is_none() && parent_id.is_some() {
|
||||||
flow_id,
|
if !skip_direct {
|
||||||
w_id,
|
let r = sqlx::query!(
|
||||||
)
|
"SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 ",
|
||||||
.fetch_optional(&db)
|
parent_id.unwrap(),
|
||||||
.await?
|
w_id,
|
||||||
.flatten()
|
)
|
||||||
.map(|x| serde_json::from_value(x).ok())
|
.fetch_optional(&db)
|
||||||
.flatten();
|
.await?;
|
||||||
|
if let Some(r) = r {
|
||||||
|
let value = r
|
||||||
|
.flow_status
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::InternalErr(format!("requiring a flow status value")))?
|
||||||
|
.to_owned();
|
||||||
|
parent_id = r.parent_job;
|
||||||
|
let status_o = serde_json::from_value::<FlowStatus>(value).ok();
|
||||||
|
result_id = status_o.and_then(|status| {
|
||||||
|
status
|
||||||
|
.modules
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.id() == node_id)
|
||||||
|
.and_then(|m| m.job_result())
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
parent_id = None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let q_parent = sqlx::query_scalar!(
|
||||||
|
"SELECT parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT parent_job FROM queue WHERE id = $1 AND workspace_id = $2",
|
||||||
|
parent_id.unwrap(),
|
||||||
|
w_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&db)
|
||||||
|
.await?
|
||||||
|
.flatten();
|
||||||
|
parent_id = q_parent;
|
||||||
|
skip_direct = false
|
||||||
|
}
|
||||||
|
}
|
||||||
let result_id = windmill_common::utils::not_found_if_none(
|
let result_id = windmill_common::utils::not_found_if_none(
|
||||||
job_result,
|
result_id,
|
||||||
"Flow result by id",
|
"Flow result by id",
|
||||||
format!("{}, {}", flow_id, node_id),
|
format!("{}, {}", flow_id, node_id),
|
||||||
)?;
|
)?;
|
||||||
@@ -256,26 +234,24 @@ pub async fn get_result_by_id(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub async fn delete_job<'c, R: rsmq_async::RsmqConnection + Clone + Send>(
|
pub async fn delete_job(
|
||||||
mut tx: QueueTransaction<'c, R>,
|
db: &Pool<Postgres>,
|
||||||
w_id: &str,
|
w_id: &str,
|
||||||
job_id: Uuid,
|
job_id: Uuid,
|
||||||
) -> windmill_common::error::Result<QueueTransaction<'c, R>> {
|
) -> windmill_common::error::Result<()> {
|
||||||
if *METRICS_ENABLED {
|
QUEUE_DELETE_COUNT.inc();
|
||||||
QUEUE_DELETE_COUNT.inc();
|
|
||||||
}
|
|
||||||
let job_removed = sqlx::query_scalar!(
|
let job_removed = sqlx::query_scalar!(
|
||||||
"DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1",
|
"DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1",
|
||||||
w_id,
|
w_id,
|
||||||
job_id
|
job_id
|
||||||
)
|
)
|
||||||
.fetch_one(&mut tx)
|
.fetch_one(db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("Error during deletion of job {job_id}: {e}")))?
|
.map_err(|e| Error::InternalErr(format!("Error during deletion of job {job_id}: {e}")))?
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
== 1;
|
== 1;
|
||||||
tracing::debug!("Job {job_id} deleted: {job_removed}");
|
tracing::debug!("Job {job_id} deleted: {job_removed}");
|
||||||
Ok(tx)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_queued_job<'c>(
|
pub async fn get_queued_job<'c>(
|
||||||
@@ -294,9 +270,9 @@ pub async fn get_queued_job<'c>(
|
|||||||
Ok(r)
|
Ok(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
pub async fn push<'c>(
|
||||||
mut tx: QueueTransaction<'c, R>,
|
mut tx: Transaction<'c, Postgres>,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
job_payload: JobPayload,
|
job_payload: JobPayload,
|
||||||
args: serde_json::Map<String, serde_json::Value>,
|
args: serde_json::Map<String, serde_json::Value>,
|
||||||
@@ -306,18 +282,18 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
|||||||
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
|
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
schedule_path: Option<String>,
|
schedule_path: Option<String>,
|
||||||
parent_job: Option<Uuid>,
|
parent_job: Option<Uuid>,
|
||||||
root_job: Option<Uuid>,
|
|
||||||
is_flow_step: bool,
|
is_flow_step: bool,
|
||||||
mut same_worker: bool,
|
mut same_worker: bool,
|
||||||
pre_run_error: Option<&windmill_common::error::Error>,
|
pre_run_error: Option<&windmill_common::error::Error>,
|
||||||
visible_to_owner: bool,
|
visible_to_owner: bool,
|
||||||
) -> Result<(Uuid, QueueTransaction<'c, R>), Error> {
|
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
|
||||||
|
let scheduled_for = scheduled_for_o.unwrap_or_else(chrono::Utc::now);
|
||||||
let args_json = serde_json::Value::Object(args);
|
let args_json = serde_json::Value::Object(args);
|
||||||
let job_id: Uuid = Ulid::new().into();
|
let job_id: Uuid = Ulid::new().into();
|
||||||
|
|
||||||
if cfg!(feature = "enterprise") {
|
if cfg!(feature = "enterprise") {
|
||||||
let premium_workspace = *CLOUD_HOSTED
|
let premium_workspace =
|
||||||
&& sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", workspace_id)
|
sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", workspace_id)
|
||||||
.fetch_one(&mut tx)
|
.fetch_one(&mut tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -540,6 +516,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
|||||||
modules.push(FlowModule {
|
modules.push(FlowModule {
|
||||||
id: format!("{}-v", flow.modules[flow.modules.len() - 1].id),
|
id: format!("{}-v", flow.modules[flow.modules.len() - 1].id),
|
||||||
value: FlowModuleValue::Identity,
|
value: FlowModuleValue::Identity,
|
||||||
|
input_transforms: HashMap::new(),
|
||||||
stop_after_if: None,
|
stop_after_if: None,
|
||||||
summary: Some(
|
summary: Some(
|
||||||
"Virtual module needed for suspend/sleep when last module".to_string(),
|
"Virtual module needed for suspend/sleep when last module".to_string(),
|
||||||
@@ -557,13 +534,12 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
|||||||
.unwrap_or_else(|| (None, None));
|
.unwrap_or_else(|| (None, None));
|
||||||
|
|
||||||
let flow_status = raw_flow.as_ref().map(FlowStatus::new);
|
let flow_status = raw_flow.as_ref().map(FlowStatus::new);
|
||||||
|
|
||||||
let uuid = sqlx::query_scalar!(
|
let uuid = sqlx::query_scalar!(
|
||||||
"INSERT INTO queue
|
"INSERT INTO queue
|
||||||
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
|
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
|
||||||
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
|
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
|
||||||
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)
|
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, now()), $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) \
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) \
|
||||||
RETURNING id",
|
RETURNING id",
|
||||||
workspace_id,
|
workspace_id,
|
||||||
job_id,
|
job_id,
|
||||||
@@ -571,7 +547,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
|||||||
parent_job,
|
parent_job,
|
||||||
user,
|
user,
|
||||||
permissioned_as,
|
permissioned_as,
|
||||||
scheduled_for_o,
|
scheduled_for,
|
||||||
script_hash,
|
script_hash,
|
||||||
script_path.clone(),
|
script_path.clone(),
|
||||||
raw_code,
|
raw_code,
|
||||||
@@ -586,16 +562,13 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
|||||||
same_worker,
|
same_worker,
|
||||||
pre_run_error.map(|e| e.to_string()),
|
pre_run_error.map(|e| e.to_string()),
|
||||||
email,
|
email,
|
||||||
visible_to_owner,
|
visible_to_owner
|
||||||
root_job
|
|
||||||
)
|
)
|
||||||
.fetch_one(&mut tx)
|
.fetch_one(&mut tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("Could not insert into queue {job_id}: {e}")))?;
|
.map_err(|e| Error::InternalErr(format!("Could not insert into queue {job_id}: {e}")))?;
|
||||||
// TODO: technically the job isn't queued yet, as the transaction can be rolled back. Should be solved when moving these metrics to the queue abstraction.
|
// TODO: technically the job isn't queued yet, as the transaction can be rolled back. Should be solved when moving these metrics to the queue abstraction.
|
||||||
if *METRICS_ENABLED {
|
QUEUE_PUSH_COUNT.inc();
|
||||||
QUEUE_PUSH_COUNT.inc();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let uuid_string = job_id.to_string();
|
let uuid_string = job_id.to_string();
|
||||||
@@ -630,10 +603,6 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
|||||||
.instrument(tracing::info_span!("job_run", email = &email))
|
.instrument(tracing::info_span!("job_run", email = &email))
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
if let Some(ref mut rsmq) = tx.rsmq {
|
|
||||||
rsmq.send_message(job_id.to_bytes_le().to_vec(), scheduled_for_o);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((uuid, tx))
|
Ok((uuid, tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,10 +675,6 @@ pub struct QueuedJob {
|
|||||||
pub suspend: Option<i32>,
|
pub suspend: Option<i32>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mem_peak: Option<i32>,
|
pub mem_peak: Option<i32>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub root_job: Option<Uuid>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub leaf_jobs: Option<serde_json::Value>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueuedJob {
|
impl QueuedJob {
|
||||||
@@ -717,7 +682,7 @@ impl QueuedJob {
|
|||||||
self.script_path
|
self.script_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(String::as_str)
|
.map(String::as_str)
|
||||||
.unwrap_or("tmp/main")
|
.unwrap_or("NO_FLOW_PATH")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
mod jobs;
|
mod jobs;
|
||||||
mod queue_transaction;
|
|
||||||
pub mod schedule;
|
pub mod schedule;
|
||||||
|
|
||||||
pub use jobs::*;
|
pub use jobs::*;
|
||||||
pub use queue_transaction::*;
|
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
use futures_core::{future::BoxFuture, stream::BoxStream};
|
|
||||||
use rsmq_async::{RedisBytes, RsmqConnection};
|
|
||||||
use sqlx::{Postgres, Transaction};
|
|
||||||
|
|
||||||
pub enum RedisOp {
|
|
||||||
SendMessage(RedisBytes, Option<chrono::DateTime<chrono::Utc>>),
|
|
||||||
DeleteMessage(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RedisOp {
|
|
||||||
pub async fn apply<R: RsmqConnection>(self, rsmq: &mut R) -> Result<(), rsmq_async::RsmqError> {
|
|
||||||
match self {
|
|
||||||
RedisOp::SendMessage(bytes, time) => {
|
|
||||||
rsmq.send_message(
|
|
||||||
"main_queue",
|
|
||||||
bytes,
|
|
||||||
time.map(|t| (t - chrono::Utc::now()).num_seconds())
|
|
||||||
.and_then(|e| e.try_into().ok()),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
RedisOp::DeleteMessage(id) => {
|
|
||||||
rsmq.delete_message("main_queue", &id).await?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RedisTransaction<R: RsmqConnection> {
|
|
||||||
rsmq: R,
|
|
||||||
queued_ops: Vec<RedisOp>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: RsmqConnection> From<R> for RedisTransaction<R> {
|
|
||||||
fn from(value: R) -> Self {
|
|
||||||
Self { rsmq: value, queued_ops: Vec::new() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: RsmqConnection> RedisTransaction<R> {
|
|
||||||
pub async fn commit(self) -> Result<(), rsmq_async::RsmqError> {
|
|
||||||
let mut rsmq = self.rsmq;
|
|
||||||
for op in self.queued_ops {
|
|
||||||
op.apply(&mut rsmq).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_message<E: Into<RedisBytes>>(
|
|
||||||
&mut self,
|
|
||||||
bytes: E,
|
|
||||||
delay_until: Option<chrono::DateTime<chrono::Utc>>,
|
|
||||||
) {
|
|
||||||
self.queued_ops
|
|
||||||
.push(RedisOp::SendMessage(bytes.into(), delay_until))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_message(&mut self, id: String) {
|
|
||||||
self.queued_ops.push(RedisOp::DeleteMessage(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct QueueTransaction<'c, R: RsmqConnection> {
|
|
||||||
pub rsmq: Option<RedisTransaction<R>>,
|
|
||||||
transaction: Transaction<'c, Postgres>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'c, R: RsmqConnection> From<(Option<R>, Transaction<'c, Postgres>)>
|
|
||||||
for QueueTransaction<'c, R>
|
|
||||||
{
|
|
||||||
fn from(value: (Option<R>, Transaction<'c, Postgres>)) -> Self {
|
|
||||||
Self { rsmq: value.0.map(|e| e.into()), transaction: value.1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'c, R: RsmqConnection> Debug for QueueTransaction<'c, R> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("QueueTransaction")
|
|
||||||
.field("rsmq", &self.rsmq.as_ref().map(|_| ())) // do not require R: Debug
|
|
||||||
.field("transaction", &self.transaction)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'c, R: RsmqConnection> QueueTransaction<'c, R> {
|
|
||||||
pub async fn commit(self) -> Result<(), windmill_common::error::Error> {
|
|
||||||
self.transaction.commit().await?;
|
|
||||||
if let Some(rsmq) = self.rsmq {
|
|
||||||
rsmq.commit().await.map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn transaction_mut<'a>(&'a mut self) -> &'a mut Transaction<'c, Postgres> {
|
|
||||||
&mut self.transaction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'c, 'b, R: RsmqConnection + Send> sqlx::Executor<'b> for &'b mut QueueTransaction<'c, R> {
|
|
||||||
type Database = Postgres;
|
|
||||||
|
|
||||||
fn fetch_many<'e, 'q: 'e, E: 'q>(
|
|
||||||
self,
|
|
||||||
query: E,
|
|
||||||
) -> BoxStream<
|
|
||||||
'e,
|
|
||||||
Result<
|
|
||||||
sqlx::Either<
|
|
||||||
<Self::Database as sqlx::Database>::QueryResult,
|
|
||||||
<Self::Database as sqlx::Database>::Row,
|
|
||||||
>,
|
|
||||||
sqlx::Error,
|
|
||||||
>,
|
|
||||||
>
|
|
||||||
where
|
|
||||||
'b: 'e,
|
|
||||||
E: sqlx::Execute<'q, Self::Database>,
|
|
||||||
{
|
|
||||||
self.transaction.fetch_many(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_optional<'e, 'q: 'e, E: 'q>(
|
|
||||||
self,
|
|
||||||
query: E,
|
|
||||||
) -> BoxFuture<'e, Result<Option<<Self::Database as sqlx::Database>::Row>, sqlx::Error>>
|
|
||||||
where
|
|
||||||
'b: 'e,
|
|
||||||
E: sqlx::Execute<'q, Self::Database>,
|
|
||||||
{
|
|
||||||
self.transaction.fetch_optional(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_with<'e, 'q: 'e>(
|
|
||||||
self,
|
|
||||||
sql: &'q str,
|
|
||||||
parameters: &'e [<Self::Database as sqlx::Database>::TypeInfo],
|
|
||||||
) -> BoxFuture<
|
|
||||||
'e,
|
|
||||||
Result<<Self::Database as sqlx::database::HasStatement<'q>>::Statement, sqlx::Error>,
|
|
||||||
>
|
|
||||||
where
|
|
||||||
'b: 'e,
|
|
||||||
{
|
|
||||||
self.transaction.prepare_with(sql, parameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn describe<'e, 'q: 'e>(
|
|
||||||
self,
|
|
||||||
sql: &'q str,
|
|
||||||
) -> BoxFuture<'e, Result<sqlx::Describe<Self::Database>, sqlx::Error>>
|
|
||||||
where
|
|
||||||
'b: 'e,
|
|
||||||
{
|
|
||||||
self.transaction.describe(sql)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,39 +6,33 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{push, JobPayload};
|
|
||||||
use sqlx::{query_scalar, Postgres, Transaction};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::Duration;
|
||||||
|
use sqlx::{query_scalar, Postgres, Transaction};
|
||||||
use windmill_common::{
|
use windmill_common::{
|
||||||
error::{self, Result},
|
error::{self, Result},
|
||||||
schedule::Schedule,
|
schedule::Schedule,
|
||||||
users::username_to_permissioned_as,
|
users::username_to_permissioned_as,
|
||||||
utils::{now_from_db, StripPath},
|
utils::{now_from_db, StripPath},
|
||||||
};
|
};
|
||||||
use crate::{QueueTransaction};
|
|
||||||
|
|
||||||
pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
use crate::{push, JobPayload};
|
||||||
mut tx: QueueTransaction<'c, R>,
|
|
||||||
|
pub async fn push_scheduled_job<'c>(
|
||||||
|
mut tx: Transaction<'c, Postgres>,
|
||||||
schedule: Schedule,
|
schedule: Schedule,
|
||||||
) -> Result<QueueTransaction<'c, R>> {
|
) -> Result<Transaction<'c, Postgres>> {
|
||||||
let sched = cron::Schedule::from_str(&schedule.schedule)
|
let sched = cron::Schedule::from_str(&schedule.schedule)
|
||||||
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
||||||
|
|
||||||
let tz = chrono_tz::Tz::from_str(&schedule.timezone)
|
let offset = Duration::minutes(schedule.offset_.into());
|
||||||
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
let now = now_from_db(&mut tx).await?;
|
||||||
|
|
||||||
let now = now_from_db(&mut tx).await?.with_timezone(&tz);
|
|
||||||
|
|
||||||
let next = sched
|
let next = sched
|
||||||
.after(&now)
|
.after(&(now - offset + Duration::seconds(1)))
|
||||||
.next()
|
.next()
|
||||||
.expect("a schedule should have a next event");
|
.expect("a schedule should have a next event")
|
||||||
|
+ offset;
|
||||||
// println!("next event ({:?}): {}", tz, next);
|
|
||||||
// println!("next event(UTC): {}", next.with_timezone(&chrono::Utc));
|
|
||||||
|
|
||||||
// Scheduled events must be stored in the database in UTC
|
|
||||||
let next = next.with_timezone(&chrono::Utc);
|
|
||||||
|
|
||||||
let already_exists: bool = query_scalar!(
|
let already_exists: bool = query_scalar!(
|
||||||
"SELECT EXISTS (SELECT 1 FROM queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)",
|
"SELECT EXISTS (SELECT 1 FROM queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)",
|
||||||
@@ -71,7 +65,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
|||||||
} else {
|
} else {
|
||||||
JobPayload::ScriptHash {
|
JobPayload::ScriptHash {
|
||||||
hash: windmill_common::get_latest_hash_for_path(
|
hash: windmill_common::get_latest_hash_for_path(
|
||||||
tx.transaction_mut(),
|
&mut tx,
|
||||||
&schedule.workspace_id,
|
&schedule.workspace_id,
|
||||||
&schedule.script_path,
|
&schedule.script_path,
|
||||||
)
|
)
|
||||||
@@ -80,15 +74,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sqlx::query!(
|
let (_, mut tx) = push(
|
||||||
"UPDATE schedule SET error = NULL WHERE workspace_id = $1 AND path = $2",
|
|
||||||
&schedule.workspace_id,
|
|
||||||
&schedule.path
|
|
||||||
)
|
|
||||||
.execute(&mut tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let (_, tx) = push(
|
|
||||||
tx,
|
tx,
|
||||||
&schedule.workspace_id,
|
&schedule.workspace_id,
|
||||||
payload,
|
payload,
|
||||||
@@ -99,14 +85,20 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
|||||||
Some(next),
|
Some(next),
|
||||||
Some(schedule.path.clone()),
|
Some(schedule.path.clone()),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(tx) // TODO: Bubble up pushed UUID from here
|
sqlx::query!(
|
||||||
|
"UPDATE schedule SET error = NULL WHERE workspace_id = $1 AND path = $2",
|
||||||
|
&schedule.workspace_id,
|
||||||
|
&schedule.path
|
||||||
|
)
|
||||||
|
.execute(&mut tx)
|
||||||
|
.await?;
|
||||||
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_schedule_opt<'c>(
|
pub async fn get_schedule_opt<'c>(
|
||||||
|
|||||||
@@ -48,5 +48,3 @@ deno_core.workspace = true
|
|||||||
const_format.workspace = true
|
const_format.workspace = true
|
||||||
git-version.workspace = true
|
git-version.workspace = true
|
||||||
dyn-iter.workspace = true
|
dyn-iter.workspace = true
|
||||||
once_cell.workspace = true
|
|
||||||
rsmq_async.workspace = true
|
|
||||||
|
|||||||
135
backend/windmill-worker/nsjail/run.deno.config.proto
Normal file
135
backend/windmill-worker/nsjail/run.deno.config.proto
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
name: "deno run script"
|
||||||
|
|
||||||
|
mode: ONCE
|
||||||
|
hostname: "deno"
|
||||||
|
log_level: ERROR
|
||||||
|
|
||||||
|
rlimit_as: 16000
|
||||||
|
rlimit_cpu: 1000
|
||||||
|
rlimit_fsize: 1000
|
||||||
|
rlimit_nofile: 10000
|
||||||
|
|
||||||
|
cwd: "/tmp"
|
||||||
|
|
||||||
|
|
||||||
|
clone_newnet: false
|
||||||
|
clone_newuser: {CLONE_NEWUSER}
|
||||||
|
|
||||||
|
keep_caps: false
|
||||||
|
keep_env: true
|
||||||
|
mount_proc: true
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "/bin"
|
||||||
|
dst: "/bin"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "/lib"
|
||||||
|
dst: "/lib"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "/lib64"
|
||||||
|
dst: "/lib64"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "/usr"
|
||||||
|
dst: "/usr"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "/dev/null"
|
||||||
|
dst: "/dev/null"
|
||||||
|
is_bind: true
|
||||||
|
rw: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
dst: "/tmp"
|
||||||
|
fstype: "tmpfs"
|
||||||
|
rw: true
|
||||||
|
options: "size=500000000"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "{JOB_DIR}/inner.ts"
|
||||||
|
dst: "/tmp/inner.ts"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "{JOB_DIR}/main.ts"
|
||||||
|
dst: "/tmp/main.ts"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "{JOB_DIR}/import_map.json"
|
||||||
|
dst: "/tmp/import_map.json"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "{JOB_DIR}/lock.json"
|
||||||
|
dst: "/tmp/lock.json"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "{JOB_DIR}/args.json"
|
||||||
|
dst: "/tmp/args.json"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "{JOB_DIR}/result.json"
|
||||||
|
dst: "/tmp/result.json"
|
||||||
|
is_bind: true
|
||||||
|
rw: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "/etc"
|
||||||
|
dst: "/etc"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "/dev/random"
|
||||||
|
dst: "/dev/random"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "/dev/urandom"
|
||||||
|
dst: "/dev/urandom"
|
||||||
|
is_bind: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mount {
|
||||||
|
src: "{CACHE_DIR}"
|
||||||
|
dst: "/tmp/.cache/deno"
|
||||||
|
is_bind: true
|
||||||
|
rw: true
|
||||||
|
mandatory: false
|
||||||
|
}
|
||||||
|
|
||||||
|
{SHARED_MOUNT}
|
||||||
|
|
||||||
|
iface_no_lo: true
|
||||||
|
|
||||||
|
envar: "DENO_DIR=/tmp/.cache/deno"
|
||||||
|
envar: "NO_COLOR=true"
|
||||||
|
envar: "HOME=/tmp"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ mount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mount {
|
mount {
|
||||||
src: "{JOB_DIR}/{MAIN}.py"
|
src: "{JOB_DIR}/inner.py"
|
||||||
dst: "/tmp/{MAIN}.py"
|
dst: "/tmp/inner.py"
|
||||||
is_bind: true
|
is_bind: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +79,8 @@ mount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mount {
|
mount {
|
||||||
src: "{JOB_DIR}/wrapper.py"
|
src: "{JOB_DIR}/main.py"
|
||||||
dst: "/tmp/wrapper.py"
|
dst: "/tmp/main.py"
|
||||||
is_bind: true
|
is_bind: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,30 +6,23 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres, Transaction};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use windmill_common::{
|
use windmill_common::{error::Error, flow_status::FlowStatusModule, schedule::Schedule};
|
||||||
error::Error, flow_status::FlowStatusModule, schedule::Schedule, METRICS_ENABLED,
|
use windmill_queue::{delete_job, schedule::get_schedule_opt, JobKind, QueuedJob};
|
||||||
};
|
|
||||||
use windmill_queue::{
|
|
||||||
delete_job, schedule::get_schedule_opt, JobKind, QueueTransaction, QueuedJob, CLOUD_HOSTED,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub async fn add_completed_job_error<R: rsmq_async::RsmqConnection + Clone + Send>(
|
pub async fn add_completed_job_error(
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
queued_job: &QueuedJob,
|
queued_job: &QueuedJob,
|
||||||
logs: String,
|
logs: String,
|
||||||
e: serde_json::Value,
|
e: serde_json::Value,
|
||||||
metrics: Option<crate::worker::Metrics>,
|
metrics: Option<crate::worker::Metrics>,
|
||||||
rsmq: Option<R>,
|
|
||||||
) -> Result<serde_json::Value, Error> {
|
) -> Result<serde_json::Value, Error> {
|
||||||
if *METRICS_ENABLED {
|
metrics.map(|m| m.worker_execution_failed.inc());
|
||||||
metrics.map(|m| m.worker_execution_failed.inc());
|
|
||||||
}
|
|
||||||
let result = serde_json::json!({ "error": e });
|
let result = serde_json::json!({ "error": e });
|
||||||
let _ = add_completed_job(db, &queued_job, false, false, result.clone(), logs, rsmq).await?;
|
let _ = add_completed_job(db, &queued_job, false, false, result.clone(), logs).await?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,14 +45,13 @@ fn flatten_jobs(modules: Vec<FlowStatusModule>) -> Vec<Uuid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
|
pub async fn add_completed_job(
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
queued_job: &QueuedJob,
|
queued_job: &QueuedJob,
|
||||||
success: bool,
|
success: bool,
|
||||||
skipped: bool,
|
skipped: bool,
|
||||||
result: serde_json::Value,
|
result: serde_json::Value,
|
||||||
logs: String,
|
logs: String,
|
||||||
rsmq: Option<R>,
|
|
||||||
) -> Result<Uuid, Error> {
|
) -> Result<Uuid, Error> {
|
||||||
let duration =
|
let duration =
|
||||||
if queued_job.job_kind == JobKind::Flow || queued_job.job_kind == JobKind::FlowPreview {
|
if queued_job.job_kind == JobKind::Flow || queued_job.job_kind == JobKind::FlowPreview {
|
||||||
@@ -91,7 +83,7 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
|
|||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.flatten();
|
.flatten();
|
||||||
let mut tx: QueueTransaction<'_, R> = (rsmq, db.begin().await?).into();
|
let mut tx = db.begin().await?;
|
||||||
let job_id = queued_job.id.clone();
|
let job_id = queued_job.id.clone();
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO completed_job AS cj
|
"INSERT INTO completed_job AS cj
|
||||||
@@ -161,7 +153,7 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
|
|||||||
.execute(&mut tx)
|
.execute(&mut tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?;
|
.map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?;
|
||||||
tx = delete_job(tx, &queued_job.workspace_id, job_id).await?;
|
let _ = delete_job(db, &queued_job.workspace_id, job_id).await?;
|
||||||
if !queued_job.is_flow_step
|
if !queued_job.is_flow_step
|
||||||
&& queued_job.job_kind != JobKind::Flow
|
&& queued_job.job_kind != JobKind::Flow
|
||||||
&& queued_job.job_kind != JobKind::FlowPreview
|
&& queued_job.job_kind != JobKind::FlowPreview
|
||||||
@@ -183,8 +175,8 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
|
|||||||
let additional_usage = duration.unwrap() as i32 / 1000;
|
let additional_usage = duration.unwrap() as i32 / 1000;
|
||||||
|
|
||||||
let w_id = &queued_job.workspace_id;
|
let w_id = &queued_job.workspace_id;
|
||||||
let premium_workspace = *CLOUD_HOSTED
|
let premium_workspace =
|
||||||
&& sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", w_id)
|
sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", w_id)
|
||||||
.fetch_one(db)
|
.fetch_one(db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("fetching if {w_id} is premium: {e}")))?;
|
.map_err(|e| Error::InternalErr(format!("fetching if {w_id} is premium: {e}")))?;
|
||||||
@@ -216,24 +208,21 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub async fn schedule_again_if_scheduled<'c, R: rsmq_async::RsmqConnection + Clone + Send + 'c>(
|
pub async fn schedule_again_if_scheduled<'c>(
|
||||||
mut tx: QueueTransaction<'c, R>,
|
mut tx: Transaction<'c, Postgres>,
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
schedule_path: &str,
|
schedule_path: &str,
|
||||||
script_path: &str,
|
script_path: &str,
|
||||||
w_id: &str,
|
w_id: &str,
|
||||||
) -> windmill_common::error::Result<QueueTransaction<'c, R>> {
|
) -> windmill_common::error::Result<Transaction<'c, Postgres>> {
|
||||||
let schedule = get_schedule_opt(tx.transaction_mut(), w_id, schedule_path).await?;
|
let schedule = get_schedule_opt(&mut tx, w_id, schedule_path)
|
||||||
|
.await?
|
||||||
if schedule.is_none() {
|
.ok_or_else(|| {
|
||||||
tracing::error!(
|
Error::InternalErr(format!(
|
||||||
"Schedule {schedule_path} in {w_id} not found. Impossible to schedule again"
|
"Could not find schedule {:?} for workspace {}",
|
||||||
);
|
schedule_path, w_id
|
||||||
return Ok(tx);
|
))
|
||||||
}
|
})?;
|
||||||
|
|
||||||
let schedule = schedule.unwrap();
|
|
||||||
|
|
||||||
if schedule.enabled && script_path == schedule.script_path {
|
if schedule.enabled && script_path == schedule.script_path {
|
||||||
let res = windmill_queue::schedule::push_scheduled_job(
|
let res = windmill_queue::schedule::push_scheduled_job(
|
||||||
tx,
|
tx,
|
||||||
@@ -243,7 +232,7 @@ pub async fn schedule_again_if_scheduled<'c, R: rsmq_async::RsmqConnection + Clo
|
|||||||
edited_by: schedule.edited_by,
|
edited_by: schedule.edited_by,
|
||||||
edited_at: schedule.edited_at,
|
edited_at: schedule.edited_at,
|
||||||
schedule: schedule.schedule,
|
schedule: schedule.schedule,
|
||||||
timezone: schedule.timezone,
|
offset_: schedule.offset_,
|
||||||
enabled: schedule.enabled,
|
enabled: schedule.enabled,
|
||||||
script_path: schedule.script_path,
|
script_path: schedule.script_path,
|
||||||
is_flow: schedule.is_flow,
|
is_flow: schedule.is_flow,
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use deno_core::{
|
use deno_core::{op, serde_v8, v8, v8::IsolateHandle, Extension, JsRuntime, RuntimeOptions};
|
||||||
op, serde_v8, v8, v8::IsolateHandle, Extension, JsRuntime, OpState, RuntimeOptions,
|
|
||||||
};
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@@ -19,7 +17,10 @@ use tokio::{sync::oneshot, time::timeout};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use windmill_common::{error::Error, flow_status::JobResult};
|
use windmill_common::{error::Error, flow_status::JobResult};
|
||||||
|
|
||||||
use crate::AuthedClient;
|
pub struct EvalCreds {
|
||||||
|
pub workspace: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct IdContext {
|
pub struct IdContext {
|
||||||
@@ -28,23 +29,22 @@ pub struct IdContext {
|
|||||||
pub previous_id: String,
|
pub previous_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OptAuthedClient(Option<AuthedClient>);
|
|
||||||
pub async fn eval_timeout(
|
pub async fn eval_timeout(
|
||||||
expr: String,
|
expr: String,
|
||||||
env: Vec<(String, serde_json::Value)>,
|
env: Vec<(String, serde_json::Value)>,
|
||||||
authed_client: Option<&AuthedClient>,
|
creds: Option<EvalCreds>,
|
||||||
by_id: Option<IdContext>,
|
by_id: Option<IdContext>,
|
||||||
|
base_internal_url: &str,
|
||||||
) -> anyhow::Result<serde_json::Value> {
|
) -> anyhow::Result<serde_json::Value> {
|
||||||
let expr2 = expr.clone();
|
let expr2 = expr.clone();
|
||||||
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
|
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
|
||||||
let has_client = authed_client.is_some();
|
let base_internal_url: String = base_internal_url.to_string();
|
||||||
let authed_client = authed_client.cloned();
|
|
||||||
timeout(
|
timeout(
|
||||||
std::time::Duration::from_millis(10000),
|
std::time::Duration::from_millis(2000),
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let mut ops = vec![];
|
let mut ops = vec![];
|
||||||
|
|
||||||
if authed_client.is_some() {
|
if creds.is_some() {
|
||||||
ops.extend([
|
ops.extend([
|
||||||
// An op for summing an array of numbers
|
// An op for summing an array of numbers
|
||||||
// The op-layer automatically deserializes inputs
|
// The op-layer automatically deserializes inputs
|
||||||
@@ -54,12 +54,12 @@ pub async fn eval_timeout(
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
if by_id.is_some() && authed_client.is_some() {
|
if by_id.is_some() {
|
||||||
ops.push(op_get_result::decl());
|
ops.push(op_get_result::decl());
|
||||||
ops.push(op_get_id::decl());
|
ops.push(op_get_id::decl());
|
||||||
}
|
}
|
||||||
|
|
||||||
let ext = Extension::builder("js_eval").ops(ops).build();
|
let ext = Extension::builder().ops(ops).build();
|
||||||
// Use our snapshot to provision our new runtime
|
// Use our snapshot to provision our new runtime
|
||||||
let options = RuntimeOptions {
|
let options = RuntimeOptions {
|
||||||
extensions: vec![ext],
|
extensions: vec![ext],
|
||||||
@@ -68,11 +68,6 @@ pub async fn eval_timeout(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut js_runtime = JsRuntime::new(options);
|
let mut js_runtime = JsRuntime::new(options);
|
||||||
{
|
|
||||||
let op_state = js_runtime.op_state();
|
|
||||||
let mut op_state = op_state.borrow_mut();
|
|
||||||
op_state.put(OptAuthedClient(authed_client.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
sender
|
sender
|
||||||
.send(js_runtime.v8_isolate().thread_safe_handle())
|
.send(js_runtime.v8_isolate().thread_safe_handle())
|
||||||
@@ -91,7 +86,14 @@ pub async fn eval_timeout(
|
|||||||
|
|
||||||
let expr = replace_with_await_result(expr);
|
let expr = replace_with_await_result(expr);
|
||||||
|
|
||||||
let r = runtime.block_on(eval(&mut js_runtime, &expr, env, by_id, has_client))?;
|
let r = runtime.block_on(eval(
|
||||||
|
&mut js_runtime,
|
||||||
|
&expr,
|
||||||
|
env,
|
||||||
|
creds,
|
||||||
|
by_id,
|
||||||
|
&base_internal_url,
|
||||||
|
))?;
|
||||||
|
|
||||||
Ok(r) as anyhow::Result<Value>
|
Ok(r) as anyhow::Result<Value>
|
||||||
}),
|
}),
|
||||||
@@ -102,7 +104,7 @@ pub async fn eval_timeout(
|
|||||||
isolate.terminate_execution();
|
isolate.terminate_execution();
|
||||||
};
|
};
|
||||||
Error::ExecutionErr(format!(
|
Error::ExecutionErr(format!(
|
||||||
"The expression of evaluation `{expr2}` took too long to execute (>10000ms)"
|
"The expression of evaluation `{expr2}` took too long to execute (>2000ms)"
|
||||||
))
|
))
|
||||||
})??
|
})??
|
||||||
}
|
}
|
||||||
@@ -143,30 +145,24 @@ fn add_closing_bracket(s: &str) -> String {
|
|||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
const SPLIT_PAT: &str = ";";
|
const SPLIT_PAT: &str = ";\n";
|
||||||
async fn eval(
|
async fn eval(
|
||||||
context: &mut JsRuntime,
|
context: &mut JsRuntime,
|
||||||
expr: &str,
|
expr: &str,
|
||||||
env: Vec<(String, serde_json::Value)>,
|
env: Vec<(String, serde_json::Value)>,
|
||||||
|
creds: Option<EvalCreds>,
|
||||||
by_id: Option<IdContext>,
|
by_id: Option<IdContext>,
|
||||||
has_client: bool,
|
base_internal_url: &str,
|
||||||
) -> anyhow::Result<serde_json::Value> {
|
) -> anyhow::Result<serde_json::Value> {
|
||||||
let exprs = expr
|
let expr = expr.trim();
|
||||||
.trim()
|
let expr = format!(
|
||||||
.split(SPLIT_PAT)
|
"{}\nreturn {};",
|
||||||
.map(|x| x.trim())
|
expr.split(SPLIT_PAT)
|
||||||
.filter(|x| !x.is_empty())
|
.take(expr.split(SPLIT_PAT).count() - 1)
|
||||||
.collect::<Vec<&str>>();
|
.join("\n"),
|
||||||
let expr = if exprs.is_empty() {
|
expr.split(SPLIT_PAT).last().unwrap_or_else(|| "")
|
||||||
"return undefined;".to_string()
|
);
|
||||||
} else {
|
let (api_code, by_id_code) = if let Some(EvalCreds { workspace, token }) = creds {
|
||||||
format!(
|
|
||||||
"{};\n return {};",
|
|
||||||
exprs.iter().take(exprs.len() - 1).join(";\n"),
|
|
||||||
exprs.last().unwrap()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let (api_code, by_id_code) = if has_client {
|
|
||||||
let by_id_code = if let Some(by_id) = by_id {
|
let by_id_code = if let Some(by_id) = by_id {
|
||||||
format!(
|
format!(
|
||||||
r#"
|
r#"
|
||||||
@@ -183,12 +179,12 @@ async function result_by_id(node_id) {{
|
|||||||
}}
|
}}
|
||||||
}} else {{
|
}} else {{
|
||||||
let flow_job_id = "{}";
|
let flow_job_id = "{}";
|
||||||
return await Deno.core.opAsync("op_get_id", [ flow_job_id, node_id]);
|
return await Deno.core.opAsync("op_get_id", [workspace, flow_job_id, token, base_url, node_id]);
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
async function get_result(id) {{
|
async function get_result(id) {{
|
||||||
return await Deno.core.opAsync("op_get_result", [id]);
|
return await Deno.core.opAsync("op_get_result", [workspace, id, token, base_url]);
|
||||||
}}
|
}}
|
||||||
const results = new Proxy({{}}, {{
|
const results = new Proxy({{}}, {{
|
||||||
get: function(target, name, receiver) {{
|
get: function(target, name, receiver) {{
|
||||||
@@ -202,12 +198,12 @@ const results = new Proxy({{}}, {{
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
let v_str = match v {
|
let v_str = match v {
|
||||||
JobResult::SingleJob(x) => format!("\"{x}\""),
|
JobResult::SingleJob(x) => x.to_string(),
|
||||||
JobResult::ListJob(x) => {
|
JobResult::ListJob(x) => {
|
||||||
format!("[{}]", x.iter().map(|x| format!("\"{x}\"")).join(","))
|
format!("[{}]", x.iter().map(|x| x.to_string()).join(","))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
format!("\"{k}\": {v_str}")
|
format!("\"{k}\": \"{v_str}\"")
|
||||||
})
|
})
|
||||||
.join(","),
|
.join(","),
|
||||||
by_id.previous_id,
|
by_id.previous_id,
|
||||||
@@ -219,13 +215,17 @@ const results = new Proxy({{}}, {{
|
|||||||
|
|
||||||
let api_code = format!(
|
let api_code = format!(
|
||||||
r#"
|
r#"
|
||||||
|
let workspace = "{workspace}";
|
||||||
|
let base_url = "{}";
|
||||||
|
let token = "{token}";
|
||||||
async function variable(path) {{
|
async function variable(path) {{
|
||||||
return await Deno.core.opAsync("op_variable", [path]);
|
return await Deno.core.opAsync("op_variable", [workspace, path, token, base_url]);
|
||||||
}}
|
}}
|
||||||
async function resource(path) {{
|
async function resource(path) {{
|
||||||
return await Deno.core.opAsync("op_resource", [path]);
|
return await Deno.core.opAsync("op_resource", [workspace, path, token, base_url]);
|
||||||
}}
|
}}
|
||||||
"#,
|
"#,
|
||||||
|
base_internal_url,
|
||||||
);
|
);
|
||||||
(api_code, by_id_code)
|
(api_code, by_id_code)
|
||||||
} else {
|
} else {
|
||||||
@@ -252,7 +252,7 @@ async function resource(path) {{
|
|||||||
.join(""),
|
.join(""),
|
||||||
);
|
);
|
||||||
tracing::debug!("{}", code);
|
tracing::debug!("{}", code);
|
||||||
let global = context.execute_script("<anon>", code)?;
|
let global = context.execute_script("<anon>", &code)?;
|
||||||
let global = context.resolve_value(global).await?;
|
let global = context.resolve_value(global).await?;
|
||||||
|
|
||||||
let scope = &mut context.handle_scope();
|
let scope = &mut context.handle_scope();
|
||||||
@@ -274,83 +274,60 @@ async function resource(path) {{
|
|||||||
|
|
||||||
// TODO: Can we a) share the api configuration here somehow or b) just implement this natively in deno, via the deno client?
|
// TODO: Can we a) share the api configuration here somehow or b) just implement this natively in deno, via the deno client?
|
||||||
#[op]
|
#[op]
|
||||||
async fn op_variable(
|
async fn op_variable(args: Vec<String>) -> Result<String, anyhow::Error> {
|
||||||
op_state: Rc<RefCell<OpState>>,
|
let workspace = &args[0];
|
||||||
args: Vec<String>,
|
let path = &args[1];
|
||||||
) -> Result<String, anyhow::Error> {
|
let token = &args[2];
|
||||||
let path = &args[0];
|
let base_url = &args[3];
|
||||||
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
|
let client = windmill_api_client::create_client(base_url, token.clone());
|
||||||
if let Some(client) = client {
|
let result = client.get_variable(workspace, path, None).await?;
|
||||||
let result = client
|
Ok(result.into_inner().value.unwrap_or_else(|| "".to_owned()))
|
||||||
.get_client()
|
|
||||||
.get_variable(&client.workspace, path, None)
|
|
||||||
.await?;
|
|
||||||
Ok(result.into_inner().value.unwrap_or_else(|| "".to_owned()))
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("No client found in op state");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op]
|
#[op]
|
||||||
async fn op_get_result(
|
async fn op_get_result(args: Vec<String>) -> Result<serde_json::Value, anyhow::Error> {
|
||||||
op_state: Rc<RefCell<OpState>>,
|
let workspace = &args[0];
|
||||||
args: Vec<String>,
|
let id = &args[1];
|
||||||
) -> Result<serde_json::Value, anyhow::Error> {
|
let token = &args[2];
|
||||||
let id = &args[0];
|
let base_url = &args[3];
|
||||||
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
|
let client = windmill_api_client::create_client(base_url, token.clone());
|
||||||
if let Some(client) = client {
|
let result = client
|
||||||
let result = client
|
.get_completed_job(workspace, &id.parse()?)
|
||||||
.get_client()
|
.await?
|
||||||
.get_completed_job_result(&client.workspace, &id.parse()?)
|
.result
|
||||||
.await?
|
.clone();
|
||||||
.clone();
|
Ok(serde_json::json!(result))
|
||||||
Ok(serde_json::json!(result))
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("No client found in op state");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op]
|
#[op]
|
||||||
async fn op_get_id(
|
async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyhow::Error> {
|
||||||
op_state: Rc<RefCell<OpState>>,
|
let workspace = &args[0];
|
||||||
args: Vec<String>,
|
let flow_job_id = &args[1];
|
||||||
) -> Result<Option<serde_json::Value>, anyhow::Error> {
|
let token = &args[2];
|
||||||
let flow_job_id = &args[0];
|
let base_url = &args[3];
|
||||||
let node_id = &args[1];
|
let node_id = &args[4];
|
||||||
|
|
||||||
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
|
let client = windmill_api_client::create_client(base_url, token.clone());
|
||||||
if let Some(client) = client {
|
let result = client
|
||||||
let result = client
|
.result_by_id(workspace, flow_job_id, node_id, Some(true))
|
||||||
.get_client()
|
.await
|
||||||
.result_by_id(&client.workspace, flow_job_id, node_id)
|
.map_or(None, |e| Some(e.into_inner()));
|
||||||
.await
|
|
||||||
.map_or(None, |e| Some(e.into_inner()));
|
Ok(result)
|
||||||
Ok(result)
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("No client found in op state");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op]
|
#[op]
|
||||||
async fn op_resource(
|
async fn op_resource(args: Vec<String>) -> Result<serde_json::Value, anyhow::Error> {
|
||||||
op_state: Rc<RefCell<OpState>>,
|
let workspace = &args[0];
|
||||||
args: Vec<String>,
|
let path = &args[1];
|
||||||
) -> Result<serde_json::Value, anyhow::Error> {
|
let token = &args[2];
|
||||||
let path = &args[0];
|
let base_url = &args[3];
|
||||||
|
let client = windmill_api_client::create_client(base_url, token.clone());
|
||||||
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
|
let result = client.get_resource(workspace, path).await?;
|
||||||
if let Some(client) = client {
|
Ok(result
|
||||||
let result = client
|
.into_inner()
|
||||||
.get_client()
|
.value
|
||||||
.get_resource(&client.workspace, path)
|
.unwrap_or_else(|| serde_json::json!({})))
|
||||||
.await?;
|
|
||||||
Ok(result
|
|
||||||
.into_inner()
|
|
||||||
.value
|
|
||||||
.unwrap_or_else(|| serde_json::json!({})))
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("No client found in op state");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -370,7 +347,7 @@ mod tests {
|
|||||||
let code = "value.test + params.test";
|
let code = "value.test + params.test";
|
||||||
|
|
||||||
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
||||||
let res = eval(&mut runtime, code, env, None, false).await?;
|
let res = eval(&mut runtime, code, env, None, None, String::new().as_str()).await?;
|
||||||
assert_eq!(res, json!(4));
|
assert_eq!(res, json!(4));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -383,7 +360,7 @@ mod tests {
|
|||||||
multiline template`";
|
multiline template`";
|
||||||
|
|
||||||
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
||||||
let res = eval(&mut runtime, code, env, None, false).await?;
|
let res = eval(&mut runtime, code, env, None, None, String::new().as_str()).await?;
|
||||||
assert_eq!(res, json!("my 5\nmultiline template"));
|
assert_eq!(res, json!("my 5\nmultiline template"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -396,7 +373,7 @@ multiline template`";
|
|||||||
];
|
];
|
||||||
let code = r#"params.test"#;
|
let code = r#"params.test"#;
|
||||||
|
|
||||||
let res = eval_timeout(code.to_string(), env, None, None).await?;
|
let res = eval_timeout(code.to_string(), env, None, None, String::new().as_str()).await?;
|
||||||
assert_eq!(res, json!(2));
|
assert_eq!(res, json!(2));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
53
benchmarks/example_fake_user.json
Normal file
53
benchmarks/example_fake_user.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"type": "RANDOM",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"weight": 1,
|
||||||
|
"action": {
|
||||||
|
"type": "RANDOM",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"weight": 1,
|
||||||
|
"action": {
|
||||||
|
"type": "PREVIEW_SCRIPT",
|
||||||
|
"workspace": "demo",
|
||||||
|
"language": "deno",
|
||||||
|
"args": {},
|
||||||
|
"content": "export async function main() { return \"Hello World\"; }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weight": 1,
|
||||||
|
"action": {
|
||||||
|
"type": "PREVIEW_SCRIPT",
|
||||||
|
"workspace": "demo",
|
||||||
|
"language": "python3",
|
||||||
|
"args": {},
|
||||||
|
"content": "def main(): return \"Hello World\";"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weight": 1,
|
||||||
|
"action": {
|
||||||
|
"type": "PREVIEW_SCRIPT",
|
||||||
|
"workspace": "demo",
|
||||||
|
"language": "go",
|
||||||
|
"args": {},
|
||||||
|
"content": "func main() { return \"Hello World\" }"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weight": 0.5,
|
||||||
|
"action": {
|
||||||
|
"type": "PREVIEW_SCRIPT",
|
||||||
|
"workspace": "demo",
|
||||||
|
"language": "deno",
|
||||||
|
"args": {},
|
||||||
|
"content": "import { delay } from \"https://deno.land/std@0.131.0/async/delay.ts\"; export async function main() { await delay(1000); return \"Hello World\"; }"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "PREVIEW_SCRIPT",
|
|
||||||
"workspace": "demo",
|
|
||||||
"language": "deno",
|
|
||||||
"args": {},
|
|
||||||
"content": "export async function main() { return \"Hello World\"; }"
|
|
||||||
}
|
|
||||||
@@ -27,60 +27,60 @@ await new Command()
|
|||||||
"The number of workers to run at once.",
|
"The number of workers to run at once.",
|
||||||
{
|
{
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-s --seconds <seconds:number>",
|
"-s --seconds <seconds:number>",
|
||||||
"How long to run the benchmark for (in seconds).",
|
"How long to run the benchmark for (in seconds).",
|
||||||
{
|
{
|
||||||
default: 30,
|
default: 30,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.option("--max <max:number>", "Maximum number of operations performed.")
|
.option("--max <max:number>", "Maximum number of operations performed.")
|
||||||
.option("-e --email <email:string>", "The email to use to login.")
|
.option("-e --email <email:string>", "The email to use to login.")
|
||||||
.option("-p --password <password:string>", "The password to use to login.")
|
.option("-p --password <password:string>", "The password to use to login.")
|
||||||
.env(
|
.env(
|
||||||
"WM_TOKEN=<token:string>",
|
"WM_TOKEN=<token:string>",
|
||||||
"The token to use when talking to the API server. Preferred over manual login.",
|
"The token to use when talking to the API server. Preferred over manual login."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-t --token <token:string>",
|
"-t --token <token:string>",
|
||||||
"The token to use when talking to the API server. Preferred over manual login.",
|
"The token to use when talking to the API server. Preferred over manual login."
|
||||||
)
|
)
|
||||||
.env(
|
.env(
|
||||||
"WM_WORKSPACE=<workspace:string>",
|
"WM_WORKSPACE=<workspace:string>",
|
||||||
"The workspace to spawn scripts from.",
|
"The workspace to spawn scripts from."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-w --workspace <workspace:string>",
|
"-w --workspace <workspace:string>",
|
||||||
"The workspace to spawn scripts from.",
|
"The workspace to spawn scripts from.",
|
||||||
{ default: "starter" },
|
{ default: "starter" }
|
||||||
)
|
)
|
||||||
.option("-m --metrics <metrics:string>", "The url to scrape metrics from.", {
|
.option("-m --metrics <metrics:string>", "The url to scrape metrics from.", {
|
||||||
default: "http://localhost:8001/metrics",
|
default: "http://localhost:8001/metrics",
|
||||||
})
|
})
|
||||||
.option(
|
.option(
|
||||||
"--export-json <export_json:string>",
|
"--export-json <export_json:string>",
|
||||||
"If set, exports will be into a JSON file.",
|
"If set, exports will be into a JSON file."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--export-csv <export_csv:string>",
|
"--export-csv <export_csv:string>",
|
||||||
"If set, exports will be into a csv file.",
|
"If set, exports will be into a csv file."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--export-histograms [histograms...:string]",
|
"--export-histograms [histograms...:string]",
|
||||||
"Mark metrics (without label) that are reported as histograms to export.",
|
"Mark metrics (without label) that are reported as histograms to export."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--export-simple [simple...:string]",
|
"--export-simple [simple...:string]",
|
||||||
"Mark metrics (without label) that are reported as simple values.",
|
"Mark metrics (without label) that are reported as simple values."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--maximum-throughput <maximum_throughput:number>",
|
"--maximum-throughput <maximum_throughput:number>",
|
||||||
"Maximum number of jobs/flows to start in one second.",
|
"Maximum number of jobs/flows to start in one second.",
|
||||||
{
|
{
|
||||||
default: Infinity,
|
default: Infinity,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.option("--use-flows", "Run flows instead of jobs.")
|
.option("--use-flows", "Run flows instead of jobs.")
|
||||||
.option("--custom <custom_path:string>", "Use custom actions during bench")
|
.option("--custom <custom_path:string>", "Use custom actions during bench")
|
||||||
@@ -89,11 +89,11 @@ await new Command()
|
|||||||
"The maximum time in ms to wait for jobs to complete.",
|
"The maximum time in ms to wait for jobs to complete.",
|
||||||
{
|
{
|
||||||
default: 90000,
|
default: 90000,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--continous",
|
"--continous",
|
||||||
"Run the benchmark forever. This effectively disables metric collection & exports. No zombie jobs will be tracked.",
|
"Run the benchmark forever. This effectively disables metric collection & exports. No zombie jobs will be tracked."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--histogram-buckets [buckets...:string]",
|
"--histogram-buckets [buckets...:string]",
|
||||||
@@ -114,7 +114,7 @@ await new Command()
|
|||||||
"0.01",
|
"0.01",
|
||||||
"0.005",
|
"0.005",
|
||||||
],
|
],
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.action(
|
.action(
|
||||||
async ({
|
async ({
|
||||||
@@ -162,7 +162,7 @@ await new Command()
|
|||||||
new URL("./scraper.ts", import.meta.url).href,
|
new URL("./scraper.ts", import.meta.url).href,
|
||||||
{
|
{
|
||||||
type: "module",
|
type: "module",
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
metrics_worker.postMessage({
|
metrics_worker.postMessage({
|
||||||
@@ -192,8 +192,8 @@ await new Command()
|
|||||||
zombieTimeout,
|
zombieTimeout,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
4,
|
4
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
@@ -245,22 +245,19 @@ await new Command()
|
|||||||
const updateState = setInterval(async () => {
|
const updateState = setInterval(async () => {
|
||||||
const elapsed = start ? Math.ceil((Date.now() - start) / 1000) : 0;
|
const elapsed = start ? Math.ceil((Date.now() - start) / 1000) : 0;
|
||||||
const sum = jobsSent.reduce((a, b) => a + b, 0);
|
const sum = jobsSent.reduce((a, b) => a + b, 0);
|
||||||
const queue_length = (await (await fetch(
|
const queue_length = (
|
||||||
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
|
await windmill.JobService.listQueue({
|
||||||
{ headers: { ["Authorization"]: "Bearer " + config.token } },
|
workspace: config.workspace_id,
|
||||||
)).json()).database_length;
|
})
|
||||||
|
).length;
|
||||||
await Deno.stdout.write(
|
await Deno.stdout.write(
|
||||||
enc(
|
enc(
|
||||||
`elapsed: ${elapsed}/${seconds} | jobs sent: ${
|
`elapsed: ${elapsed}/${seconds} | jobs sent: ${JSON.stringify(
|
||||||
JSON.stringify(
|
jobsSent
|
||||||
jobsSent,
|
)} (sum: ${sum} thr: ${(sum / elapsed).toFixed(
|
||||||
)
|
2
|
||||||
} (sum: ${sum} thr: ${
|
)}) | queue: ${queue_length} \r`
|
||||||
(sum / elapsed).toFixed(
|
)
|
||||||
2,
|
|
||||||
)
|
|
||||||
}) | queue: ${queue_length} \r`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
@@ -287,7 +284,7 @@ await new Command()
|
|||||||
|
|
||||||
const sum = jobsSent.reduce((a, b) => a + b, 0);
|
const sum = jobsSent.reduce((a, b) => a + b, 0);
|
||||||
await Deno.stdout.write(
|
await Deno.stdout.write(
|
||||||
enc(" ".padStart(30) + `\rduration: ${seconds} | jobs sent: ${sum}\n`),
|
enc(" ".padStart(30) + `\rduration: ${seconds} | jobs sent: ${sum}\n`)
|
||||||
);
|
);
|
||||||
|
|
||||||
const shutdown_start = Date.now();
|
const shutdown_start = Date.now();
|
||||||
@@ -305,7 +302,7 @@ await new Command()
|
|||||||
};
|
};
|
||||||
worker.addEventListener("message", l);
|
worker.addEventListener("message", l);
|
||||||
worker.postMessage(
|
worker.postMessage(
|
||||||
Number.isSafeInteger(zombieTimeout) ? zombieTimeout : 90000,
|
Number.isSafeInteger(zombieTimeout) ? zombieTimeout : 90000
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -321,10 +318,11 @@ await new Command()
|
|||||||
console.log("incorrect results: ", incorrect_results);
|
console.log("incorrect results: ", incorrect_results);
|
||||||
console.log(
|
console.log(
|
||||||
"queue length:",
|
"queue length:",
|
||||||
(await (await fetch(
|
(
|
||||||
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
|
await windmill.JobService.listQueue({
|
||||||
{ headers: { ["Authorization"]: "Bearer " + config.token } },
|
workspace: config.workspace_id,
|
||||||
)).json()).database_length,
|
})
|
||||||
|
).length
|
||||||
);
|
);
|
||||||
|
|
||||||
metrics_worker!.postMessage("stop");
|
metrics_worker!.postMessage("stop");
|
||||||
@@ -348,7 +346,7 @@ await new Command()
|
|||||||
const value = values[i]!;
|
const value = values[i]!;
|
||||||
const mean = value.reduce((acc, e) => acc + e, 0) / values.length;
|
const mean = value.reduce((acc, e) => acc + e, 0) / values.length;
|
||||||
const stdev = Math.sqrt(
|
const stdev = Math.sqrt(
|
||||||
value.reduce((acc, e) => acc + (e - mean) ** 2) / values.length,
|
value.reduce((acc, e) => acc + (e - mean) ** 2) / values.length
|
||||||
);
|
);
|
||||||
obj[name] = { mean, stdev };
|
obj[name] = { mean, stdev };
|
||||||
}
|
}
|
||||||
@@ -376,6 +374,6 @@ await new Command()
|
|||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
console.log("done");
|
console.log("done");
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.parse();
|
.parse();
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ const promise = new Promise<{
|
|||||||
continous: boolean;
|
continous: boolean;
|
||||||
max_per_worker: number;
|
max_per_worker: number;
|
||||||
custom: Action | undefined;
|
custom: Action | undefined;
|
||||||
server: string;
|
|
||||||
token: string;
|
|
||||||
}>((resolve, _reject) => {
|
}>((resolve, _reject) => {
|
||||||
self.onmessage = (evt) => {
|
self.onmessage = (evt) => {
|
||||||
const sharedConfig = evt.data;
|
const sharedConfig = evt.data;
|
||||||
@@ -26,8 +24,6 @@ const promise = new Promise<{
|
|||||||
continous: sharedConfig.continous,
|
continous: sharedConfig.continous,
|
||||||
max_per_worker: sharedConfig.max_per_worker,
|
max_per_worker: sharedConfig.max_per_worker,
|
||||||
custom: sharedConfig.custom,
|
custom: sharedConfig.custom,
|
||||||
server: sharedConfig.server,
|
|
||||||
token: sharedConfig.token,
|
|
||||||
};
|
};
|
||||||
self.name = "Worker " + sharedConfig.i;
|
self.name = "Worker " + sharedConfig.i;
|
||||||
resolve(config);
|
resolve(config);
|
||||||
@@ -50,20 +46,19 @@ const updateStatusInterval = setInterval(() => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
while (cont) {
|
while (cont) {
|
||||||
const queue_length = (await (await fetch(
|
const queue_length = (
|
||||||
config.server + "/api/w/" + config.workspace_id + "/jobs/queue/count",
|
await windmill.JobService.listQueue({ workspace: config.workspace_id })
|
||||||
{ headers: { ["Authorization"]: "Bearer " + config.token } },
|
).length;
|
||||||
)).json()).database_length;
|
if (queue_length > 500) {
|
||||||
if (queue_length > 2500) {
|
|
||||||
console.log(
|
console.log(
|
||||||
`queue length: ${queue_length} > 2500. waiting... `,
|
`queue length: ${queue_length} > 500. waiting... `
|
||||||
);
|
);
|
||||||
await sleep(0.5);
|
await sleep(0.5);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(total_spawned * 1000) / (Date.now() - start_time) >
|
(total_spawned * 1000) / (Date.now() - start_time) >
|
||||||
config.per_worker_throughput
|
config.per_worker_throughput
|
||||||
) {
|
) {
|
||||||
console.log("at maximum throughput. waiting...");
|
console.log("at maximum throughput. waiting...");
|
||||||
await sleep(0.1);
|
await sleep(0.1);
|
||||||
@@ -143,13 +138,13 @@ while (outstanding.length > 0 && Date.now() < end_time) {
|
|||||||
await Deno.stdout.write(
|
await Deno.stdout.write(
|
||||||
enc(
|
enc(
|
||||||
`uuid: ${uuid}, queue length: ${
|
`uuid: ${uuid}, queue length: ${
|
||||||
(await (await fetch(
|
(
|
||||||
config.server + "/api/w/" + config.workspace_id +
|
await windmill.JobService.listQueue({
|
||||||
"/jobs/queue/count",
|
workspace: config.workspace_id,
|
||||||
{ headers: { ["Authorization"]: "Bearer " + config.token } },
|
})
|
||||||
)).json()).database_length
|
).length
|
||||||
} \r`,
|
} \r`
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
} else if (!config.useFlows) {
|
} else if (!config.useFlows) {
|
||||||
r = r as api.CompletedJob;
|
r = r as api.CompletedJob;
|
||||||
@@ -161,7 +156,7 @@ while (outstanding.length > 0 && Date.now() < end_time) {
|
|||||||
" != " +
|
" != " +
|
||||||
uuid +
|
uuid +
|
||||||
"job: \n" +
|
"job: \n" +
|
||||||
JSON.stringify(r, null, 2),
|
JSON.stringify(r, null, 2)
|
||||||
);
|
);
|
||||||
incorrect_results++;
|
incorrect_results++;
|
||||||
}
|
}
|
||||||
|
|||||||
130
cli/apps.ts
130
cli/apps.ts
@@ -1,22 +1,12 @@
|
|||||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
import { Any, model, property } from "./decoverto.ts";
|
||||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
|
||||||
import {
|
import {
|
||||||
AppService,
|
AppService,
|
||||||
AppWithLastVersion,
|
AppWithLastVersion,
|
||||||
colors,
|
colors,
|
||||||
Command,
|
|
||||||
ListableApp,
|
|
||||||
microdiff,
|
microdiff,
|
||||||
Policy,
|
Policy,
|
||||||
Table,
|
|
||||||
} from "./deps.ts";
|
} from "./deps.ts";
|
||||||
import {
|
import { Difference, PushDiffs, Resource, setValueByPath } from "./types.ts";
|
||||||
Difference,
|
|
||||||
GlobalOptions,
|
|
||||||
PushDiffs,
|
|
||||||
Resource,
|
|
||||||
setValueByPath,
|
|
||||||
} from "./types.ts";
|
|
||||||
|
|
||||||
@model()
|
@model()
|
||||||
export class AppFile implements Resource, PushDiffs {
|
export class AppFile implements Resource, PushDiffs {
|
||||||
@@ -27,6 +17,7 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
@property(Any)
|
@property(Any)
|
||||||
policy: Policy;
|
policy: Policy;
|
||||||
|
|
||||||
|
|
||||||
constructor(value: string, summary: string, policy: Policy) {
|
constructor(value: string, summary: string, policy: Policy) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.summary = summary;
|
this.summary = summary;
|
||||||
@@ -35,20 +26,16 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
async pushDiffs(
|
async pushDiffs(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[]
|
diffs: Difference[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let app: AppWithLastVersion | undefined = undefined;
|
if (await AppService.existsApp({ workspace, path: remotePath })) {
|
||||||
try {
|
|
||||||
app = await AppService.getAppByPath({ workspace, path: remotePath });
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
if (app) {
|
|
||||||
console.log(
|
console.log(
|
||||||
colors.bold.yellow(
|
colors.bold.yellow(
|
||||||
`Applying ${diffs.length} diffs to existing app... ${remotePath}`
|
`Applying ${diffs.length} diffs to existing app...`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const changeset: {
|
const changeset: {
|
||||||
|
path?: string | undefined;
|
||||||
summary?: string | undefined;
|
summary?: string | undefined;
|
||||||
value?: any;
|
value?: any;
|
||||||
policy?: Policy | undefined;
|
policy?: Policy | undefined;
|
||||||
@@ -56,10 +43,14 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
for (const diff of diffs) {
|
for (const diff of diffs) {
|
||||||
if (
|
if (
|
||||||
diff.type !== "REMOVE" &&
|
diff.type !== "REMOVE" &&
|
||||||
diff.path[0] !== "value" &&
|
(
|
||||||
diff.path[0] !== "policy" &&
|
diff.path[0] !== "value" && diff.path[0] !== "policy" && (
|
||||||
(diff.path.length !== 1 ||
|
diff.path.length !== 1 ||
|
||||||
!["summary"].includes(diff.path[0] as string))
|
!["path", "summary"].includes(
|
||||||
|
diff.path[0] as string,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
throw new Error("Invalid app diff with path " + diff.path);
|
throw new Error("Invalid app diff with path " + diff.path);
|
||||||
}
|
}
|
||||||
@@ -70,21 +61,8 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const hasChanges = Object.values(changeset).some((v) =>
|
||||||
(!changeset?.policy ||
|
v !== null && typeof v !== "undefined"
|
||||||
JSON.stringify(changeset?.policy) == JSON.stringify(app.policy)) &&
|
|
||||||
(!changeset?.value ||
|
|
||||||
JSON.stringify(changeset?.value) == JSON.stringify(app.value)) &&
|
|
||||||
(!changeset?.summary || changeset.summary == app.summary)
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
colors.yellow(`No changes to push for app ${remotePath}, skipping`)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasChanges = Object.values(changeset).some(
|
|
||||||
(v) => v !== null && typeof v !== "undefined"
|
|
||||||
);
|
);
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return;
|
return;
|
||||||
@@ -97,7 +75,6 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(colors.yellow.bold("Creating new app..."));
|
console.log(colors.yellow.bold("Creating new app..."));
|
||||||
|
|
||||||
await AppService.createApp({
|
await AppService.createApp({
|
||||||
workspace,
|
workspace,
|
||||||
requestBody: {
|
requestBody: {
|
||||||
@@ -110,70 +87,19 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
|
let existing: AppWithLastVersion | undefined;
|
||||||
|
try {
|
||||||
|
existing = await AppService.getAppByPath({
|
||||||
|
workspace: workspace,
|
||||||
|
path: remotePath,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
existing = undefined;
|
||||||
|
}
|
||||||
await this.pushDiffs(
|
await this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff({}, this, { cyclesFix: false })
|
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function list(opts: GlobalOptions) {
|
|
||||||
const workspace = await resolveWorkspace(opts);
|
|
||||||
await requireLogin(opts);
|
|
||||||
|
|
||||||
let page = 0;
|
|
||||||
const perPage = 10;
|
|
||||||
const total: ListableApp[] = [];
|
|
||||||
while (true) {
|
|
||||||
const res = await AppService.listApps({
|
|
||||||
workspace: workspace.workspaceId,
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
});
|
|
||||||
page += 1;
|
|
||||||
total.push(...res);
|
|
||||||
if (res.length < perPage) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new Table()
|
|
||||||
.header(["path", "summary"])
|
|
||||||
.padding(2)
|
|
||||||
.border(true)
|
|
||||||
.body(total.map((x) => [x.path, x.summary]))
|
|
||||||
.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function push(opts: GlobalOptions, filePath: string) {
|
|
||||||
const remotePath = filePath.split(".")[0];
|
|
||||||
if (!validatePath(remotePath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const workspace = await resolveWorkspace(opts);
|
|
||||||
await requireLogin(opts);
|
|
||||||
|
|
||||||
await pushApp(filePath, workspace.workspaceId, remotePath);
|
|
||||||
console.log(colors.bold.underline.green("App pushed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function pushApp(
|
|
||||||
filePath: string,
|
|
||||||
workspace: string,
|
|
||||||
remotePath: string
|
|
||||||
) {
|
|
||||||
const data = decoverto
|
|
||||||
.type(AppFile)
|
|
||||||
.rawToInstance(await Deno.readTextFile(filePath));
|
|
||||||
await data.push(workspace, remotePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = new Command()
|
|
||||||
.description("app related commands")
|
|
||||||
.action(list as any)
|
|
||||||
.command("push", "push a local app ")
|
|
||||||
.arguments("<file_path:file>")
|
|
||||||
.action(push as any);
|
|
||||||
|
|
||||||
export default command;
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ export type Context = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function tryResolveWorkspace(
|
async function tryResolveWorkspace(
|
||||||
opts: GlobalOptions
|
opts: GlobalOptions,
|
||||||
): Promise<
|
): Promise<
|
||||||
{ isError: false; value: Workspace } | { isError: true; error: string }
|
{ isError: false; value: Workspace } | { isError: true; error: string }
|
||||||
> {
|
> {
|
||||||
const cache = (opts as any).__secret_workspace;
|
const cache = (opts as any).__secret_workspace;
|
||||||
if (cache) return { isError: false, value: cache };
|
if (cache) return cache;
|
||||||
|
|
||||||
if (opts.workspace) {
|
if (opts.workspace) {
|
||||||
const e = await getWorkspaceByName(opts.workspace);
|
const e = await getWorkspaceByName(opts.workspace);
|
||||||
@@ -49,23 +49,21 @@ async function tryResolveWorkspace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveWorkspace(
|
export async function resolveWorkspace(
|
||||||
opts: GlobalOptions
|
opts: GlobalOptions,
|
||||||
): Promise<Workspace> {
|
): Promise<Workspace> {
|
||||||
const res = await tryResolveWorkspace(opts);
|
const res = await tryResolveWorkspace(opts);
|
||||||
if (res.isError) {
|
if (res.isError) {
|
||||||
console.log(colors.red.bold(res.error));
|
console.log(res.error);
|
||||||
return Deno.exit(-1);
|
return Deno.exit(-1);
|
||||||
} else {
|
} else {
|
||||||
return res.value;
|
return res.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requireLogin(
|
export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo> {
|
||||||
opts: GlobalOptions
|
|
||||||
): Promise<GlobalUserInfo> {
|
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
let token = await tryGetLoginInfo(opts);
|
|
||||||
|
|
||||||
|
let token = await tryGetLoginInfo(opts);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
token = workspace.token;
|
token = workspace.token;
|
||||||
}
|
}
|
||||||
@@ -76,26 +74,26 @@ export async function requireLogin(
|
|||||||
return await UserService.globalWhoami();
|
return await UserService.globalWhoami();
|
||||||
} catch {
|
} catch {
|
||||||
console.log(
|
console.log(
|
||||||
"! Could not reach API given existing credentials. Attempting to reauth..."
|
"! Could not reach API given existing credentials. Attempting to reauth...",
|
||||||
);
|
);
|
||||||
const newToken = await loginInteractive(workspace.remote);
|
const newToken = await loginInteractive(workspace.remote);
|
||||||
if (!newToken) {
|
if (!newToken) {
|
||||||
throw new Error("Could not reauth");
|
throw new Error("Could not reauth");
|
||||||
}
|
}
|
||||||
removeWorkspace(workspace.name, false, opts);
|
removeWorkspace(workspace.name);
|
||||||
workspace.token = newToken;
|
workspace.token = newToken;
|
||||||
addWorkspace(workspace, opts);
|
addWorkspace(workspace);
|
||||||
|
|
||||||
setClient(
|
setClient(
|
||||||
token,
|
token,
|
||||||
workspace.remote.substring(0, workspace.remote.length - 1)
|
workspace.remote.substring(0, workspace.remote.length - 1),
|
||||||
);
|
);
|
||||||
return await UserService.globalWhoami();
|
return await UserService.globalWhoami();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function tryResolveVersion(
|
export async function tryResolveVersion(
|
||||||
opts: GlobalOptions
|
opts: GlobalOptions,
|
||||||
): Promise<number | undefined> {
|
): Promise<number | undefined> {
|
||||||
if ((opts as any).__cache_version) {
|
if ((opts as any).__cache_version) {
|
||||||
return (opts as any).__cache_version;
|
return (opts as any).__cache_version;
|
||||||
@@ -105,24 +103,41 @@ export async function tryResolveVersion(
|
|||||||
if (workspaceRes.isError) return undefined;
|
if (workspaceRes.isError) return undefined;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
new URL(new URL(workspaceRes.value.remote).origin + "/api/version")
|
new URL(new URL(workspaceRes.value.remote).origin + "/api/version"),
|
||||||
);
|
);
|
||||||
const version = await response.text();
|
const version = await response.text();
|
||||||
try {
|
try {
|
||||||
return Number.parseInt(
|
return Number.parseInt(
|
||||||
version.split("-", 1)[0].replaceAll(".", "").replace("v", "")
|
version.split("-", 1)[0].replaceAll(".", "").replace("v", ""),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validatePath(path: string): boolean {
|
export async function validatePath(
|
||||||
if (!(path.startsWith("g") || path.startsWith("u") || path.startsWith("f"))) {
|
opts: GlobalOptions,
|
||||||
|
path: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const backendVersion = await tryResolveVersion(opts);
|
||||||
|
if (path.startsWith("f")) {
|
||||||
|
if (!backendVersion || backendVersion >= 1550) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`Attempting to use folders, but the current remote does not have support. Remote version is ${backendVersion} but folders are supported from 1560.`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(path.startsWith("g") ||
|
||||||
|
path.startsWith("u"))
|
||||||
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.red(
|
colors.red(
|
||||||
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/..."
|
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/...",
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export {
|
|||||||
DenoLandProvider,
|
DenoLandProvider,
|
||||||
UpgradeCommand,
|
UpgradeCommand,
|
||||||
} from "https://deno.land/x/cliffy@v0.25.7/command/upgrade/mod.ts";
|
} from "https://deno.land/x/cliffy@v0.25.7/command/upgrade/mod.ts";
|
||||||
export { CompletionsCommand } from "https://deno.land/x/cliffy@v0.25.7/command/completions/mod.ts";
|
|
||||||
// std
|
// std
|
||||||
export * as path from "https://deno.land/std@0.176.0/path/mod.ts";
|
export * as path from "https://deno.land/std@0.176.0/path/mod.ts";
|
||||||
export { ensureDir } from "https://deno.land/std@0.176.0/fs/ensure_dir.ts";
|
export { ensureDir } from "https://deno.land/std@0.176.0/fs/ensure_dir.ts";
|
||||||
@@ -33,7 +33,9 @@ export { passwordGenerator } from "https://deno.land/x/password_generator@latest
|
|||||||
export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
|
export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
|
||||||
export * as cbor from "https://deno.land/x/cbor@v1.4.1/index.js";
|
export * as cbor from "https://deno.land/x/cbor@v1.4.1/index.js";
|
||||||
export { default as Murmurhash3 } from "https://deno.land/x/murmurhash@v1.0.0/mod.ts";
|
export { default as Murmurhash3 } from "https://deno.land/x/murmurhash@v1.0.0/mod.ts";
|
||||||
export { default as microdiff } from "https://deno.land/x/microdiff@v1.3.1/index.ts";
|
export {
|
||||||
|
default as microdiff,
|
||||||
|
} from "https://deno.land/x/microdiff@v1.3.1/index.ts";
|
||||||
export { default as objectHash } from "https://deno.land/x/object_hash@2.0.3.1/mod.ts";
|
export { default as objectHash } from "https://deno.land/x/object_hash@2.0.3.1/mod.ts";
|
||||||
export { default as gitignore_parser } from "npm:gitignore-parser";
|
export { default as gitignore_parser } from "npm:gitignore-parser";
|
||||||
export { default as JSZip } from "npm:jszip@3.7.1";
|
export { default as JSZip } from "npm:jszip@3.7.1";
|
||||||
|
|||||||
74
cli/flow.ts
74
cli/flow.ts
@@ -20,6 +20,7 @@ import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
|||||||
import { resolve, track_job } from "./script.ts";
|
import { resolve, track_job } from "./script.ts";
|
||||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||||
|
|
||||||
|
|
||||||
// this is effectively "OpenFlow" but a copy as it is accepted by the CLI
|
// this is effectively "OpenFlow" but a copy as it is accepted by the CLI
|
||||||
@model()
|
@model()
|
||||||
export class FlowFile implements Resource, PushDiffs {
|
export class FlowFile implements Resource, PushDiffs {
|
||||||
@@ -39,7 +40,7 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
async pushDiffs(
|
async pushDiffs(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[]
|
diffs: Difference[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (
|
if (
|
||||||
await FlowService.existsFlowByPath({
|
await FlowService.existsFlowByPath({
|
||||||
@@ -49,8 +50,8 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.bold.yellow(
|
colors.bold.yellow(
|
||||||
`Applying ${diffs.length} diffs to existing flow... ${remotePath}`
|
`Applying ${diffs.length} diffs to existing flow... ${remotePath}`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Make these optional in backend (not path ofc)
|
// TODO: Make these optional in backend (not path ofc)
|
||||||
@@ -65,11 +66,14 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
for (const diff of diffs) {
|
for (const diff of diffs) {
|
||||||
if (
|
if (
|
||||||
diff.type !== "REMOVE" &&
|
diff.type !== "REMOVE" &&
|
||||||
diff.path[0] !== "value" &&
|
(
|
||||||
(diff.path.length !== 1 ||
|
diff.path[0] !== "value" && (
|
||||||
!["summary", "description", "schema"].includes(
|
diff.path.length !== 1 ||
|
||||||
diff.path[0] as string
|
!["summary", "description", "schema"].includes(
|
||||||
))
|
diff.path[0] as string,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
throw new Error("Invalid flow diff with path " + diff.path);
|
throw new Error("Invalid flow diff with path " + diff.path);
|
||||||
}
|
}
|
||||||
@@ -79,8 +83,8 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
setValueByPath(changeset, diff.path, null);
|
setValueByPath(changeset, diff.path, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const hasChanges = Object.values(changeset).some(
|
const hasChanges = Object.values(changeset).some((v) =>
|
||||||
(v) => v !== null && typeof v !== "undefined"
|
v !== null && typeof v !== "undefined"
|
||||||
);
|
);
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return;
|
return;
|
||||||
@@ -89,8 +93,7 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
const update = {
|
const update = {
|
||||||
...changeset,
|
...changeset,
|
||||||
...base_changeset,
|
...base_changeset,
|
||||||
};
|
}
|
||||||
|
|
||||||
await FlowService.updateFlow({
|
await FlowService.updateFlow({
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
path: remotePath,
|
path: remotePath,
|
||||||
@@ -111,10 +114,20 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
|
let remote: Flow | undefined;
|
||||||
|
try {
|
||||||
|
remote = await FlowService.getFlowByPath({
|
||||||
|
workspace,
|
||||||
|
path: remotePath,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
remote = undefined;
|
||||||
|
}
|
||||||
await this.pushDiffs(
|
await this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff({}, this, { cyclesFix: false })
|
microdiff(remote ?? {}, this, { cyclesFix: false }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,24 +135,24 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
type Options = GlobalOptions;
|
type Options = GlobalOptions;
|
||||||
|
|
||||||
async function push(opts: Options, filePath: string, remotePath: string) {
|
async function push(opts: Options, filePath: string, remotePath: string) {
|
||||||
if (!validatePath(remotePath)) {
|
if (!await validatePath(opts, remotePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
await pushFlow(filePath, workspace.workspaceId, remotePath);
|
await pushFlow(filePath, workspace.remote, remotePath);
|
||||||
console.log(colors.bold.underline.green("Flow pushed"));
|
console.log(colors.bold.underline.green("Flow pushed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pushFlow(
|
export async function pushFlow(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string
|
remotePath: string,
|
||||||
) {
|
) {
|
||||||
const data = decoverto
|
const data = decoverto.type(FlowFile).rawToInstance(
|
||||||
.type(FlowFile)
|
await Deno.readTextFile(filePath),
|
||||||
.rawToInstance(await Deno.readTextFile(filePath));
|
);
|
||||||
await data.push(workspace, remotePath);
|
await data.push(workspace, remotePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,20 +181,26 @@ async function list(opts: GlobalOptions & { showArchived?: boolean }) {
|
|||||||
.header(["path", "summary", "edited by"])
|
.header(["path", "summary", "edited by"])
|
||||||
.padding(2)
|
.padding(2)
|
||||||
.border(true)
|
.border(true)
|
||||||
.body(total.map((x) => [x.path, x.summary, x.edited_by]))
|
.body(
|
||||||
|
total.map((x) => [
|
||||||
|
x.path,
|
||||||
|
x.summary,
|
||||||
|
x.edited_by,
|
||||||
|
]),
|
||||||
|
)
|
||||||
.render();
|
.render();
|
||||||
}
|
}
|
||||||
async function run(
|
async function run(
|
||||||
opts: GlobalOptions & {
|
opts: GlobalOptions & {
|
||||||
data?: string;
|
input: string[];
|
||||||
silent: boolean;
|
silent: boolean;
|
||||||
},
|
},
|
||||||
path: string
|
path: string,
|
||||||
) {
|
) {
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
const input = opts.data ? await resolve(opts.data) : {};
|
const input = await resolve(opts.input);
|
||||||
|
|
||||||
const id = await JobService.runFlowByPath({
|
const id = await JobService.runFlowByPath({
|
||||||
workspace: workspace.workspaceId,
|
workspace: workspace.workspaceId,
|
||||||
@@ -217,7 +236,6 @@ async function run(
|
|||||||
|
|
||||||
if (!opts.silent) {
|
if (!opts.silent) {
|
||||||
console.log(colors.green.underline.bold("Flow ran to completion"));
|
console.log(colors.green.underline.bold("Flow ran to completion"));
|
||||||
console.log();
|
|
||||||
}
|
}
|
||||||
const jobInfo = await JobService.getCompletedJob({
|
const jobInfo = await JobService.getCompletedJob({
|
||||||
workspace: workspace.workspaceId,
|
workspace: workspace.workspaceId,
|
||||||
@@ -232,19 +250,19 @@ const command = new Command()
|
|||||||
.action(list as any)
|
.action(list as any)
|
||||||
.command(
|
.command(
|
||||||
"push",
|
"push",
|
||||||
"push a local flow spec. This overrides any remote versions."
|
"push a local flow spec. This overrides any remote versions.",
|
||||||
)
|
)
|
||||||
.arguments("<file_path:string> <remote_path:string>")
|
.arguments("<file_path:string> <remote_path:string>")
|
||||||
.action(push as any)
|
.action(push as any)
|
||||||
.command("run", "run a flow by path.")
|
.command("run", "run a flow by path.")
|
||||||
.arguments("<path:string>")
|
.arguments("<path:string>")
|
||||||
.option(
|
.option(
|
||||||
"-d --data <data:string>",
|
"-i --input [inputs...:string]",
|
||||||
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-."
|
"Inputs specified as JSON objects or simply as <name>=<value>. Supports file inputs using @<filename> and stdin using @- these also need to be formatted as JSON. Later inputs override earlier ones.",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-s --silent",
|
"-s --silent",
|
||||||
"Do not ouput anything other then the final output. Useful for scripting."
|
"Do not ouput anything other then the final output. Useful for scripting.",
|
||||||
)
|
)
|
||||||
.action(run as any);
|
.action(run as any);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { colors, Command, FolderService, microdiff } from "./deps.ts";
|
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
|
||||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||||
import {
|
import {
|
||||||
Difference,
|
Difference,
|
||||||
@@ -20,16 +20,8 @@ import {
|
|||||||
export class FolderFile implements Resource, PushDiffs {
|
export class FolderFile implements Resource, PushDiffs {
|
||||||
@property(array(() => String))
|
@property(array(() => String))
|
||||||
owners: Array<string> | undefined;
|
owners: Array<string> | undefined;
|
||||||
@property(
|
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
|
||||||
map(
|
|
||||||
() => String,
|
|
||||||
() => Boolean,
|
|
||||||
{ shape: MapShape.Object }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
extra_perms: Map<string, boolean> | undefined;
|
extra_perms: Map<string, boolean> | undefined;
|
||||||
@property(() => String)
|
|
||||||
display_name: string | undefined;
|
|
||||||
|
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
if (remotePath.startsWith("/")) {
|
if (remotePath.startsWith("/")) {
|
||||||
@@ -39,17 +31,23 @@ export class FolderFile implements Resource, PushDiffs {
|
|||||||
remotePath = remotePath.substring(2);
|
remotePath = remotePath.substring(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let existing: Folder | undefined;
|
||||||
|
try {
|
||||||
|
existing = await FolderService.getFolder({ workspace, name: remotePath });
|
||||||
|
} catch {
|
||||||
|
existing = undefined;
|
||||||
|
}
|
||||||
await this.pushDiffs(
|
await this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff({}, this, { cyclesFix: false })
|
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pushDiffs(
|
async pushDiffs(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[]
|
diffs: Difference[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (remotePath.startsWith("/")) {
|
if (remotePath.startsWith("/")) {
|
||||||
remotePath = remotePath.substring(1);
|
remotePath = remotePath.substring(1);
|
||||||
@@ -61,34 +59,29 @@ export class FolderFile implements Resource, PushDiffs {
|
|||||||
// TODO: Support this in backend
|
// TODO: Support this in backend
|
||||||
let exists: boolean;
|
let exists: boolean;
|
||||||
try {
|
try {
|
||||||
exists = !!(await FolderService.getFolder({
|
exists = !!await FolderService.getFolder({ workspace, name: remotePath });
|
||||||
workspace,
|
|
||||||
name: remotePath,
|
|
||||||
}));
|
|
||||||
} catch {
|
} catch {
|
||||||
exists = false;
|
exists = false;
|
||||||
}
|
}
|
||||||
if (exists) {
|
if (exists) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.bold.yellow(
|
colors.bold.yellow(
|
||||||
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`
|
`Applying ${diffs.length} diffs to existing folder...`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const changeset: {
|
const changeset: {
|
||||||
owners?: string[] | undefined;
|
owners?: string[] | undefined;
|
||||||
extra_perms?: any;
|
extra_perms?: any;
|
||||||
display_name?: string | undefined;
|
|
||||||
} = {};
|
} = {};
|
||||||
for (const diff of diffs) {
|
for (const diff of diffs) {
|
||||||
if (
|
if (
|
||||||
diff.type !== "REMOVE" &&
|
diff.type !== "REMOVE" &&
|
||||||
(diff.path.length !== 1 ||
|
(
|
||||||
!["owners", "extra_perms", "display_name"].includes(
|
diff.path.length !== 1 ||
|
||||||
diff.path[0] as string
|
!["owners", "extra_perms"].includes(diff.path[0] as string)
|
||||||
))
|
)
|
||||||
) {
|
) {
|
||||||
console.log(diff.path);
|
|
||||||
throw new Error("Invalid folder diff with path " + diff.path);
|
throw new Error("Invalid folder diff with path " + diff.path);
|
||||||
}
|
}
|
||||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||||
@@ -98,27 +91,18 @@ export class FolderFile implements Resource, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanges = Object.values(changeset).some(
|
const hasChanges = Object.values(changeset).some((v) =>
|
||||||
(v) => v !== null && typeof v !== "undefined"
|
v !== null && typeof v !== "undefined"
|
||||||
);
|
);
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await FolderService.updateFolder({
|
await FolderService.updateFolder({
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
name: remotePath,
|
name: remotePath,
|
||||||
requestBody: {
|
requestBody: changeset,
|
||||||
...changeset,
|
});
|
||||||
extra_perms: changeset.extra_perms
|
|
||||||
? Object.fromEntries(this.extra_perms?.entries() ?? [])
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error(colors.red.bold(e.body));
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log(colors.bold.yellow("Creating new folder: " + remotePath));
|
console.log(colors.bold.yellow("Creating new folder: " + remotePath));
|
||||||
await FolderService.createFolder({
|
await FolderService.createFolder({
|
||||||
@@ -137,7 +121,7 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
|||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
if (!validatePath(remotePath)) {
|
if (!await validatePath(opts, remotePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +139,11 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
|||||||
export async function pushFolder(
|
export async function pushFolder(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
remotePath: string
|
remotePath: string,
|
||||||
) {
|
) {
|
||||||
const data = decoverto
|
const data = decoverto.type(FolderFile).rawToInstance(
|
||||||
.type(FolderFile)
|
await Deno.readTextFile(filePath),
|
||||||
.rawToInstance(await Deno.readTextFile(filePath));
|
);
|
||||||
data.push(workspace, remotePath);
|
data.push(workspace, remotePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +151,7 @@ const command = new Command()
|
|||||||
.description("resource related commands")
|
.description("resource related commands")
|
||||||
.command(
|
.command(
|
||||||
"push",
|
"push",
|
||||||
"push a local folder spec. This overrides any remote versions."
|
"push a local folder spec. This overrides any remote versions.",
|
||||||
)
|
)
|
||||||
.arguments("<file_path:string> <remote_path:string>")
|
.arguments("<file_path:string> <remote_path:string>")
|
||||||
.action(push as any);
|
.action(push as any);
|
||||||
|
|||||||
25
cli/main.ts
25
cli/main.ts
@@ -1,11 +1,5 @@
|
|||||||
import {
|
import { Command, DenoLandProvider, UpgradeCommand } from "./deps.ts";
|
||||||
Command,
|
|
||||||
CompletionsCommand,
|
|
||||||
DenoLandProvider,
|
|
||||||
UpgradeCommand,
|
|
||||||
} from "./deps.ts";
|
|
||||||
import flow from "./flow.ts";
|
import flow from "./flow.ts";
|
||||||
import app from "./apps.ts";
|
|
||||||
import script from "./script.ts";
|
import script from "./script.ts";
|
||||||
import workspace from "./workspace.ts";
|
import workspace from "./workspace.ts";
|
||||||
import resource from "./resource.ts";
|
import resource from "./resource.ts";
|
||||||
@@ -19,21 +13,21 @@ import sync from "./sync.ts";
|
|||||||
import { tryResolveVersion } from "./context.ts";
|
import { tryResolveVersion } from "./context.ts";
|
||||||
import { GlobalOptions } from "./types.ts";
|
import { GlobalOptions } from "./types.ts";
|
||||||
|
|
||||||
export const VERSION = "v1.87.0";
|
const VERSION = "v1.70.1";
|
||||||
|
|
||||||
let command: any = new Command()
|
let command: any = new Command()
|
||||||
.name("wmill")
|
.name("wmill")
|
||||||
.description("A simple CLI tool for windmill.")
|
.description("A simple CLI tool for windmill.")
|
||||||
|
.action(() => command.showHelp())
|
||||||
.globalOption(
|
.globalOption(
|
||||||
"--workspace <workspace:string>",
|
"--workspace <workspace:string>",
|
||||||
"Specify the target workspace. This overrides the default workspace."
|
"Specify the target workspace. This overrides the default workspace.",
|
||||||
)
|
)
|
||||||
.globalOption(
|
.globalOption(
|
||||||
"--token <token:string>",
|
"--token <token:string>",
|
||||||
"Specify an API token. This will override any stored token."
|
"Specify an API token. This will override any stored token.",
|
||||||
)
|
)
|
||||||
.version(VERSION)
|
.version(VERSION)
|
||||||
.command("app", app)
|
|
||||||
.command("flow", flow)
|
.command("flow", flow)
|
||||||
.command("script", script)
|
.command("script", script)
|
||||||
.command("workspace", workspace)
|
.command("workspace", workspace)
|
||||||
@@ -65,12 +59,13 @@ let command: any = new Command()
|
|||||||
"--unstable",
|
"--unstable",
|
||||||
],
|
],
|
||||||
provider: new DenoLandProvider({ name: "wmill" }),
|
provider: new DenoLandProvider({ name: "wmill" }),
|
||||||
})
|
}),
|
||||||
)
|
);
|
||||||
.command("completions", new CompletionsCommand());
|
|
||||||
|
|
||||||
if (Number.parseInt(VERSION.replace("v", "").replace(".", "")) > 1700) {
|
if (Number.parseInt(VERSION.replace("v", "").replace(".", "")) > 1700) {
|
||||||
command = command.command("push", push).command("pull", pull);
|
command = command
|
||||||
|
.command("push", push)
|
||||||
|
.command("pull", pull);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
22
cli/pull.ts
22
cli/pull.ts
@@ -5,27 +5,25 @@ import { Workspace } from "./workspace.ts";
|
|||||||
|
|
||||||
export async function downloadZip(
|
export async function downloadZip(
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
plainSecrets: boolean | undefined
|
|
||||||
): Promise<JSZip | undefined> {
|
): Promise<JSZip | undefined> {
|
||||||
const requestHeaders: HeadersInit = new Headers();
|
const requestHeaders: HeadersInit = new Headers();
|
||||||
requestHeaders.set("Authorization", "Bearer " + workspace.token);
|
requestHeaders.set("Authorization", "Bearer " + workspace.token);
|
||||||
requestHeaders.set("Content-Type", "application/octet-stream");
|
requestHeaders.set("Content-Type", "application/octet-stream");
|
||||||
|
|
||||||
const zipResponse = await fetch(
|
const zipResponse = await fetch(
|
||||||
workspace.remote +
|
workspace.remote + "api/w/" + workspace.workspaceId +
|
||||||
"api/w/" +
|
"/workspaces/tarball?archive_type=zip",
|
||||||
workspace.workspaceId +
|
|
||||||
"/workspaces/tarball?archive_type=zip&plain_secret=" +
|
|
||||||
(plainSecrets ?? false),
|
|
||||||
{
|
{
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!zipResponse.ok) {
|
if (!zipResponse.ok) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.red("Failed to request tarball from API " + zipResponse.statusText)
|
colors.red(
|
||||||
|
"Failed to request tarball from API " + zipResponse.statusText,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
throw new Error(await zipResponse.text());
|
throw new Error(await zipResponse.text());
|
||||||
}
|
}
|
||||||
@@ -35,18 +33,18 @@ export async function downloadZip(
|
|||||||
|
|
||||||
async function stub(
|
async function stub(
|
||||||
_opts: GlobalOptions & { override: boolean },
|
_opts: GlobalOptions & { override: boolean },
|
||||||
_dir: string
|
_dir: string,
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.red.underline(
|
colors.red.underline(
|
||||||
'Pull is deprecated. Use "sync pull --raw" instead. See <TODO_LINK_HERE> for more information.'
|
'Pull is deprecated. Use "sync pull --raw" instead. See <TODO_LINK_HERE> for more information.',
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = new Command()
|
const command = new Command()
|
||||||
.description(
|
.description(
|
||||||
"Pull all definitions in the current workspace from the API and write them to disk."
|
"Pull all definitions in the current workspace from the API and write them to disk.",
|
||||||
)
|
)
|
||||||
.arguments("<dir:string>")
|
.arguments("<dir:string>")
|
||||||
.action(stub as any);
|
.action(stub as any);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
EditResourceType,
|
EditResourceType,
|
||||||
microdiff,
|
microdiff,
|
||||||
ResourceService,
|
ResourceService,
|
||||||
|
ResourceType,
|
||||||
Table,
|
Table,
|
||||||
} from "./deps.ts";
|
} from "./deps.ts";
|
||||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||||
@@ -25,10 +26,19 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
|||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
await this.pushDiffs(
|
let existing: ResourceType | undefined;
|
||||||
|
try {
|
||||||
|
existing = await ResourceService.getResourceType({
|
||||||
|
workspace,
|
||||||
|
path: remotePath,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
existing = undefined;
|
||||||
|
}
|
||||||
|
this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff({}, this, { cyclesFix: false }),
|
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +65,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
colors.yellow.bold(
|
colors.yellow(
|
||||||
`Applying ${diffs.length} diffs to existing resource type... ${remotePath}`,
|
`Applying ${diffs.length} diffs to existing resource type...`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const changeset: EditResourceType = {};
|
const changeset: EditResourceType = {};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
async pushDiffs(
|
async pushDiffs(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[]
|
diffs: Difference[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (
|
if (
|
||||||
await ResourceService.existsResource({
|
await ResourceService.existsResource({
|
||||||
@@ -43,9 +43,7 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.yellow.bold(
|
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
|
||||||
`Applying ${diffs.length} diffs to existing resource... ${remotePath}`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const changeset: EditResource = {
|
const changeset: EditResource = {
|
||||||
@@ -53,18 +51,22 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
};
|
};
|
||||||
for (const diff of diffs) {
|
for (const diff of diffs) {
|
||||||
if (diff.path[0] === "is_oauth") {
|
if (diff.path[0] === "is_oauth") {
|
||||||
//is_oauth is not updatable
|
console.log(
|
||||||
|
colors.yellow(
|
||||||
|
"! is_oauth has been removed in newer versions. Ignoring.",
|
||||||
|
),
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
diff.type !== "REMOVE" &&
|
diff.type !== "REMOVE" &&
|
||||||
diff.path[0] !== "value" &&
|
(
|
||||||
(diff.path.length !== 1 || diff.path[0] !== "description") &&
|
diff.path[0] !== "value" && (
|
||||||
diff.path[0] !== "resource_type"
|
diff.path.length !== 1 ||
|
||||||
|
diff.path[0] !== "description"
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
console.log(
|
|
||||||
colors.red("Invalid variable diff with path " + diff.path)
|
|
||||||
);
|
|
||||||
throw new Error("Invalid folder diff with path " + diff.path);
|
throw new Error("Invalid folder diff with path " + diff.path);
|
||||||
}
|
}
|
||||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||||
@@ -74,8 +76,8 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanges = Object.values(changeset).some(
|
const hasChanges = Object.values(changeset).some((v) =>
|
||||||
(v) => v !== null && typeof v !== "undefined"
|
v !== null && typeof v !== "undefined"
|
||||||
);
|
);
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return;
|
return;
|
||||||
@@ -90,8 +92,8 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
if (typeof this.is_oauth !== "undefined") {
|
if (typeof this.is_oauth !== "undefined") {
|
||||||
console.log(
|
console.log(
|
||||||
colors.yellow(
|
colors.yellow(
|
||||||
"! is_oauth has been removed in newer versions. Ignoring."
|
"! is_oauth has been removed in newer versions. Ignoring.",
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,10 +110,19 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
|
let existing: Resource | undefined;
|
||||||
|
try {
|
||||||
|
existing = await ResourceService.getResource({
|
||||||
|
workspace,
|
||||||
|
path: remotePath,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
existing = undefined;
|
||||||
|
}
|
||||||
await this.pushDiffs(
|
await this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff({}, this, { cyclesFix: false })
|
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,11 +130,11 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
export async function pushResource(
|
export async function pushResource(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
remotePath: string
|
remotePath: string,
|
||||||
) {
|
) {
|
||||||
const data = decoverto
|
const data = decoverto.type(ResourceFile).rawToInstance(
|
||||||
.type(ResourceFile)
|
await Deno.readTextFile(filePath),
|
||||||
.rawToInstance(await Deno.readTextFile(filePath));
|
);
|
||||||
await data.push(workspace, remotePath);
|
await data.push(workspace, remotePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +143,7 @@ async function push(opts: PushOptions, filePath: string, remotePath: string) {
|
|||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
if (!validatePath(remotePath)) {
|
if (!await validatePath(opts, remotePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +190,7 @@ const command = new Command()
|
|||||||
.action(list as any)
|
.action(list as any)
|
||||||
.command(
|
.command(
|
||||||
"push",
|
"push",
|
||||||
"push a local resource spec. This overrides any remote versions."
|
"push a local resource spec. This overrides any remote versions.",
|
||||||
)
|
)
|
||||||
.arguments("<file_path:string> <remote_path:string>")
|
.arguments("<file_path:string> <remote_path:string>")
|
||||||
.action(push as any);
|
.action(push as any);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user