Compare commits

..

13 Commits

Author SHA1 Message Date
Ruben Fiszel
0ffe9f95a1 progress 2023-01-21 17:31:34 +01:00
Ruben Fiszel
433ebe023b Merge branch 'main' into rf/strip2 2023-01-21 10:20:45 +01:00
Ruben Fiszel
23f77dc21d progress 2023-01-21 07:55:53 +01:00
Ruben Fiszel
fabf09f55b Merge branch 'main' into rf/strip2 2023-01-20 21:19:04 +01:00
Ruben Fiszel
ca11713a1e progress 2023-01-20 18:59:05 +01:00
Ruben Fiszel
d004e08270 progress 2023-01-20 11:47:31 +01:00
Ruben Fiszel
85bbb79d22 progress 2023-01-20 11:47:24 +01:00
Ruben Fiszel
536f9cf58f progress 2023-01-19 21:00:50 +01:00
Ruben Fiszel
d7a2323c4e progress 2023-01-19 20:24:55 +01:00
Ruben Fiszel
dbb10c8737 progress 2023-01-19 19:53:32 +01:00
Ruben Fiszel
42d02c3c90 progress 2023-01-19 17:55:52 +01:00
Ruben Fiszel
cdca022a64 Merge branch 'main' into rf/strip2 2023-01-19 13:51:03 +01:00
Ruben Fiszel
7eeffcd618 progress 2023-01-19 11:24:55 +01:00
221 changed files with 3593 additions and 7615 deletions

View File

@@ -14,13 +14,13 @@ services:
windmill:
image: '${WINDMILL_IMAGE}'
privileged: false
restart: unless-stopped
ports:
- 8000:8000
entrypoint: ['/bin/sh', '-c']
command: 'echo ${OAUTH_JSON_BASE64} | base64 --decode > /usr/src/app/oauth.json && ./windmill'
environment:
- DATABASE_URL=postgres://postgres:changeme@localhost/windmill?sslmode=disable
- BASE_URL=${EXPECTED_URL}
- BASE_URL=http://localhost
- BASE_INTERNAL_URL=http://localhost:8000
- RUST_LOG=info
- NUM_WORKERS=3
@@ -28,16 +28,12 @@ services:
- DENO_PATH=/usr/bin/deno
- PYTHON_PATH=/usr/local/bin/python3
- METRICS_ADDR=false
- OAUTH_JSON_BASE64=${OAUTH_JSON_BASE64}
volumes:
- worker_dependency_cache:/tmp/windmill/cache
deploy:
resources:
limits:
memory: 250M
lsp:
image: '${LSP_IMAGE}'
restart: unless-stopped
ports:
- 3001:3001

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.3.6
uses: dependabot/fetch-metadata@v1.3.5
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs

View File

@@ -1,47 +0,0 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
name: Build and push windmill with python 3.10 and openbb
on: workflow_dispatch
concurrency:
group: ${{ github.ref }}-openbb
cancel-in-progress: true
permissions:
contents: read
id-token: write
packages: write
jobs:
build_ee:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
uses: depot/build-push-action@v1
with:
context: .
push: true
file: ./docker/DockerfileOpenbb
build-args: |
features=enterprise
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:openbb
labels: |
org.opencontainers.image.licenses=Windmill-Enterprise-License

View File

@@ -4,8 +4,6 @@ on:
types: [opened,synchronize,reopened,closed]
paths:
- "backend/**"
- ".github/uffizzi/**"
- ".github/workflows/**"
workflow_dispatch:
jobs:

View File

@@ -47,35 +47,22 @@ jobs:
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: Predict Deployment URL
id: url
# Replace dots in the repo name with the plus sign
run: |
REPO=$(echo ${{ github.repository }} | sed 's/\./+/g')
echo "EXPECTED_URL=https://app.uffizzi.com/github.com/$REPO/pull/$PR_NUMBER" >> $GITHUB_ENV
- name: Re-Render Compose File
run: |
OAUTH_JSON_BASE64=${{ secrets.OAUTH_JSON_BASE64 }}
export OAUTH_JSON_BASE64
envsubst '${OAUTH_JSON_BASE64} ${EXPECTED_URL}' < docker-compose.rendered.yml > docker-compose.uffizzi.yml
# cat docker-compose.uffizzi.yml
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.uffizzi.yml | awk '{ print $1 }')" >> $GITHUB_ENV
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.uffizzi.yml
path: docker-compose.rendered.yml
key: ${{ env.COMPOSE_FILE_HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
@@ -93,7 +80,7 @@ jobs:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.uffizzi.yml
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:

View File

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

View File

@@ -1,91 +1,6 @@
# Changelog
## [1.62.0](https://github.com/windmill-labs/windmill/compare/v1.61.1...v1.62.0) (2023-02-03)
### Features
* add INCLUDE_HEADERS env variable to pass value from request headers ([0921ba0](https://github.com/windmill-labs/windmill/commit/0921ba008535e945f2ec3255728c2e8c1f4c36dc))
* add WHITELIST_WORKSPACES and BLACKLIST_WORKSPACES ([99568ea](https://github.com/windmill-labs/windmill/commit/99568eaa473d57123a7dde4007f8812e0053fb3f))
* Add workspace webhook ([#1158](https://github.com/windmill-labs/windmill/issues/1158)) ([b9ac60f](https://github.com/windmill-labs/windmill/commit/b9ac60f8bb0662e364606c4b7b8a6e3c1e7e4041))
* adding worker_busy ([23007f7](https://github.com/windmill-labs/windmill/commit/23007f7a71630fc2040e1be39db83ba56689e3c4))
* **cli:** 2-Way sync ([#1071](https://github.com/windmill-labs/windmill/issues/1071)) ([cdd1619](https://github.com/windmill-labs/windmill/commit/cdd16195aeaf32e1f1d0648f48e4843954d16d9c))
* **frontend:** App initial loading animations ([#1176](https://github.com/windmill-labs/windmill/issues/1176)) ([3305481](https://github.com/windmill-labs/windmill/commit/3305481d5d4ce598ceb57256cea851869cdaf25e))
* **python:** add ADDITIONAL_PYTHON_PATHS ([14b32be](https://github.com/windmill-labs/windmill/commit/14b32be8b229372c57a167fd74cb958a96f0e8e6))
### Bug Fixes
* **frontend:** Render popups above components in app editor ([#1171](https://github.com/windmill-labs/windmill/issues/1171)) ([bc8d1a3](https://github.com/windmill-labs/windmill/commit/bc8d1a375ec7886357ce0ef5971bb35013c94d61))
* **frontend:** Various fixes and improvements ([#1177](https://github.com/windmill-labs/windmill/issues/1177)) ([9f5500c](https://github.com/windmill-labs/windmill/commit/9f5500c1965ea50796d3bf289c0f9e0c929427f4))
* navigate to new script page before saving script ([f171cd8](https://github.com/windmill-labs/windmill/commit/f171cd8b7c46677173572bac256cbb489a1b8526))
## [1.61.1](https://github.com/windmill-labs/windmill/compare/v1.61.0...v1.61.1) (2023-01-31)
### Bug Fixes
* **backend:** compile issue ([df8cc1f](https://github.com/windmill-labs/windmill/commit/df8cc1f2482b3d8b1530cdaef1361303ff5cadff))
## [1.61.0](https://github.com/windmill-labs/windmill/compare/v1.60.0...v1.61.0) (2023-01-31)
### Features
* add openapi viewer ([#1094](https://github.com/windmill-labs/windmill/issues/1094)) ([1337811](https://github.com/windmill-labs/windmill/commit/1337811438d48e23133f68e9157bd185d5fe4a82))
* add PIP_LOCAL_DEPENDENCIES ([b7db4c7](https://github.com/windmill-labs/windmill/commit/b7db4c78c4629f1fd2dfd7a338f783b16f07b24d))
* add QUEUE_LIMIT_WAIT_RESULT ([51a8810](https://github.com/windmill-labs/windmill/commit/51a8810aa0a9ab7702df459dd270278d42bd3899))
* add resource and resource type from json ([080ecb0](https://github.com/windmill-labs/windmill/commit/080ecb04d7a08d035fe07f179975b52bc0f77297))
* add sql as a valid type in Python ([0172587](https://github.com/windmill-labs/windmill/commit/0172587b129ce54d96dc99336a1f56c66ebdbef5))
* add sync webhook for flows ([f377c84](https://github.com/windmill-labs/windmill/commit/f377c84f5a2148a2bbb7c16e93f13e1d85ceb17e))
* **backend:** add queue_limit + configurable timeout + fix timeout cancel ([eef3bab](https://github.com/windmill-labs/windmill/commit/eef3bab6e4d9f1af1435db868c707a692558ab74))
* **deno:** add support for DENO_AUTH_TOKENS ([832ddab](https://github.com/windmill-labs/windmill/commit/832ddabdf2239521368e5f96df144abce0db31c2))
* **deno:** allow overriding deno sandboxing with DENO_FLAGS' ([7f40373](https://github.com/windmill-labs/windmill/commit/7f40373fd64005d87972854a565c6cf521232982))
* **frontend:** Add app inputs configurations ([#1142](https://github.com/windmill-labs/windmill/issues/1142)) ([3ed16b8](https://github.com/windmill-labs/windmill/commit/3ed16b88a42e4db6e12f8557c5bbaa2d832b1c17))
* **frontend:** Add app preview lock ([#1127](https://github.com/windmill-labs/windmill/issues/1127)) ([6a88e8c](https://github.com/windmill-labs/windmill/commit/6a88e8c4f4d6fa5c393ce27b2040784a74a73b06))
* **frontend:** Add copy button option to app text display component ([#1090](https://github.com/windmill-labs/windmill/issues/1090)) ([bdfc38d](https://github.com/windmill-labs/windmill/commit/bdfc38d954a3c5548fb7f9ee6f80f741eff8cb67))
* **frontend:** Add default codes to app editor ([#1099](https://github.com/windmill-labs/windmill/issues/1099)) ([c50c740](https://github.com/windmill-labs/windmill/commit/c50c7406f267b480af2a01b47e3fcfa1d763db7a))
* **frontend:** Add HTML result rendering ([#1160](https://github.com/windmill-labs/windmill/issues/1160)) ([c01bf70](https://github.com/windmill-labs/windmill/commit/c01bf70f62680a4b77812ac6eb64aca2b15d9a8d))
* **frontend:** Add more integration icons ([#1097](https://github.com/windmill-labs/windmill/issues/1097)) ([2191e85](https://github.com/windmill-labs/windmill/commit/2191e852318f069489f77a4f1c44aadf248c7f53))
* **frontend:** add plotly support ([a4f8f9e](https://github.com/windmill-labs/windmill/commit/a4f8f9e1cf80395d5cd1229c8dd5dda244e2ba7f))
* **frontend:** add selectedRowIndex to the table outputs ([#1145](https://github.com/windmill-labs/windmill/issues/1145)) ([f05f9e4](https://github.com/windmill-labs/windmill/commit/f05f9e4edb928e7a8e3e66a62de9c6487684a14b))
* **frontend:** Add Supabase resource ([#1107](https://github.com/windmill-labs/windmill/issues/1107)) ([12b00a8](https://github.com/windmill-labs/windmill/commit/12b00a808d1f12827a7bc26518cc6f972bdde917))
* **frontend:** add support for background scripts + add FormButtonCo… ([#1124](https://github.com/windmill-labs/windmill/issues/1124)) ([e969af9](https://github.com/windmill-labs/windmill/commit/e969af9e44d1b4409064080e8662552ee3e262e8))
* **frontend:** Add surreal db logo ([#1102](https://github.com/windmill-labs/windmill/issues/1102)) ([d811675](https://github.com/windmill-labs/windmill/commit/d81167588227f2cc433aab64551d96d21a589c5b))
* **frontend:** Add tooltip to app recompute ([#1122](https://github.com/windmill-labs/windmill/issues/1122)) ([4dfdf37](https://github.com/windmill-labs/windmill/commit/4dfdf374af358ef46ee8057373546719c6570067))
* **frontend:** add vega-lite component ([bd79938](https://github.com/windmill-labs/windmill/commit/bd79938bed6da3875a4a2dd72dad14dedbf25ddf))
* **frontend:** Display error as an icon in order to avoid clutter wh… ([#1143](https://github.com/windmill-labs/windmill/issues/1143)) ([22b8fed](https://github.com/windmill-labs/windmill/commit/22b8fed9d904a37aae66f6d957f4987f6ca9955c))
* **frontend:** Open debug runs from component ([#1155](https://github.com/windmill-labs/windmill/issues/1155)) ([73bc13b](https://github.com/windmill-labs/windmill/commit/73bc13bb7d4b1eb25a3a726ac9e6bb80120a495f))
* **frontend:** Update app table component styles ([#1100](https://github.com/windmill-labs/windmill/issues/1100)) ([172b5db](https://github.com/windmill-labs/windmill/commit/172b5dba8f4c3aaf11569c72313ad74845c668a6))
* **python:** add support for extra args in python ([772c768](https://github.com/windmill-labs/windmill/commit/772c768cda094f208a5efb7aab03eee3a8f38f68))
### Bug Fixes
* **frontend:** Add default value for text, number and date input + fix issues with number input + add date input in the settings panel ([#1135](https://github.com/windmill-labs/windmill/issues/1135)) ([8f90602](https://github.com/windmill-labs/windmill/commit/8f906026b3203702c3b6a30bcac9fb2aca985c29))
* **frontend:** Add highlight to selected workspace ([#1159](https://github.com/windmill-labs/windmill/issues/1159)) ([f221a6c](https://github.com/windmill-labs/windmill/commit/f221a6c17f145d0c42f7faf785c37f4037308973))
* **frontend:** add missing condition to properly select first row ([#1128](https://github.com/windmill-labs/windmill/issues/1128)) ([3d873ed](https://github.com/windmill-labs/windmill/commit/3d873ed51c769005981a8d8dfb95faa3ca33bb83))
* **frontend:** App form component display ([#1096](https://github.com/windmill-labs/windmill/issues/1096)) ([339742c](https://github.com/windmill-labs/windmill/commit/339742ca77dd0fda19d5a262617e42c341ef5871))
* **frontend:** App script list panel overflow ([#1101](https://github.com/windmill-labs/windmill/issues/1101)) ([7bc59d9](https://github.com/windmill-labs/windmill/commit/7bc59d9d2650b623a2b481a727ffc495b4216f22))
* **frontend:** App table action button cell ([#1149](https://github.com/windmill-labs/windmill/issues/1149)) ([e989662](https://github.com/windmill-labs/windmill/commit/e98966283dd9b57cc07da34876a90d19210c2927))
* **frontend:** App table header z-index ([#1120](https://github.com/windmill-labs/windmill/issues/1120)) ([59c4cc2](https://github.com/windmill-labs/windmill/commit/59c4cc2058f86deea793b61de59e2936e50e5577))
* **frontend:** Check if hiddenInlineScripts are undefined before iterating over them ([#1134](https://github.com/windmill-labs/windmill/issues/1134)) ([71a443e](https://github.com/windmill-labs/windmill/commit/71a443e3c56d2b8c951de6e3701a411ad1a0ce34))
* **frontend:** fix first row selection ([#1125](https://github.com/windmill-labs/windmill/issues/1125)) ([6c9daf7](https://github.com/windmill-labs/windmill/commit/6c9daf70021859dcd7cef717bc3acdfa88cffd02))
* **frontend:** Fix id generation when a second action ([#1110](https://github.com/windmill-labs/windmill/issues/1110)) ([4f86981](https://github.com/windmill-labs/windmill/commit/4f869811fee73826b2b10965241d2d8dba59dc2a))
* **frontend:** Make sure AppSelect items are an array ([#1144](https://github.com/windmill-labs/windmill/issues/1144)) ([24b1fa0](https://github.com/windmill-labs/windmill/commit/24b1fa0ae327c984841f9ed8b163b3fccc6da258))
* **frontend:** Make sure that old apps are rendering properly ([#1132](https://github.com/windmill-labs/windmill/issues/1132)) ([a78486d](https://github.com/windmill-labs/windmill/commit/a78486d7e08f76e22406063288b35e9030974d7a))
* **frontend:** Playwright ([#1108](https://github.com/windmill-labs/windmill/issues/1108)) ([f0435f5](https://github.com/windmill-labs/windmill/commit/f0435f5f81941c5b49500003aa27956d627daadb))
* **frontend:** Prepare app scripts code for export ([#1123](https://github.com/windmill-labs/windmill/issues/1123)) ([173093a](https://github.com/windmill-labs/windmill/commit/173093a40321f6ad35bf766a5554b21cea388771))
* **frontend:** Prevent modal from hijacking all keypress event ([#1136](https://github.com/windmill-labs/windmill/issues/1136)) ([aa6de3b](https://github.com/windmill-labs/windmill/commit/aa6de3bb5746b9d99c8e3a52e6a9fff10d97bc6a))
* **frontend:** Revert component input panel change ([#1092](https://github.com/windmill-labs/windmill/issues/1092)) ([0419e7e](https://github.com/windmill-labs/windmill/commit/0419e7e1c9239fd3cbc49acf82a73e9c01938153))
* **frontend:** Runnable table overflow ([#1119](https://github.com/windmill-labs/windmill/issues/1119)) ([462adbe](https://github.com/windmill-labs/windmill/commit/462adbe42f823646413a5003fd71f3dd473c0728))
* **frontend:** Select the first row by default, and remove the abilit… ([#1121](https://github.com/windmill-labs/windmill/issues/1121)) ([3c483f5](https://github.com/windmill-labs/windmill/commit/3c483f533759b9b4e589055dbddb31f294bea8fa))
* **frontend:** Show app builder header always on top ([#1118](https://github.com/windmill-labs/windmill/issues/1118)) ([631a3da](https://github.com/windmill-labs/windmill/commit/631a3da17f05a3d29defdf96a50d7e96a9f8baad))
* **frontend:** Update app scripts pane ([#1146](https://github.com/windmill-labs/windmill/issues/1146)) ([18f30c8](https://github.com/windmill-labs/windmill/commit/18f30c8286f8240158643ade8b0ef4607a80fbb0))
* **frontend:** Use absolute path on connect images ([#1095](https://github.com/windmill-labs/windmill/issues/1095)) ([43e069e](https://github.com/windmill-labs/windmill/commit/43e069eb96c0af7d3a1fe1db4f4b69f8e31e7438))
* improvements for error handling as first step of flow ([b77c239](https://github.com/windmill-labs/windmill/commit/b77c239f307a37777acb083b0cdb5c0d214a9dd8))
## [1.60.0](https://github.com/windmill-labs/windmill/compare/v1.59.0...v1.60.0) (2023-01-11)

View File

@@ -1,15 +1,5 @@
{
auto_https off
}
http://{$BASE_URL} {
bind {$ADDRESS}
{$BASE_URL} {
bind {$ADDRESS}
reverse_proxy /ws/* http://lsp:3001
}
https://{$BASE_URL} {
bind {$ADDRESS}
reverse_proxy /ws/* http://localhost:3001
}
reverse_proxy /* http://windmill:8000
}

View File

@@ -30,7 +30,7 @@ Open-source developer infrastructure for internal tools. Self-hostable alternati
**Hub**: <https://hub.windmill.dev>
**Contributor's guide**: <https://docs.windmill.dev/docs/misc/contributing>
**Contributor's guide**: <https://docs.windmill.dev/docs/contributors_guide>
**Roadmap**: <https://github.com/orgs/windmill-labs/projects/2>
@@ -175,14 +175,14 @@ back to the database is ~50ms. A typical lightweight deno job will take around
We only provide docker-compose setup here. For more advanced setups, like
compiling from source or using without a postgres super user, see
[documentation](https://docs.windmill.dev/docs/advanced/self_host)
[documentation](https://docs.windmill.dev/docs/how-tos/self_host)
### Docker compose
`docker compose up` with the following docker-compose is sufficient:
<https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml>
Go to http://localhost et voilà :)
Go to https://localhost et voilà :)
For older kernels < 4.18, set `DISABLE_NUSER=true` as env variable, otherwise
nsjail will not be able to launch the isolated scripts.
@@ -195,8 +195,7 @@ From there, you can create other users (do not forget to change the password!)
### Kubernetes (k8s) and Helm charts
We publish helm charts at:
<https://github.com/windmill-labs/windmill-helm-charts>
We publish helm charts at: <https://github.com/windmill-labs/windmill-helm-charts>
### Commercial license
@@ -232,13 +231,12 @@ the following format:
and mount it at `/usr/src/app/oauth.json`.
The redirect url for the oauth clients is:
`<instance_url>/user/login_callback/<client>`
The redirect url for the oauth clients is: `<instance_url>/user/login_callback/<client>`
[The list of all possible "connect an app" oauth clients](https://github.com/windmill-labs/windmill/blob/main/backend/oauth_connect.json)
To add more "connect an app" OAuth clients to the Windmill project, read the
[Contributor's guide](https://docs.windmill.dev/docs/misc/contributing). We
[Contributor's guide](https://docs.windmill.dev/docs/contributors_guide). We
welcome contributions!
You may also add your own custom OAuth2 IdP and OAuth2 Resource provider:
@@ -280,8 +278,7 @@ upcoming CLI tool.
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | |
| 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 |
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker) | 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 |
| 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 |
@@ -309,14 +306,9 @@ upcoming CLI tool.
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | 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_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 |
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
| WHITELIST_WORKSPACES | None | Whitelist of workspaces this worker takes job from | Worker |
| BLACKLIST_WORKSPACES | None | Blacklist of workspaces this worker takes job from | Worker |
| 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

498
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.62.0"
version = "1.60.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.62.0"
version = "1.60.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -83,7 +83,7 @@ chrono = { version = "^0", features = ["serde"] }
tracing = "^0"
tracing-subscriber = { version = "^0", features = ["env-filter", "json"] }
prometheus = { version = "^0", default-features = false }
cookie = { version = "0.16.2" }
cookie = { version = "^0" }
phf = { version = "0.11", features = ["macros"] }
rust-embed = "^6"
mime_guess = "^2"

View File

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

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
-- Add up migration script here
ALTER TABLE account ALTER COLUMN refresh_token TYPE VARCHAR(1500);

View File

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

View File

@@ -1,4 +0,0 @@
-- Add up migration script here
GRANT ALL PRIVILEGES ON TABLE favorite TO windmill_admin;
GRANT ALL PRIVILEGES ON TABLE favorite TO windmill_user;

View File

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

View File

@@ -1,25 +0,0 @@
-- Add up migration script here
CREATE POLICY see_folder_extra_perms_user ON capture FOR ALL
USING (SPLIT_PART(capture.path, '/', 1) = 'f' AND SPLIT_PART(capture.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]))
WITH CHECK (SPLIT_PART(capture.path, '/', 1) = 'f' AND SPLIT_PART(capture.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
DO
$do$
DECLARE
i text;
arr text[] := array['queue', 'completed_job'];
BEGIN
FOREACH i IN ARRAY arr
LOOP
EXECUTE FORMAT(
$$
CREATE POLICY see_folder_extra_perms_user ON %1$I FOR ALL
USING (%1$I.visible_to_owner IS true AND SPLIT_PART(%1$I.script_path, '/', 1) = 'f' AND SPLIT_PART(%1$I.script_path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
$$,
i
);
END LOOP;
END
$do$;

View File

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

View File

@@ -1,5 +0,0 @@
-- Add up migration script here
CREATE POLICY see_folder_extra_perms_user ON account FOR ALL
USING (SPLIT_PART(account.owner, '/', 1) = 'f' AND SPLIT_PART(account.owner, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]))
WITH CHECK (SPLIT_PART(account.owner, '/', 1) = 'f' AND SPLIT_PART(account.owner, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));

View File

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

View File

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

View File

@@ -80,7 +80,7 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
let def_arg_start = params.args.len() - params.defaults.len();
Ok(MainArgSignature {
star_args: params.vararg.is_some(),
star_kwargs: params.kwarg.is_some(),
star_kwargs: params.vararg.is_some(),
args: params
.args
.into_iter()
@@ -104,7 +104,6 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
"Sql" | "sql" => Typ::Sql,
_ => Typ::Resource(id),
},
_ => Typ::Unknown,
@@ -136,10 +135,7 @@ fn to_value(et: &ExprKind) -> Option<serde_json::Value> {
.into_iter()
.zip(values)
.map(|(k, v)| {
let key = k
.as_ref()
.map(|x| x.node.clone())
.and_then(|n| to_value(&n))
let key = to_value(&k.node)
.and_then(|x| match x {
serde_json::Value::String(s) => Some(s),
_ => None,
@@ -185,7 +181,6 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
"f" => "requests",
"shopify" => "ShopifyAPI",
"seleniumwire" => "selenium-wire",
"openbb-terminal" => "openbb[all]",
};
fn replace_import(x: String) -> String {
@@ -197,13 +192,13 @@ fn replace_import(x: String) -> String {
}
lazy_static! {
static ref RE: Regex = Regex::new(r"^\#\s?(\S+)$").unwrap();
static ref RE: Regex = Regex::new(r"^\#(\S+)$").unwrap();
}
pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
let find_requirements = code
.lines()
.find_position(|x| x.starts_with("#requirements:") || x.starts_with("# requirements:"));
.find_position(|x| x.starts_with("#requirements:"));
if let Some((pos, _)) = find_requirements {
let lines = code
.lines()

View File

@@ -117,25 +117,17 @@
},
"query": "SELECT script_path FROM queue WHERE id = $1"
},
"0715955b7e98cc669a88eca6556cd46b7f9c07fcf32a24f85b69720b54e6e95f": {
"062859f1d0e5cfba3115f4241115753b86a4ad239708851c998ff5620ebca5b8": {
"describe": {
"columns": [
{
"name": "mem_peak",
"ordinal": 0,
"type_info": "Int4"
}
],
"nullable": [
true
],
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid"
]
}
},
"query": "SELECT mem_peak FROM queue WHERE id = $1"
"query": "UPDATE queue SET last_ping = now() WHERE id = $1"
},
"07551a32c49da8c0693dd39c6a63b5b2a596ccc0e52e8918160604a5e133dd32": {
"describe": {
@@ -417,6 +409,28 @@
},
"query": "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)\n VALUES ($1, $2, $3, 'password', $4, $5, $6)"
},
"11eb4dd4a2c9b0b759294dde5e8b505c5a4391aa0d8cb629c665711ee0fc04a0": {
"describe": {
"columns": [
{
"name": "logs",
"ordinal": 0,
"type_info": "Text"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Int4",
"Text",
"Uuid"
]
}
},
"query": "SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3"
},
"122090a0f89e5248a0a0f199ebd24582fdb302883aebd2da187ac0084e767ea3": {
"describe": {
"columns": [
@@ -534,21 +548,6 @@
"name": "auto_invite_operator",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "customer_id",
"ordinal": 7,
"type_info": "Varchar"
},
{
"name": "plan",
"ordinal": 8,
"type_info": "Varchar"
},
{
"name": "webhook",
"ordinal": 9,
"type_info": "Text"
}
],
"nullable": [
@@ -558,9 +557,6 @@
true,
false,
true,
true,
true,
true,
true
],
"parameters": {
@@ -1223,18 +1219,6 @@
},
"query": "INSERT INTO usr\n (workspace_id, email, username, is_admin, operator)\n VALUES ($1, $2, $3, $4, $5)"
},
"33d69b3915ddfde40323ace65c14e39fa4bbc8b5dd50a34e165765eaea1f4966": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1"
},
"355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": {
"describe": {
"columns": [
@@ -1481,74 +1465,6 @@
},
"query": "UPDATE app SET versions = array_append(versions, $1) WHERE id = $2"
},
"41659beec0e8d6d95258a728e8e4b2b929045a05bd0cfb2293031c0e4a62f04e": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Uuid",
"Varchar",
"Timestamptz",
"Timestamptz",
"Bool",
"Int8",
"Varchar",
"Jsonb",
"Jsonb",
"Text",
"Text",
"Text",
"Bool",
"Varchar",
"Text",
{
"Custom": {
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies"
]
},
"name": "job_kind"
}
},
"Varchar",
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
"Bool",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
"Numeric",
"Varchar",
"Bool",
"Int4"
]
}
},
"query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email\n , visible_to_owner\n , mem_peak\n )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)"
},
"42e1b5634a9e51247115fa73f85a97b1467c913d012cac9c45bb6a349082dc71": {
"describe": {
"columns": [
@@ -1638,21 +1554,6 @@
},
"query": "SELECT app.id, app.path, app.summary, app.versions, app.policy,\n app.extra_perms, app_version.value, \n app_version.created_at, app_version.created_by from app, app_version \n WHERE app_version.id = $1 AND app.id = app_version.app_id AND app.workspace_id = $2"
},
"44ded7852b72f50099ff6508a7d13cab73c92ba1d07f2304a3bd3f890fcf3b19": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Jsonb"
]
}
},
"query": "INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING"
},
"453501fbd61efd26647baf9b6ef702ce0bc2e920914e9f08fe5f2a5f4ab03f02": {
"describe": {
"columns": [
@@ -1858,6 +1759,23 @@
},
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(\n JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2),\n ARRAY['step'], $3)\n WHERE id = $4\n "
},
"53f2836a652d34aafe6ffb266a092d7390212bc5532dcc226529a61a53deec42": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Text",
"Bool"
]
}
},
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6)"
},
"541ebd3bac65431237cf3b882dfdcd61ca97c253d9754d05bba59fda89841067": {
"describe": {
"columns": [
@@ -1927,21 +1845,6 @@
"name": "auto_invite_operator",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "customer_id",
"ordinal": 7,
"type_info": "Varchar"
},
{
"name": "plan",
"ordinal": 8,
"type_info": "Varchar"
},
{
"name": "webhook",
"ordinal": 9,
"type_info": "Text"
}
],
"nullable": [
@@ -1951,9 +1854,6 @@
true,
false,
true,
true,
true,
true,
true
],
"parameters": {
@@ -2164,26 +2064,6 @@
},
"query": "DELETE FROM group_ WHERE name = $1 AND workspace_id = $2"
},
"5ba4b87528ad49f17d72b53c3db30f5ca4b3b0b0afbd5d9721c8b5d692af601b": {
"describe": {
"columns": [
{
"name": "count",
"ordinal": 0,
"type_info": "Int8"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT COUNT(id) FROM queue WHERE email = $1"
},
"5c377fffc224a06f693c125f4c13b0a9ccfc217190ba6cf78246294bbc6c93bc": {
"describe": {
"columns": [],
@@ -2803,19 +2683,6 @@
},
"query": "UPDATE script SET archived = true WHERE path = $1 AND workspace_id = $2 RETURNING hash"
},
"8292b7b2cce5319575bc09ad18f29b63270872b6e5c6df1f0a326370058f13b0": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2"
},
"82f3c4cd1c1f6aea86d66f675442587684391bc32be9ab55ae20aab549b7bba5": {
"describe": {
"columns": [],
@@ -3465,27 +3332,6 @@
},
"query": "UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS true RETURNING *"
},
"913c634de21d46b8841f8a7c25c408da7c572f9e685db6351848cbf6e9253efc": {
"describe": {
"columns": [
{
"name": "canceled",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Int4",
"Uuid"
]
}
},
"query": "UPDATE queue SET mem_peak = GREATEST($1, mem_peak), last_ping = now() WHERE id = $2 RETURNING canceled"
},
"924e1d5cbb682db0d6121dd911f38cf8948d0e59ca47428daa4f43fce686ba80": {
"describe": {
"columns": [
@@ -3542,22 +3388,6 @@
},
"query": "SELECT value from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')"
},
"95ebdfaf0510b9cad861568cd25d479759d4ea3c3ff4e136aad13a3521525372": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"VarcharArray",
"Jsonb"
]
}
},
"query": "INSERT INTO folder\n (workspace_id, name, display_name, owners, extra_perms)\n VALUES ($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING"
},
"9681c348429f5e647ab55a4a54933faa64018f8539a143641f10d14ddd3a09c4": {
"describe": {
"columns": [
@@ -3908,26 +3738,6 @@
},
"query": "SELECT email FROM usr where username = $1 AND workspace_id = $2"
},
"a34b79872766941cae2d62c99d80e28b7214dd2fcbb68020a63325bbcb34f417": {
"describe": {
"columns": [
{
"name": "webhook",
"ordinal": 0,
"type_info": "Text"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT webhook FROM workspace_settings WHERE workspace_id = $1"
},
"a38059dc3574da498ce986c916b6d385b1f18d5bd659ef13c43fafa9daff6bda": {
"describe": {
"columns": [
@@ -3963,6 +3773,73 @@
},
"query": "UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3"
},
"a46d97b0c65bbbcc29022c2d010a4853541987a58f0098f9c2e3fd6e40b11a52": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Uuid",
"Varchar",
"Timestamptz",
"Timestamptz",
"Bool",
"Int8",
"Varchar",
"Jsonb",
"Jsonb",
"Text",
"Text",
"Text",
"Bool",
"Varchar",
"Text",
{
"Custom": {
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies"
]
},
"name": "job_kind"
}
},
"Varchar",
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
"Bool",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
"Numeric",
"Varchar",
"Bool"
]
}
},
"query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email\n , visible_to_owner\n )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)"
},
"a6145b0482c9e5da245059a80b1563cad20318fd2dd8aef33f9ca97de1826b8b": {
"describe": {
"columns": [],
@@ -4200,26 +4077,6 @@
},
"query": "\n SELECT resume_id, approver\n FROM resume_job\n WHERE job = $1\n "
},
"b053117536c067095e2fb2864ce5af33f84b22c24c92fcb870f37f0501f8ea9a": {
"describe": {
"columns": [
{
"name": "count",
"ordinal": 0,
"type_info": "Int8"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT COUNT(id) FROM queue WHERE running = true AND email = $1"
},
"b05c5f62ef4aa21d33369130cced0e9d7d128727eb58a9be7ae69cbb16bcbb27": {
"describe": {
"columns": [],
@@ -4410,34 +4267,6 @@
},
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at FROM token WHERE email = $1\n ORDER BY created_at DESC"
},
"b69f747eae8b13a0a8d6914a3a7ad322554fcfe62cd28c5f0a475f18dd770d61": {
"describe": {
"columns": [
{
"name": "logs",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "mem_peak",
"ordinal": 1,
"type_info": "Int4"
}
],
"nullable": [
null,
true
],
"parameters": {
"Left": [
"Int4",
"Text",
"Uuid"
]
}
},
"query": "SELECT substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3"
},
"b7dd791cd69748ef51b7520f505c0c8bb1b4014a273476eddfecf1ab658a18b4": {
"describe": {
"columns": [
@@ -4735,26 +4564,6 @@
},
"query": "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *"
},
"c07c9276945663d062cf0ff5b3323be681a0e2cb07a457ea9aede2daeff551cc": {
"describe": {
"columns": [
{
"name": "customer_id",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT customer_id FROM workspace_settings WHERE workspace_id = $1"
},
"c1149f74ef0cca4a48be80f523d339d78e8e20e2bfacac9ad1b6b75c5ae370fb": {
"describe": {
"columns": [],
@@ -5157,6 +4966,21 @@
},
"query": "SELECT count(path) FROM app WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2"
},
"d444e1c1e12a82e9aee5c2ffc4d1d3841bd41dd71344ab155c9842b45bcf30b6": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"Jsonb"
]
}
},
"query": "INSERT INTO folder\n (workspace_id, name, owners, extra_perms)\n VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING"
},
"d47bff7d6b54cd6da8bb330f7321c37af5dcbd76f9acad73b5ba1b8a4afb5091": {
"describe": {
"columns": [
@@ -5732,24 +5556,6 @@
},
"query": "DELETE FROM password WHERE email = $1"
},
"e9c0e331c16312bf086b17c91466c5389d41454fd3f18d73c2e9554845ee9a72": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Text",
"Bool",
"Varchar"
]
}
},
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin, email)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7)"
},
"ea8ebb8d972fe99c960b5a69f794ee2b57bfb1914bf370c5b10313e45fa9b65f": {
"describe": {
"columns": [],

View File

@@ -262,7 +262,7 @@ mod suspend_resume {
// print_job(second, &db).await;
let tx = db.begin().await.unwrap();
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").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!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
@@ -365,7 +365,7 @@ mod suspend_resume {
let second = completed.next().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();
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!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
@@ -395,7 +395,7 @@ mod suspend_resume {
let result = completed_job(flow, &db).await.result.unwrap();
assert_eq!(
json!( {"error": {"name": "Canceled", "reason": "approval request disapproved", "message": "Job canceled: approval request disapproved by unknown", "canceler": "unknown"}}),
json!( {"error": {"name": "InternalErr", "message": "{\"message\":\"Job canceled: approval request disapproved by unknown\",\"name\":\"Canceled\",\"reason\":\"approval request disapproved\",\"canceler\":\"unknown\"}"}}),
result
);
}

View File

@@ -70,5 +70,3 @@ cookie.workspace = true
sha2.workspace = true
urlencoding.workspace = true
async-stripe.workspace = true
lazy_static.workspace = true
prometheus.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.62.0
version: 1.60.0
title: Windmill API
contact:
@@ -883,8 +883,6 @@ paths:
type: string
customer_id:
type: string
webhook:
type: string
/w/{workspace}/workspaces/premium_info:
get:
@@ -964,33 +962,6 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/edit_webhook:
post:
summary: edit webhook
operationId: editWebhook
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: WorkspaceWebhook
required: true
content:
application/json:
schema:
type: object
properties:
webhook:
type: string
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/users/list:
get:
summary: list users
@@ -2474,44 +2445,6 @@ paths:
application/json:
schema: {}
/w/{workspace}/jobs/run_wait_result/f/{path}:
post:
summary: run flow by path and wait until completion
operationId: runWaitResultFlowByPath
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $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/QueueLimit"
requestBody:
description: script args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: job result
content:
application/json:
schema: {}
/w/{workspace}/jobs/result_by_id/{flow_job_id}/{node_id}:
get:
summary: get job result by id
@@ -3292,8 +3225,6 @@ paths:
type: boolean
new_logs:
type: string
mem_peak:
type: integer
/w/{workspace}/jobs/completed/get/{id}:
get:
@@ -4792,8 +4723,6 @@ components:
type: string
visible_to_owner:
type: boolean
mem_peak:
type: integer
required:
- id
- running
@@ -4880,8 +4809,6 @@ components:
type: string
visible_to_owner:
type: boolean
mem_peak:
type: integer
required:
- id
- created_by

View File

@@ -12,7 +12,6 @@ use crate::{
jobs::script_path_to_payload,
users::{require_owner_of_path, Authed, OptAuthed},
variables::build_crypt,
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Json, Path, Query},
@@ -311,7 +310,6 @@ async fn get_secret_id(
async fn create_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
@@ -358,12 +356,7 @@ async fn create_app(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: app.path.clone() },
);
Ok((StatusCode::CREATED, app.path))
}
@@ -402,7 +395,6 @@ pub async fn get_hub_app_by_id(
async fn delete_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -426,10 +418,6 @@ async fn delete_app(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() },
);
Ok(format!("app {} deleted", path))
}
@@ -437,7 +425,6 @@ async fn delete_app(
async fn update_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
@@ -527,14 +514,6 @@ async fn update_app(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("app {} updated (npath: {:?})", path, npath))
}

View File

@@ -32,7 +32,6 @@ use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
pub fn workspaced_service() -> Router {
@@ -182,7 +181,6 @@ async fn check_path_conflict<'c>(
async fn create_flow(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> {
@@ -223,10 +221,6 @@ async fn create_flow(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
);
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
@@ -286,7 +280,6 @@ async fn update_flow(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
) -> Result<String> {
@@ -375,14 +368,6 @@ async fn update_flow(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFlow {
workspace: w_id.clone(),
old_path: flow_path.to_owned(),
new_path: nf.path.clone(),
},
);
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
@@ -465,7 +450,6 @@ async fn exists_flow_by_path(
async fn archive_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -490,10 +474,6 @@ async fn archive_flow_by_path(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::ArchiveFlow { workspace: w_id, path: path.to_owned() },
);
Ok(format!("Flow {path} archived"))
}

View File

@@ -9,7 +9,6 @@
use crate::{
db::{UserDB, DB},
users::Authed,
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Path, Query},
@@ -140,7 +139,6 @@ async fn check_name_conflict<'c>(
async fn create_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(ng): Json<NewFolder>,
) -> Result<String> {
@@ -195,12 +193,8 @@ async fn create_folder(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFolder { workspace: w_id, name: ng.name.clone() },
);
tx.commit().await?;
Ok(format!("Created folder {}", ng.name))
}
@@ -251,7 +245,6 @@ pub async fn require_is_owner(
async fn update_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(ng): Json<UpdateFolder>,
) -> Result<String> {
@@ -305,12 +298,8 @@ async fn update_folder(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.to_owned() },
);
tx.commit().await?;
Ok(format!("Updated folder {}", name))
}
@@ -427,7 +416,6 @@ async fn get_folder_usage(
async fn delete_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
@@ -452,12 +440,6 @@ async fn delete_folder(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("delete folder at name {}", name))
}
@@ -465,7 +447,6 @@ async fn add_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
@@ -496,12 +477,6 @@ async fn add_owner(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("Added {} to folder {}", owner, name))
}
@@ -535,7 +510,6 @@ async fn remove_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
@@ -566,11 +540,5 @@ async fn remove_owner(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("Removed {} to folder {}", owner, name))
}

View File

@@ -53,10 +53,6 @@ pub fn workspaced_service() -> Router {
"/run_wait_result/h/:hash",
post(run_wait_result_job_by_hash),
)
.route(
"/run_wait_result/f/*script_path",
post(run_wait_result_flow_by_path),
)
.route("/run/h/:hash", post(run_job_by_hash))
.route("/run/preview", post(run_preview_job))
.route("/run/preview_flow", post(run_preview_flow_job))
@@ -258,8 +254,6 @@ pub struct CompletedJob {
pub is_skipped: bool,
pub email: String,
pub visible_to_owner: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
}
#[derive(Deserialize, Clone)]
@@ -272,14 +266,6 @@ pub struct RunJobQuery {
queue_limit: Option<i64>,
}
lazy_static::lazy_static! {
static ref INCLUDE_HEADERS: Vec<String> = std::env::var("INCLUDE_HEADERS")
.ok().map(|x| x
.split(',')
.map(|s| s.to_string())
.collect()).unwrap_or_default();
}
impl RunJobQuery {
async fn get_scheduled_for<'c>(
&self,
@@ -300,14 +286,11 @@ impl RunJobQuery {
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let whitelist = self
.include_header
self.include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default();
whitelist
.unwrap_or_default()
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
@@ -474,7 +457,7 @@ async fn list_jobs(
"running",
"script_hash",
"script_path",
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END",
"args",
"null as duration_ms",
"null as success",
"false as deleted",
@@ -489,7 +472,6 @@ async fn list_jobs(
"email",
"visible_to_owner",
"suspend",
"mem_peak",
],
);
let sqlc = list_completed_jobs_query(
@@ -509,7 +491,7 @@ async fn list_jobs(
"null as running",
"script_hash",
"script_path",
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END",
"args",
"duration_ms",
"success",
"deleted",
@@ -524,7 +506,6 @@ async fn list_jobs(
"email",
"visible_to_owner",
"null as suspend",
"mem_peak",
],
);
let sql = format!(
@@ -1002,7 +983,6 @@ struct UnifiedJob {
email: String,
visible_to_owner: bool,
suspend: Option<i32>,
mem_peak: Option<i32>,
}
impl From<UnifiedJob> for Job {
@@ -1037,7 +1017,6 @@ impl From<UnifiedJob> for Job {
is_skipped: uj.is_skipped,
email: uj.email,
visible_to_owner: uj.visible_to_owner,
mem_peak: uj.mem_peak,
}),
"QueuedJob" => Job::QueuedJob(QueuedJob {
workspace_id: uj.workspace_id,
@@ -1070,7 +1049,6 @@ impl From<UnifiedJob> for Job {
email: uj.email,
visible_to_owner: uj.visible_to_owner,
suspend: uj.suspend,
mem_peak: uj.mem_peak,
}),
t => panic!("job type {} not valid", t),
}
@@ -1410,53 +1388,6 @@ pub async fn run_wait_result_job_by_hash(
.await
}
pub async fn run_wait_result_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(timeout): Extension<Arc<TimeoutWaitResult>>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::JsonResult<serde_json::Value> {
check_queue_too_long(db, run_query.queue_limit).await?;
let flow_path = flow_path.to_path();
let mut tx = user_db.clone().begin(&authed).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 (uuid, tx) = push(
tx,
&w_id,
JobPayload::Flow(flow_path.to_string()),
args,
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
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.0,
uuid,
Path((w_id, flow_path)),
)
.await
}
// a similar function exists on the worker
pub async fn script_path_to_payload<'c>(
script_path: &str,
@@ -1589,7 +1520,6 @@ pub struct JobUpdate {
pub running: Option<bool>,
pub completed: Option<bool>,
pub new_logs: Option<String>,
pub mem_peak: Option<i32>,
}
async fn get_job_update(
@@ -1599,8 +1529,8 @@ async fn get_job_update(
) -> error::JsonResult<JobUpdate> {
let mut tx = db.begin().await?;
let record = sqlx::query!(
"SELECT substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3",
let logs = query_scalar!(
"SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3",
log_offset,
&w_id,
&id
@@ -1608,13 +1538,12 @@ async fn get_job_update(
.fetch_optional(&mut tx)
.await?;
if let Some(record) = record {
if let Some(logs) = logs {
tx.commit().await?;
Ok(Json(JobUpdate {
running: if !running { Some(true) } else { None },
completed: None,
new_logs: record.logs,
mem_peak: record.mem_peak,
new_logs: logs,
}))
} else {
let logs = query_scalar!(
@@ -1632,7 +1561,6 @@ async fn get_job_update(
running: Some(false),
completed: Some(true),
new_logs: logs,
mem_peak: record.map(|r| r.mem_peak).flatten(),
}))
}
}
@@ -1750,7 +1678,6 @@ async fn list_completed_jobs(
"is_skipped",
"email",
"visible_to_owner",
"mem_peak",
],
)
.sql()?;

View File

@@ -21,7 +21,6 @@ use crate::{
oauth2::{build_oauth_clients, SlackVerifier},
tracing_init::{MyMakeSpan, MyOnResponse},
users::{Authed, OptAuthed},
webhook_util::WebhookShared,
};
mod apps;
@@ -43,7 +42,6 @@ mod tracing_init;
mod users;
mod utils;
mod variables;
mod webhook_util;
mod worker_ping;
mod workspaces;
@@ -108,8 +106,7 @@ pub async fn run_server(
std::env::var("COOKIE_DOMAIN").ok(),
))))
.layer(Extension(http_client))
.layer(CookieManagerLayer::new())
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
.layer(CookieManagerLayer::new());
// build our application with a route
let app = Router::new()
.nest(

View File

@@ -32,10 +32,9 @@ use sqlx::{Postgres, Transaction};
use tokio::{fs::File, io::AsyncReadExt};
use tower_cookies::{Cookie, Cookies};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::users::username_to_permissioned_as;
use windmill_common::utils::{not_found_if_none, now_from_db};
use crate::users::{truncate_token, Authed, NEW_USER_WEBHOOK};
use crate::users::{truncate_token, Authed};
use crate::workspaces::invite_user_to_all_auto_invite_worspaces;
use crate::{
db::{UserDB, DB},
@@ -96,7 +95,6 @@ pub struct OAuthConfig {
extra_params: Option<HashMap<String, String>>,
extra_params_callback: Option<HashMap<String, String>>,
req_body_auth: Option<bool>,
pkce: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -216,7 +214,6 @@ pub async fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
extra_params: None,
extra_params_callback: None,
req_body_auth: None,
pkce: None,
},
v.clone(),
false,
@@ -660,26 +657,14 @@ async fn connect_slack_callback(
)
.execute(&mut tx)
.await?;
sqlx::query_as!(
Group,
"INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
w_id,
"slack",
"The group slack commands act on belhalf of",
serde_json::json!({username_to_permissioned_as(&authed.username): true})
)
.execute(&mut tx)
.await?;
sqlx::query!(
"INSERT INTO folder
(workspace_id, name, display_name, owners, extra_perms)
VALUES ($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING",
(workspace_id, name, owners, extra_perms)
VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
&w_id,
"slack_bot",
"Slack bot",
&["g/slack".to_string()],
serde_json::json!({"g/slack": true})
&[],
serde_json::json!({})
)
.execute(&mut tx)
.await?;
@@ -976,7 +961,6 @@ async fn login_callback(
Some([("method", &client_name[..])].into()),
)
.await?;
let demo_exists =
sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'demo')")
.fetch_one(&mut tx)
@@ -998,16 +982,6 @@ async fn login_callback(
}
}
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" : &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())
} else {
Err(error::Error::BadRequest(format!(
@@ -1113,19 +1087,14 @@ fn oauth_redirect(
}
let mut auth_url = client.authorize_url(&state);
{
let mut query_string = auth_url.query_pairs_mut();
let pkce = PkceCodeVerifierS256::new_random();
for (key, value) in pkce.authorize_url_params() {
if let Some(extra_params) = extra_params {
let mut query_string = auth_url.query_pairs_mut();
for (key, value) in extra_params {
query_string.append_pair(&key, &value);
}
if let Some(extra_params) = extra_params {
for (key, value) in extra_params {
query_string.append_pair(&key, &value);
}
}
}
set_cookie(&state, cookies, is_secure);
Ok(Redirect::to(auth_url.as_str()))
}

View File

@@ -9,7 +9,6 @@
use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Path, Query},
@@ -264,7 +263,6 @@ async fn check_path_conflict<'c>(
async fn create_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(resource): Json<CreateResource>,
) -> Result<(StatusCode, String)> {
@@ -295,11 +293,6 @@ async fn create_resource(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateResource { workspace: w_id, path: resource.path.clone() },
);
Ok((
StatusCode::CREATED,
format!("resource {} created", resource.path),
@@ -309,7 +302,6 @@ async fn create_resource(
async fn delete_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -341,18 +333,12 @@ async fn delete_resource(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResource { workspace: w_id, path: path.to_owned() },
);
Ok(format!("resource {} deleted", path))
}
async fn update_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditResource>,
@@ -414,15 +400,6 @@ async fn update_resource(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResource {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("resource {} updated (npath: {:?})", path, npath))
}
@@ -434,7 +411,6 @@ struct UpdateResource {
async fn update_resource_value(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(nv): Json<UpdateResource>,
) -> Result<String> {
@@ -460,14 +436,6 @@ async fn update_resource_value(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResource {
workspace: w_id,
old_path: path.to_owned(),
new_path: path.to_owned(),
},
);
Ok(format!("value of resource {} updated", path))
}
@@ -545,7 +513,6 @@ async fn exists_resource_type(
async fn create_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(resource_type): Json<CreateResourceType>,
) -> Result<(StatusCode, String)> {
@@ -576,11 +543,6 @@ async fn create_resource_type(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateResourceType { name: resource_type.name.clone() },
);
Ok((
StatusCode::CREATED,
format!("resource_type {} created", resource_type.name),
@@ -612,7 +574,6 @@ async fn check_rt_path_conflict<'c>(
async fn delete_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
@@ -637,10 +598,6 @@ async fn delete_resource_type(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResourceType { name: name.clone() },
);
Ok(format!("resource_type {} deleted", name))
}
@@ -648,7 +605,6 @@ async fn delete_resource_type(
async fn update_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(ns): Json<EditResourceType>,
) -> Result<String> {
@@ -678,10 +634,6 @@ async fn update_resource_type(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResourceType { name: name.clone() },
);
Ok(format!("resource_type {} updated", name))
}

View File

@@ -14,7 +14,6 @@ use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Path, Query},
@@ -185,7 +184,6 @@ fn hash_script(ns: &NewScript) -> i64 {
async fn create_script(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
@@ -402,14 +400,6 @@ async fn create_script(
Some([("hash", hash.to_string().as_str())].into()),
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateScript {
workspace: w_id,
path: ns.path.clone(),
hash: hash.to_string(),
},
);
} else {
audit_log(
&mut tx,
@@ -427,14 +417,6 @@ async fn create_script(
),
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateScript {
workspace: w_id,
path: ns.path.clone(),
hash: hash.to_string(),
},
);
}
tx.commit().await?;
@@ -618,7 +600,6 @@ async fn get_deployment_status(
async fn archive_script_by_path(
authed: Authed,
Extension(webhook): Extension<WebhookShared>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -645,10 +626,6 @@ async fn archive_script_by_path(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(())
}
@@ -656,7 +633,6 @@ async fn archive_script_by_path(
async fn archive_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
let mut tx = user_db.begin(&authed).await?;
@@ -681,18 +657,12 @@ async fn archive_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(Json(script))
}
async fn delete_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
@@ -721,11 +691,6 @@ async fn delete_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(Json(script))
}

View File

@@ -26,7 +26,6 @@ use axum::{
};
use hyper::{header::LOCATION, StatusCode};
use rand::rngs::OsRng;
use reqwest::Client;
use retainer::Cache;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
@@ -1271,15 +1270,10 @@ async fn delete_user(
Ok(format!("email {} deleted", &email_to_delete))
}
lazy_static::lazy_static! {
pub static ref NEW_USER_WEBHOOK: Option<String> = std::env::var("NEW_USER_WEBHOOK").ok();
}
async fn create_user(
Authed { email, .. }: Authed,
Extension(db): Extension<DB>,
Extension(argon2): Extension<Arc<Argon2<'_>>>,
Extension(http_client): Extension<Client>,
Json(nu): Json<NewUser>,
) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?;
@@ -1311,16 +1305,6 @@ async fn create_user(
)
.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?;
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))

View File

@@ -12,7 +12,6 @@ use crate::{
db::{UserDB, DB},
oauth2::{AllClients, _refresh_token},
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
BaseUrl,
};
/*
@@ -225,7 +224,6 @@ async fn check_path_conflict<'c>(
async fn create_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(variable): Json<CreateVariable>,
) -> Result<(StatusCode, String)> {
@@ -267,11 +265,6 @@ async fn create_variable(
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateVariable { workspace: w_id, path: variable.path.clone() },
);
Ok((
StatusCode::CREATED,
format!("variable {} created", variable.path),
@@ -281,7 +274,6 @@ async fn create_variable(
async fn delete_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -314,11 +306,6 @@ async fn delete_variable(
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteVariable { workspace: w_id, path: path.to_owned() },
);
Ok(format!("variable {} deleted", path))
}
@@ -333,7 +320,6 @@ struct EditVariable {
async fn update_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditVariable>,
@@ -419,15 +405,6 @@ async fn update_variable(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateVariable {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("variable {} updated (npath: {:?})", path, npath))
}

View File

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

View File

@@ -12,7 +12,7 @@ use crate::{
db::{UserDB, DB},
folders::Folder,
resources::{Resource, ResourceType},
users::{Authed, WorkspaceInvite, NEW_USER_WEBHOOK},
users::{Authed, WorkspaceInvite},
utils::require_super_admin,
BaseUrl,
};
@@ -24,7 +24,6 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
use reqwest::Client;
use stripe::CustomerId;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
@@ -52,7 +51,6 @@ pub fn workspaced_service() -> Router {
.route("/delete_invite", post(delete_invite))
.route("/get_settings", get(get_settings))
.route("/edit_slack_command", post(edit_slack_command))
.route("/edit_webhook", post(edit_webhook))
.route("/edit_auto_invite", post(edit_auto_invite))
.route("/tarball", get(tarball_workspace))
.route("/premium_info", get(premium_info))
@@ -92,7 +90,6 @@ pub struct WorkspaceSettings {
pub auto_invite_operator: Option<bool>,
pub customer_id: Option<String>,
pub plan: Option<String>,
pub webhook: Option<String>,
}
#[derive(FromRow, Serialize, Debug)]
@@ -120,11 +117,6 @@ struct EditAutoInvite {
operator: Option<bool>,
}
#[derive(Deserialize)]
struct EditWebhook {
webhook: Option<String>,
}
#[derive(Deserialize)]
struct CreateWorkspace {
id: String,
@@ -241,38 +233,28 @@ async fn stripe_checkout(
params.line_items = match plan.plan.as_str() {
"team" => Some(vec![
stripe::CreateCheckoutSessionLineItems {
quantity: Some(1),
price: Some("price_1MUlrWGU3NdFi9eLE9GBZhoY".to_string()),
quantity: None,
price: Some("price_1MSdSyGU3NdFi9eLMdV6cS6F".to_string()),
..Default::default()
},
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MUlreGU3NdFi9eLi6sOyvVa".to_string()),
..Default::default()
},
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MUlrlGU3NdFi9eLFLggSXZV".to_string()),
..Default::default()
},
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MUlr3GU3NdFi9eLbZYFjR9p".to_string()),
price: Some("price_1MShsNGU3NdFi9eLJMEZUW8b".to_string()),
..Default::default()
},
]),
"enterprise" => Some(vec![
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MSdf6GU3NdFi9eLJFRkntlx".to_string()),
..Default::default()
},
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MShsNGU3NdFi9eLJMEZUW8b".to_string()),
..Default::default()
},
]),
// "enterprise" => Some(vec![
// stripe::CreateCheckoutSessionLineItems {
// quantity: None,
// price: Some("price_1MSdf6GU3NdFi9eLJFRkntlx".to_string()),
// ..Default::default()
// },
// stripe::CreateCheckoutSessionLineItems {
// quantity: None,
// price: Some("price_1MShsNGU3NdFi9eLJMEZUW8b".to_string()),
// ..Default::default()
// },
// ]),
_ => Err(Error::BadRequest("invalid plan".to_string()))?,
};
params.customer_email = Some(&authed.email);
@@ -527,48 +509,6 @@ async fn edit_auto_invite(
))
}
async fn edit_webhook(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, username, .. }: Authed,
Json(ew): Json<EditWebhook>,
) -> Result<String> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
if let Some(webhook) = &ew.webhook {
sqlx::query!(
"UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2",
webhook,
&w_id
)
.execute(&mut tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&authed.username,
"workspaces.edit_webhook",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("webhook", &format!("{:?}", ew.webhook)[..])].into()),
)
.await?;
tx.commit().await?;
Ok(format!("Edit webhook for workspace {}", &w_id))
}
async fn list_workspaces_as_super_admin(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -926,7 +866,6 @@ pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> R
async fn invite_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
Extension(http_client): Extension<Client>,
Path(w_id): Path<String>,
Json(nu): Json<NewWorkspaceInvite>,
) -> Result<(StatusCode, String)> {
@@ -948,15 +887,6 @@ async fn invite_user(
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, "event": "new_invite"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
}
Ok((
StatusCode::CREATED,
format!("user with email {} invited", nu.email),

View File

@@ -58,8 +58,6 @@ pub enum Error {
ExitStatus(i32),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error("Error: {0:#?}")]
JsonErr(serde_json::Value),
}
impl Error {

View File

@@ -8,7 +8,6 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres, Transaction};
use tracing::{instrument, Instrument};
@@ -45,7 +44,6 @@ lazy_static::lazy_static! {
}
const MAX_FREE_EXECS: i32 = 1000;
const MAX_FREE_CONCURRENT_RUNS: i32 = 15;
pub async fn cancel_job<'c>(
username: &str,
@@ -82,32 +80,7 @@ pub async fn cancel_job<'c>(
Ok((tx, job_option))
}
pub async fn pull(
db: &Pool<Postgres>,
whitelist_workspaces: Option<Vec<String>>,
blacklist_workspaces: Option<Vec<String>>,
) -> windmill_common::error::Result<Option<QueuedJob>> {
let mut workspaces_filter = String::new();
if let Some(whitelist) = whitelist_workspaces {
workspaces_filter.push_str(&format!(
" AND workspace_id IN ({})",
whitelist
.into_iter()
.map(|x| format!("'{x}'"))
.collect::<Vec<String>>()
.join(",")
));
}
if let Some(blacklist) = blacklist_workspaces {
workspaces_filter.push_str(&format!(
" AND workspace_id NOT IN ({})",
blacklist
.into_iter()
.map(|x| format!("'{x}'"))
.collect::<Vec<String>>()
.join(",")
));
}
pub async fn pull(db: &Pool<Postgres>) -> windmill_common::error::Result<Option<QueuedJob>> {
/* Jobs can be started if they:
* - haven't been started before,
* running = false
@@ -115,7 +88,7 @@ pub async fn pull(
* suspend_until is non-null
* and suspend = 0 when the resume messages are received
* or suspend_until <= now() if it has timed out */
let job: Option<QueuedJob> = sqlx::query_as::<_, QueuedJob>(&format!(
let job: Option<QueuedJob> = sqlx::query_as::<_, QueuedJob>(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
@@ -124,17 +97,17 @@ pub async fn pull(
WHERE id = (
SELECT id
FROM queue
WHERE ((running = false
WHERE ( running = false
AND scheduled_for <= now())
OR (suspend_until IS NOT NULL
AND ( suspend <= 0
OR suspend_until <= now()))) {workspaces_filter}
OR suspend_until <= now()))
ORDER BY scheduled_for
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *"
))
RETURNING *",
)
.fetch_optional(db)
.await?;
@@ -344,38 +317,10 @@ pub async fn push<'c>(
.fetch_optional(&mut tx)
.await?
.unwrap_or(false);
if !is_super_admin {
if usage > MAX_FREE_EXECS {
return Err(error::Error::BadRequest(format!(
if !is_super_admin && usage > MAX_FREE_EXECS {
return Err(error::Error::BadRequest(format!(
"User {email} has exceeded the free usage limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
)));
}
let in_queue =
sqlx::query_scalar!("SELECT COUNT(id) FROM queue WHERE email = $1", email)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
if in_queue > MAX_FREE_EXECS.into() {
return Err(error::Error::BadRequest(format!(
"User {email} has exceeded the jobs in queue limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
)));
}
let concurrent_runs = sqlx::query_scalar!(
"SELECT COUNT(id) FROM queue WHERE running = true AND email = $1",
email
)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
if concurrent_runs > MAX_FREE_CONCURRENT_RUNS.into() {
return Err(error::Error::BadRequest(format!(
"User {email} has exceeded the concurrent runs limit of {MAX_FREE_CONCURRENT_RUNS} that applies outside of premium workspaces."
)));
}
}
}
}
@@ -406,9 +351,7 @@ pub async fn push<'c>(
)
}
JobPayload::ScriptHub { path } => {
let script = get_hub_script(path.clone(), email)
.await
.context("error fetching hub script")?;
let script = get_hub_script(path.clone(), email).await?;
(
None,
Some(path),
@@ -671,8 +614,6 @@ pub struct QueuedJob {
pub visible_to_owner: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub suspend: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
}
impl QueuedJob {

View File

@@ -72,12 +72,6 @@ mount {
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"

View File

@@ -6,6 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use serde_json::{Map, Value};
use sqlx::{Pool, Postgres, Transaction};
use tracing::instrument;
use uuid::Uuid;
@@ -26,6 +27,16 @@ pub async fn add_completed_job_error(
Ok(result)
}
pub fn error_to_result<E: ToString + std::fmt::Debug>(
output_map: &mut Map<String, Value>,
err: &E,
) {
output_map.insert(
"error".to_string(),
serde_json::Value::String(err.to_string()),
);
}
fn flatten_jobs(modules: Vec<FlowStatusModule>) -> Vec<Uuid> {
modules
.into_iter()
@@ -77,12 +88,6 @@ pub async fn add_completed_job(
None
};
let mem_peak = sqlx::query_scalar!("SELECT mem_peak FROM queue WHERE id = $1", &queued_job.id)
.fetch_optional(db)
.await
.ok()
.flatten()
.flatten();
let mut tx = db.begin().await?;
let job_id = queued_job.id.clone();
sqlx::query!(
@@ -115,10 +120,9 @@ pub async fn add_completed_job(
, language
, email
, visible_to_owner
, mem_peak
)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,\
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29)
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28)
ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)",
queued_job.workspace_id,
queued_job.id,
@@ -147,8 +151,7 @@ pub async fn add_completed_job(
queued_job.language: ScriptLang,
duration: Option<i64>,
queued_job.email,
queued_job.visible_to_owner,
mem_peak
queued_job.visible_to_owner
)
.execute(&mut tx)
.await

View File

@@ -26,7 +26,7 @@ use windmill_common::{
utils::rd_string,
variables,
};
use windmill_queue::{canceled_job_to_result, get_queued_job, pull, JobKind, QueuedJob, CLOUD_HOSTED};
use windmill_queue::{canceled_job_to_result, get_queued_job, pull, JobKind, QueuedJob};
use serde_json::{json, Value};
@@ -35,7 +35,8 @@ use tokio::{
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
process::{Child, Command},
sync::{
mpsc::{self, Sender}, watch,
mpsc::{self, Sender},
oneshot, watch,
},
time::{interval, sleep, Instant, MissedTickBehavior},
};
@@ -48,7 +49,7 @@ use futures::{
use async_recursion::async_recursion;
use crate::{
jobs::{add_completed_job, add_completed_job_error},
jobs::{add_completed_job, add_completed_job_error, error_to_result},
worker_flow::{
handle_flow, update_flow_status_after_job_completion, update_flow_status_in_progress,
},
@@ -132,7 +133,7 @@ async fn copy_cache_to_bucket_as_tar(bucket: &str) {
.current_dir(ROOT_CACHE_DIR)
.arg("-c")
.arg("-f")
.arg(format!("{ROOT_CACHE_DIR}{TAR_CACHE_FILENAME}"))
.arg(format!("{ROOT_CACHE_DIR}/{TAR_CACHE_FILENAME}"))
.args(&["pip", "go", "deno"])
.stdin(Stdio::null())
.stdout(Stdio::null())
@@ -180,7 +181,7 @@ async fn copy_cache_from_bucket_as_tar(bucket: &str) -> bool {
match Command::new("rclone")
.arg("copyto")
.arg(format!(":s3,env_auth=true:{bucket}/{TAR_CACHE_FILENAME}"))
.arg(format!("{ROOT_CACHE_DIR}{TAR_CACHE_FILENAME}"))
.arg(format!("{ROOT_CACHE_DIR}/{TAR_CACHE_FILENAME}"))
.arg("--size-only")
.arg("--fast-list")
.stdin(Stdio::null())
@@ -233,7 +234,6 @@ pub async fn create_token_for_owner<'c>(
owner: &str,
label: &str,
expires_in: i32,
email: &str,
) -> error::Result<(Transaction<'c, Postgres>, String)> {
// TODO: Bad implementation. We should not have access to this DB here.
let token: String = rd_string(30);
@@ -245,15 +245,14 @@ pub async fn create_token_for_owner<'c>(
sqlx::query_scalar!(
"INSERT INTO token
(workspace_id, token, owner, label, expiration, super_admin, email)
VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7)",
(workspace_id, token, owner, label, expiration, super_admin)
VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6)",
&w_id,
token,
owner,
label,
expires_in.to_string(),
is_super_admin,
email
is_super_admin
)
.execute(&mut tx)
.await?;
@@ -316,11 +315,6 @@ lazy_static::lazy_static! {
);
}
//only matter if CLOUD_HOSTED
const MAX_RESULT_SIZE: usize = 1024 * 1024 * 2; // 2MB
#[tracing::instrument(level = "trace")]
pub async fn run_worker(
db: &Pool<Postgres>,
@@ -408,13 +402,6 @@ pub async fn run_worker(
)
.expect("register prometheus metric");
let worker_busy: prometheus::IntGauge = prometheus::register_int_gauge!(prometheus::Opts::new(
"worker_busy",
"Is the worker busy executing a job?",
)
.const_label("name", &worker_name))
.unwrap();
let mut jobs_executed = 0;
let deno_path = std::env::var("DENO_PATH").unwrap_or_else(|_| "/usr/bin/deno".to_string());
@@ -427,32 +414,10 @@ pub async fn run_worker(
let pip_index_url = std::env::var("PIP_INDEX_URL").ok();
let pip_extra_index_url = std::env::var("PIP_EXTRA_INDEX_URL").ok();
let pip_trusted_host = std::env::var("PIP_TRUSTED_HOST").ok();
let deno_auth_tokens = std::env::var("DENO_AUTH_TOKENS")
let max_log_size = std::env::var("MAX_LOG_SIZE")
.ok()
.map(|x| format!(";{x}"))
.unwrap_or_else(|| String::new());
let deno_flags = std::env::var("DENO_FLAGS")
.ok()
.map(|x| x.split(' ').map(|x| x.to_string()).collect());
let pip_local_dependencies = std::env::var("PIP_LOCAL_DEPENDENCIES")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect());
let whitelist_workspaces = std::env::var("WHITELIST_WORKSPACES")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect());
let blacklist_workspaces = std::env::var("BLACKLIST_WORKSPACES")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect());
let pip_local_dependencies = if pip_local_dependencies == Some(vec!["".to_string()]) {
None
} else {
pip_local_dependencies
};
.and_then(|x| x.parse::<i64>().ok())
.unwrap_or(500000);
#[cfg(feature = "enterprise")]
let tar_cache_rate = std::env::var("TAR_CACHE_RATE")
@@ -460,10 +425,6 @@ pub async fn run_worker(
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(100);
let additional_python_paths = std::env::var("ADDITIONAL_PYTHON_PATHS")
.ok()
.map(|x| x.split(':').map(|x| x.to_string()).collect());
let envs = Envs {
deno_path,
go_path,
@@ -474,10 +435,7 @@ pub async fn run_worker(
pip_index_url,
pip_extra_index_url,
pip_trusted_host,
deno_flags,
deno_auth_tokens,
pip_local_dependencies,
additional_python_paths,
max_log_size,
};
WORKER_STARTED.inc();
@@ -497,14 +455,11 @@ pub async fn run_worker(
let (same_worker_tx, mut same_worker_rx) = mpsc::channel::<Uuid>(5);
loop {
worker_busy.set(0);
uptime_metric.inc_by(
((Instant::now() - start_time).as_millis() - uptime_metric.get() as u128)
.try_into()
.unwrap(),
);
let do_break = async {
if last_ping.elapsed().as_secs() > NUM_SECS_ENV_CHECK {
sqlx::query!(
@@ -545,9 +500,7 @@ pub async fn run_worker(
.await
.map_err(|_| Error::InternalErr("Impossible to fetch same_worker job".to_string())))
},
(job, timer) = {
let timer = worker_pull_duration.start_timer();
pull(&db, whitelist_workspaces.clone(), blacklist_workspaces.clone()).map(|x| (x, timer)) } => {
(job, timer) = {let timer = worker_pull_duration.start_timer(); pull(&db).map(|x| (x, timer)) } => {
drop(timer);
(false, job)
},
@@ -557,7 +510,6 @@ pub async fn run_worker(
return true;
}
worker_busy.set(1);
match next_job {
Ok(Some(job)) => {
let label_values = [
@@ -618,11 +570,10 @@ pub async fn run_worker(
&job.permissioned_as,
"ephemeral-script",
timeout * 2,
&job.email,
)
.await.expect("could not create job token");
tx.commit().await.expect("could not commit job token");
let job_client = windmill_api_client::create_client(&worker_config.base_internal_url, token.clone());
let job_client = windmill_api_client::create_client(&worker_config.base_url, token.clone());
let is_flow = job.job_kind == JobKind::Flow || job.job_kind == JobKind::FlowPreview || job.job_kind == JobKind::FlowDependencies;
if let Some(err) = handle_queued_job(
@@ -698,29 +649,24 @@ async fn handle_job_error(
keep_job_dir: bool,
base_internal_url: &str,
) {
let err = match err {
Error::JsonErr(err) => err,
_ => json!({"message": err.to_string(), "name": "InternalErr"}),
};
let update_job_future = || {
add_completed_job_error(
db,
&job,
format!("Unexpected error during job execution:\n{err}"),
err.clone(),
metrics.clone(),
)
};
let _ = add_completed_job_error(
db,
&job,
format!("Unexpected error during job execution:\n{err}"),
json!({"message": err.to_string(), "name": "InternalErr"}),
metrics.clone(),
)
.await;
if job.is_flow_step || job.job_kind == JobKind::FlowPreview || job.job_kind == JobKind::Flow {
let (flow, job_status_to_update) = if let Some(parent_job_id) = job.parent_job {
let _ = update_job_future().await;
(parent_job_id, job.id)
} else {
(job.id, Uuid::nil())
};
let mut output_map = serde_json::Map::new();
error_to_result(&mut output_map, &err);
let updated_flow = update_flow_status_after_job_completion(
db,
client,
@@ -728,7 +674,7 @@ async fn handle_job_error(
&job_status_to_update,
&job.workspace_id,
false,
json!({ "error": err }),
serde_json::Value::Object(output_map),
metrics.clone(),
unrecoverable,
same_worker_tx,
@@ -738,8 +684,9 @@ async fn handle_job_error(
None,
)
.await;
if let Err(err) = updated_flow {
println!("error updating flow status: {}", err);
if let Some(parent_job_id) = job.parent_job {
if let Ok(mut tx) = db.begin().await {
if let Ok(Some(parent_job)) =
@@ -758,10 +705,7 @@ async fn handle_job_error(
}
}
}
if job.parent_job.is_none() {
let _ = update_job_future().await;
}
tracing::error!(job_id = %job.id, "error handling job: {err:#?} {} {} {}", job.id, job.workspace_id, job.created_by);
tracing::error!(job_id = %job.id, err = err.alt(), "error handling job: {} {} {}", job.id, job.workspace_id, job.created_by);
}
async fn insert_initial_ping(
@@ -791,10 +735,7 @@ struct Envs {
pip_index_url: Option<String>,
pip_extra_index_url: Option<String>,
pip_trusted_host: Option<String>,
deno_auth_tokens: String,
deno_flags: Option<Vec<String>>,
pip_local_dependencies: Option<Vec<String>>,
additional_python_paths: Option<Vec<String>>,
max_log_size: i64,
}
fn extract_error_value(log_lines: &str) -> serde_json::Value {
@@ -817,7 +758,9 @@ async fn handle_queued_job(
base_internal_url: &str,
) -> windmill_common::error::Result<()> {
if job.canceled {
return Err(Error::JsonErr(canceled_job_to_result(&job)))?;
return Err(Error::ExecutionErr(
canceled_job_to_result(&job).to_string(),
))?;
}
if let Some(e) = job.pre_run_error {
return Err(Error::ExecutionErr(e));
@@ -943,7 +886,7 @@ async fn handle_queued_job(
}
}
err @ _ => {
json!({"message": format!("error during execution of the script:\n{}", err), "name": "ExecutionErr"})
json!({"message": format!("error before termination: {err:#?}"), "name": "ExecutionErr"})
}
};
@@ -989,7 +932,6 @@ async fn write_file(dir: &str, path: &str, content: &str) -> error::Result<File>
#[async_recursion]
async fn transform_json_value(
name: &str,
client: &windmill_api_client::Client,
workspace: &str,
v: Value,
@@ -1000,7 +942,7 @@ async fn transform_json_value(
let v = client
.get_variable(workspace, path, Some(true))
.await
.map_err(|_| Error::NotFound(format!("Variable {path} not found for `{name}`")))
.map_err(|_| Error::NotFound(format!("Variable {path} not found")))
.map(|v| v.into_inner())?
.value
.unwrap_or_else(|| String::new());
@@ -1009,23 +951,20 @@ async fn transform_json_value(
Value::String(y) if y.starts_with("$res:") => {
let path = y.strip_prefix("$res:").unwrap();
if path.split("/").count() < 2 {
return Err(Error::InternalErr(format!(
"Argument `{name}` is an invalid resource path: {path}",
)));
return Err(Error::InternalErr(
format!("invalid resource path: {path}",),
));
}
let v = client
.get_resource_value(workspace, path)
.await
.map_err(|_| Error::NotFound(format!("Resource {path} not found for `{name}`")))?
.map_err(|_| Error::NotFound(format!("Resource {path} not found")))?
.into_inner();
transform_json_value(name, client, workspace, v).await
transform_json_value(client, workspace, v).await
}
Value::Object(mut m) => {
for (a, b) in m.clone().into_iter() {
m.insert(
a.clone(),
transform_json_value(&a, client, workspace, b).await?,
);
m.insert(a, transform_json_value(client, workspace, b).await?);
}
Ok(Value::Object(m))
}
@@ -1188,7 +1127,7 @@ mount {{
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_go_job(
WorkerConfig { base_internal_url, disable_nuser, disable_nsjail, base_url, .. }: &WorkerConfig,
Envs { nsjail_path, go_path, path_env, home_env, .. }: &Envs,
Envs { nsjail_path, go_path, path_env, home_env, max_log_size, .. }: &Envs,
logs: &mut String,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
@@ -1226,6 +1165,7 @@ async fn handle_go_job(
job_dir,
db,
timeout,
*max_log_size,
go_path,
true,
skip_go_mod,
@@ -1346,7 +1286,7 @@ func Run(req Req) (interface{{}}, error){{
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(&job.id, db, logs, timeout, build_go, false).await?;
handle_child(&job.id, db, logs, timeout, *max_log_size, build_go).await?;
Command::new(nsjail_path)
.current_dir(job_dir)
@@ -1372,14 +1312,14 @@ func Run(req Req) (interface{{}}, error){{
.stderr(Stdio::piped())
.spawn()?
};
handle_child(&job.id, db, logs, timeout, child, !disable_nsjail).await?;
handle_child(&job.id, db, logs, timeout, *max_log_size, child).await?;
read_result(job_dir).await
}
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_bash_job(
WorkerConfig { base_internal_url, disable_nuser, disable_nsjail, base_url, .. }: &WorkerConfig,
Envs { nsjail_path, path_env, home_env, .. }: &Envs,
Envs { nsjail_path, path_env, home_env, max_log_size, .. }: &Envs,
logs: &mut String,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
@@ -1451,7 +1391,7 @@ async fn handle_bash_job(
.stderr(Stdio::piped())
.spawn()?
};
handle_child(&job.id, db, logs, timeout, child, !disable_nsjail).await?;
handle_child(&job.id, db, logs, timeout, *max_log_size, child).await?;
//for now bash jobs have an empty result object
Ok(serde_json::json!(logs
.lines()
@@ -1471,7 +1411,7 @@ fn capitalize(s: &str) -> String {
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_deno_job(
WorkerConfig { base_internal_url, base_url, disable_nuser, disable_nsjail, .. }: &WorkerConfig,
Envs { nsjail_path, deno_path, path_env, deno_auth_tokens, deno_flags, .. }: &Envs,
Envs { nsjail_path, deno_path, path_env, max_log_size, .. }: &Envs,
logs: &mut String,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
@@ -1521,23 +1461,12 @@ run().catch(async (e) => {{
"#,
);
write_file(job_dir, "main.ts", &wrapper_content).await?;
let w_id = job.workspace_id.clone();
let import_map = format!(
r#"{{
"imports": {{
"/": "{base_internal_url}/api/w/{w_id}/scripts/raw/p/",
"./": "./"
}}
}}"#
);
write_file(job_dir, "import_map.json", &import_map).await?;
let mut reserved_variables = get_reserved_variables(job, &token, &base_url, db).await?;
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
let hostname_base = base_url.split("://").last().unwrap_or("localhost");
let hostname_internal = base_internal_url.split("://").last().unwrap_or("localhost");
let deno_auth_tokens =
format!("{token}@{hostname_base};{token}@{hostname_internal}{deno_auth_tokens}",);
let deno_auth_tokens = format!("{token}@{hostname_base};{token}@{hostname_internal}");
let child = async {
Ok(if !disable_nsjail {
let _ = write_file(
@@ -1559,16 +1488,9 @@ run().catch(async (e) => {{
if lockfile.is_some() {
args.push("--lock=/tmp/lock.json");
}
args.push("--import-map");
args.push("/tmp/import_map.json");
args.push("--unstable");
if let Some(deno_flags) = deno_flags {
for flag in deno_flags {
args.push(flag);
}
} else {
args.push("-A");
}
args.push("--v8-flags=--max-heap-size=2048");
args.push("-A");
args.push("/tmp/main.ts");
Command::new(nsjail_path)
@@ -1585,18 +1507,13 @@ run().catch(async (e) => {{
} else {
let mut args = Vec::new();
let script_path = format!("{job_dir}/main.ts");
let import_map_path = format!("{job_dir}/import_map.json");
args.push("run");
args.push("--import-map");
args.push(&import_map_path);
args.push("--unstable");
if let Some(deno_flags) = deno_flags {
for flag in deno_flags {
args.push(flag);
}
} else {
args.push("-A");
if lockfile.is_some() {
args.push("--lock=/tmp/lock.json");
}
args.push("--unstable");
args.push("--v8-flags=--max-heap-size=2048");
args.push("-A");
args.push(&script_path);
Command::new(deno_path)
.current_dir(job_dir)
@@ -1615,7 +1532,7 @@ run().catch(async (e) => {{
}
.instrument(trace_span!("create_deno_jail"))
.await?;
handle_child(&job.id, db, logs, timeout, child, !disable_nsjail).await?;
handle_child(&job.id, db, logs, timeout, *max_log_size, child).await?;
read_result(job_dir).await
}
@@ -1626,7 +1543,7 @@ async fn create_args_and_out_file(
job_dir: &str,
) -> Result<(), Error> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone()).await?)
Some(transform_json_value(client, &job.workspace_id, args.clone()).await?)
} else {
None
};
@@ -1643,9 +1560,7 @@ lazy_static! {
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_python_job(
WorkerConfig { base_internal_url, base_url, disable_nuser, disable_nsjail, .. }: &WorkerConfig,
envs @ Envs {
nsjail_path, python_path, path_env, additional_python_paths, ..
}: &Envs,
envs @ Envs { nsjail_path, python_path, path_env, max_log_size, .. }: &Envs,
requirements_o: Option<String>,
job_dir: &str,
worker_dir: &str,
@@ -1661,8 +1576,7 @@ async fn handle_python_job(
) -> error::Result<serde_json::Value> {
create_dependencies_dir(job_dir).await;
let mut additional_python_paths: Vec<String> =
additional_python_paths.to_owned().unwrap_or_else(|| vec![]);
let mut additional_python_paths: Vec<String> = vec![];
let requirements = match requirements_o {
Some(r) => r,
@@ -1769,15 +1683,6 @@ async fn handle_python_job(
} else {
""
};
let spread = if sig.star_kwargs {
"args = kwargs".to_string()
} else {
sig.args
.into_iter()
.map(|x| format!("args[\"{}\"] = kwargs.get(\"{}\")", x.name, x.name))
.join("\n")
};
let wrapper_content: String = format!(
r#"
import json
@@ -1791,15 +1696,12 @@ inner_script = __import__("inner")
with open("args.json") as f:
kwargs = json.load(f, strict=False)
args = {{}}
{spread}
{transforms}
for k, v in list(args.items()):
for k, v in list(kwargs.items()):
if v == '<function call>':
del args[k]
del kwargs[k]
{transforms}
try:
res = inner_script.main(**args)
res = inner_script.main(**kwargs)
res_json = json.dumps(res, separators=(',', ':'), default=str).replace('\n', '')
with open("result.json", 'w') as f:
f.write(res_json)
@@ -1889,7 +1791,7 @@ mount {{
.spawn()?
};
handle_child(&job.id, db, logs, timeout, child, !disable_nsjail).await?;
handle_child(&job.id, db, logs, timeout, *max_log_size, child).await?;
read_result(job_dir).await
}
@@ -1905,10 +1807,6 @@ async fn read_result(job_dir: &str) -> error::Result<serde_json::Value> {
let mut file = File::open(format!("{job_dir}/result.json")).await?;
let mut content = "".to_string();
file.read_to_string(&mut content).await?;
if *CLOUD_HOSTED && content.len() > MAX_RESULT_SIZE {
return Err(Error::ExecutionErr("Result is too large for the cloud app (limit 2MB).
If using this script as part of the flow, use the shared folder to pass heavy data between steps.".to_owned()));
}
serde_json::from_str(&content)
.map_err(|e| Error::ExecutionErr(format!("Error parsing result: {e}")))
}
@@ -2089,7 +1987,7 @@ async fn generate_deno_lock(
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
timeout: i32,
Envs { deno_path, .. }: &Envs,
Envs { deno_path, max_log_size, .. }: &Envs,
) -> error::Result<String> {
let _ = write_file(job_dir, "main.ts", code).await?;
@@ -2107,7 +2005,7 @@ async fn generate_deno_lock(
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, timeout, * child).await?;
handle_child(job_id, db, logs, timeout, *max_log_size, child).await?;
let path_lock = format!("{job_dir}/lock.json");
let mut file = File::open(path_lock).await?;
@@ -2138,6 +2036,7 @@ async fn capture_dependency_job(
job_dir,
db,
timeout,
envs.max_log_size,
&envs.go_path,
false,
false,
@@ -2156,14 +2055,7 @@ async fn pip_compile(
requirements: &str,
logs: &mut String,
job_dir: &str,
Envs {
pip_extra_index_url,
pip_index_url,
pip_trusted_host,
pip_local_dependencies,
..
}: &Envs,
Envs { pip_extra_index_url, pip_index_url, pip_trusted_host, max_log_size, .. }: &Envs,
db: &Pool<Postgres>,
timeout: i32,
) -> error::Result<String> {
@@ -2171,15 +2063,6 @@ async fn pip_compile(
set_logs(logs, job_id, db).await;
logs.push_str(&format!("\ncontent of requirements:\n{}", requirements));
let file = "requirements.in";
let requirements = if let Some(pip_local_dependencies) = pip_local_dependencies {
let deps = pip_local_dependencies.clone();
requirements
.lines()
.filter(|s| !deps.contains(&s.to_string()))
.join("\n")
} else {
requirements.to_string()
};
write_file(job_dir, file, &requirements).await?;
let mut args = vec!["-q", "--no-header", file, "--resolver=backtracking"];
@@ -2198,7 +2081,7 @@ async fn pip_compile(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, timeout, child, false)
handle_child(job_id, db, logs, timeout, *max_log_size, child)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
let path_lock = format!("{job_dir}/requirements.txt");
@@ -2220,6 +2103,7 @@ async fn install_go_dependencies(
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
timeout: i32,
max_log_size: i64,
go_path: &str,
preview: bool,
skip_go_mod: bool,
@@ -2233,7 +2117,7 @@ async fn install_go_dependencies(
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, timeout, child, false).await?;
handle_child(job_id, db, logs, timeout, max_log_size, child).await?;
}
let child = Command::new(go_path)
.current_dir(job_dir)
@@ -2241,7 +2125,7 @@ async fn install_go_dependencies(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, timeout, child, false)
handle_child(job_id, db, logs, timeout, max_log_size, child)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
@@ -2311,30 +2195,6 @@ async fn get_reserved_variables(
.collect())
}
async fn get_mem_peak(pid: Option<u32>, nsjail: bool) -> i32 {
if pid.is_none() {
return -1
}
let pid = if nsjail {
// This is a bit hacky, but the process id of the nsjail process is the pid of nsjail + 1.
// Ideally, we would get the number from fork() itself. This works in MOST cases.
pid.unwrap() + 1
} else {
pid.unwrap()
};
if let Ok(file) = File::open(format!("/proc/{}/status", pid)).await {
let mut lines = BufReader::new(file).lines();
while let Some(line) = lines.next_line().await.unwrap_or(None) {
if line.starts_with("VmHWM:") {
return line.split_whitespace().nth(1).and_then(|s| s.parse::<i32>().ok()).unwrap_or(-1);
};
}
-2
} else {
-3
}
}
/// - wait until child exits and return with exit status
/// - read lines from stdout and stderr and append them to the "queue"."logs"
/// quitting early if output exceedes MAX_LOG_SIZE characters (not bytes)
@@ -2347,21 +2207,14 @@ async fn handle_child(
db: &Pool<Postgres>,
logs: &mut String,
timeout: i32,
max_log_size: i64,
mut child: Child,
nsjail: bool,
) -> error::Result<()> {
let timeout = Duration::from_secs(u64::try_from(timeout).expect("invalid timeout"));
let update_job_interval = Duration::from_millis(500);
let ping_interval = Duration::from_secs(5);
let cancel_check_interval = Duration::from_millis(500);
let write_logs_delay = Duration::from_millis(500);
let pid = child.id();
if let Some(pid) = pid {
//set the highest oom priority
let mut file = File::create(format!("/proc/{pid}/oom_score_adj")).await?;
let _ = file.write_all(b"1000").await;
} else {
tracing::info!("could not get child pid");
}
let (set_too_many_logs, mut too_many_logs) = watch::channel::<bool>(false);
let output = child_joined_output_stream(&mut child);
@@ -2371,20 +2224,17 @@ async fn handle_child(
/* the cancellation future is polled on by `wait_on_child` while
* waiting for the child to exit normally */
let update_job = async {
let cancel_check = async {
let db = db.clone();
let mut interval = interval(update_job_interval);
let mut interval = interval(cancel_check_interval);
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
loop {
tokio::select!(
_ = rx.recv() => break,
_ = interval.tick() => {
let mem_peak = get_mem_peak(pid, nsjail).await;
tracing::info!("{job_id} still running. mem peak: {}kB", mem_peak);
let mem_peak = if mem_peak > 0 { Some(mem_peak) } else { None };
if sqlx::query_scalar!("UPDATE queue SET mem_peak = GREATEST($1, mem_peak), last_ping = now() WHERE id = $2 RETURNING canceled", mem_peak, job_id)
if sqlx::query_scalar!("SELECT canceled FROM queue WHERE id = $1", job_id)
.fetch_optional(&db)
.await
.map(|v| Some(true) == v)
@@ -2414,7 +2264,7 @@ async fn handle_child(
biased;
result = child.wait() => return result.map(Ok),
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
_ = update_job => KillReason::Cancelled,
_ = cancel_check => KillReason::Cancelled,
_ = sleep(timeout) => KillReason::Timeout,
};
tx.send(()).await.expect("rx should never be dropped");
@@ -2448,13 +2298,8 @@ async fn handle_child(
/* a future that reads output from the child and appends to the database */
let lines = async move {
let max_log_size = if *CLOUD_HOSTED {
MAX_RESULT_SIZE
} else {
usize::MAX
};
/* log_remaining is zero when output limit was reached */
let mut log_remaining = max_log_size.saturating_sub(logs.chars().count());
let mut log_remaining = (max_log_size as usize).saturating_sub(logs.chars().count());
let mut result = io::Result::Ok(());
let mut output = output;
/* `do_write` resolves the task, but does not contain the Result.
@@ -2485,7 +2330,7 @@ async fn handle_child(
tracing::info!(%job_id, "Too many logs lines for job {job_id}");
let _ = set_too_many_logs.send(true);
joined.push_str(&format!(
"Job logs or result reached character limit of {MAX_RESULT_SIZE}; killing job."
"Job logs or result reached character limit of {max_log_size}; killing job."
));
/* stop reading and drop our streams fairly quickly */
break;
@@ -2542,12 +2387,35 @@ async fn handle_child(
}
}.instrument(trace_span!("child_lines"));
/* a stream updating "queue"."last_ping" at an interval */
let (kill_tx, mut kill_rx) = oneshot::channel::<()>();
let mut interval = interval(ping_interval);
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
let db1 = db.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = interval.tick() => {
if let Err(err) = sqlx::query!("UPDATE queue SET last_ping = now() WHERE id = $1", job_id)
.execute(&db1)
.await
{
tracing::error!(%job_id, %err, "error setting last ping for job {job_id}: {err}");
};
},
_ = (&mut kill_rx) => return,
}
}
});
let (wait_result, _) = tokio::join!(wait_on_child, lines);
kill_tx.send(()).expect("send should always work");
match wait_result {
_ if *too_many_logs.borrow() => Err(Error::ExecutionErr(format!(
"logs or result reached limit. (current max size: {MAX_RESULT_SIZE} characters)"
"logs or result reached limit. Set MAX_LOG_SIZE higher (current: {max_log_size})"
))),
Ok(Ok(status)) => {
if status.success() {
@@ -2727,7 +2595,6 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, timeout: i32, base_url: &str) {
&job.permissioned_as,
"ephemeral-zombie-jobs",
timeout * 2,
&job.email,
)
.await
.expect("could not create job token");
@@ -2760,7 +2627,7 @@ async fn handle_python_reqs(
pip_extra_index_url,
pip_trusted_host,
nsjail_path,
max_log_size,
..
}: &Envs,
job: &QueuedJob,
@@ -2855,7 +2722,7 @@ async fn handle_python_reqs(
.spawn()?
};
let child = handle_child(&job.id, db, logs, timeout, child, false).await;
let child = handle_child(&job.id, db, logs, timeout, *max_log_size, child).await;
tracing::info!(
worker_name = %worker_name,
job_id = %job.id,

View File

@@ -427,10 +427,8 @@ pub async fn update_flow_status_after_job_completion(
"Flow job canceled".to_string()
} else if stop_early {
format!("Flow job stopped early because of a stop early predicate returning true")
} else if success {
"Flow job completed with success".to_string()
} else {
"Flow job completed with error".to_string()
"Flow job completed".to_string()
};
if flow_job.canceled {
add_completed_job_error(
@@ -988,7 +986,7 @@ async fn push_next_flow_job(
let success = false;
let skipped = false;
let logs = "Timed out waiting to be resumed".to_string();
let result = json!({ "error": {"message": logs, "name": "SuspendedTimeout"}});
let result = json!({ "error": logs });
let _uuid =
add_completed_job(db, &flow_job, success, skipped, result, logs).await?;
@@ -1883,7 +1881,6 @@ async fn get_transform_context(
&flow_job.permissioned_as,
"transform-input",
10,
&flow_job.email,
)
.await?;
//we need to commit asap otherwise the token won't be valid for auth to check outside of this transaction

View File

@@ -1,12 +1,10 @@
// deno-lint-ignore-file no-explicit-any
import { colors, setClient, UserService } from "./deps.ts";
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
import { colors, setClient } from "./deps.ts";
import { tryGetLoginInfo } from "./login.ts";
import { GlobalOptions } from "./types.ts";
import {
addWorkspace,
getActiveWorkspace,
getWorkspaceByName,
removeWorkspace,
Workspace,
} from "./workspace.ts";
@@ -69,27 +67,6 @@ export async function requireLogin(opts: GlobalOptions) {
}
setClient(token, workspace.remote.substring(0, workspace.remote.length - 1));
try {
await UserService.globalWhoami();
} catch {
console.log(
"! Could not reach API given existing credentials. Attempting to reauth...",
);
const newToken = await loginInteractive(workspace.remote);
if (!newToken) {
throw new Error("Could not reauth");
}
removeWorkspace(workspace.name);
workspace.token = newToken;
addWorkspace(workspace);
setClient(
token,
workspace.remote.substring(0, workspace.remote.length - 1),
);
await UserService.globalWhoami();
}
}
export async function tryResolveVersion(

View File

@@ -1,8 +0,0 @@
// globally shared decoverto instance
import { Decoverto } from "npm:decoverto";
const decoverto = new Decoverto();
// TODO: Properly type FlowModule
export { Any, array, map, MapShape, model, property } from "npm:decoverto";
export { decoverto };

View File

@@ -1,40 +1,32 @@
// windmill
export { setClient } from "https://deno.land/x/windmill@v1.61.1/mod.ts";
export * from "https://deno.land/x/windmill@v1.61.1/windmill-api/index.ts";
export { setClient } from "https://deno.land/x/windmill@v1.56.0/mod.ts";
export * from "https://deno.land/x/windmill@v1.56.0/windmill-api/index.ts";
// cliffy
export { Command } from "https://deno.land/x/cliffy@v0.25.7/command/command.ts";
export { Table } from "https://deno.land/x/cliffy@v0.25.7/table/table.ts";
export { colors } from "https://deno.land/x/cliffy@v0.25.7/ansi/colors.ts";
export { Secret } from "https://deno.land/x/cliffy@v0.25.7/prompt/secret.ts";
export { Select } from "https://deno.land/x/cliffy@v0.25.7/prompt/select.ts";
export { Confirm } from "https://deno.land/x/cliffy@v0.25.7/prompt/confirm.ts";
export { Input } from "https://deno.land/x/cliffy@v0.25.7/prompt/input.ts";
export { Command } from "https://deno.land/x/cliffy@v0.25.6/command/command.ts";
export { Table } from "https://deno.land/x/cliffy@v0.25.6/table/table.ts";
export { colors } from "https://deno.land/x/cliffy@v0.25.6/ansi/colors.ts";
export { Secret } from "https://deno.land/x/cliffy@v0.25.6/prompt/secret.ts";
export { Select } from "https://deno.land/x/cliffy@v0.25.6/prompt/select.ts";
export { Confirm } from "https://deno.land/x/cliffy@v0.25.6/prompt/confirm.ts";
export { Input } from "https://deno.land/x/cliffy@v0.25.6/prompt/input.ts";
export {
DenoLandProvider,
UpgradeCommand,
} from "https://deno.land/x/cliffy@v0.25.7/command/upgrade/mod.ts";
} from "https://deno.land/x/cliffy@v0.25.6/command/upgrade/mod.ts";
// std
export { Untar } from "https://deno.land/std@0.176.0/archive/untar.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 { Untar } from "https://deno.land/std@0.170.0/archive/untar.ts";
export * as path from "https://deno.land/std@0.170.0/path/mod.ts";
export { ensureDir } from "https://deno.land/std@0.170.0/fs/ensure_dir.ts";
export {
copy,
readAll,
readerFromStreamReader,
} from "https://deno.land/std@0.176.0/streams/mod.ts";
export { DelimiterStream } from "https://deno.land/std@0.176.0/streams/mod.ts";
export { iterateReader } from "https://deno.land/std@0.176.0/streams/iterate_reader.ts";
} from "https://deno.land/std@0.170.0/streams/mod.ts";
export { DelimiterStream } from "https://deno.land/std@0.170.0/streams/mod.ts";
// other
export { getAvailablePort } from "https://deno.land/x/port@1.0.0/mod.ts";
export { default as dir } from "https://deno.land/x/dir@1.5.1/mod.ts";
export { passwordGenerator } from "https://deno.land/x/password_generator@latest/mod.ts"; // TODO: I think the version is called latest, but it's still pinned.
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 { 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 objectHash } from "https://deno.land/x/object_hash@2.0.3.1/mod.ts";

View File

@@ -1,145 +1,16 @@
// deno-lint-ignore-file no-explicit-any
import {
Difference,
GlobalOptions,
PushDiffs,
Resource,
setValueByPath,
} from "./types.ts";
import { GlobalOptions } from "./types.ts";
import {
colors,
Command,
Flow,
FlowModule,
FlowService,
JobService,
microdiff,
OpenFlowWPath,
OpenFlow,
Table,
} from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { resolve, track_job } from "./script.ts";
import { Any, array, decoverto, model, property } from "./decoverto.ts";
@model()
export class FlowValueFilePart {
@property(array(Any))
modules: Array<FlowModule>;
@property(Any)
failure_module?: FlowModule;
@property(() => Boolean)
same_worker?: boolean;
constructor(modules: Array<FlowModule>) {
this.modules = modules;
}
}
// this is effectively "OpenFlow" but a copy as it is accepted by the CLI
@model()
export class FlowFile implements Resource, PushDiffs {
@property(() => String)
summary: string;
@property(() => String)
description?: string;
@property(() => FlowValueFilePart)
value: FlowValueFilePart;
@property(Any)
schema?: any;
constructor(summary: string, value: FlowValueFilePart) {
this.summary = summary;
this.value = value;
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (
await FlowService.existsFlowByPath({
workspace: workspace,
path: remotePath,
})
) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing flow...`,
),
);
// TODO: Make these optional in backend (not path ofc)
const changeset: OpenFlowWPath = {
path: remotePath,
summary: this.summary,
value: this.value,
description: this.description, // This is OpenAPIed as optional, but isn't
schema: this.schema, // Same
};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path[0] !== "value" && (
diff.path.length !== 1 ||
!["summary", "description", "schema"].includes(
diff.path[0] as string,
)
)
)
) {
throw new Error("Invalid flow diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
requestBody: changeset,
});
} else {
console.log(colors.bold.yellow("Creating new flow..."));
await FlowService.createFlow({
workspace: workspace,
requestBody: {
path: remotePath,
summary: this.summary,
value: this.value,
schema: this.schema,
description: this.description,
},
});
}
}
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(
workspace,
remotePath,
microdiff(remote ?? {}, this, { cyclesFix: false }),
);
}
}
type Options = GlobalOptions;
@@ -159,10 +30,38 @@ export async function pushFlow(
workspace: string,
remotePath: string,
) {
const data = decoverto.type(FlowFile).rawToInstance(
await Deno.readTextFile(filePath),
);
await data.push(workspace, remotePath);
const data: OpenFlow = JSON.parse(await Deno.readTextFile(filePath));
if (
await FlowService.existsFlowByPath({
workspace: workspace,
path: remotePath,
})
) {
console.log(colors.bold.yellow("Updating existing flow..."));
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
requestBody: {
path: remotePath,
summary: data.summary,
value: data.value,
schema: data.schema,
description: data.description,
},
});
} else {
console.log(colors.bold.yellow("Creating new flow..."));
await FlowService.createFlow({
workspace: workspace,
requestBody: {
path: remotePath,
summary: data.summary,
value: data.value,
schema: data.schema,
description: data.description,
},
});
}
}
async function list(opts: GlobalOptions & { showArchived?: boolean }) {

View File

@@ -1,122 +1,6 @@
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
import { colors, Command, Folder, FolderService } from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
Difference,
GlobalOptions,
PushDiffs,
Resource,
setValueByPath,
} from "./types.ts";
import {
array,
decoverto,
map,
MapShape,
model,
property,
} from "./decoverto.ts";
@model()
export class FolderFile implements Resource, PushDiffs {
@property(array(() => String))
owners: Array<string> | undefined;
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
extra_perms: Map<string, boolean> | undefined;
async push(workspace: string, remotePath: string): Promise<void> {
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
}
if (remotePath.startsWith("f/")) {
remotePath = remotePath.substring(2);
}
let existing: Folder | undefined;
try {
existing = await FolderService.getFolder({ workspace, name: remotePath });
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
}
if (remotePath.startsWith("f/")) {
remotePath = remotePath.substring(2);
}
// TODO: Support this in backend
let exists: boolean;
try {
exists = !!await FolderService.getFolder({ workspace, name: remotePath });
} catch {
exists = false;
}
if (exists) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing folder...`,
),
);
const changeset: {
owners?: string[] | undefined;
extra_perms?: any;
} = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["owners", "extra_perms"].includes(diff.path[0] as string)
)
) {
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await FolderService.updateFolder({
workspace: workspace,
name: remotePath,
requestBody: changeset,
});
} else {
console.log(colors.bold.yellow("Creating new folder..."));
await FolderService.createFolder({
workspace: workspace,
requestBody: {
name: remotePath,
extra_perms: this.extra_perms,
owners: this.owners,
},
});
}
}
}
import { GlobalOptions } from "./types.ts";
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
@@ -137,15 +21,61 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
console.log(colors.bold.underline.green("Resource successfully pushed"));
}
type FolderFile = {
owners: Array<string> | undefined;
extra_perms: Record<string, boolean> | undefined;
};
export async function pushFolder(
workspace: string,
filePath: string,
remotePath: string,
) {
const data = decoverto.type(FolderFile).rawToInstance(
await Deno.readTextFile(filePath),
);
data.push(workspace, remotePath);
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
}
if (remotePath.startsWith("f/")) {
remotePath = remotePath.substring(2);
}
const data: FolderFile = JSON.parse(await Deno.readTextFile(filePath));
let optFolder: Folder | undefined;
try {
optFolder = await FolderService.getFolder({ workspace, name: remotePath });
} catch {
optFolder = undefined;
}
if (optFolder) {
// for (const [k, v] of Object.entries(optFolder.extra_perms)) {
// if (!data.extra_perms || data.extra_perms[k] !== v) {
// console.log(colors.red.underline.bold(`Extra Perms missmatch on ${k}`));
// return;
// }
// }
console.log(colors.yellow("Updating existing folder..."));
await FolderService.updateFolder({
workspace,
name: remotePath,
requestBody: {
extra_perms: data.extra_perms,
owners: data.owners,
},
});
} else {
console.log(colors.yellow("Creating new folder..."));
await FolderService.createFolder({
workspace,
requestBody: {
name: remotePath,
extra_perms: data.extra_perms,
owners: data.owners,
},
});
// HACK: Workaround backend automatically adding current user to folder.
await pushFolder(workspace, filePath, remotePath);
}
}
const command = new Command()

View File

@@ -1,6 +1,6 @@
import { Command } from "./deps.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import { ResourceTypeFile } from "./resource-type.ts";
import { pushResourceTypeDef } from "./resource-type.ts";
import { GlobalOptions } from "./types.ts";
async function pull(opts: GlobalOptions) {
@@ -56,10 +56,14 @@ async function pull(opts: GlobalOptions) {
const x of list
) {
console.log("syncing " + x.name);
const f = new ResourceTypeFile();
f.description = x.description;
f.schema = JSON.parse(x.schema);
await f.push(workspace.workspaceId, x.name);
await pushResourceTypeDef(
workspace.workspaceId,
x.name,
{
description: x.description,
schema: JSON.parse(x.schema),
},
);
}
}

View File

@@ -8,14 +8,13 @@ import variable from "./variable.ts";
import push from "./push.ts";
import pull from "./pull.ts";
import hub from "./hub.ts";
import folder from "./folder.ts";
import sync from "./sync.ts";
// import folder from "./folder.ts";
import { tryResolveVersion } from "./context.ts";
import { GlobalOptions } from "./types.ts";
const VERSION = "v1.62.0";
const VERSION = "v1.60.0";
let command: any = new Command()
const command = new Command()
.name("wmill")
.description("A simple CLI tool for windmill.")
.globalOption(
@@ -33,9 +32,10 @@ let command: any = new Command()
.command("resource", resource)
.command("user", user)
.command("variable", variable)
.command("push", push)
.command("pull", pull)
.command("hub", hub)
.command("folder", folder)
.command("sync", sync)
// .command("folder", folder)
.command("version", "Show version information")
.action(async (opts) => {
console.log("CLI build against " + VERSION);
@@ -61,12 +61,6 @@ let command: any = new Command()
}),
);
if (Number.parseInt(VERSION.replace("v", "").replace(".", "")) > 1700) {
command = command
.command("push", push)
.command("pull", pull);
}
try {
await command.parse(Deno.args);
} catch (e) {

View File

@@ -1,11 +1,20 @@
// deno-lint-ignore-file no-explicit-any
import { resolveWorkspace } from "./context.ts";
import { GlobalOptions } from "./types.ts";
import { colors, Command, readerFromStreamReader, Untar } from "./deps.ts";
import { Workspace } from "./workspace.ts";
import {
colors,
Command,
Confirm,
copy,
ensureDir,
path,
readerFromStreamReader,
Untar,
} from "./deps.ts";
async function pull(opts: GlobalOptions & { override: boolean }, dir: string) {
const workspace = await resolveWorkspace(opts);
export async function downloadTar(
workspace: Workspace,
): Promise<Untar | undefined> {
const requestHeaders: HeadersInit = new Headers();
requestHeaders.set("Authorization", "Bearer " + workspace.token);
requestHeaders.set("Content-Type", "application/octet-stream");
@@ -26,29 +35,51 @@ export async function downloadTar(
),
);
console.log(await tarResponse.text());
return undefined;
return;
}
const streamReader = tarResponse.body?.getReader();
if (!streamReader) {
console.log(colors.red("Failed to read tar request body"));
return undefined;
return;
}
console.log(colors.yellow("Streaming tarball to disk..."));
const denoReader = readerFromStreamReader(streamReader);
const untar = new Untar(denoReader);
return untar;
}
async function stub(
_opts: GlobalOptions & { override: boolean },
_dir: string,
) {
console.log(
colors.red.underline(
'Pull is deprecated. Use "sync pull --raw" instead. See <TODO_LINK_HERE> for more information.',
),
);
for await (const entry of untar) {
console.log(entry.fileName);
const filePath = path.resolve(dir, entry.fileName);
if (entry.type === "directory") {
await ensureDir(filePath);
continue;
}
await ensureDir(path.dirname(filePath));
if (!opts.override) {
let exists = false;
try {
const _stat = await Deno.stat(filePath);
exists = true;
} catch {
exists = false;
}
if (exists) {
if (
!(await Confirm.prompt(
"Conflict at " +
filePath +
" do you want to override the local version?",
))
) {
continue;
}
}
}
const file = await Deno.open(filePath, { write: true, create: true });
const len = await copy(entry, file);
await file.truncate(len);
file.close();
}
console.log(colors.green("Done. Wrote all files to disk."));
}
const command = new Command()
@@ -56,6 +87,6 @@ const command = new Command()
"Pull all definitions in the current workspace from the API and write them to disk.",
)
.arguments("<dir:string>")
.action(stub as any);
.action(pull as any);
export default command;

View File

@@ -1,21 +1,266 @@
// deno-lint-ignore-file no-explicit-any
import { colors, Command } from "./deps.ts";
import { colors, Command, path } from "./deps.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import { pushFlow } from "./flow.ts";
import { pushResource } from "./resource.ts";
import { findContentFile, pushScript } from "./script.ts";
import { GlobalOptions } from "./types.ts";
import { pushVariable } from "./variable.ts";
import { pushResourceType } from "./resource-type.ts";
import { pushFolder } from "./folder.ts";
async function stub(
_opts: GlobalOptions,
_dir?: string,
) {
type Candidate = {
path: string;
namespaceKind: "user" | "group" | "folder";
namespaceName: string;
};
type ResourceTypeCandidate = {
path: string;
};
type FolderCandidate = {
path: string;
namespaceName: string;
};
async function findCandidateFiles(
dir: string,
): Promise<
{
normal: Candidate[];
resourceTypes: ResourceTypeCandidate[];
folders: FolderCandidate[];
}
> {
dir = path.resolve(dir);
if (path.dirname(dir).startsWith(".")) {
return { normal: [], resourceTypes: [], folders: [] };
}
const normalCandidates: Candidate[] = [];
const resourceTypeCandidates: ResourceTypeCandidate[] = [];
const folderCandidates: FolderCandidate[] = [];
for await (const e of Deno.readDir(dir)) {
if (e.isDirectory) {
if (e.name == "u" || e.name == "g" || e.name == "f") { // TODO: Check version for f
const newDir = dir + (dir.endsWith("/") ? "" : "/") + e.name;
for await (const e2 of Deno.readDir(newDir)) {
if (e2.isDirectory) {
if (e2.name.startsWith(".")) continue;
const namespaceName = e2.name;
const stack: string[] = [];
{
const path = newDir + "/" + namespaceName + "/";
stack.push(path);
try {
await Deno.stat(path + "folder.meta.json");
folderCandidates.push({
namespaceName,
path: path + "folder.meta.json",
});
} catch {}
}
while (stack.length > 0) {
const dir2 = stack.pop()!;
for await (const e3 of Deno.readDir(dir2)) {
if (e3.isFile) {
if (e3.name === "folder.meta.json") continue;
normalCandidates.push({
path: dir2 + e3.name,
namespaceKind: e.name == "g"
? "group"
: e.name == "u"
? "user"
: "folder",
namespaceName: namespaceName,
});
} else {
stack.push(dir2 + e3.name + "/");
}
}
}
}
}
} else {
console.log(
colors.yellow(
"Including organizational folder " + e.name + " in push!",
),
);
const { normal, resourceTypes, folders } = await findCandidateFiles(
path.join(dir, e.name),
);
normalCandidates.push(...normal);
resourceTypeCandidates.push(...resourceTypes);
folderCandidates.push(...folders);
}
} else {
// handle root files
if (e.name.endsWith(".resource-type.json")) {
resourceTypeCandidates.push({
path: dir + (dir.endsWith("/") ? "" : "/") + e.name,
});
}
}
}
return {
normal: normalCandidates,
folders: folderCandidates,
resourceTypes: resourceTypeCandidates,
};
}
async function push(opts: GlobalOptions, dir?: string) {
dir = dir ?? Deno.cwd();
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
console.log(colors.blue("Searching Directory..."));
const { normal, resourceTypes, folders } = await findCandidateFiles(dir);
console.log(
colors.red.underline(
'Push is deprecated. Use "sync push --raw" instead. See <TODO_LINK_HERE> for more information.',
colors.blue(
"Found " + (normal.length + resourceTypes.length + folders.length) +
" candidates",
),
);
for (const resourceType of resourceTypes) {
const fileName = resourceType.path.substring(
resourceType.path.lastIndexOf("/") + 1,
);
const fileNameParts = fileName.split(".");
// invalid file names, like my.cool.script.script.json. Not valid.
if (fileNameParts.length != 3) {
console.log(
colors.yellow("invalid file name found at " + resourceType.path),
);
continue;
}
// filter out non-json files. Note that we filter out script contents above, so this is really an error.
if (fileNameParts.at(-1) != "json") {
console.log(colors.yellow("non-JSON file found at " + resourceType.path));
continue;
}
console.log("pushing resource type " + fileNameParts.at(-3)!);
await pushResourceType(
workspace.workspaceId,
resourceType.path,
fileNameParts.at(-3)!,
);
}
for (const folder of folders) {
await pushFolder(
workspace.workspaceId,
folder.path,
"f/" + folder.namespaceName,
);
}
for (const candidate of normal) {
// full file name. No leading /. includes .type.json
const fileName = candidate.path.substring(
candidate.path.lastIndexOf("/") + 1,
);
// figure out just the path after ...../u|g/username|group/ (in extra dir)
const dirParts = candidate.path.split("/").filter((x) => x.length > 0);
// TODO: check version for folder
const gIndex = dirParts.findIndex((x) => x == "u" || x == "g" || x == "f");
const extraDir = dirParts.slice(gIndex + 2, -1).join("/");
// file name parts has .json (hopefully) at -1, type at -2, and the actual name at -3. Dots in names are not allowed.
const fileNameParts = fileName.split(".");
// filter out script content files
if (
fileNameParts.at(-1) == "ts" ||
fileNameParts.at(-1) == "py" ||
fileNameParts.at(-1) == "go"
) {
// probably part of a script. Silent ignore.
continue;
}
// invalid file names, like my.cool.script.script.json. Not valid.
if (fileNameParts.length != 3) {
console.log(
colors.yellow("invalid file name found at " + candidate.path),
);
continue;
}
// filter out non-json files. Note that we filter out script contents above, so this is really an error.
if (fileNameParts.at(-1) != "json") {
console.log(colors.yellow("non-JSON file found at " + candidate.path));
continue;
}
// get the type & filter it for valid ones.
const type = fileNameParts.at(-2);
if (type == "resource-type") {
console.log(
colors.yellow(
"Found resource type file at " +
candidate.path +
" this appears to be inside a path folder. Resource types are not addressed by path. Place them at the root or inside only an organizational folder. Ignoring this file!",
),
);
continue;
}
if (
type != "flow" &&
type != "resource" &&
type != "script" &&
type != "variable"
) {
console.log(
colors.yellow(
"file with invalid type " + type + " found at " + candidate.path,
),
);
continue;
}
// create the remotePath for the API
const remotePath = (candidate.namespaceKind === "group"
? "g/"
: (candidate.namespaceKind === "user" ? "u/" : "f/")) +
candidate.namespaceName +
"/" +
(extraDir.length > 0 ? extraDir + "/" : "") +
fileNameParts.at(-3);
console.log("pushing " + type + " to " + remotePath);
if (type == "flow") {
await pushFlow(candidate.path, workspace.workspaceId, remotePath);
} else if (type == "resource") {
await pushResource(workspace.workspaceId, candidate.path, remotePath);
} else if (type == "script") {
let contentPath: string;
try {
contentPath = await findContentFile(candidate.path);
} catch (e) {
console.log(colors.red(e.toString()));
continue;
}
await pushScript(
candidate.path,
contentPath,
workspace.workspaceId,
remotePath,
);
} else if (type == "variable") {
await pushVariable(workspace.workspaceId, candidate.path, remotePath);
}
}
console.log(colors.underline.bold.green("Successfully Pushed all files."));
}
const command = new Command()
.description("Push all files from a folder")
.arguments("[dir:string]")
.action(stub as any);
.action(push as any);
export default command;

View File

@@ -1,129 +1,65 @@
// deno-lint-ignore-file no-explicit-any
import {
Difference,
GlobalOptions,
PushDiffs,
Resource as ResourceI,
setValueByPath,
} from "./types.ts";
import { GlobalOptions } from "./types.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import {
colors,
Command,
EditResourceType,
microdiff,
ResourceService,
ResourceType,
Table,
} from "./deps.ts";
import { Any, decoverto, model, property } from "./decoverto.ts";
import { colors, Command, ResourceService, Table } from "./deps.ts";
@model()
export class ResourceTypeFile implements ResourceI, PushDiffs {
@property(Any)
type ResourceTypeFile = {
schema?: any;
@property(() => String)
description?: string;
async push(workspace: string, remotePath: string): Promise<void> {
let existing: ResourceType | undefined;
try {
existing = await ResourceService.getResourceType({
workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (
await ResourceService.existsResourceType({
workspace: workspace,
path: remotePath,
})
) {
if (
(await ResourceService.listResourceType({ workspace })).findIndex((x) =>
x.name === remotePath
) === -1
) {
console.log(
"Resource type " + remotePath +
" is already taken for the current workspace, but cannot be updated. Is this a conflict with starter?",
);
return;
}
console.log(
colors.yellow(
`Applying ${diffs.length} diffs to existing resource type...`,
),
);
const changeset: EditResourceType = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["schema", "description"].includes(diff.path[0] as string)
)
) {
throw new Error("Invalid resource type diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await ResourceService.updateResourceType({
workspace: workspace,
path: remotePath,
requestBody: changeset,
});
} else {
console.log(colors.yellow("Creating new resource type..."));
await ResourceService.createResourceType({
workspace: workspace,
requestBody: {
name: remotePath,
description: this.description,
schema: this.schema,
workspace_id: workspace,
},
});
}
}
}
};
export async function pushResourceType(
workspace: string,
filePath: string,
name: string,
) {
const data: ResourceTypeFile = decoverto.type(ResourceTypeFile).rawToInstance(
await Deno.readTextFile(filePath),
);
await data.push(workspace, name);
const data: ResourceTypeFile = JSON.parse(await Deno.readTextFile(filePath));
await pushResourceTypeDef(workspace, name, data);
}
export async function pushResourceTypeDef(
workspace: string,
name: string,
data: ResourceTypeFile,
) {
if (
await ResourceService.existsResourceType({
workspace: workspace,
path: name,
})
) {
console.log(colors.yellow("Updating existing resource type..."));
if (
(await ResourceService.listResourceType({ workspace })).findIndex((x) =>
x.name === name
) === -1
) {
console.log(
"Resource type " + name +
" is already taken for the current workspace, but cannot be updated. Is this a conflict with starter?",
);
return;
}
await ResourceService.updateResourceType({
workspace: workspace,
path: name,
requestBody: {
description: data.description,
schema: data.schema,
},
});
} else {
console.log(colors.yellow("Creating new resource type..."));
await ResourceService.createResourceType({
workspace: workspace,
requestBody: {
name: name,
description: data.description,
schema: data.schema,
workspace_id: workspace,
},
});
}
}
type PushOptions = GlobalOptions;

View File

@@ -1,142 +1,80 @@
import {
Difference,
GlobalOptions,
PushDiffs,
Resource as Resource2,
setValueByPath,
} from "./types.ts";
import { GlobalOptions } from "./types.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
colors,
Command,
EditResource,
microdiff,
Resource,
ResourceService,
Table,
} from "./deps.ts";
import { Any, decoverto, model, property } from "./decoverto.ts";
import { colors, Command, Resource, ResourceService, Table } from "./deps.ts";
@model()
export class ResourceFile implements Resource2, PushDiffs {
@property(Any)
value?: any;
@property(() => String)
type ResourceFile = {
value: any;
description?: string;
@property(() => String)
resource_type: string;
@property(() => Boolean)
is_oauth?: boolean; // deprecated
constructor(resource_type: string) {
this.resource_type = resource_type;
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (
await ResourceService.existsResource({
workspace: workspace,
path: remotePath,
})
) {
console.log(
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
);
const changeset: EditResource = {
path: remotePath, // TODO: Remove this in backend
};
for (const diff of diffs) {
if (diff.path[0] === "is_oauth") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
continue;
}
if (
diff.type !== "REMOVE" &&
(
diff.path[0] !== "value" && (
diff.path.length !== 1 ||
diff.path[0] !== "description"
)
)
) {
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await ResourceService.updateResource({
workspace: workspace,
path: remotePath,
requestBody: changeset,
});
} else {
if (typeof this.is_oauth !== "undefined") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
}
console.log(colors.yellow("Creating new resource..."));
await ResourceService.createResource({
workspace: workspace,
requestBody: {
path: remotePath,
resource_type: this.resource_type,
value: this.value,
description: this.description,
},
});
}
}
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(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
}
};
export async function pushResource(
workspace: string,
filePath: string,
remotePath: string,
) {
const data = decoverto.type(ResourceFile).rawToInstance(
await Deno.readTextFile(filePath),
);
await data.push(workspace, remotePath);
const data: ResourceFile = JSON.parse(await Deno.readTextFile(filePath));
if (
await ResourceService.existsResource({
workspace: workspace,
path: remotePath,
})
) {
console.log(colors.yellow("Updating existing resource..."));
const existing = await ResourceService.getResource({
workspace: workspace,
path: remotePath,
});
if (existing.resource_type != data.resource_type) {
console.log(
colors.red.underline.bold(
"Remote resource at " +
remotePath +
" exists & has a different resource type. This cannot be updated. If you wish to do this anyways, consider deleting the remote resource.",
),
);
return;
}
if (typeof data.is_oauth !== "undefined") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
}
await ResourceService.updateResource({
workspace: workspace,
path: remotePath,
requestBody: {
path: remotePath,
value: data.value,
description: data.description,
},
});
} else {
if (typeof data.is_oauth !== "undefined") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
}
console.log(colors.yellow("Creating new resource..."));
await ResourceService.createResource({
workspace: workspace,
requestBody: {
path: remotePath,
resource_type: data.resource_type,
value: data.value,
description: data.description,
},
});
}
}
type PushOptions = GlobalOptions;

View File

@@ -2,52 +2,20 @@
import { GlobalOptions } from "./types.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
colors,
Command,
JobService,
readAll,
Script,
ScriptService,
Table,
} from "./deps.ts";
import { Any, array, decoverto, model, property } from "./decoverto.ts";
} from "https://deno.land/x/windmill@v1.50.0/windmill-api/index.ts";
import { colors, Command, readAll, ScriptService, Table } from "./deps.ts";
@model()
export class ScriptFile {
@property(() => String)
type ScriptFile = {
parent_hash?: string;
@property(() => String)
summary: string;
@property(() => String)
description: string;
@property(Any)
schema?: any;
@property(() => Boolean)
is_template?: boolean;
@property(array(() => String))
lock?: Array<string>;
@property({
toInstance: (data) => {
if (data == null) return data;
if (
data === "script" || data === "failure" || data === "trigger" ||
data === "command" || data === "approvial"
) {
return data;
}
throw new Error("Invalid kind " + data);
},
toPlain: (data) => data,
})
kind?: "script" | "failure" | "trigger" | "command" | "approval";
constructor(summary: string, description: string) {
this.summary = summary;
this.description = description;
}
}
};
type PushOptions = GlobalOptions;
async function push(
@@ -80,12 +48,10 @@ async function push(
}
export async function findContentFile(filePath: string) {
console.log("Searching " + filePath);
const candidates = [
filePath.replace(".script.json", ".ts"),
filePath.replace(".script.json", ".py"),
filePath.replace(".script.json", ".go"),
filePath.replace(".script.json", ".sh"),
];
const validCandidates = (
await Promise.all(
@@ -113,35 +79,23 @@ export async function findContentFile(filePath: string) {
return validCandidates[0];
}
export function inferContentTypeFromFilePath(
contentPath: string,
): "python3" | "deno" | "go" | "bash" {
let language = contentPath.substring(contentPath.lastIndexOf("."));
if (language == ".ts") language = "deno";
if (language == ".py") language = "python3";
if (language == ".sh") language = "bash";
if (language == ".go") language = "go";
if (
language != "python3" && language != "deno" && language != "go" &&
language != "bash"
) {
throw new Error("Invalid language: " + language);
}
return language;
}
export async function pushScript(
filePath: string,
contentPath: string,
workspace: string,
remotePath: string,
) {
const data = decoverto.type(ScriptFile).rawToInstance(
await Deno.readTextFile(filePath),
);
const data: ScriptFile = JSON.parse(await Deno.readTextFile(filePath));
const content = await Deno.readTextFile(contentPath);
const language = inferContentTypeFromFilePath(contentPath);
let language = contentPath.substring(contentPath.lastIndexOf("."));
if (language == ".ts") language = "deno";
if (language == ".py") language = "python3";
if (language == ".go") language = "go";
if (language != "python3" && language != "deno" && language != "go") {
throw new Error("Invalid language: " + language);
}
let parent_hash = data.parent_hash;
if (!parent_hash) {
try {

View File

@@ -1,969 +0,0 @@
import { requireLogin, resolveWorkspace } from "./context.ts";
import { getWorkspaceStream, Workspace } from "./workspace.ts";
import { decoverto, map, MapShape, model, property } from "./decoverto.ts";
import {
cbor,
colors,
Command,
Confirm,
copy,
ensureDir,
iterateReader,
microdiff,
nanoid,
objectHash,
path,
ScriptService,
} from "./deps.ts";
import {
Difference,
getTypeStrFromPath,
GlobalOptions,
inferTypeFromPath,
setValueByPath,
} from "./types.ts";
import { downloadTar } from "./pull.ts";
import { FolderFile } from "./folder.ts";
import { ResourceTypeFile } from "./resource-type.ts";
import {
findContentFile,
inferContentTypeFromFilePath,
pushScript,
ScriptFile,
} from "./script.ts";
import { ResourceFile } from "./resource.ts";
import { FlowFile } from "./flow.ts";
import { VariableFile } from "./variable.ts";
type TrackedId = string;
const TrackedId = String;
const CONTENT_ENCODER: cbor.Encoder = new cbor.Encoder({ pack: true });
export class Tracked {
#id: TrackedId;
#parent: State;
path: string;
#content_cache?: string;
constructor(id: TrackedId, parent: State, path: string) {
this.#id = id;
this.#parent = parent;
this.path = path;
}
async getContent(): Promise<unknown | undefined> {
if (this.#content_cache) {
return this.#content_cache;
}
if (!this.#parent.stateRoot) {
throw new Error("Parent uninitialized");
}
const f = this.#parent.contentFile(this.#id);
if (!f) {
return undefined;
}
const data = await Deno.readFile(
path.join(this.#parent.stateRoot, ".wmill", f),
);
const content = CONTENT_ENCODER.decode(data);
this.#content_cache = content;
return content;
}
getHash(): string {
return this.#parent.hashes.get(this.#id)!;
}
getId(): TrackedId {
return this.#id;
}
}
@model()
export class State {
// TODO: Handle script contents, as they are separate files & need to be tracked separately.
// I think the way this makes most sense is actually reating them as a separate kind of file everywhere
// And then move to using a diff-based push system everywhere. This should greatly simplify code & enable this easily
@property(map(() => TrackedId, () => String, { shape: MapShape.Object }))
hashes: Map<TrackedId, string>;
@property(map(() => TrackedId, () => String, { shape: MapShape.Object }))
contentFiles: Map<TrackedId, string>;
@property(map(() => String, () => TrackedId, { shape: MapShape.Object }))
tracked: Map<string, TrackedId>;
@property(() => String)
workspaceId: string;
@property(() => String)
remoteUrl: string;
stateRoot?: string;
constructor(
hashes: Map<TrackedId, string>,
tracked: Map<string, TrackedId>,
contentFiles: Map<TrackedId, string>,
workspaceId: string,
remoteUrl: string,
) {
this.hashes = hashes;
this.tracked = tracked;
this.contentFiles = contentFiles;
this.workspaceId = workspaceId;
this.remoteUrl = remoteUrl;
}
add(path: string) {
if (this.tracked.get(path)) {
throw new Error("Cannot newly track already tracked paths");
} else {
this.tracked.set(path, nanoid());
}
}
public forget(path: string) {
const id = this.tracked.get(path);
if (id) {
this.tracked.delete(path);
this.hashes.delete(id);
}
}
public contentFile(id: TrackedId): string | undefined {
return this.contentFiles.get(id);
}
public get(path: string): Tracked {
const id = this.tracked.get(path);
if (!id) {
throw new Error("Could not resolve path " + path);
}
return new Tracked(id, this, path);
}
public async save(): Promise<void> {
if (!this.stateRoot) {
throw new Error("Uninitialized state root");
}
const encoder = new cbor.Encoder({});
const plain = decoverto.type(State).instanceToPlain(this);
const result: Uint8Array = encoder.encode(plain, {});
await Deno.writeFile(path.join(this.stateRoot, ".wmill", "main"), result, {
create: true,
});
}
public static async loadState(dir: string): Promise<State> {
const source = await Deno.readFile(path.join(dir, ".wmill", "main"));
const encoder = new cbor.Encoder({});
const raw = encoder.decode(source);
const state = decoverto.type(State).plainToInstance(
raw,
);
state.stateRoot = dir;
return Object.freeze(state);
}
}
async function getState(opts: GlobalOptions) {
const existingState = await State.loadState(Deno.cwd());
const workspaceStream = await getWorkspaceStream();
const reader = workspaceStream.getReader();
while (true) {
const res = await reader.read();
if (res.value) {
if (
new URL(res.value.remote) == new URL(existingState.remoteUrl) &&
res.value.workspaceId == existingState.workspaceId
) {
opts.workspace = res.value.name;
(opts as any).__secret_workspace = res.value;
break;
}
}
if (res.done) {
break;
}
}
return existingState;
}
async function updateStateFromRemote(
workspace: Workspace,
state: State,
callback: (filename: string) => PromiseLike<boolean> | boolean,
) {
const untar = await downloadTar(workspace);
if (!untar) throw new Error("Failed to pull Tar");
const decoder = new TextDecoder();
for await (const entry of untar) {
const id = state.tracked.get(entry.fileName);
if (id) {
let val = "";
for await (const e of iterateReader(entry)) {
const tmp = decoder.decode(e);
val += tmp;
}
if (entry.fileName.endsWith(".json")) {
const parsed = JSON.parse(val);
const typed = inferTypeFromPath(entry.fileName, parsed);
const oldHash = state.hashes.get(id);
const newHash = objectHash(typed);
if (!oldHash || oldHash !== newHash) {
if (!await callback(entry.fileName)) {
return; // notice that we are not saving
}
state.hashes.set(id, newHash);
const encoded = CONTENT_ENCODER.encode(typed);
const fileName = nanoid();
await Deno.writeFile(
path.join(state.stateRoot!, ".wmill", fileName),
encoded,
{ create: true },
);
state.contentFiles.set(id, fileName);
}
} else {
const fileName = nanoid();
await Deno.writeTextFile(
path.join(state.stateRoot!, ".wmill", fileName),
val,
{ create: true },
);
state.contentFiles.set(id, fileName);
}
}
}
await state.save();
}
async function pull(
opts: GlobalOptions & { raw: boolean; rawOverride: boolean },
) {
if (opts.raw) {
const opts2 = opts as any;
opts2.override = opts2.rawOverride;
opts2.raw = undefined;
opts2.rawOverride = undefined;
await pullRaw(opts2, Deno.cwd());
return;
}
const state = await getState(opts);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
console.log("Pulling remote changes");
await updateStateFromRemote(workspace, state, (_) => true);
const diffs = diffState(state);
await copyNonJsonFiles(state);
console.log(`Applying changes to files`);
for await (const diff of diffs) {
await applyDiff(
diff.diff,
path.join(state.stateRoot!, diff.localPath),
);
}
console.log(colors.green.underline("Done! All changes applied."));
async function applyDiff(diffs: Difference[], file: string) {
ensureDir(path.dirname(file));
let json;
try {
json = JSON.parse(await Deno.readTextFile(file));
} catch {
json = {};
}
// TODO: Delegate the below to the object itself
// This would work by infering the type of `JSON` (which includes then statically typing it using decoverto) and then
// delegating the applying of the diffs to the object via an interface
for (const diff of diffs) {
if (diff.type === "CREATE") {
setValueByPath(json, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(json, diff.path, undefined);
} else if (diff.type === "CHANGE") {
setValueByPath(json, diff.path, diff.value);
}
}
// TODO: Use decoverto instanceToPlain below.
await Deno.writeTextFile(file, JSON.stringify(json, undefined, " "), {
create: true,
});
}
async function copyNonJsonFiles(state: State): Promise<void> {
for (const t of state.tracked.keys()) {
if (t.endsWith(".json")) {
continue;
}
const entry = state.get(t);
const target = await Deno.open(entry.path, { create: true, write: true });
const source = await Deno.open(
path.join(
state.stateRoot!,
".wmill",
state.contentFile(entry.getId())!,
),
{
read: true,
write: false,
},
);
await source.readable.pipeTo(target.writable);
}
}
}
async function* diffState(state: State): AsyncGenerator<StateDiff, void, void> {
for (const t of state.tracked.keys()) {
if (!t.endsWith(".json")) {
continue;
}
const entry = state.get(t);
let fileText;
try {
fileText = await Deno.readTextFile(
path.join(state.stateRoot!, entry.path),
);
} catch {
fileText = "{}";
}
const old = JSON.parse(fileText);
const fileHash = objectHash(old);
if (fileHash !== entry.getHash()) {
const stateContent = await entry.getContent() as any;
const diff = microdiff(
old,
stateContent ?? {},
{ cyclesFix: false },
);
yield new StateDiff(entry.getId(), entry.path, diff);
}
}
}
function prettyDiff(diffs: Difference[]) {
for (const diff of diffs) {
let pathString = "";
for (const pathSegment of diff.path) {
if (typeof pathSegment === "string") {
pathString += ".";
pathString += pathSegment;
} else {
pathString += "[";
pathString += pathSegment;
pathString += "]";
}
}
if (diff.type === "REMOVE" || diff.type === "CHANGE") {
console.log(colors.red("- " + pathString + " = " + diff.oldValue));
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
console.log(colors.green("+ " + pathString + " = " + diff.value));
}
}
}
async function push(opts: GlobalOptions & { raw: boolean }) {
if (opts.raw) {
const opts2 = opts as any;
opts2.raw = undefined;
await pushRaw(opts2, undefined);
return;
}
const state = await getState(opts);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
let error = false;
await updateStateFromRemote(workspace, state, async (filename) => {
const e = state.get(filename);
if (!e) {
throw new Error("!? State change on untracked file ?!");
}
let fileJSON;
try {
fileJSON = JSON.parse(
await Deno.readTextFile(path.join(state.stateRoot!, e.path)),
);
} catch {
fileJSON = {};
}
const file = inferTypeFromPath(e.path, fileJSON);
const eContent = (inferTypeFromPath(e.path, await e.getContent())) ?? {};
const fileHash = objectHash(file);
const eHash = objectHash(eContent);
if (fileHash !== eHash) {
console.log(
colors.red("!! Local and Remote change present. Local diff:"),
);
prettyDiff(microdiff(eContent as any, file, { cyclesFix: false }));
console.log(
colors.red(
"Consider comitting or otherwise saving your work and pulling to load any remote changes",
),
);
error = true;
return false;
}
return true;
});
if (error) {
return;
}
for (const p of state.tracked.keys()) {
if (!p.endsWith(".json")) continue;
const entry = state.get(p);
let fileJSON;
try {
fileJSON = JSON.parse(
await Deno.readTextFile(path.join(state.stateRoot!, entry.path)),
);
} catch {
fileJSON = {};
}
const file = inferTypeFromPath(entry.path, fileJSON);
const eContent =
(inferTypeFromPath(entry.path, await entry.getContent())) ?? {};
const fileHash = objectHash(file);
const eHash = objectHash(eContent);
if (fileHash !== eHash) {
const remotePath = entry.path.split(".")[0];
const type = getTypeStrFromPath(entry.path);
if (type === "script") {
// Diffing makes no sense for scripts - instead fetch parent hash & check hash again.
// If hash is still missmatched - create new script as child.
const typed = decoverto.type(ScriptFile).plainToInstance(file);
const contentPath = await findContentFile(entry.path);
const language = inferContentTypeFromFilePath(contentPath);
const content = await Deno.readTextFile(contentPath);
try {
const remote = await ScriptService.getScriptByPath({
workspace: workspace.workspaceId,
path: remotePath,
});
if (objectHash(remote) !== fileHash) {
await ScriptService.createScript({
workspace: workspace.workspaceId,
requestBody: {
content,
description: typed.description,
language,
path: remotePath,
summary: typed.summary,
is_template: typed.is_template,
kind: typed.kind,
lock: undefined,
parent_hash: remote.hash,
schema: typed.schema,
},
});
}
} catch {
// no parent hash
await ScriptService.createScript({
workspace: workspace.workspaceId,
requestBody: {
content,
description: typed.description,
language,
path: remotePath,
summary: typed.summary,
is_template: typed.is_template,
kind: typed.kind,
lock: undefined,
parent_hash: undefined,
schema: typed.schema,
},
});
}
} else {
const diff = microdiff(eContent as any, file, { cyclesFix: false });
await applyDiff(
workspace.workspaceId,
remotePath,
file,
diff,
);
}
}
}
let anyRemoteChanges = false;
await updateStateFromRemote(workspace, state, (_) => {
anyRemoteChanges = true;
return true;
});
if (anyRemoteChanges) {
console.log("New remote changes - consider pulling");
}
function applyDiff(
workspace: string,
remotePath: string,
file:
| ScriptFile
| VariableFile
| FlowFile
| ResourceFile
| ResourceTypeFile
| FolderFile,
diffs: Difference[],
) {
if (file instanceof ScriptFile) {
throw new Error(
"This code path should be unreachable - we should never generate diffs for scripts",
);
} else if (file instanceof FolderFile) {
const parts = remotePath.split("/");
if (parts[0] === "f") {
remotePath = parts[1];
} else {
remotePath = parts[0];
}
}
return file.pushDiffs(workspace, remotePath, diffs);
}
}
class StateDiff {
trackedId: TrackedId;
localPath: string;
diff: Difference[];
constructor(
trackedId: TrackedId,
localPath: string,
diff: Difference[],
) {
this.trackedId = trackedId;
this.localPath = localPath;
this.diff = diff;
}
}
async function add(opts: GlobalOptions, path: string) {
const state = await getState(opts);
// TODO: Automatically check whether this path exists either locally or on the remote
state.add(path);
if (path.endsWith(".script.json")) {
try {
const f = await findContentFile(path);
state.add(f);
} catch {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
try {
const old = await ScriptService.getScriptByPath({
workspace: workspace.workspaceId,
path: path.split(".")[0],
});
if (old.language === "python3") {
state.add(path.replace(".script.json", ".py"));
} else if (old.language === "bash") {
state.add(path.replace(".script.json", ".sh"));
} else if (old.language === "deno") {
state.add(path.replace(".script.json", ".ts"));
} else if (old.language === "go") {
state.add(path.replace(".script.json", ".go"));
} else {
throw new Error("Remote returned invalid language?! " + old.language);
}
} catch {
throw new Error(
"Could not infer script language from local or remote. Exiting.",
);
}
}
}
await state.save();
}
async function init(opts: GlobalOptions) {
const root = Deno.cwd();
try {
await Deno.mkdir(path.join(root, ".wmill"));
} catch {
console.log(
colors.red(
"! Looks like this folder is already initialized or we are missing permissions to do so. Exiting.",
),
);
return;
}
const workspace = await resolveWorkspace(opts);
const newState = new State(
new Map(),
new Map(),
new Map(),
workspace.workspaceId,
workspace.remote,
);
newState.stateRoot = root;
await newState.save();
}
async function pullRaw(
opts: GlobalOptions & { override: boolean },
dir: string,
) {
const workspace = await resolveWorkspace(opts);
const untar = await downloadTar(workspace);
if (!untar) return;
for await (const entry of untar) {
console.log(entry.fileName);
const filePath = path.resolve(dir, entry.fileName);
if (entry.type === "directory") {
await ensureDir(filePath);
continue;
}
await ensureDir(path.dirname(filePath));
if (!opts.override) {
let exists = false;
try {
const _stat = await Deno.stat(filePath);
exists = true;
} catch {
exists = false;
}
if (exists) {
if (
!(await Confirm.prompt(
"Conflict at " +
filePath +
" do you want to override the local version?",
))
) {
continue;
}
}
}
const file = await Deno.open(filePath, { write: true, create: true });
const len = await copy(entry, file);
await file.truncate(len);
file.close();
}
console.log(colors.green("Done. Wrote all files to disk."));
}
type PushRawCandidate = {
path: string;
namespaceKind: "user" | "group" | "folder";
namespaceName: string;
};
type PushRawResourceTypeCandidate = {
path: string;
};
type PushRawFolderCandidate = {
path: string;
namespaceName: string;
};
async function pushRawFindCandidateFiles(
dir: string,
): Promise<
{
normal: PushRawCandidate[];
resourceTypes: PushRawResourceTypeCandidate[];
folders: PushRawFolderCandidate[];
}
> {
dir = path.resolve(dir);
if (path.dirname(dir).startsWith(".")) {
return { normal: [], resourceTypes: [], folders: [] };
}
const normalCandidates: PushRawCandidate[] = [];
const resourceTypeCandidates: PushRawResourceTypeCandidate[] = [];
const folderCandidates: PushRawFolderCandidate[] = [];
for await (const e of Deno.readDir(dir)) {
if (e.isDirectory) {
if (e.name == "u" || e.name == "g" || e.name == "f") { // TODO: Check version for f
const newDir = dir + (dir.endsWith("/") ? "" : "/") + e.name;
for await (const e2 of Deno.readDir(newDir)) {
if (e2.isDirectory) {
if (e2.name.startsWith(".")) continue;
const namespaceName = e2.name;
const stack: string[] = [];
{
const path = newDir + "/" + namespaceName + "/";
stack.push(path);
try {
await Deno.stat(path + "folder.meta.json");
folderCandidates.push({
namespaceName,
path: path + "folder.meta.json",
});
} catch {}
}
while (stack.length > 0) {
const dir2 = stack.pop()!;
for await (const e3 of Deno.readDir(dir2)) {
if (e3.isFile) {
if (e3.name === "folder.meta.json") continue;
normalCandidates.push({
path: dir2 + e3.name,
namespaceKind: e.name == "g"
? "group"
: e.name == "u"
? "user"
: "folder",
namespaceName: namespaceName,
});
} else {
stack.push(dir2 + e3.name + "/");
}
}
}
}
}
} else {
console.log(
colors.yellow(
"Including organizational folder " + e.name + " in push!",
),
);
const { normal, resourceTypes, folders } =
await pushRawFindCandidateFiles(
path.join(dir, e.name),
);
normalCandidates.push(...normal);
resourceTypeCandidates.push(...resourceTypes);
folderCandidates.push(...folders);
}
} else {
// handle root files
if (e.name.endsWith(".resource-type.json")) {
resourceTypeCandidates.push({
path: dir + (dir.endsWith("/") ? "" : "/") + e.name,
});
}
}
}
return {
normal: normalCandidates,
folders: folderCandidates,
resourceTypes: resourceTypeCandidates,
};
}
async function pushRaw(opts: GlobalOptions, dir?: string) {
dir = dir ?? Deno.cwd();
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
console.log(colors.blue("Searching Directory..."));
const { normal, resourceTypes, folders } = await pushRawFindCandidateFiles(
dir,
);
console.log(
colors.blue(
"Found " + (normal.length + resourceTypes.length + folders.length) +
" candidates",
),
);
for (const resourceType of resourceTypes) {
const fileName = resourceType.path.substring(
resourceType.path.lastIndexOf("/") + 1,
);
const fileNameParts = fileName.split(".");
// invalid file names, like my.cool.script.script.json. Not valid.
if (fileNameParts.length != 3) {
console.log(
colors.yellow("invalid file name found at " + resourceType.path),
);
continue;
}
// filter out non-json files. Note that we filter out script contents above, so this is really an error.
if (fileNameParts.at(-1) != "json") {
console.log(colors.yellow("non-JSON file found at " + resourceType.path));
continue;
}
console.log("pushing resource type " + fileNameParts.at(-3)!);
await decoverto.type(ResourceTypeFile).rawToInstance(
await Deno.readTextFile(resourceType.path),
).push(workspace.workspaceId, fileNameParts.at(-3)!);
}
for (const folder of folders) {
await decoverto.type(FolderFile).plainToInstance(
JSON.parse(await Deno.readTextFile(folder.path)),
).push(
workspace.workspaceId,
"f/" + folder.namespaceName,
);
}
for (const candidate of normal) {
// full file name. No leading /. includes .type.json
const fileName = candidate.path.substring(
candidate.path.lastIndexOf("/") + 1,
);
// figure out just the path after ...../u|g/username|group/ (in extra dir)
const dirParts = candidate.path.split("/").filter((x) => x.length > 0);
// TODO: check version for folder
const gIndex = dirParts.findIndex((x) => x == "u" || x == "g" || x == "f");
const extraDir = dirParts.slice(gIndex + 2, -1).join("/");
// file name parts has .json (hopefully) at -1, type at -2, and the actual name at -3. Dots in names are not allowed.
const fileNameParts = fileName.split(".");
// filter out script content files
if (
fileNameParts.at(-1) == "ts" ||
fileNameParts.at(-1) == "py" ||
fileNameParts.at(-1) == "go"
) {
// probably part of a script. Silent ignore.
continue;
}
// invalid file names, like my.cool.script.script.json. Not valid.
if (fileNameParts.length != 3) {
console.log(
colors.yellow("invalid file name found at " + candidate.path),
);
continue;
}
// filter out non-json files. Note that we filter out script contents above, so this is really an error.
if (fileNameParts.at(-1) != "json") {
console.log(colors.yellow("non-JSON file found at " + candidate.path));
continue;
}
// get the type & filter it for valid ones.
const type = fileNameParts.at(-2);
if (type == "resource-type") {
console.log(
colors.yellow(
"Found resource type file at " +
candidate.path +
" this appears to be inside a path folder. Resource types are not addressed by path. Place them at the root or inside only an organizational folder. Ignoring this file!",
),
);
continue;
}
if (
type != "flow" &&
type != "resource" &&
type != "script" &&
type != "variable"
) {
console.log(
colors.yellow(
"file with invalid type " + type + " found at " + candidate.path,
),
);
continue;
}
// create the remotePath for the API
const remotePath = (candidate.namespaceKind === "group"
? "g/"
: (candidate.namespaceKind === "user" ? "u/" : "f/")) +
candidate.namespaceName +
"/" +
(extraDir.length > 0 ? extraDir + "/" : "") +
fileNameParts.at(-3);
console.log("pushing " + type + " to " + remotePath);
const typed = inferTypeFromPath(
candidate.path,
JSON.parse(await Deno.readTextFile(candidate.path)),
);
if (typed instanceof ResourceTypeFile || typed instanceof FolderFile) {
throw new Error(
"Resource Types and Folders should be filtered out at this point!",
);
} else if (typed instanceof ScriptFile) {
let contentPath: string;
try {
contentPath = await findContentFile(candidate.path);
} catch (e) {
console.log(colors.red(e.toString()));
continue;
}
await pushScript(
candidate.path,
contentPath,
workspace.workspaceId,
remotePath,
);
} else {
typed.push(workspace.workspaceId, remotePath);
}
}
console.log(colors.underline.bold.green("Successfully Pushed all files."));
}
const command = new Command()
.command("init")
.description(
"Initialize this folder as sync root for the currently selected workspace & remote.",
)
.action(init as any)
.command("add")
.description("Add a local file for tracking")
.arguments("<path:string>")
.action(add as any)
.command("pull")
.description("Pull any remote changes and apply them locally")
.option("--raw", "Pull without using state.")
.option("--raw-override", "Always override local files with remote.", {
depends: ["raw"],
})
.action(pull as any)
.command("push")
.description("Push any local changes and apply them remotely")
.option("--raw", "Push without using state.")
.action(push as any);
export default command;

View File

@@ -0,0 +1,10 @@
{
"workspace_id": "admins",
"name": "my_folder",
"display_name": "my_folder",
"owners": [],
"extra_perms": {
"u/test": true,
"u/admin@windmill.dev": false
}
}

View File

@@ -0,0 +1,12 @@
{
"summary": "Syncronize Hub Resource types with starter workspace",
"description": "Basic administrative script to sync latest resource types from hub. Recommended to run at least once. On a schedule by default.",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {},
"required": [],
"type": "object"
},
"is_template": false,
"lock": []
}

View File

@@ -0,0 +1,13 @@
import wmill from "https://deno.land/x/wmill@v1.55.0/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "starter", Deno.env.get("WM_BASE_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);
}

View File

@@ -1,137 +1,4 @@
import { decoverto } from "./decoverto.ts";
import { FlowFile } from "./flow.ts";
import { ResourceTypeFile } from "./resource-type.ts";
import { ResourceFile } from "./resource.ts";
import { ScriptFile } from "./script.ts";
import { VariableFile } from "./variable.ts";
import { path } from "./deps.ts";
import { FolderFile } from "./folder.ts";
// TODO: Remove this & replace with a "pull" that lets the object either pull the remote version or return undefined.
// Then combine those with diffing, which then gives the new push impl
export interface Resource {
push(workspace: string, remotePath: string): Promise<void>;
}
export interface PushDiffs {
pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void>;
}
export interface DifferenceCreate {
type: "CREATE";
path: (string | number)[];
value: any;
}
export interface DifferenceRemove {
type: "REMOVE";
path: (string | number)[];
oldValue: any;
}
export interface DifferenceChange {
type: "CHANGE";
path: (string | number)[];
value: any;
oldValue: any;
}
export type Difference = DifferenceCreate | DifferenceRemove | DifferenceChange;
export function setValueByPath(
obj: any,
path: (string | number)[],
value: any,
) {
let i;
let lastObj = undefined;
for (i = 0; i < path.length - 1; i++) {
if (!obj) {
let oldNewObj;
if (typeof path[i] === "number") {
oldNewObj = [];
} else {
oldNewObj = {};
}
lastObj[path[i - 1]] = oldNewObj;
obj = oldNewObj;
}
lastObj = obj;
obj = obj[path[i]];
}
if (!obj) {
let oldNewObj;
if (typeof path[i] === "number") {
oldNewObj = [];
} else {
oldNewObj = {};
}
lastObj[path[i - 1]] = oldNewObj;
obj = oldNewObj;
}
obj[path[i]] = value;
}
export type GlobalOptions = {
workspace: string | undefined;
token: string | undefined;
};
export function inferTypeFromPath(
p: string,
obj: any,
):
| ScriptFile
| VariableFile
| FlowFile
| ResourceFile
| ResourceTypeFile
| FolderFile {
const typeEnding = getTypeStrFromPath(p);
if (typeEnding === "folder") {
return decoverto.type(FolderFile).plainToInstance(obj);
} else if (typeEnding === "script") {
return decoverto.type(ScriptFile).plainToInstance(obj);
} else if (typeEnding === "variable") {
return decoverto.type(VariableFile).plainToInstance(obj);
} else if (typeEnding === "flow") {
return decoverto.type(FlowFile).plainToInstance(obj);
} else if (typeEnding === "resource") {
return decoverto.type(ResourceFile).plainToInstance(obj);
} else if (typeEnding === "resource-type") {
return decoverto.type(ResourceTypeFile).plainToInstance(obj);
} else {
throw new Error("infer type unreachable");
}
}
export function getTypeStrFromPath(
p: string,
): "script" | "variable" | "flow" | "resource" | "resource-type" | "folder" {
const parsed = path.parse(p);
if (parsed.ext !== ".json") {
throw new Error(
"Cannot infer type of non-json file " + JSON.stringify(parsed),
);
}
if (parsed.name === "folder.meta") {
return "folder";
}
const typeEnding = parsed.name.split(".").at(-1);
if (
typeEnding === "script" || typeEnding === "variable" ||
typeEnding === "flow" || typeEnding === "resource" ||
typeEnding === "resource-type"
) {
return typeEnding;
} else {
throw new Error("Could not infer type of path " + JSON.stringify(parsed));
}
}

View File

@@ -1,22 +1,7 @@
// deno-lint-ignore-file no-explicit-any
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
Difference,
GlobalOptions,
PushDiffs,
Resource,
setValueByPath,
} from "./types.ts";
import {
colors,
Command,
EditVariable,
ListableVariable,
microdiff,
Table,
VariableService,
} from "./deps.ts";
import { decoverto, model, property } from "./decoverto.ts";
import { GlobalOptions } from "./types.ts";
import { colors, Command, Table, VariableService } from "./deps.ts";
async function list(opts: GlobalOptions) {
const workspace = await resolveWorkspace(opts);
@@ -41,100 +26,13 @@ async function list(opts: GlobalOptions) {
.render();
}
@model()
export class VariableFile implements Resource, PushDiffs {
@property(() => String)
type VariableFile = {
value: string;
@property(() => Boolean)
is_secret: boolean;
@property(() => String)
description: string;
@property(() => Number)
account?: number;
@property(() => Boolean)
is_oauth?: boolean;
constructor(value: string, is_secret: boolean, description: string) {
this.value = value;
this.is_secret = is_secret;
this.description = description;
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing variable...`,
),
);
const changeset: EditVariable = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["path", "value", "is_secret", "description"].includes(
diff.path[0] as string,
)
)
) {
throw new Error("Invalid variable diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await VariableService.updateVariable({
workspace,
path: remotePath,
requestBody: changeset,
});
} else {
console.log(colors.yellow("Creating new variable..."));
await VariableService.createVariable({
workspace,
requestBody: {
path: remotePath,
description: this.description,
is_secret: this.is_secret,
value: this.value,
account: this.account,
is_oauth: this.is_oauth,
},
});
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: ListableVariable | undefined;
try {
existing = await VariableService.getVariable({
workspace: workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
}
};
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
@@ -160,10 +58,72 @@ export async function pushVariable(
filePath: string,
remotePath: string,
) {
const data = decoverto.type(VariableFile).rawToInstance(
await Deno.readTextFile(filePath),
);
await data.push(workspace, remotePath);
const data: VariableFile = JSON.parse(await Deno.readTextFile(filePath));
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
const existing = await VariableService.getVariable({
workspace: workspace,
path: remotePath,
});
if (existing.is_oauth != data.is_oauth) {
console.log(
colors.red.underline.bold(
"Remote variable at " +
remotePath +
" exists & has a different oauth state. This cannot be updated. If you wish to do this anyways, consider deleting the remote resource.",
),
);
return;
}
if (existing.account != data.account) {
console.log(
colors.red.underline.bold(
"Remote variable at " +
remotePath +
" exists & has a different account state. This cannot be updated. If you wish to do this anyways, consider deleting the remote resource.",
),
);
return;
}
if (existing.is_secret && !data.is_secret) {
console.log(
colors.red.underline.bold(
"Remote variable at " +
remotePath +
" exists & is secret. Variables cannot be updated to be no longer secret. If you wish to do this anyways, consider deleting the remote resource.",
),
);
return;
}
const actual_secret = data.is_secret ? true : undefined;
console.log(colors.yellow("Updating existing variable..."));
await VariableService.updateVariable({
workspace,
path: remotePath,
requestBody: {
description: data.description,
is_secret: actual_secret,
path: remotePath,
value: data.value,
},
});
} else {
console.log(colors.yellow("Creating new variable..."));
await VariableService.createVariable({
workspace,
requestBody: {
path: remotePath,
description: data.description,
is_secret: data.is_secret,
value: data.value,
account: data.account,
is_oauth: data.is_oauth,
},
});
}
}
const command = new Command()

View File

@@ -11,31 +11,14 @@ import {
Table,
WorkspaceService,
} from "./deps.ts";
import { decoverto, model, property } from "./decoverto.ts";
import { requireLogin } from "./context.ts";
@model()
export class Workspace {
@property(() => String)
export type Workspace = {
remote: string;
@property(() => String)
workspaceId: string;
@property(() => String)
name: string;
@property(() => String)
token: string;
constructor(
remote: string,
workspaceId: string,
name: string,
token: string,
) {
this.remote = remote;
this.workspaceId = workspaceId;
this.name = name;
this.token = token;
}
}
};
function makeWorkspaceStream(
readable: ReadableStream<Uint8Array>,
@@ -50,9 +33,7 @@ function makeWorkspaceStream(
if (line.length <= 2) {
return;
}
const workspace = decoverto.type(Workspace).rawToInstance(line);
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
controller.enqueue(workspace);
controller.enqueue(JSON.parse(line) as Workspace);
} catch {
/* ignore */
}
@@ -61,17 +42,14 @@ function makeWorkspaceStream(
);
}
export async function getWorkspaceStream() {
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
write: false,
read: true,
});
return makeWorkspaceStream(file.readable);
}
export async function allWorkspaces(): Promise<Workspace[]> {
async function allWorkspaces(): Promise<Workspace[]> {
try {
const workspaceStream = await getWorkspaceStream();
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
write: false,
read: true,
});
const workspaceStream = makeWorkspaceStream(file.readable);
const workspaces: Workspace[] = [];
for await (const workspace of workspaceStream) {
workspaces.push(workspace);
@@ -109,7 +87,8 @@ export async function getActiveWorkspace(
export async function getWorkspaceByName(
workspaceName: string,
): Promise<Workspace | undefined> {
const workspaceStream = await getWorkspaceStream();
const file = await Deno.open((await getRootStore()) + "remotes.ndjson");
const workspaceStream = makeWorkspaceStream(file.readable);
for await (const workspace of workspaceStream) {
if (workspace.name === workspaceName) {
return workspace;
@@ -206,7 +185,6 @@ export async function add(
remote = new URL(await Input.prompt("Enter the Remote URL")).toString();
}
}
remote = new URL(remote).toString(); // add trailing slash in all cases!
let token = await tryGetLoginInfo(opts);
while (!token) {
@@ -214,10 +192,7 @@ export async function add(
}
if (opts.create) {
setClient(
token,
remote.endsWith("/") ? remote.substring(0, remote.length - 1) : remote,
);
setClient(token, remote.endsWith('/') ? remote.substring(0, remote.length - 1) : remote);
if (
!await WorkspaceService.existsWorkspace({
@@ -249,7 +224,6 @@ export async function add(
}
export async function addWorkspace(workspace: Workspace) {
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
append: true,
write: true,
@@ -260,7 +234,7 @@ export async function addWorkspace(workspace: Workspace) {
file.close();
}
export async function removeWorkspace(name: string) {
async function remove(_opts: GlobalOptions, name: string) {
const orgWorkspaces = await allWorkspaces();
await Deno.writeTextFile(
(await getRootStore()) + "remotes.ndjson",
@@ -269,10 +243,6 @@ export async function removeWorkspace(name: string) {
.map((x) => JSON.stringify(x))
.join("\n"),
);
}
async function remove(_opts: GlobalOptions, name: string) {
await removeWorkspace(name);
console.log(colors.green.underline("Succesfully removed workspace!"));
}

View File

@@ -7,12 +7,6 @@ export {
UserService, WorkspaceService
} from './windmill-api/index.ts'
// @ts-ignore: Otherwise BigInt is not supported for export
BigInt.prototype.toJSON = function () {
return this.toString();
};
export { pgSql, pgClient } from './pg.ts'
export { mySql, mysqlClient } from './mysql.ts'

View File

@@ -20,41 +20,29 @@ services:
timeout: 5s
retries: 5
windmill_server:
windmill:
image: ghcr.io/windmill-labs/windmill:main
deploy:
replicas: 1
# Set privileged to true if enabling nsjail
privileged: false
restart: unless-stopped
ports:
- 8000:8000
environment:
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
- BASE_URL=http://${WM_BASE_URL}
- BASE_INTERNAL_URL=http://localhost:8000
- RUST_LOG=info
## You can set the number of workers to > 0 and not need any separate worker service
- NUM_WORKERS=0
- DISABLE_SERVER=false
- METRICS_ADDR=false
depends_on:
db:
condition: service_healthy
windmill_worker:
image: ghcr.io/windmill-labs/windmill:main
deploy:
replicas: 3
restart: unless-stopped
environment:
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
- BASE_URL=http://${WM_BASE_URL}
- BASE_INTERNAL_URL=http://windmill_server:8000
- RUST_LOG=info
- NUM_WORKERS=1
- DISABLE_SERVER=true
- NUM_WORKERS=3
- KEEP_JOB_DIR=false
- DENO_PATH=/usr/bin/deno
- PYTHON_PATH=/usr/local/bin/python3
- METRICS_ADDR=false
# for ease of use, nsjail which provide isolation in untrusted environment is disabled by default.
# To enable it, uncomment the following line, set the container as privileged and
# rebuild the image with nsjail=true or use the enterprise edition
# - DISABLE_NSJAIL=false
# - DISABLE_NUSER=false
# - NSJAIL_PATH=nsjail
depends_on:
db:
condition: service_healthy

View File

@@ -1,140 +0,0 @@
FROM debian:buster-slim as nsjail
WORKDIR /nsjail
ARG nsjail=""
RUN if [ "$nsjail" = "true" ]; then apt-get -y update \
&& apt-get install -y \
bison=2:3.3.* \
flex=2.6.* \
g++=4:8.3.* \
gcc=4:8.3.* \
git=1:2.20.* \
libprotobuf-dev=3.6.* \
libnl-route-3-dev=3.4.* \
make=4.2.* \
pkg-config=0.29-6 \
protobuf-compiler=3.6.*; fi
RUN if [ "$nsjail" = "true" ]; then git clone -b master --single-branch https://github.com/google/nsjail.git . \
&& git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800; fi
RUN if [ "$nsjail" = "true" ]; then make; else touch nsjail; fi
FROM rust:slim-buster AS rust_base
RUN apt-get update && apt-get install -y git libssl-dev pkg-config npm
RUN apt-get -y update \
&& apt-get install -y \
curl lld nodejs npm
RUN rustup component add rustfmt
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install cargo-chef
WORKDIR /windmill
ENV SQLX_OFFLINE=true
ENV CARGO_INCREMENTAL=1
FROM node:19-alpine as frontend
# install dependencies
WORKDIR /frontend
COPY ./frontend/package.json ./frontend/package-lock.json ./
RUN npm ci
# Copy all local files into the image.
COPY frontend .
RUN mkdir /backend
COPY /backend/windmill-api/openapi.yaml /backend/windmill-api/openapi.yaml
COPY /openflow.openapi.yaml /openflow.openapi.yaml
COPY /backend/windmill-api/build_openapi.sh /backend/windmill-api/build_openapi.sh
RUN cd /backend/windmill-api && . ./build_openapi.sh
RUN npm run generate-backend-client
ENV NODE_OPTIONS "--max-old-space-size=8192"
RUN npm run build
RUN npm run check
FROM rust_base AS planner
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef prepare --recipe-path recipe.json
FROM rust_base AS builder
ARG features=""
COPY --from=planner /windmill/recipe.json 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 ./backend ./
COPY --from=frontend /frontend /frontend
COPY --from=frontend /backend/windmill-api/openapi-deref.yaml ./windmill-api/openapi-deref.yaml
COPY .git/ .git/
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
FROM python:3.10.8-slim-buster
ARG APP=/usr/src/app
RUN apt-get update \
&& apt-get install -y ca-certificates wget curl git jq libprotobuf-dev libnl-route-3-dev unzip \
&& apt-get install -y ca-certificates wget curl git jq libprotobuf-dev libnl-route-3-dev unzip build-essential \
&& rm -rf /var/lib/apt/lists/*
RUN arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
curl -o rclone.zip "https://downloads.rclone.org/v1.60.1/rclone-v1.60.1-linux-$arch.zip"; \
unzip -p rclone.zip rclone-v1.60.1-linux-$arch/rclone > /usr/bin/rclone; rm rclone.zip; \
chown root:root /usr/bin/rclone; chmod 755 /usr/bin/rclone
RUN set -eux; \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
url=; \
case "$arch" in \
'amd64') \
targz='go1.19.3.linux-amd64.tar.gz'; \
;; \
'arm64') \
targz='go1.19.3.linux-arm64.tar.gz'; \
;; \
'armhf') \
targz='go1.19.3.linux-armv6l.tar.gz'; \
;; \
*) echo >&2 "error: unsupported architecture '$arch' (likely packaging update needed)"; exit 1 ;; \
esac; \
wget "https://golang.org/dl/$targz" -nv && tar -C /usr/local -xzf "$targz" && rm "$targz";
ENV PATH="${PATH}:/usr/local/go/bin"
ENV GO_PATH=/usr/local/go/bin/go
ENV TZ=Etc/UTC
RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
COPY --from=denoland/deno:latest /usr/bin/deno /usr/bin/deno
RUN /usr/local/bin/python3 -m pip install openbb[all]
RUN mkdir -p ${APP}
WORKDIR ${APP}
EXPOSE 8000
CMD ["./windmill"]

View File

@@ -1,20 +1,20 @@
{
"name": "windmill",
"version": "1.62.0",
"version": "1.60.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill",
"version": "1.62.0",
"version": "1.60.0",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@leeoniya/ufuzzy": "^0.9.1",
"@leeoniya/ufuzzy": "^0.9.0",
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.7.6",
"async-mutex": "^0.4.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-zoom": "^2.0.0",
"date-fns": "^2.29.3",
"highlight.js": "^11.7.0",
@@ -32,12 +32,12 @@
"@playwright/test": "^1.29.2",
"@sveltejs/adapter-static": "^1.0.0",
"@sveltejs/kit": "^1.0.0-next.589",
"@sveltejs/package": "^1.0.2",
"@sveltejs/package": "^1.0.1",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/node": "^18.11.18",
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelvet": "^4.0.20",
"@zerodevx/svelte-toast": "^0.8.1",
@@ -65,13 +65,13 @@
"svelte-highlight": "^6.2.1",
"svelte-overlay": "^1.4.1",
"svelte-popperjs": "^1.3.2",
"svelte-preprocess": "^5.0.1",
"svelte-preprocess": "^5.0.0",
"svelte-splitpanes": "^0.7.3",
"svelte2tsx": "^0.5.22",
"tailwindcss": "^3.2.2",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.4"
"vite": "^4.0.3"
},
"peerDependencies": {
"svelte": "^3.53.1"
@@ -709,9 +709,9 @@
"dev": true
},
"node_modules/@leeoniya/ufuzzy": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-0.9.1.tgz",
"integrity": "sha512-xlL3HMhHdZX/UVTyjiLB0hpUa71HERhegvrfkUD9NxGnYh9kk9+/llynEWS+NmGRB2KlRKoY+HmWZzt/c0whJQ=="
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-0.9.0.tgz",
"integrity": "sha512-p2zWsX0GwO1x723Yhb3KLAoSwp1geQvzRPHgIoOR/0qn8Ptpsb3b01+W47iAYR/NWo0pX36XQoTU0alVRykMAg=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
@@ -828,15 +828,15 @@
}
},
"node_modules/@sveltejs/package": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-1.0.2.tgz",
"integrity": "sha512-VY9U+05d9uNFDj7ScKRlHORYlfPSHwJewBjV+V2RsnViexpLFPUrboC9SiPYDCpLnbeqwXerxhO6twGHUBGeIA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-1.0.1.tgz",
"integrity": "sha512-iYoDz4AEJQUfdKUfBcwtYEGYkf4NMByQL3Sl2ESnu+IXsLNsHvhH0zUDhAmUmAgcrH8fVjiR7FuJeyh+7EQtiw==",
"dev": true,
"dependencies": {
"chokidar": "^3.5.3",
"kleur": "^4.1.5",
"sade": "^1.8.1",
"svelte2tsx": "~0.6.0"
"svelte2tsx": "~0.5.20"
},
"bin": {
"svelte-package": "svelte-package.js"
@@ -848,20 +848,6 @@
"svelte": "^3.44.0"
}
},
"node_modules/@sveltejs/package/node_modules/svelte2tsx": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.6.0.tgz",
"integrity": "sha512-TrxfQkO7CKi8Pu2eC/FyteDCdk3OOeQV5u6z7OjYAsOhsd0ClzAKqxJdvp6xxNQLrbFzf/XvCi9Fy8MQ1MleFA==",
"dev": true,
"dependencies": {
"dedent-js": "^1.0.1",
"pascal-case": "^3.1.1"
},
"peerDependencies": {
"svelte": "^3.55",
"typescript": "^4.9.4"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.0.2.tgz",
@@ -1015,14 +1001,14 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz",
"integrity": "sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz",
"integrity": "sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.49.0",
"@typescript-eslint/type-utils": "5.49.0",
"@typescript-eslint/utils": "5.49.0",
"@typescript-eslint/scope-manager": "5.46.1",
"@typescript-eslint/type-utils": "5.46.1",
"@typescript-eslint/utils": "5.46.1",
"debug": "^4.3.4",
"ignore": "^5.2.0",
"natural-compare-lite": "^1.4.0",
@@ -1149,13 +1135,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.49.0.tgz",
"integrity": "sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz",
"integrity": "sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.49.0",
"@typescript-eslint/visitor-keys": "5.49.0"
"@typescript-eslint/types": "5.46.1",
"@typescript-eslint/visitor-keys": "5.46.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1166,13 +1152,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.49.0.tgz",
"integrity": "sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz",
"integrity": "sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "5.49.0",
"@typescript-eslint/utils": "5.49.0",
"@typescript-eslint/typescript-estree": "5.46.1",
"@typescript-eslint/utils": "5.46.1",
"debug": "^4.3.4",
"tsutils": "^3.21.0"
},
@@ -1193,9 +1179,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz",
"integrity": "sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.1.tgz",
"integrity": "sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1206,13 +1192,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz",
"integrity": "sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz",
"integrity": "sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.49.0",
"@typescript-eslint/visitor-keys": "5.49.0",
"@typescript-eslint/types": "5.46.1",
"@typescript-eslint/visitor-keys": "5.46.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1233,16 +1219,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz",
"integrity": "sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.1.tgz",
"integrity": "sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.49.0",
"@typescript-eslint/types": "5.49.0",
"@typescript-eslint/typescript-estree": "5.49.0",
"@typescript-eslint/scope-manager": "5.46.1",
"@typescript-eslint/types": "5.46.1",
"@typescript-eslint/typescript-estree": "5.46.1",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0",
"semver": "^7.3.7"
@@ -1259,12 +1245,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.49.0.tgz",
"integrity": "sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz",
"integrity": "sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.49.0",
"@typescript-eslint/types": "5.46.1",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
@@ -1882,12 +1868,11 @@
"peer": true
},
"node_modules/chartjs-adapter-date-fns": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
"peerDependencies": {
"chart.js": ">=2.8.0",
"date-fns": ">=2.0.0"
"chart.js": "^3.0.0"
}
},
"node_modules/chartjs-plugin-zoom": {
@@ -5832,18 +5817,18 @@
}
},
"node_modules/sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
"integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
"integrity": "sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.14",
"buffer-crc32": "^0.2.5",
"minimist": "^1.2.0",
"sander": "^0.5.0"
"sander": "^0.5.0",
"sourcemap-codec": "^1.3.0"
},
"bin": {
"sorcery": "bin/sorcery"
"sorcery": "bin/index.js"
}
},
"node_modules/source-map": {
@@ -5864,6 +5849,12 @@
"node": ">=0.10.0"
}
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"node_modules/spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@@ -6272,9 +6263,9 @@
"integrity": "sha512-jhtZWtD6cUE2nMw46dJ5VXWYiqnER+JH+V/BmNBQ5fNP/YdsJCpJi+DemUy9msklqGb0f+wLhJPBtKHLWQvzjg=="
},
"node_modules/svelte-preprocess": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.1.tgz",
"integrity": "sha512-0HXyhCoc9rsW4zGOgtInylC6qj259E1hpFnJMJWTf+aIfeqh4O/QHT31KT2hvPEqQfdjmqBR/kO2JDkkciBLrQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz",
"integrity": "sha512-q7lpa7i2FBu8Pa+G0MmuQQWETBwCKgsGmuq1Sf6n8q4uaG9ZLcLP0Y+etC6bF4sE6EbLxfiI38zV6RfPe3RSfg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -6282,7 +6273,7 @@
"@types/sass": "^1.43.1",
"detect-indent": "^6.1.0",
"magic-string": "^0.27.0",
"sorcery": "^0.11.0",
"sorcery": "^0.10.0",
"strip-indent": "^3.0.0"
},
"engines": {
@@ -6795,9 +6786,9 @@
}
},
"node_modules/vite": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz",
"integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.3.tgz",
"integrity": "sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==",
"dev": true,
"dependencies": {
"esbuild": "^0.16.3",
@@ -7546,9 +7537,9 @@
"dev": true
},
"@leeoniya/ufuzzy": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-0.9.1.tgz",
"integrity": "sha512-xlL3HMhHdZX/UVTyjiLB0hpUa71HERhegvrfkUD9NxGnYh9kk9+/llynEWS+NmGRB2KlRKoY+HmWZzt/c0whJQ=="
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-0.9.0.tgz",
"integrity": "sha512-p2zWsX0GwO1x723Yhb3KLAoSwp1geQvzRPHgIoOR/0qn8Ptpsb3b01+W47iAYR/NWo0pX36XQoTU0alVRykMAg=="
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
@@ -7633,27 +7624,15 @@
}
},
"@sveltejs/package": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-1.0.2.tgz",
"integrity": "sha512-VY9U+05d9uNFDj7ScKRlHORYlfPSHwJewBjV+V2RsnViexpLFPUrboC9SiPYDCpLnbeqwXerxhO6twGHUBGeIA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-1.0.1.tgz",
"integrity": "sha512-iYoDz4AEJQUfdKUfBcwtYEGYkf4NMByQL3Sl2ESnu+IXsLNsHvhH0zUDhAmUmAgcrH8fVjiR7FuJeyh+7EQtiw==",
"dev": true,
"requires": {
"chokidar": "^3.5.3",
"kleur": "^4.1.5",
"sade": "^1.8.1",
"svelte2tsx": "~0.6.0"
},
"dependencies": {
"svelte2tsx": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.6.0.tgz",
"integrity": "sha512-TrxfQkO7CKi8Pu2eC/FyteDCdk3OOeQV5u6z7OjYAsOhsd0ClzAKqxJdvp6xxNQLrbFzf/XvCi9Fy8MQ1MleFA==",
"dev": true,
"requires": {
"dedent-js": "^1.0.1",
"pascal-case": "^3.1.1"
}
}
"svelte2tsx": "~0.5.20"
}
},
"@sveltejs/vite-plugin-svelte": {
@@ -7776,14 +7755,14 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz",
"integrity": "sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz",
"integrity": "sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "5.49.0",
"@typescript-eslint/type-utils": "5.49.0",
"@typescript-eslint/utils": "5.49.0",
"@typescript-eslint/scope-manager": "5.46.1",
"@typescript-eslint/type-utils": "5.46.1",
"@typescript-eslint/utils": "5.46.1",
"debug": "^4.3.4",
"ignore": "^5.2.0",
"natural-compare-lite": "^1.4.0",
@@ -7848,41 +7827,41 @@
}
},
"@typescript-eslint/scope-manager": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.49.0.tgz",
"integrity": "sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz",
"integrity": "sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.49.0",
"@typescript-eslint/visitor-keys": "5.49.0"
"@typescript-eslint/types": "5.46.1",
"@typescript-eslint/visitor-keys": "5.46.1"
}
},
"@typescript-eslint/type-utils": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.49.0.tgz",
"integrity": "sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz",
"integrity": "sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng==",
"dev": true,
"requires": {
"@typescript-eslint/typescript-estree": "5.49.0",
"@typescript-eslint/utils": "5.49.0",
"@typescript-eslint/typescript-estree": "5.46.1",
"@typescript-eslint/utils": "5.46.1",
"debug": "^4.3.4",
"tsutils": "^3.21.0"
}
},
"@typescript-eslint/types": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz",
"integrity": "sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.1.tgz",
"integrity": "sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz",
"integrity": "sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz",
"integrity": "sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.49.0",
"@typescript-eslint/visitor-keys": "5.49.0",
"@typescript-eslint/types": "5.46.1",
"@typescript-eslint/visitor-keys": "5.46.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -7891,28 +7870,28 @@
}
},
"@typescript-eslint/utils": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz",
"integrity": "sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.1.tgz",
"integrity": "sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.9",
"@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.49.0",
"@typescript-eslint/types": "5.49.0",
"@typescript-eslint/typescript-estree": "5.49.0",
"@typescript-eslint/scope-manager": "5.46.1",
"@typescript-eslint/types": "5.46.1",
"@typescript-eslint/typescript-estree": "5.46.1",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0",
"semver": "^7.3.7"
}
},
"@typescript-eslint/visitor-keys": {
"version": "5.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.49.0.tgz",
"integrity": "sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg==",
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz",
"integrity": "sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.49.0",
"@typescript-eslint/types": "5.46.1",
"eslint-visitor-keys": "^3.3.0"
}
},
@@ -8335,9 +8314,9 @@
"peer": true
},
"chartjs-adapter-date-fns": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
"requires": {}
},
"chartjs-plugin-zoom": {
@@ -11225,15 +11204,15 @@
}
},
"sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
"integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
"integrity": "sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==",
"dev": true,
"requires": {
"@jridgewell/sourcemap-codec": "^1.4.14",
"buffer-crc32": "^0.2.5",
"minimist": "^1.2.0",
"sander": "^0.5.0"
"sander": "^0.5.0",
"sourcemap-codec": "^1.3.0"
}
},
"source-map": {
@@ -11248,6 +11227,12 @@
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@@ -11575,16 +11560,16 @@
"integrity": "sha512-jhtZWtD6cUE2nMw46dJ5VXWYiqnER+JH+V/BmNBQ5fNP/YdsJCpJi+DemUy9msklqGb0f+wLhJPBtKHLWQvzjg=="
},
"svelte-preprocess": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.1.tgz",
"integrity": "sha512-0HXyhCoc9rsW4zGOgtInylC6qj259E1hpFnJMJWTf+aIfeqh4O/QHT31KT2hvPEqQfdjmqBR/kO2JDkkciBLrQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz",
"integrity": "sha512-q7lpa7i2FBu8Pa+G0MmuQQWETBwCKgsGmuq1Sf6n8q4uaG9ZLcLP0Y+etC6bF4sE6EbLxfiI38zV6RfPe3RSfg==",
"dev": true,
"requires": {
"@types/pug": "^2.0.6",
"@types/sass": "^1.43.1",
"detect-indent": "^6.1.0",
"magic-string": "^0.27.0",
"sorcery": "^0.11.0",
"sorcery": "^0.10.0",
"strip-indent": "^3.0.0"
}
},
@@ -11925,9 +11910,9 @@
"optional": true
},
"vite": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz",
"integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.3.tgz",
"integrity": "sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==",
"dev": true,
"requires": {
"esbuild": "^0.16.3",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.62.0",
"version": "1.60.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -18,12 +18,12 @@
"@playwright/test": "^1.29.2",
"@sveltejs/adapter-static": "^1.0.0",
"@sveltejs/kit": "^1.0.0-next.589",
"@sveltejs/package": "^1.0.2",
"@sveltejs/package": "^1.0.1",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/node": "^18.11.18",
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelvet": "^4.0.20",
"@zerodevx/svelte-toast": "^0.8.1",
@@ -51,23 +51,23 @@
"svelte-highlight": "^6.2.1",
"svelte-overlay": "^1.4.1",
"svelte-popperjs": "^1.3.2",
"svelte-preprocess": "^5.0.1",
"svelte-preprocess": "^5.0.0",
"svelte-splitpanes": "^0.7.3",
"svelte2tsx": "^0.5.22",
"tailwindcss": "^3.2.2",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.4"
"vite": "^4.0.3"
},
"type": "module",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@leeoniya/ufuzzy": "^0.9.1",
"@leeoniya/ufuzzy": "^0.9.0",
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.7.6",
"async-mutex": "^0.4.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-zoom": "^2.0.0",
"date-fns": "^2.29.3",
"highlight.js": "^11.7.0",

View File

@@ -19,7 +19,7 @@ export interface SchemaProperty {
}
export type Schema = {
$schema: string | undefined
$schema: string
type: string
properties: { [name: string]: SchemaProperty }
required: string[]

View File

@@ -4,7 +4,6 @@
import { emptySchema, emptyString } from '$lib/utils'
import SchemaForm from './SchemaForm.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import Toggle from './Toggle.svelte'
export let resource_type: string
export let args: Record<string, any> | any = {}
@@ -13,10 +12,7 @@
let schema = emptySchema()
let notFound = false
async function loadSchema() {
rawCode = '{}'
viewJsonSchema = false
try {
const rt = await ResourceService.getResourceType({
workspace: $workspaceStore!,
@@ -25,6 +21,8 @@
schema = rt.schema
notFound = false
} catch (e) {
rawCode = '{}'
notFound = true
}
}
@@ -45,40 +43,17 @@
}
let error = ''
let rawCode = ''
let viewJsonSchema = false
$: rawCode && parseJson()
function switchTab(asJson: boolean) {
viewJsonSchema = asJson
if (asJson) {
rawCode = JSON.stringify(args, null, 2)
} else {
parseJson()
}
}
</script>
{#if !notFound}
<div class="w-full flex flex-row-reverse">
<Toggle
on:change={(e) => switchTab(e.detail)}
options={{
right: 'As JSON'
}}
/>
</div>
{:else}
{#if notFound}
<p class="italic text-gray-500 text-xs mb-4"
>No corresponding resource type found in your workspace for {resource_type}. Define the value in
JSON directly</p
>
{/if}
{#if notFound || viewJsonSchema}
{#if !emptyString(error)}<span class="text-red-400 text-xs mb-1 flex flex-row-reverse"
>{error}</span
>{:else}<div class="py-2" />{/if}
<SimpleEditor autoHeight lang="json" bind:code={rawCode} fixedOverflowWidgets={false} />
{:else}
<SchemaForm noDelete {password} isValid {schema} bind:args />
<SchemaForm {password} isValid {schema} bind:args />
{/if}

View File

@@ -155,7 +155,6 @@
async function next() {
if (step == 1 && manual) {
step += 1
args = {}
} else if (step == 1 && !manual) {
const url = new URL(`/api/oauth/connect/${resource_type}`, $page.url.origin)
url.searchParams.append('scopes', scopes.join('+'))
@@ -411,20 +410,19 @@
</ol>
</div>
{#if apiTokenApps[resource_type].img}
<div class="mt-4 w-full overflow-hidden">
<img class="m-auto max-h-60" alt="connect" src={apiTokenApps[resource_type].img} />
<div class="mt-4 w-full">
<img class="max-h-96 m-auto" alt="connect" src={apiTokenApps[resource_type].img} />
</div>
{/if}
{/if}
<h2 class="mt-4">Value</h2>
<div class="mt-4">
{#key resource_type}
<ApiConnectForm password={key ?? ''} {resource_type} bind:args bind:isValid />
{/key}
<ApiConnectForm password={key ?? ''} {resource_type} bind:args bind:isValid />
</div>
<h2 class="mt-4 mb-2">Description</h2>
<textarea type="text" autocomplete="off" use:autosize bind:value={description} />
{:else}
<Path

View File

@@ -166,7 +166,7 @@
<FieldHeader {label} {required} {type} {contentEncoding} {format} {itemsType} />
{/if}
{#if editableSchema}
<div class="p-2 my-1 text-xs border-solid border border-gray-400">
<div class="my-1 text-xs border-solid border border-gray-400 rounded p-2">
<span
class="underline"
on:click={() => {

View File

@@ -1,51 +0,0 @@
<script lang="ts">
import { page } from '$app/stores'
import { userWorkspaces, workspaceStore } from '$lib/stores'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { slide } from 'svelte/transition'
import InlineCodeCopy from './InlineCodeCopy.svelte'
$: opened = false
$: workspace = $userWorkspaces.find((e) => e.id === $workspaceStore)
$: workspaceName = workspace?.name
$: workspaceId = workspace?.id
$: url = `${$page.url.protocol}//${$page.url.hostname}/`
</script>
<div class="text-sm mt-2 flex">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class="underline mr-4"
on:click={() => {
opened = !opened
}}
>
CLI quick setup
<Icon class="ml-2" data={opened ? faChevronUp : faChevronDown} scale={0.7} />
</span>
</div>
{#if opened}
<div
transition:slide|local
class="bg-gray-100 border-l-4 border-gray-600 text-gray-700 p-4 m-4"
role="alert"
id="dynamic-input-help-box"
>
<ul class="pl-0 list-decimal list-inside">
<li
>Install the latest wmill CLI from deno.land: <InlineCodeCopy
content={'deno install --unstable -A https://deno.land/x/wmill/main.ts'}
/></li
>
<li
>Setup the wmill cli for this workspace & remote: <InlineCodeCopy
content={`wmill workspace add ${workspaceName} ${workspaceId} ${url}`}
/></li
>
<li>Follow the prompts in your terminal</li>
<li>Use the run command above!</li>
</ul>
</div>
{/if}

View File

@@ -7,27 +7,21 @@
import autosize from 'svelte-autosize'
export let result: any
export let requireHtmlApproval = false
let resultKind:
| 'json'
| 'table-col'
| 'table-row'
| 'html'
| 'png'
| 'file'
| 'jpeg'
| 'gif'
| 'error'
| 'approval'
| 'svg'
| 'filename'
| undefined
$: resultKind = inferResultKind(result)
let forceJson = false
let enableHtml = false
function isRectangularArray(obj: any) {
if (!Array.isArray(obj) || obj.length == 0) {
@@ -59,20 +53,14 @@
return 'table-row'
} else if (keys.map((k) => Array.isArray(result[k])).reduce((a, b) => a && b)) {
return 'table-col'
} else if (keys.length == 1 && keys[0] == 'html') {
return 'html'
} else if (keys.length == 1 && keys[0] == 'png') {
return 'png'
} else if (keys.length == 1 && keys[0] == 'svg') {
return 'svg'
} else if (keys.length == 1 && keys[0] == 'jpeg') {
return 'jpeg'
} else if (keys.length == 1 && keys[0] == 'file') {
return 'file'
} else if (keys.length == 1 && keys[0] == 'error') {
return 'error'
} else if (keys.length === 2 && keys.includes('file') && keys.includes('filename')) {
return 'filename'
} else if (
keys.length == 3 &&
keys.includes('resume') &&
@@ -131,34 +119,6 @@
</tbody>
</TableCustom>
</div>
{:else if !forceJson && resultKind == 'html'}
<div class="h-full">
{#if !requireHtmlApproval || enableHtml}
{@html result.html}
{:else}
<div class="font-main text-sm">
<div class="flex flex-col">
<div class="bg-red-400 py-1 rounded-t text-white font-bold text-center">
Warning
</div>
<p
class="text-gray-600 mb-2 text-left border-2 !border-t-0 rounded-b border-red-400 overflow-auto p-1"
>Rendering HTML can expose you to <a
href="https://owasp.org/www-community/attacks/xss/"
target="_blank"
rel="noreferrer"
class="hover:underline">XSS attacks</a
>. Only enable it if you trust the author of the script.
</p>
</div>
<div class="center-center">
<Button size="sm" color="dark" on:click={() => (enableHtml = true)}>
Enable HTML rendering
</Button>
</div>
</div>
{/if}
</div>
{:else if !forceJson && resultKind == 'png'}
<div class="h-full"
><img alt="png rendered" class="w-auto h-full" src="data:image/png;base64,{result.png}" />
@@ -171,11 +131,6 @@
src="data:image/jpeg;base64,{result.jpeg}"
/>
</div>
{:else if !forceJson && resultKind == 'svg'}
<div
><a download="windmill.svg" href="data:text/plain;base64,{btoa(result.svg)}">Download</a>
</div>
<div class="h-full overflow-auto">{@html result.svg} </div>
{:else if !forceJson && resultKind == 'gif'}
<div class="h-full"
><img alt="gif rendered" class="w-auto h-full" src="data:image/gif;base64,{result.gif}" />
@@ -186,14 +141,8 @@
>Download</a
>
</div>
{:else if !forceJson && resultKind === 'filename'}
<div>
<a download={result.filename} href="data:application/octet-stream;base64,{result.file}">
Download
</a>
</div>
{:else if !forceJson && resultKind == 'error'}<div>
<span class="text-red-500 font-semibold text-sm whitespace-pre-wrap"
<span class="text-red-500 font-semibold text-sm"
>{result.error.name}: {result.error.message}</span
>
<pre class="text-sm whitespace-pre-wrap text-gray-900">{result.error.stack ?? ''}</pre>

View File

@@ -298,9 +298,8 @@
}
}
const wsProtocol = $page.url.protocol == 'https:' ? 'wss' : 'ws'
if (lang == 'typescript') {
await connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/deno`, 'deno', {
await connectToLanguageServer(`wss://${$page.url.host}/ws/deno`, 'deno', {
certificateStores: null,
enablePaths: [],
config: null,
@@ -331,7 +330,7 @@
}
})
} else if (lang === 'python') {
await connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/pyright`, 'pyright', {
await connectToLanguageServer(`wss://${$page.url.host}/ws/pyright`, 'pyright', {
executionEnvironments: [
{
root: '/tmp/pyright',
@@ -342,7 +341,7 @@
]
})
connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/black`, 'black', {
connectToLanguageServer(`wss://${$page.url.host}/ws/black`, 'black', {
formatters: {
black: {
command: 'black',
@@ -354,7 +353,7 @@
}
})
} else if (lang === 'go') {
connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/go`, 'go', {
connectToLanguageServer(`wss://${$page.url.host}/ws/go`, 'go', {
'build.allowImplicitNetworkAccess': true
})
}
@@ -412,8 +411,7 @@
model.updateOptions(updateOptions)
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(model, code, lang, automaticLayout, fixedOverflowWidgets),
tabSize: lang == 'python' ? 4 : 2
...editorConfig(model, code, lang, automaticLayout, fixedOverflowWidgets)
})
let timeoutModel: NodeJS.Timeout | undefined = undefined

View File

@@ -29,8 +29,6 @@
import PickHubScript from './flows/pickers/PickHubScript.svelte'
import ToggleHubWorkspace from './ToggleHubWorkspace.svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Popover from './Popover.svelte'
import Kbd from './common/kbd/Kbd.svelte'
export let lang: 'python3' | 'deno' | 'go' | 'bash'
export let editor: Editor
@@ -227,137 +225,121 @@
<ResourceEditor bind:this={resourceEditor} on:refresh={resourcePicker.openDrawer} />
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openDrawer} />
<div class="flex justify-between items-center overflow-y-auto w-full p-1">
<div class="flex items-center">
<Badge color={validCode ? 'green' : 'red'} class="min-w-[60px] mr-3">
{validCode ? 'Valid' : 'Invalid'}
</Badge>
<div class="flex items-center divide-x">
<Popover notClickable placement="bottom" disapperTimoout={0} class="pr-1" disablePopup={!iconOnly}>
<Button
color="light"
btnClasses="!font-medium !h-full"
on:click={contextualVariablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Context Var
</Button>
<svelte:fragment slot="text">
Add context variable
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
color="light"
btnClasses="!font-medium !h-full"
on:click={variablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Variable
</Button>
<svelte:fragment slot="text">
Add variable
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={resourcePicker.openDrawer}
{iconOnly}
startIcon={{ icon: faCube }}
>
+Resource
</Button>
<svelte:fragment slot="text">
Add resource
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor.clearContent}
{iconOnly}
startIcon={{ icon: faRotateLeft }}
>
Reset
</Button>
<svelte:fragment slot="text">
Reset
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor.reloadWebsocket}
startIcon={{ icon: faRotate }}
>
{#if !iconOnly}
Assistant
<div class="flex flex-row justify-between items-center overflow-hidden w-full px-1">
<div class="flex flex-row divide-x items-center">
<div class="mx-2">
{#if validCode}
<Badge color="green">Inputs</Badge>
{:else}
<Badge color="red">Inputs</Badge>
{/if}
</div>
<div>
<Button
color="light"
btnClasses="mr-1 !font-medium"
on:click={contextualVariablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Context Var
</Button>
</div>
<div>
<Button
color="light"
btnClasses="mx-1 !font-medium"
on:click={variablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Variable
</Button>
</div>
<div>
<Button
btnClasses="mx-1 !font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={resourcePicker.openDrawer}
{iconOnly}
startIcon={{ icon: faCube }}
>
+Resource
</Button>
</div>
<div>
<Button
btnClasses="mx-1 !font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor.clearContent}
{iconOnly}
startIcon={{ icon: faRotateLeft }}
>
Reset
</Button>
</div>
<div>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor.reloadWebsocket}
startIcon={{ icon: faRotate }}
>
{#if !iconOnly}
Assistant
{/if}
<span class="ml-1 -my-1">
{#if lang == 'deno'}
(<span class={websocketAlive.deno ? 'green' : 'text-red-700'}>Deno</span>)
{:else if lang == 'go'}
(<span class={websocketAlive.go ? 'green' : 'text-red-700'}>Go</span>)
{:else if lang == 'python3'}
(<span class={websocketAlive.pyright ? 'green' : 'text-red-700'}>Pyright</span>
<span class={websocketAlive.black ? 'green' : 'text-red-700'}>Black</span>)
{/if}
<span class="ml-1 -my-1">
{#if lang == 'deno'}
(<span class={websocketAlive.deno ? 'green' : 'text-red-700'}>Deno</span>)
{:else if lang == 'go'}
(<span class={websocketAlive.go ? 'green' : 'text-red-700'}>Go</span>)
{:else if lang == 'python3'}
(<span class={websocketAlive.pyright ? 'green' : 'text-red-700'}>Pyright</span>
<span class={websocketAlive.black ? 'green' : 'text-red-700'}>Black</span>)
{/if}
</span>
</Button>
<svelte:fragment slot="text">
Reload assistant
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor.format}
{iconOnly}
startIcon={{ icon: faBroom }}
>
</span>
</Button></div
>
<div>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor.format}
startIcon={{ icon: faBroom }}
>
{#if !iconOnly}
Format (Ctrl+S)
</Button>
<svelte:fragment slot="text">
Format <Kbd class="!text-gray-800">Ctrl</Kbd> + <Kbd class="!text-gray-800">S</Kbd>
</svelte:fragment>
</Popover>
{/if}
</Button></div
>
</div>
<div class="py-1">
<div>
<Button
btnClasses="mx-1 !font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={scriptPicker.openDrawer}
{iconOnly}
startIcon={{ icon: faEye }}
>
Script
</Button>
</div>
</div>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={scriptPicker.openDrawer}
{iconOnly}
startIcon={{ icon: faEye }}
>
Script
</Button>
<svelte:fragment slot="text">
Script
</svelte:fragment>
</Popover>
</div>
<style>

View File

@@ -112,8 +112,8 @@
await createSchedule(flow.path)
}
}
sendUserToast(`Flow saved at ${$flowStore.path}`)
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
sendUserToast(`Success! flow saved at ${$flowStore.path}`)
goto(`/flows/get/${$flowStore.path}`)
}
let timeout: NodeJS.Timeout | undefined = undefined

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { Loader2 } from 'lucide-svelte'
import DisplayResult from './DisplayResult.svelte'
import LogViewer from './LogViewer.svelte'
@@ -7,7 +6,6 @@
export let logs: string
export let col: boolean = false
export let noBorder = false
export let loading
</script>
<div
@@ -16,13 +14,7 @@
>
<div class="bg-white max-h-80 h-full p-1 overflow-auto relative">
<span class="text-gray-500">Result</span>
{#if result}
<DisplayResult {result} />
{:else if loading}
<Loader2 class="animate-spin" />
{:else}
<div class="text-gray-400">No result (result is undefined)</div>
{/if}
<DisplayResult {result} />
</div>
<div class="overflow-auto max-h-80 h-full relative">
<LogViewer content={logs ?? ''} isLoading={false} />

View File

@@ -2,15 +2,14 @@
import type { Job } from '$lib/gen'
import JobStatus from '$lib/components/JobStatus.svelte'
import Icon from 'svelte-awesome'
import { displayDate, displayDaysAgo } from '$lib/utils'
import { displayDaysAgo } from '$lib/utils'
import {
faCalendar,
faClock,
faRobot,
faScroll,
faUser,
faBarsStaggered,
faMemory
faBarsStaggered
} from '@fortawesome/free-solid-svg-icons'
import ScheduleEditor from './ScheduleEditor.svelte'
import { onDestroy, onMount } from 'svelte'
@@ -40,31 +39,16 @@
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
class="mx-2 text-2xs text-gray-600"
>
{#if job['success'] != undefined}
Received job: {displayDate(job.created_at ?? '')}
{:else}
{#key time}
Received job {displayDaysAgo(job.created_at ?? '')}
{/key}
{/if}</span
{#key time}
Received job {displayDaysAgo(job.created_at ?? '')}
{/key}</span
>
</div>
{#if job && 'started_at' in job && job.started_at}
<div>
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span class="mx-2">
{#if job['success'] != undefined}
Started: {displayDate(job.started_at ?? '')}
{:else}
{#key time}
Started {displayDaysAgo(job.started_at ?? '')}{/key}
{/if}
</span>
</div>
{/if}
{#if job && job['mem_peak']}
<div>
<Icon class="text-gray-700" data={faMemory} scale={SMALL_ICON_SCALE} /><span class="mx-2">
Mem peak: {(job['mem_peak'] / 1024).toPrecision(5)}MB</span
{#key time}
Started {displayDaysAgo(job.started_at ?? '')}{/key}</span
>
</div>
{/if}

View File

@@ -149,8 +149,8 @@
</div>
<FlowProgressBar {job} bind:reset={jobProgressReset} />
<div class="overflow-y-auto grow divide-y divide-gray-600 ">
<div class="max-h-1/2 overflow-auto border-b border-gray-700">
<div class="overflow-y-auto grow flex-col flex divide-y divide-gray-600 ">
<div class="max-h-1/2 overflow-auto">
<SchemaForm
noVariablePicker
compact
@@ -160,7 +160,8 @@
bind:args={$previewArgs}
/>
</div>
<div class="pt-4 grow">
<div class="h-full pt-4 grow">
{#if jobId}
<FlowStatusViewer bind:flowState={$flowStateStore} {jobId} bind:job />
{:else}

View File

@@ -13,7 +13,7 @@
import Tabs from './common/tabs/Tabs.svelte'
import { FlowGraph, type GraphModuleState } from './graph'
import ModuleStatus from './ModuleStatus.svelte'
import { displayDate, emptyString, isOwner, pluralize, truncateRev } from '$lib/utils'
import { displayDate, isOwner, pluralize, truncateRev } from '$lib/utils'
import JobArgs from './JobArgs.svelte'
import Tooltip from './Tooltip.svelte'
import SimpleEditor from './SimpleEditor.svelte'
@@ -164,14 +164,6 @@
let selected: 'graph' | 'sequence' = 'graph'
let payload: string = '"a test payload in json"'
function isSuccess(arg: any): boolean | undefined {
if (arg == undefined) {
return undefined
} else {
return arg == true
}
}
</script>
{#if job}
@@ -188,11 +180,7 @@
<FlowPreviewStatus {job} />
{#if `result` in job}
<div class="w-full h-full">
<FlowJobResult
loading={job['running'] == true}
result={job.result}
logs={job.logs ?? ''}
/>
<FlowJobResult result={job.result} logs={job.logs ?? ''} />
</div>
{:else if job.flow_status?.modules?.[job?.flow_status?.step]?.type === FlowStatusModule.type.WAITING_FOR_EVENTS}
<div class="w-full h-full mt-2 text-sm text-gray-600">
@@ -238,21 +226,14 @@
<div class="flex flex-col gap-1">
{#each innerModules as mod, i (mod.id)}
{#if mod.type == FlowStatusModule.type.IN_PROGRESS}
{@const rawMod = job.raw_flow?.modules[i]}
<div
><span class="inline-flex gap-1"
><Badge color="indigo">{mod.id}</Badge>
<span class="font-medium text-gray-900">
{#if !emptyString(rawMod?.summary)}
{rawMod?.summary ?? ''}
{:else if rawMod?.value.type == 'script'}
{rawMod.value.path ?? ''}
{:else if rawMod?.value.type}
{rawMod?.value.type}
{/if}
</span>
{#if job.raw_flow?.modules[i]?.summary}
<span class="font-medium text-gray-900">
{job.raw_flow?.modules[i]?.summary ?? ''}
</span>
{/if}
<Loader2 class="animate-spin" /></span
></div
>
@@ -445,7 +426,6 @@
</div>
<FlowGraph
success={isSuccess(job?.['success'])}
flowModuleStates={localFlowModuleStates}
on:click={(e) => {
if (e.detail.id) {
@@ -464,13 +444,7 @@
{#if selectedNode}
{@const node = localFlowModuleStates[selectedNode]}
{#if selectedNode == 'end'}
<FlowJobResult
loading={job['running']}
noBorder
col
result={job['result']}
logs={job.logs ?? ''}
/>
<FlowJobResult noBorder col result={job['result'] ?? {}} logs={job.logs ?? ''} />
{:else if selectedNode == 'start'}
{#if job.args}
<div class="p-2">
@@ -497,13 +471,7 @@
</div>
{/if}
</div>
<FlowJobResult
loading={job['running'] == true}
noBorder
col
result={node.result}
logs={node.logs ?? ''}
/>
<FlowJobResult noBorder col result={node.result ?? {}} logs={node.logs ?? ''} />
{:else}
<p class="p-2 text-gray-600 italic"
>The execution of this node has no information attached to it. The job likely did

View File

@@ -26,10 +26,4 @@
$: lang = getLang(language)
</script>
{#if code?.length < 5000}
<Highlight className="nowrap {$$props.class}" language={lang} {code} />
{:else}
<pre class="overflow-auto max-h-screen {$$props.class}"
><code class="language-{language}">{code}</code></pre
>
{/if}
<Highlight className="nowrap {$$props.class}" language={lang} {code} />

View File

@@ -1,13 +0,0 @@
<script lang="ts">
import { copyToClipboard } from '$lib/utils'
import { faClipboard } from '@fortawesome/free-solid-svg-icons'
import { Icon } from 'svelte-awesome'
export let content: string
</script>
<pre class="bg-gray-700 text-gray-100 p-2 font-mono text-sm whitespace-pre-wrap"
>{content} <span on:click={() => copyToClipboard(content)} class="cursor-pointer ml-2"
><Icon data={faClipboard} /></span
></pre
>

View File

@@ -60,11 +60,7 @@
}
if (isCodeInjection(rawValue)) {
arg.expr = getDefaultExpr(
argName,
previousModuleId,
`\`${rawValue.toString().replaceAll('`', '\\`')}\``
)
arg.expr = getDefaultExpr(argName, previousModuleId, `\`${rawValue}\``)
arg.type = 'javascript'
propertyType = 'static'
} else {
@@ -121,13 +117,9 @@
const openBracket = '${'
const closeBracket = '}'
function setDefaultCode() {
if (!arg?.value) {
monacoTemplate?.setCode(schema.properties[argName].default)
}
}
$: schema.properties[argName].default && setDefaultCode()
$: schema.properties[argName].default &&
!arg?.value &&
monacoTemplate?.setCode(schema.properties[argName].default)
</script>
{#if arg != undefined}
@@ -165,7 +157,7 @@
argName,
previousModuleId,
staticTemplate
? `\`${arg?.value.toString().replaceAll('`', '\\`') ?? ''}\``
? `\`${arg?.value ?? ''}\``
: arg.value
? JSON.stringify(arg?.value, null, 4)
: ''
@@ -180,7 +172,6 @@
if (staticTemplate) {
if (arg) {
arg.value = codeToStaticTemplate(arg.expr)
arg.expr = undefined
}
setPropertyType(arg?.value)
} else {

View File

@@ -5,7 +5,6 @@
export let content: string | undefined
export let isLoading: boolean
export let duration: number | undefined = undefined
export let mem: number | undefined = undefined
let scroll = true
let div: HTMLElement | null = null
@@ -51,11 +50,6 @@
{:else if duration}
<span class="absolute text-xs text-gray-500 top-2 left-2">took {duration}ms</span>
{/if}
{#if mem}
<span class="absolute text-xs text-gray-500 top-2 left-36"
>mem peak: {(mem / 1024).toPrecision(4)}MB</span
>
{/if}
<pre class="whitespace-pre-wrap break-words bg-gray-50 text-xs w-full p-2"
>{#if content}<span>{content}</span>{:else if !isLoading}<span class="text-gray-600"
>No logs are available yet</span

View File

@@ -96,7 +96,7 @@
<input
type="text"
bind:value={summary}
placeholder="Short summary to be displayed when listed"
placeholder="A short summary displayed when it is listed"
disabled={!own}
/>

View File

@@ -34,11 +34,7 @@
}
function resourceToValue() {
if (path) {
value = `$res:${path}`
} else {
value = undefined
}
value = `$res:${path}`
}
function isResource() {

View File

@@ -5,7 +5,6 @@
export let placement: PopoverPlacement = 'auto'
export let notClickable = false
export let popupClass = ''
export let disablePopup = false
export let disapperTimoout = 100
const [popperRef, popperContent] = createPopperActions({ placement })
@@ -21,12 +20,7 @@
const extraOpts = {
modifiers: [
betterPreventOverflow({ padding: 10 }),
{
name: 'offset',
options: {
offset: [8, 8]
}
},
{ name: 'offset', options: { offset: [8, 8] } },
{
name: 'arrow',
options: {
@@ -68,7 +62,7 @@
<slot />
</button>
{/if}
{#if showTooltip && !disablePopup}
{#if showTooltip}
<div
use:popperContent={extraOpts}
on:mouseenter={open}

View File

@@ -13,7 +13,6 @@
import SimpleEditor from './SimpleEditor.svelte'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import SchemaForm from './SchemaForm.svelte'
import Toggle from './Toggle.svelte'
let path = ''
let initialPath = ''
@@ -39,7 +38,6 @@
path = p
resourceToEdit = undefined
resourceSchema = undefined
viewJsonSchema = false
loadingSchema = true
drawer.openDrawer?.()
resourceToEdit = await ResourceService.getResource({ workspace: $workspaceStore!, path: p })
@@ -113,16 +111,6 @@
args[k] = `$var:${path}`
})
}
let viewJsonSchema = false
function switchTab(asJson: boolean) {
viewJsonSchema = asJson
if (asJson) {
rawCode = JSON.stringify(args, null, 2)
} else {
parseJson()
}
}
</script>
<Drawer bind:this={drawer} size="800px">
@@ -148,21 +136,12 @@
kind="resource"
/>
</div>
<div class="flex w-full justify-between max-w-lg items-center mt-4">
<h3>Value</h3>
<Toggle
on:change={(e) => switchTab(e.detail)}
options={{
right: 'As JSON'
}}
/>
</div>
<h3 class="mt-4">Value</h3>
<div class="text-sm">
{#if loadingSchema}
<Skeleton layout={[[4]]} />
{:else if !viewJsonSchema && resourceSchema && resourceSchema?.properties}
{:else if resourceSchema && resourceSchema?.properties}
<SchemaForm
noDelete
disabled={!can_write}
compact
schema={resourceSchema}
@@ -172,13 +151,10 @@
{:else if !can_write}
<input type="text" disabled value={rawCode} />
{:else}
{#if !viewJsonSchema}
<p class="italic text-gray-500 text-xs mb-4"
>No corresponding resource type found in your workspace for {selectedResourceType}.
Define the value in JSON directly</p
>
{/if}
<p class="italic text-gray-500 text-xs mb-4"
>No corresponding resource type found in your workspace for {selectedResourceType}.
Define the value in JSON directly</p
>
{#if !emptyString(jsonError)}<span
class="text-red-400 text-xs mb-1 flex flex-row-reverse">{jsonError}</span
>{:else}<div class="py-2" />{/if}

View File

@@ -8,6 +8,7 @@
import Select from 'svelte-select'
import AppConnect from './AppConnect.svelte'
import ResourceEditor from './ResourceEditor.svelte'
import { truncate } from '$lib/utils'
const dispatch = createEventDispatcher()
let resources: Resource[] = []

View File

@@ -18,8 +18,6 @@
import Toggle from './Toggle.svelte'
import { userStore } from '$lib/stores'
import Tooltip from './Tooltip.svelte'
import CliHelpBox from './CliHelpBox.svelte'
import InlineCodeCopy from './InlineCodeCopy.svelte'
export let runnable:
| {
@@ -57,15 +55,10 @@
export let isValid = true
// Run later
let viewOptions = false
let viewCliOptions = false
let scheduledForStr: string | undefined
let invisible_to_owner: false
$: cliCommand = `wmill ${runnable?.kind} run ${runnable?.path} ${Object.entries(args)
.map(([k, v]) => `-i ${k}=${JSON.stringify(v)}`)
.join(' ')}`
</script>
<div class="max-w-6xl">
@@ -178,7 +171,7 @@
<div class="flex items-center gap-1">
<Toggle
options={{
right: `make run invisible to others`
right: `run only visible to you`
}}
bind:checked={invisible_to_owner}
/>
@@ -207,20 +200,4 @@
{buttonText}
</Button>
{/if}
<div class="my-10" />
<Button
color="light"
size="sm"
endIcon={{ icon: viewCliOptions ? faChevronUp : faChevronDown }}
on:click={() => (viewCliOptions = !viewCliOptions)}
>
Run it from the CLI
</Button>
{#if viewCliOptions}
<div transition:slide class="mt-2 px-4 pt-2">
<InlineCodeCopy content={cliCommand} />
<CliHelpBox />
</div>
{/if}
</div>

View File

@@ -178,8 +178,8 @@
/>
<div class="ml-2">
<Tooltip>
Arguments can be edited either using the wizard, or by editing their JSON Schema,
<a href="https://docs.windmill.dev/docs/reference/#script-parameters-to-json-schema">see docs</a>
Arguments can be edited either using the wizard, or by editing their json-schema
<a href="https://docs.windmill.dev/docs/reference/script_arguments_reference">docs</a>
</Tooltip>
</div>
</div>

View File

@@ -30,7 +30,6 @@
export let filter: string[] | undefined = undefined
export let noDynamicToggle = false
export let flexWrap = false
export let noDelete = false
let clazz: string = ''
export { clazz as class }
@@ -44,31 +43,23 @@
function removeExtraKey() {
Object.keys(args ?? {}).forEach((key) => {
if (!keys.includes(key)) {
if (!Object.keys(schema?.properties ?? {}).includes(key)) {
delete args[key]
delete inputCheck[key]
}
})
}
$: schema?.properties && removeExtraKey()
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let keys: string[] = []
$: {
let lkeys = Object.keys(schema?.properties ?? {})
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
keys = lkeys
if (!noDelete) {
removeExtraKey()
}
}
}
</script>
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 gap-y-2' : ''}">
{#if keys.length > 0}
{#each keys as argName, i (argName)}
{#if Object.keys(schema?.properties ?? {}).length > 0}
{#each Object.keys(schema?.properties ?? {}) as argName, i (argName)}
{#if !filter || filter.includes(argName)}
<div transition:slide|local>
{#if inputTransform}

View File

@@ -14,7 +14,7 @@
import CenteredPage from './CenteredPage.svelte'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { Button, Kbd } from './common'
import { Button } from './common'
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
import Toggle from './Toggle.svelte'
@@ -60,15 +60,9 @@
localStorage.removeItem(script.path)
script.schema = script.schema ?? emptySchema()
try {
if (!script.schema) {
await inferArgs(script.language, script.content, script.schema)
} catch (error) {
sendUserToast(
`Impossible to infer the schema. Assuming this is a script without main function`,
true
)
}
const newHash = await ScriptService.createScript({
workspace: $workspaceStore!,
requestBody: {
@@ -83,9 +77,8 @@
kind: script.kind
}
})
sendUserToast(`New script created at hash ${newHash}`)
await goto(`/scripts/edit/${newHash}?step=2`)
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
sendUserToast(`Success! New script version created with hash ${newHash}`)
goto(`/scripts/get/${newHash}`)
} catch (error) {
sendUserToast(`Impossible to save the script: ${error.body}`, true)
}
@@ -94,28 +87,13 @@
async function changeStep(step: number) {
if (step > 1) {
script.schema = script.schema ?? emptySchema()
try {
await inferArgs(script.language, script.content, script.schema)
} catch (error) {
console.info(
'Impossible to infer the schema. Assuming this is a script without main function'
)
}
await inferArgs(script.language, script.content, script.schema)
}
goto(`?step=${step}`)
}
$: kind = script.kind as 'script' | 'trigger' | 'approval' | undefined
function onKeyDown(event: KeyboardEvent) {
if (event.key == 'Enter' && step == 1) {
changeStep(2)
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if !$userStore?.operator}
<UnsavedConfirmationModal />
<div class="flex flex-col h-screen">
@@ -177,11 +155,11 @@
<Button
size="sm"
variant={step == 1 ? 'contained' : 'border'}
btnClasses={step == 3 ? 'invisible' : 'inline-flex gap-2'}
btnClasses={step == 3 ? 'invisible' : ''}
disabled={step === 1 && pathError !== ''}
on:click={() => changeStep(step + 1)}
>
Next {#if step == 1}<Kbd>Enter</Kbd>{/if}
Next
</Button>
<Button
size="sm"
@@ -198,7 +176,7 @@
<!-- metadata -->
{#if step === 1}
<CenteredPage>
<h2 class="border-b pb-1 mt-8 mb-2">Path</h2>
<h2 class="border-b pb-1 mt-4 mb-2">Path</h2>
<Path
bind:this={pathC}
bind:error={pathError}
@@ -208,14 +186,14 @@
namePlaceholder="my_script"
kind="script"
/>
<h2 class="border-b pb-1 mt-12 mb-4">Summary</h2>
<h2 class="border-b pb-1 mt-8 mb-4">Summary</h2>
<input
type="text"
bind:this={summaryC}
bind:value={script.summary}
placeholder="Short summary to be displayed when listed"
placeholder="A short summary of the script displayed when the script is listed"
/>
<h2 class="border-b pb-1 mt-12 mb-6">Language</h2>
<h2 class="border-b pb-1 mt-8 mb-6">Language</h2>
<div class="flex flex-row gap-2 flex-wrap">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
@@ -230,7 +208,7 @@
initContent(lang, script.kind, template)
}}
>
<LanguageIcon {lang} /><span class="ml-2 py-4">{label}</span>
<LanguageIcon {lang} /><span class="ml-2">{label}</span>
</Button>
{/each}
<Button
@@ -244,7 +222,7 @@
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-4">PostgreSQL</span>
<LanguageIcon lang="pgsql" /><span class="ml-2">PostgreSQL</span>
</Button>
<Button
size="sm"
@@ -257,10 +235,11 @@
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-4">MySQL</span>
<LanguageIcon lang="mysql" /><span class="ml-2">MySQL</span>
</Button>
</div>
<div class="mt-16 mb-4">
<h2 class="border-b pb-1 mt-8 mb-4">Advanced</h2>
<div class="mb-4">
<Button
color="light"
size="sm"
@@ -310,12 +289,12 @@
/>
</div>
{/if}
<!-- <div class="ml-3">
<div class="ml-3">
<Toggle
bind:checked={script.is_template}
options={{ right: 'Save as a workspace template' }}
/>
</div> -->
</div>
</CenteredPage>
{:else if step === 2}
<ScriptEditor

View File

@@ -1,16 +0,0 @@
<script lang="ts">
import { ChevronDown, ChevronUp } from 'lucide-svelte'
import { slide } from 'svelte/transition'
import Button from './common/button/Button.svelte'
export let text: string
export let view = false
</script>
<Button color="light" on:click={() => (view = !view)} variant="border"
>{text}
{#if !view}<ChevronDown />{:else}<ChevronUp />{/if}</Button
>
{#if view}
<div class="my-4 px-2" transition:slide><slot /></div>
{/if}

View File

@@ -70,6 +70,7 @@
['None', 'none'],
['File (base64)', 'base64'],
['Enum', 'enum'],
['Resource Path', 'resource'],
['Format', 'format'],
['Pattern', 'pattern']
]}

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