Compare commits
237 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87f6f7503b | ||
|
|
959ae8d252 | ||
|
|
8f3944433e | ||
|
|
7b55e5219c | ||
|
|
b7f9ecbda1 | ||
|
|
35b2423fe1 | ||
|
|
05e70a849e | ||
|
|
196de08775 | ||
|
|
578412978f | ||
|
|
f6e8f45af1 | ||
|
|
ca49e273de | ||
|
|
39e1ea3d57 | ||
|
|
3956d012ad | ||
|
|
b5297846fe | ||
|
|
0e022c9e4e | ||
|
|
3c79692559 | ||
|
|
08c863afd3 | ||
|
|
82e628a111 | ||
|
|
c98dbb1fc4 | ||
|
|
e8dcd5b495 | ||
|
|
9cf82b061e | ||
|
|
0fcd54bc07 | ||
|
|
0a1006aa87 | ||
|
|
22226e883e | ||
|
|
c39f3a84d6 | ||
|
|
6c69889874 | ||
|
|
0d27df3dec | ||
|
|
982c4c439e | ||
|
|
3875eec46f | ||
|
|
3a5c925c99 | ||
|
|
9668198f4b | ||
|
|
41f6bcdaa1 | ||
|
|
06aa0aa9f9 | ||
|
|
327be5bbc6 | ||
|
|
9d38c8c45f | ||
|
|
de2bea613a | ||
|
|
e3869693ec | ||
|
|
fee22fc060 | ||
|
|
c58342ddfd | ||
|
|
035bacb8a7 | ||
|
|
45cad34222 | ||
|
|
c73e7dbd8c | ||
|
|
4188383d3e | ||
|
|
edff51f022 | ||
|
|
a2c780bc8e | ||
|
|
094f50cdcc | ||
|
|
08918ec5f2 | ||
|
|
03b1abc840 | ||
|
|
770f4471b5 | ||
|
|
5def8cb52c | ||
|
|
c09ae3ebc6 | ||
|
|
f26d3e62f6 | ||
|
|
f20a4a626d | ||
|
|
8bdebcf212 | ||
|
|
f1acdd7eb7 | ||
|
|
613bc26749 | ||
|
|
96265a6f90 | ||
|
|
5d486034db | ||
|
|
cec27a87b8 | ||
|
|
e9edac4c95 | ||
|
|
200a321975 | ||
|
|
da0d78c209 | ||
|
|
963fc3decc | ||
|
|
08861c86de | ||
|
|
e8e6a6c281 | ||
|
|
aece01908b | ||
|
|
1d3e82607a | ||
|
|
2e39337355 | ||
|
|
6951424611 | ||
|
|
0be55ae98c | ||
|
|
1f997e16ee | ||
|
|
534f877a7a | ||
|
|
fe2e929068 | ||
|
|
7a8ffbea46 | ||
|
|
b3f3df0d01 | ||
|
|
29258a3de0 | ||
|
|
872825fbb7 | ||
|
|
2c773b8c50 | ||
|
|
f105d74d9f | ||
|
|
0ab3d1bb8e | ||
|
|
fcff457462 | ||
|
|
032de9c2a3 | ||
|
|
ae7f978180 | ||
|
|
ff361d9026 | ||
|
|
c68cd91531 | ||
|
|
a53b0bda6a | ||
|
|
8ae16d90ed | ||
|
|
f3d0bb3814 | ||
|
|
cbe3243eaf | ||
|
|
dbb691084d | ||
|
|
464a2c4f66 | ||
|
|
c52d9d97e7 | ||
|
|
48ba709627 | ||
|
|
5504114732 | ||
|
|
9d2c7544ed | ||
|
|
587c777d57 | ||
|
|
a162de0b75 | ||
|
|
62fcf086cc | ||
|
|
f9b5da53e8 | ||
|
|
fa5a490283 | ||
|
|
7e76d419f5 | ||
|
|
2459a5dfed | ||
|
|
0fdf72fc57 | ||
|
|
f3666ea140 | ||
|
|
1b131f310e | ||
|
|
024ffdeb6e | ||
|
|
f859cbb5d4 | ||
|
|
2e52feb47a | ||
|
|
ac841400ba | ||
|
|
f84a902445 | ||
|
|
c3493f838d | ||
|
|
da41e11c84 | ||
|
|
ab19625688 | ||
|
|
cdb01b0d01 | ||
|
|
ca3e2888a6 | ||
|
|
c4ccc781eb | ||
|
|
2eea00a2cd | ||
|
|
071a0ae927 | ||
|
|
0b1acc3557 | ||
|
|
fdaa49a7e9 | ||
|
|
615e95f335 | ||
|
|
3f643baf1b | ||
|
|
881915239f | ||
|
|
456b903766 | ||
|
|
77ba312ecc | ||
|
|
a8cf3ef424 | ||
|
|
39f393d72d | ||
|
|
5f8dd0e3d1 | ||
|
|
158d26fe38 | ||
|
|
c17bcd395e | ||
|
|
1658740ec6 | ||
|
|
44c9fda49a | ||
|
|
a5aca2a2f6 | ||
|
|
b69ff31000 | ||
|
|
8b4dc227d3 | ||
|
|
7c85cd808d | ||
|
|
863550a91d | ||
|
|
0b487426c8 | ||
|
|
49030760ed | ||
|
|
9010c6a97e | ||
|
|
fb00e3b0af | ||
|
|
81233ad490 | ||
|
|
6b1daabd33 | ||
|
|
71635646e8 | ||
|
|
8783fe0c7d | ||
|
|
6ea5965819 | ||
|
|
09954bdbcc | ||
|
|
60d686b2bf | ||
|
|
a687d56d45 | ||
|
|
62efbe11e7 | ||
|
|
7eca53f9e7 | ||
|
|
e9e6614486 | ||
|
|
06723629ca | ||
|
|
48ad095633 | ||
|
|
0afd68d474 | ||
|
|
3a730c73d9 | ||
|
|
ee56821a47 | ||
|
|
6b59d9fbc3 | ||
|
|
1f6cf58fb4 | ||
|
|
d1650dbed9 | ||
|
|
0d5ee64ff9 | ||
|
|
6c33f17829 | ||
|
|
e4dbac314d | ||
|
|
83710e9dd1 | ||
|
|
aaa28ce99e | ||
|
|
cd5b023033 | ||
|
|
1b0a4f1cd6 | ||
|
|
137f733663 | ||
|
|
b1f72e533f | ||
|
|
62e4ec2e14 | ||
|
|
cd07020edb | ||
|
|
f17bed9741 | ||
|
|
ce91ab81d1 | ||
|
|
c31de80a38 | ||
|
|
c88e6a8635 | ||
|
|
0c9a95c964 | ||
|
|
8525494a68 | ||
|
|
be748b3bb2 | ||
|
|
5272956153 | ||
|
|
a55aad3003 | ||
|
|
32cd206556 | ||
|
|
ea4a45ad83 | ||
|
|
a4442d4d3d | ||
|
|
29422f156d | ||
|
|
8cddef62f2 | ||
|
|
d4d8a8f756 | ||
|
|
7e4b6c3744 | ||
|
|
bb847d399b | ||
|
|
8bd37bbcbb | ||
|
|
0c824de4cd | ||
|
|
1ab1fb7e4d | ||
|
|
74631922f2 | ||
|
|
975055b90f | ||
|
|
16c9535256 | ||
|
|
8dd117528c | ||
|
|
f117539a54 | ||
|
|
db5abd37cd | ||
|
|
587824ccfa | ||
|
|
c7f0f3f359 | ||
|
|
fc0056f991 | ||
|
|
acf80dc6a0 | ||
|
|
abfdb84cdd | ||
|
|
fecb92e45e | ||
|
|
7e8807d27e | ||
|
|
a57bec8648 | ||
|
|
a52d6b30b2 | ||
|
|
a4385231f8 | ||
|
|
3b2a39f0c2 | ||
|
|
8cdae1ac96 | ||
|
|
673fce45fb | ||
|
|
2c9af9903d | ||
|
|
67c21f3837 | ||
|
|
192e47222f | ||
|
|
139bc38ddc | ||
|
|
8ee4fda1d6 | ||
|
|
e062f5413f | ||
|
|
21a7ee4c33 | ||
|
|
f9d8dde61c | ||
|
|
d61d6f520b | ||
|
|
458dea2930 | ||
|
|
48e7dfc88a | ||
|
|
9131d5cc40 | ||
|
|
08231c02d2 | ||
|
|
fba1ea7d54 | ||
|
|
632c9fb2aa | ||
|
|
205d02c703 | ||
|
|
31754569ba | ||
|
|
06c6d1023e | ||
|
|
2ec1add494 | ||
|
|
78b8c3edc9 | ||
|
|
7c4eece009 | ||
|
|
7ed7cdf5a9 | ||
|
|
72f4247983 | ||
|
|
9b88bf0758 | ||
|
|
5cee360bf7 | ||
|
|
18e7e9449e | ||
|
|
d0a0dd0f7e |
1
.github/change-versions-mac.sh
vendored
1
.github/change-versions-mac.sh
vendored
@@ -16,6 +16,7 @@ sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/
|
||||
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
sed -i '' -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
|
||||
sed -i '' -e "/^ModuleVersion =/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
|
||||
# sed -i '' -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill_pg/pyproject.toml
|
||||
sed -i '' -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
|
||||
sed -i '' -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
|
||||
|
||||
2
.github/change-versions.sh
vendored
2
.github/change-versions.sh
vendored
@@ -12,10 +12,12 @@ sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/be
|
||||
sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/backend/windmill-api/openapi.yaml
|
||||
sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/openflow.openapi.yaml
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/package.json
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/jsr.json
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
|
||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
sed -i -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
|
||||
sed -i -e "/^ModuleVersion =/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
|
||||
# sed -i -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
|
||||
sed -i -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
|
||||
sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
|
||||
|
||||
16
.github/workflows/gallery_on_release.yml
vendored
Normal file
16
.github/workflows/gallery_on_release.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Publish powershell-client
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish_gallery:
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: . ./powershell-client/publish.ps1
|
||||
shell: pwsh
|
||||
env:
|
||||
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
|
||||
16
.github/workflows/jsr_on_release.yml
vendored
Normal file
16
.github/workflows/jsr_on_release.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Publish typescript-client on JSR
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: ${{ secrets.JSR_OIDC_ID_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cd typescript-client && ./publish.jsr.sh
|
||||
930
CHANGELOG.md
930
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
20
Dockerfile
20
Dockerfile
@@ -12,12 +12,14 @@ RUN apt-get -y update \
|
||||
|
||||
RUN rustup component add rustfmt
|
||||
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install cargo-chef
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install cargo-chef --version ^0.1
|
||||
RUN cargo install sccache --version ^0.8
|
||||
ENV RUSTC_WRAPPER=sccache SCCACHE_DIR=/backend/sccache
|
||||
|
||||
WORKDIR /windmill
|
||||
|
||||
ENV SQLX_OFFLINE=true
|
||||
ENV CARGO_INCREMENTAL=1
|
||||
# ENV CARGO_INCREMENTAL=1
|
||||
|
||||
FROM node:20-alpine as frontend
|
||||
|
||||
@@ -46,7 +48,9 @@ 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
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM rust_base AS builder
|
||||
ARG features=""
|
||||
@@ -55,7 +59,9 @@ COPY --from=planner /windmill/recipe.json recipe.json
|
||||
|
||||
RUN apt-get update && apt-get install -y libxml2-dev libxmlsec1-dev clang libclang-dev cmake
|
||||
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
|
||||
|
||||
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
|
||||
COPY ./backend ./
|
||||
@@ -64,7 +70,9 @@ 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"
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
|
||||
|
||||
|
||||
FROM ${DEBIAN_IMAGE} as downloader
|
||||
@@ -163,7 +171,7 @@ COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
|
||||
|
||||
COPY --from=downloader --chmod=755 /deno /usr/bin/deno
|
||||
|
||||
COPY --from=oven/bun:1.1.0 /usr/local/bin/bun /usr/bin/bun
|
||||
COPY --from=oven/bun:1.1.5 /usr/local/bin/bun /usr/bin/bun
|
||||
|
||||
# add the docker client to call docker from a worker if enabled
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -13,7 +13,7 @@ any snippets of code that require a positive license check to be activated.
|
||||
Those snippets and files are under a proprietary and commercial license. Private
|
||||
and public forks MUST not include any of the above proprietary and commercial
|
||||
code. Windmill Labs, Inc. provide tools to clean the codebase from those
|
||||
snippets upon demand. The files under python-client/ deno-client/ go-client/ are
|
||||
snippets upon demand. The files under python-client/ deno-client/ go-client/ powershell-client/ are
|
||||
Apache 2.0 Licensed.
|
||||
|
||||
The openapi files, including the OpenFlow spec is Apache 2.0 Licensed.
|
||||
|
||||
37
README.md
37
README.md
@@ -353,6 +353,43 @@ you to have it being synced automatically everyday.
|
||||
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
|
||||
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
|
||||
|
||||
## Run a local dev setup
|
||||
### only Frontend
|
||||
This will use the backend of <https://app.windmill.dev> but your own frontend
|
||||
with hot-code reloading.
|
||||
1. Install [caddy](https://caddyserver.com)
|
||||
2. Go to `frontend/`:
|
||||
1. `npm install`, `npm run generate-backend-client` then `npm run dev`
|
||||
2. In another shell `sudo caddy run --config CaddyfileRemote`
|
||||
3. Et voilà, windmill should be available at `http://localhost/`
|
||||
### Backend + Frontend
|
||||
See the [./frontend/README_DEV.md](./frontend/README_DEV.md) file for all
|
||||
running options.
|
||||
1. Create a Postgres Database for Windmill and create an admin role inside your
|
||||
Postgres setup.
|
||||
The easiest way to get a working db is to run
|
||||
```
|
||||
cargo install sqlx-cli
|
||||
env DATABASE_URL=<YOUR_DATABASE_URL> sqlx migrate run
|
||||
```
|
||||
This will also avoid compile time issue with sqlx's `query!` macro
|
||||
2. Install [nsjail](https://github.com/google/nsjail) and have it accessible in
|
||||
your PATH
|
||||
3. Install deno and python3, have the bins at `/usr/bin/deno` and
|
||||
`/usr/local/bin/python3`
|
||||
4. Install [caddy](https://caddyserver.com)
|
||||
5. Install the [lld linker](https://lld.llvm.org/)
|
||||
6. Go to `frontend/`:
|
||||
1. `npm install`, `npm run generate-backend-client` then `npm run dev`
|
||||
2. You might need to set some extra heap space for the node runtime `export NODE_OPTIONS="--max-old-space-size=4096"`
|
||||
3. In another shell `npm run build` otherwise the backend will not find the `frontend/build` folder and will not compile.
|
||||
4. In another shell `sudo caddy run --config Caddyfile`
|
||||
7. Go to `backend/`:
|
||||
`env DATABASE_URL=<DATABASE_URL_TO_YOUR_WINDMILL_DB> RUST_LOG=info cargo run`
|
||||
8. Et voilà, windmill should be available at `http://localhost/`
|
||||
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/windmill-labs/windmill/graphs/contributors">
|
||||
|
||||
14
backend/.sqlx/query-0c349ac832cbf055fce755c8e2081eacf2b836b0396ec48a6cc2405fb7aa76de.json
generated
Normal file
14
backend/.sqlx/query-0c349ac832cbf055fce755c8e2081eacf2b836b0396ec48a6cc2405fb7aa76de.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE queue SET suspend = suspend - 1 WHERE id = $1 AND suspend > 0",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0c349ac832cbf055fce755c8e2081eacf2b836b0396ec48a6cc2405fb7aa76de"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
"query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,5 +11,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "61e9662fe42506131222412ab3de48cf6485dea10aa3a2f97c0fd6322a0cb17f"
|
||||
"hash": "0d7ba88a9810e434aa00fd63bbf416cbe222f2c67ccc8aa92e651c2bea4c2d7b"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM folder WHERE name = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "26e4ec75366d1e46a98710f29066b40e66a802f98eeabbb3ae5bebe3aeb6b3f8"
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COALESCE((SELECT MIN(started_at) as min_started_at\n FROM queue\n WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $2 AND canceled = false\n GROUP BY script_path), $3)",
|
||||
"query": "SELECT COALESCE((SELECT MIN(started_at) as min_started_at\n FROM queue\n WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $2 AND canceled = false\n GROUP BY script_path), $3) as min_started_at, now() AS now",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "coalesce",
|
||||
"name": "min_started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "now",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
@@ -17,8 +22,9 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "abc7c72dfe9b01cde6f5b206300ed33e3d15b16bce2510160166a66fb7598e61"
|
||||
"hash": "3901cce744c9b246b661c817e068bdb3b1ab504ff8070fcccf6c909ad75f1f6d"
|
||||
}
|
||||
23
backend/.sqlx/query-4613382f7b031a2b667f86d9af995065a99a63c407fd58aa293fb269e032121d.json
generated
Normal file
23
backend/.sqlx/query-4613382f7b031a2b667f86d9af995065a99a63c407fd58aa293fb269e032121d.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "4613382f7b031a2b667f86d9af995065a99a63c407fd58aa293fb269e032121d"
|
||||
}
|
||||
12
backend/.sqlx/query-6f12be65a4fe3eb39292164363f557de9cef7017dcfbcd40370b849a288c52e3.json
generated
Normal file
12
backend/.sqlx/query-6f12be65a4fe3eb39292164363f557de9cef7017dcfbcd40370b849a288c52e3.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DROP INDEX CONCURRENTLY IF EXISTS labeled_jobs_on_jobs",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6f12be65a4fe3eb39292164363f557de9cef7017dcfbcd40370b849a288c52e3"
|
||||
}
|
||||
23
backend/.sqlx/query-7142222bee1f60bc56752e377b96ac6e4e981ea219c173146088faf1ad9f3822.json
generated
Normal file
23
backend/.sqlx/query-7142222bee1f60bc56752e377b96ac6e4e981ea219c173146088faf1ad9f3822.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS (SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "7142222bee1f60bc56752e377b96ac6e4e981ea219c173146088faf1ad9f3822"
|
||||
}
|
||||
23
backend/.sqlx/query-748904c35cdbb6c7b8a8e0024b341278bf2bb727f2fe0427847565fb9c774abc.json
generated
Normal file
23
backend/.sqlx/query-748904c35cdbb6c7b8a8e0024b341278bf2bb727f2fe0427847565fb9c774abc.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM folder WHERE name = $1 AND workspace_id = $2 RETURNING 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "748904c35cdbb6c7b8a8e0024b341278bf2bb727f2fe0427847565fb9c774abc"
|
||||
}
|
||||
12
backend/.sqlx/query-89e72f997e27b9298bd923e7f2b546c5ff061bd9ed63798024306456ca148aec.json
generated
Normal file
12
backend/.sqlx/query-89e72f997e27b9298bd923e7f2b546c5ff061bd9ed63798024306456ca148aec.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "CREATE INDEX CONCURRENTLY labeled_jobs_on_jobs ON completed_job USING GIN ((result -> 'wm_labels')) WHERE result ? 'wm_label';",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "89e72f997e27b9298bd923e7f2b546c5ff061bd9ed63798024306456ca148aec"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false AND workspace_id = $2",
|
||||
"query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false AND workspace_id = $2 AND is_flow_step = false",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,5 +11,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ade89de6e8527c543b182229f1febeb2513ad58b03ab526df148582264fb3a44"
|
||||
"hash": "8d4235984f27d8b939ffd5c660d5b57dc38dd3b2643361ed3b7cdcd1534d2e21"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)",
|
||||
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -66,10 +66,11 @@
|
||||
"Bool",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "020d33ed5d47350b456783fd548422ea8dcf2d786d0e9fa849754db82c9fa378"
|
||||
"hash": "92d00c6a1f4c40f2a23c9ba758a59597deabd7ca93653ad72d2cdc37efefc9d4"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT usage.usage + 1 FROM usage \n WHERE is_workspace IS FALSE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"query": "SELECT usage.usage + 1 FROM usage \n WHERE is_workspace IS FALSE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "7d93eb90163516718c85f28f8f05093133c5cbc96414000ac364583114d0ce77"
|
||||
"hash": "94ff696b4d3904e3823ef637fa8f1f0d0bdac01040c81b31514326417eb58cee"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT concurrency_key FROM script WHERE hash = $1",
|
||||
"query": "SELECT concurrency_key FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -11,12 +11,13 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2719f910142b32476a16025bb9836b0cab019ba0a436b330ea3a53fba4725f73"
|
||||
"hash": "a3ccf362b4f6df400b3c7a084795dbf541eb14c5c374656ffb96da7283a2a6f1"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE queue SET suspend = suspend - 1 WHERE parent_job = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a4ae245dcf7e4b930cd45701db0b7c45f2a5797e8b6724bade5b964d4334c098"
|
||||
}
|
||||
23
backend/.sqlx/query-a875cb56485b812e9d4739afd0915067f7e5abe0ca0adf264b792fccf21e005b.json
generated
Normal file
23
backend/.sqlx/query-a875cb56485b812e9d4739afd0915067f7e5abe0ca0adf264b792fccf21e005b.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value->>'concurrency_key' FROM flow WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a875cb56485b812e9d4739afd0915067f7e5abe0ca0adf264b792fccf21e005b"
|
||||
}
|
||||
22
backend/.sqlx/query-b8d97d300ffe6fae99f2396ae07cef03903752d17ce3fdadca47d86da75139aa.json
generated
Normal file
22
backend/.sqlx/query-b8d97d300ffe6fae99f2396ae07cef03903752d17ce3fdadca47d86da75139aa.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id FROM queue WHERE parent_job = $1 AND suspend > 0 ORDER by suspend",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b8d97d300ffe6fae99f2396ae07cef03903752d17ce3fdadca47d86da75139aa"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE queue SET canceled = true, canceled_by = $2, scheduled_for = now(), suspend = 0 WHERE scheduled_for < now() AND workspace_id = $1 AND schedule_path IS NULL RETURNING id, running, is_flow_step",
|
||||
"query": "SELECT id, running, is_flow_step FROM queue WHERE scheduled_for < now() AND workspace_id = $1 AND schedule_path IS NULL",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -21,8 +21,7 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Varchar"
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
@@ -31,5 +30,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "18699cb0eca25b6bde05d81571dfdea8cafd0043634f61b0f652a93767c9c30a"
|
||||
"hash": "caeb49629b8673c1f1c84a6e40c3e2d2c3bc3fdbde530a0a6b6fd68a22b867c3"
|
||||
}
|
||||
23
backend/.sqlx/query-d1ded8b38e50eb01fa5e5e122dae48ec21856a0041f4aeb244349ab7648d306f.json
generated
Normal file
23
backend/.sqlx/query-d1ded8b38e50eb01fa5e5e122dae48ec21856a0041f4aeb244349ab7648d306f.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM schedule WHERE path = $1 AND workspace_id = $2 RETURNING 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d1ded8b38e50eb01fa5e5e122dae48ec21856a0041f4aeb244349ab7648d306f"
|
||||
}
|
||||
23
backend/.sqlx/query-d4fb94ee8198592c24e85d29078feb5220ab367e339510ee0e83bb7b5abfd184.json
generated
Normal file
23
backend/.sqlx/query-d4fb94ee8198592c24e85d29078feb5220ab367e339510ee0e83bb7b5abfd184.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE queue SET canceled = true, canceled_by = $1, scheduled_for = now(), suspend = 0 WHERE id = $2 RETURNING 1 as one",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "one",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d4fb94ee8198592c24e85d29078feb5220ab367e339510ee0e83bb7b5abfd184"
|
||||
}
|
||||
1055
backend/Cargo.lock
generated
1055
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.303.2"
|
||||
version = "1.321.3"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -20,10 +20,11 @@ members = [
|
||||
"./parsers/windmill-parser-bash",
|
||||
"./parsers/windmill-parser-py",
|
||||
"./parsers/windmill-parser-py-imports",
|
||||
"./parsers/windmill-sql-datatype-parser-wasm",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.303.2"
|
||||
version = "1.321.3"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -143,7 +144,6 @@ urlencoding = "^2"
|
||||
url = "^2"
|
||||
async-oauth2 = "^0"
|
||||
reqwest = { version = "^0.12", features = ["json", "stream"] }
|
||||
reqwest_11 = { package = "reqwest", version = "0.11" }
|
||||
time = "0.3.16"
|
||||
serde_urlencoded = "^0"
|
||||
tokio-tar = "^0"
|
||||
@@ -224,7 +224,7 @@ tokenizers = "0.14.1"
|
||||
candle-core = "0.3.0"
|
||||
candle-transformers = "0.3.0"
|
||||
candle-nn = "0.3.0"
|
||||
tiberius = { version = "0.12.2", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"] }
|
||||
tiberius = { git = "https://github.com/prisma/tiberius", rev = "8f66a699dfa041e7b5f736c7e94f92c945453c9e", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"]}
|
||||
pin-project = "1"
|
||||
indexmap = { version = "2.2.5", features = ["serde"]}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
ada71ad279d7e917ab64476bbcae75dd377bb161
|
||||
66d9cbb158ab9a5869a45ba253bf57f2cbb5ecb6
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE resume_job ALTER COLUMN approver TYPE VARCHAR(1000);
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE flow DROP COLUMN concurrency_key;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE flow ADD COLUMN concurrency_key VARCHAR(255);
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,228 @@
|
||||
|
||||
DO
|
||||
$do$
|
||||
DECLARE
|
||||
i text;
|
||||
arr text[] := array['resource', 'script', 'variable', 'schedule', 'flow', 'app', 'raw_app'];
|
||||
BEGIN
|
||||
FOREACH i IN ARRAY arr
|
||||
LOOP
|
||||
EXECUTE FORMAT(
|
||||
$$
|
||||
DROP POLICY IF EXISTS see_folder_extra_perms_user ON %1$I;
|
||||
DROP POLICY IF EXISTS see_folder_extra_perms_user_delete ON %1$I;
|
||||
DROP POLICY IF EXISTS see_extra_perms_user ON %1$I;
|
||||
DROP POLICY IF EXISTS see_member ON %1$I;
|
||||
DROP POLICY IF EXISTS see_own ON %1$I;
|
||||
DROP POLICY IF EXISTS see_extra_perms_user_delete ON %1$I;
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups ON %1$I;
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups_delete ON %1$I;
|
||||
|
||||
-- New policies for select, insert, update
|
||||
DROP POLICY IF EXISTS see_folder_extra_perms_user_select ON %1$I;
|
||||
DROP POLICY IF EXISTS see_folder_extra_perms_user_insert ON %1$I;
|
||||
DROP POLICY IF EXISTS see_folder_extra_perms_user_update ON %1$I;
|
||||
|
||||
DROP POLICY IF EXISTS see_own ON %1$I;
|
||||
DROP POLICY IF EXISTS see_member ON %1$I;
|
||||
|
||||
DROP POLICY IF EXISTS see_extra_perms_user_select ON %1$I;
|
||||
DROP POLICY IF EXISTS see_extra_perms_user_insert ON %1$I;
|
||||
DROP POLICY IF EXISTS see_extra_perms_user_update ON %1$I;
|
||||
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups_select ON %1$I;
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups_insert ON %1$I;
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups_update ON %1$I;
|
||||
|
||||
|
||||
-- Folder permissions split into select, insert, and update
|
||||
CREATE POLICY see_folder_extra_perms_user_select ON %1$I FOR SELECT TO windmill_user
|
||||
USING (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
|
||||
|
||||
CREATE POLICY see_folder_extra_perms_user_insert ON %1$I FOR INSERT TO windmill_user
|
||||
WITH CHECK (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
|
||||
CREATE POLICY see_folder_extra_perms_user_update ON %1$I FOR UPDATE TO windmill_user
|
||||
USING (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
|
||||
CREATE POLICY see_folder_extra_perms_user_delete ON %1$I FOR UPDATE TO windmill_user
|
||||
USING (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
|
||||
CREATE POLICY see_own ON %1$I FOR ALL TO windmill_user
|
||||
USING (SPLIT_PART(%1$I.path, '/', 1) = 'u' AND SPLIT_PART(%1$I.path, '/', 2) = current_setting('session.user'));
|
||||
|
||||
|
||||
CREATE POLICY see_member ON %1$I FOR ALL TO windmill_user
|
||||
USING (SPLIT_PART(%1$I.path, '/', 1) = 'g' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
|
||||
|
||||
|
||||
|
||||
CREATE POLICY see_extra_perms_user_select ON %1$I FOR SELECT TO windmill_user
|
||||
USING (extra_perms ? CONCAT('u/', current_setting('session.user')));
|
||||
|
||||
CREATE POLICY see_extra_perms_user_insert ON %1$I FOR INSERT TO windmill_user
|
||||
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||
|
||||
CREATE POLICY see_extra_perms_user_update ON %1$I FOR UPDATE TO windmill_user
|
||||
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||
|
||||
CREATE POLICY see_extra_perms_user_delete ON %1$I FOR DELETE TO windmill_user
|
||||
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_select ON %1$I FOR SELECT TO windmill_user
|
||||
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]);
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_insert ON %1$I FOR INSERT TO windmill_user
|
||||
WITH CHECK (exists(
|
||||
SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
AND value::boolean));
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_update ON %1$I FOR UPDATE TO windmill_user
|
||||
USING (exists(
|
||||
SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
AND value::boolean));
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_delete ON %1$I FOR DELETE TO windmill_user
|
||||
USING (exists(
|
||||
SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
AND value::boolean));
|
||||
|
||||
$$,
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END
|
||||
$do$;
|
||||
|
||||
DROP POLICY IF EXISTS see_extra_perms_user ON folder;
|
||||
DROP POLICY IF EXISTS see_extra_perms_user_select ON folder;
|
||||
DROP POLICY IF EXISTS see_extra_perms_user_insert ON folder;
|
||||
DROP POLICY IF EXISTS see_extra_perms_user_update ON folder;
|
||||
DROP POLICY IF EXISTS see_extra_perms_user_delete ON folder;
|
||||
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups ON folder;
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups_select ON folder;
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups_insert ON folder;
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups_update ON folder;
|
||||
DROP POLICY IF EXISTS see_extra_perms_groups_delete ON folder;
|
||||
|
||||
|
||||
-- Existing CREATE POLICY statements updated to reflect policy splitting for 'folder' table
|
||||
CREATE POLICY see_extra_perms_user_select ON folder FOR SELECT TO windmill_user
|
||||
USING (extra_perms ? CONCAT('u/', current_setting('session.user')) OR CONCAT('u/', current_setting('session.user')) = ANY(owners));
|
||||
|
||||
CREATE POLICY see_extra_perms_user_insert ON folder FOR INSERT TO windmill_user
|
||||
WITH CHECK ((CONCAT('u/', current_setting('session.user')) = ANY(owners)));
|
||||
|
||||
CREATE POLICY see_extra_perms_user_update ON folder FOR UPDATE TO windmill_user
|
||||
USING ((CONCAT('u/', current_setting('session.user')) = ANY(owners)));
|
||||
|
||||
CREATE POLICY see_extra_perms_user_delete ON folder FOR DELETE TO windmill_user
|
||||
USING ((CONCAT('u/', current_setting('session.user')) = ANY(owners)));
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_select ON folder FOR SELECT TO windmill_user
|
||||
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[] OR EXISTS (
|
||||
SELECT o FROM unnest(owners) AS o
|
||||
WHERE o = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])));
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_insert ON folder FOR INSERT TO windmill_user
|
||||
WITH CHECK (EXISTS (
|
||||
SELECT o FROM unnest(owners) AS o
|
||||
WHERE o = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])));
|
||||
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_update ON folder FOR UPDATE TO windmill_user
|
||||
USING (EXISTS (
|
||||
SELECT o FROM unnest(owners) AS o
|
||||
WHERE o = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])));
|
||||
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_delete ON folder FOR DELETE TO windmill_user
|
||||
USING (EXISTS (
|
||||
SELECT o FROM unnest(owners) AS o
|
||||
WHERE o = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])));
|
||||
|
||||
|
||||
-- DO
|
||||
-- $do$
|
||||
-- DECLARE
|
||||
-- i text;
|
||||
-- arr text[] := array['resource', 'script', 'variable', 'schedule', 'flow', 'app', 'raw_app'];
|
||||
-- BEGIN
|
||||
-- FOREACH i IN ARRAY arr
|
||||
-- LOOP
|
||||
-- EXECUTE FORMAT(
|
||||
-- $$
|
||||
|
||||
|
||||
-- CREATE POLICY see_folder_extra_perms_user ON %1$I FOR ALL TO windmill_user
|
||||
-- USING (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]))
|
||||
-- WITH CHECK (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
|
||||
-- CREATE POLICY see_folder_extra_perms_user_delete ON %1$I AS RESTRICTIVE FOR DELETE TO windmill_user
|
||||
-- USING (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
|
||||
-- CREATE POLICY see_own ON %1$I FOR ALL TO windmill_user
|
||||
-- USING (SPLIT_PART(%1$I.path, '/', 1) = 'u' AND SPLIT_PART(%1$I.path, '/', 2) = current_setting('session.user'));
|
||||
|
||||
-- CREATE POLICY see_member ON %1$I FOR ALL TO windmill_user
|
||||
-- USING (SPLIT_PART(%1$I.path, '/', 1) = 'g' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
|
||||
|
||||
|
||||
-- CREATE POLICY see_extra_perms_user ON %1$I FOR ALL TO windmill_user
|
||||
-- USING (extra_perms ? CONCAT('u/', current_setting('session.user')))
|
||||
-- WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||
|
||||
-- CREATE POLICY see_extra_perms_user_delete ON %1$I FOR DELETE TO windmill_user
|
||||
-- USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||
|
||||
-- CREATE POLICY see_extra_perms_groups ON %1$I FOR ALL TO windmill_user
|
||||
-- USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
-- WITH CHECK (exists(
|
||||
-- SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||
-- WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
-- AND value::boolean));
|
||||
|
||||
-- CREATE POLICY see_extra_perms_groups_delete ON %1$I FOR DELETE TO windmill_user
|
||||
-- USING (exists(
|
||||
-- SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||
-- WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
-- AND value::boolean));
|
||||
-- $$,
|
||||
-- i
|
||||
-- );
|
||||
-- END LOOP;
|
||||
-- END
|
||||
-- $do$;
|
||||
|
||||
|
||||
-- DROP POLICY see_extra_perms_user ON folder;
|
||||
-- DROP POLICY see_extra_perms_groups ON folder;
|
||||
|
||||
-- CREATE POLICY see_extra_perms_user ON folder FOR ALL to windmill_user
|
||||
-- USING (extra_perms ? CONCAT('u/', current_setting('session.user')) or (CONCAT('u/', current_setting('session.user')) = ANY(owners)))
|
||||
-- WITH CHECK ((CONCAT('u/', current_setting('session.user')) = ANY(owners)));
|
||||
|
||||
-- DROP POLICY IF EXISTS see_extra_perms_user_delete ON folder;
|
||||
-- CREATE POLICY see_extra_perms_user_delete ON folder AS RESTRICTIVE FOR DELETE to windmill_user
|
||||
-- USING ((CONCAT('u/', current_setting('session.user')) = ANY(owners)));
|
||||
|
||||
-- CREATE POLICY see_extra_perms_groups ON folder FOR ALL to windmill_user
|
||||
-- USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[] or (exists(
|
||||
-- SELECT o FROM unnest(owners) as o
|
||||
-- WHERE o = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]))))
|
||||
-- WITH CHECK (exists(
|
||||
-- SELECT o FROM unnest(owners) as o
|
||||
-- WHERE o = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])));
|
||||
|
||||
-- DROP POLICY IF EXISTS see_extra_perms_groups_delete ON folder;
|
||||
-- CREATE POLICY see_extra_perms_groups_delete ON folder AS RESTRICTIVE FOR DELETE to windmill_user
|
||||
-- USING (exists(
|
||||
-- SELECT o FROM unnest(owners) as o
|
||||
-- WHERE o = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])));
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
DO
|
||||
$do$
|
||||
DECLARE
|
||||
i text;
|
||||
arr text[] := array['resource', 'script', 'variable', 'schedule', 'flow', 'app', 'raw_app'];
|
||||
BEGIN
|
||||
FOREACH i IN ARRAY arr
|
||||
LOOP
|
||||
EXECUTE FORMAT(
|
||||
$$
|
||||
DROP POLICY IF EXISTS see_folder_extra_perms_user_delete ON %1$I;
|
||||
|
||||
CREATE POLICY see_folder_extra_perms_user_delete ON %1$I FOR DELETE TO windmill_user
|
||||
USING (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
|
||||
$$,
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END
|
||||
$do$;-- Add up migration script here
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE script ADD COLUMN no_main_func BOOLEAN;
|
||||
2
backend/migrations/20240422144808_add_labels.down.sql
Normal file
2
backend/migrations/20240422144808_add_labels.down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP INDEX IF EXISTS labeled_jobs_on_completed_jobs;
|
||||
1
backend/migrations/20240422144808_add_labels.up.sql
Normal file
1
backend/migrations/20240422144808_add_labels.up.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- Add up migration script here
|
||||
1
backend/migrations/20240424083501_add_labels_2.down.sql
Normal file
1
backend/migrations/20240424083501_add_labels_2.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
1
backend/migrations/20240424083501_add_labels_2.up.sql
Normal file
1
backend/migrations/20240424083501_add_labels_2.up.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- Add up migration script here
|
||||
@@ -141,5 +141,16 @@
|
||||
"scopes": [
|
||||
"com.intuit.quickbooks.accounting"
|
||||
]
|
||||
},
|
||||
"visma": {
|
||||
"auth_url": "https://connect.visma.com/connect/authorize",
|
||||
"token_url": "https://connect.visma.com/connect/token",
|
||||
"scopes": [
|
||||
"offline_access",
|
||||
"vismanet_erp_interactive_api:create",
|
||||
"vismanet_erp_interactive_api:delete",
|
||||
"vismanet_erp_interactive_api:read",
|
||||
"vismanet_erp_interactive_api:update"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,8 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
|
||||
"mysql" => "mysql-connector-python",
|
||||
"tenable" => "pytenable",
|
||||
"ns1" => "ns1-python",
|
||||
"pymsql" => "PyMySQL",
|
||||
"haystack" => "haystack-ai",
|
||||
};
|
||||
|
||||
fn replace_import(x: String) -> String {
|
||||
@@ -61,7 +63,7 @@ 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?(\S+)\s*$").unwrap();
|
||||
}
|
||||
|
||||
fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<String> {
|
||||
|
||||
@@ -5,7 +5,7 @@ use regex::Regex;
|
||||
use serde_json::json;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use windmill_parser::{Arg, MainArgSignature, Typ};
|
||||
pub use windmill_parser::{Arg, MainArgSignature, Typ};
|
||||
|
||||
pub fn parse_mysql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
let parsed = parse_mysql_file(&code)?;
|
||||
@@ -292,22 +292,30 @@ pub fn parse_pg_typ(typ: &str) -> Typ {
|
||||
Typ::List(Box::new(base_typ))
|
||||
} else {
|
||||
match typ {
|
||||
"varchar" => Typ::Str(None),
|
||||
"varchar" | "character varying" => Typ::Str(None),
|
||||
"text" => Typ::Str(None),
|
||||
"int" => Typ::Int,
|
||||
"int" | "integer" | "int4" => Typ::Int,
|
||||
"bigint" => Typ::Int,
|
||||
"bool" => Typ::Bool,
|
||||
"char" => Typ::Str(None),
|
||||
"smallint" => Typ::Int,
|
||||
"smallserial" => Typ::Int,
|
||||
"serial" => Typ::Int,
|
||||
"bigserial" => Typ::Int,
|
||||
"real" => Typ::Float,
|
||||
"double precision" => Typ::Float,
|
||||
"numeric" => Typ::Float,
|
||||
"decimal" => Typ::Float,
|
||||
"bool" | "boolean" => Typ::Bool,
|
||||
"char" | "character" => Typ::Str(None),
|
||||
"json" | "jsonb" => Typ::Object(vec![]),
|
||||
"smallint" | "int2" => Typ::Int,
|
||||
"smallserial" | "serial2" => Typ::Int,
|
||||
"serial" | "serial4" => Typ::Int,
|
||||
"bigserial" | "serial8" => Typ::Int,
|
||||
"real" | "float4" => Typ::Float,
|
||||
"double" | "double precision" | "float8" => Typ::Float,
|
||||
"numeric" | "decimal" => Typ::Float,
|
||||
"oid" => Typ::Int,
|
||||
"date" | "time" | "timestamp" | "timestamptz" => Typ::Datetime,
|
||||
"date"
|
||||
| "time"
|
||||
| "timetz"
|
||||
| "time with time zone"
|
||||
| "time without time zone"
|
||||
| "timestamp"
|
||||
| "timestamptz"
|
||||
| "timestamp with time zone"
|
||||
| "timestamp without time zone" => Typ::Datetime,
|
||||
_ => Typ::Str(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"collaborators": [
|
||||
"Ruben Fiszel <ruben@windmill.dev>"
|
||||
],
|
||||
"version": "1.286.2",
|
||||
"version": "1.318.0",
|
||||
"files": [
|
||||
"windmill_parser_wasm_bg.wasm",
|
||||
"windmill_parser_wasm.js",
|
||||
|
||||
@@ -561,10 +561,6 @@ async function __wbg_load(module, imports) {
|
||||
function __wbg_get_imports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbg_eval_89aaea39f7e976e8 = function(arg0, arg1) {
|
||||
const ret = eval(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||
takeObject(arg0);
|
||||
};
|
||||
@@ -616,6 +612,10 @@ function __wbg_get_imports() {
|
||||
const ret = getObject(arg0) in getObject(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_eval_33c4985197d1feaf = function(arg0, arg1) {
|
||||
const ret = eval(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
|
||||
const ret = getObject(arg0) == getObject(arg1);
|
||||
return ret;
|
||||
|
||||
Binary file not shown.
20
backend/parsers/windmill-sql-datatype-parser-wasm/Cargo.toml
Normal file
20
backend/parsers/windmill-sql-datatype-parser-wasm/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
name = "windmill_sql_datatype_parser_wasm"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test.workspace = true
|
||||
|
||||
[dependencies]
|
||||
windmill-parser.workspace = true
|
||||
windmill-parser-sql.workspace = true
|
||||
wasm-bindgen.workspace = true
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -eou pipefail
|
||||
|
||||
deno task wasmbuild --out ../../../cli/wasm/
|
||||
6
backend/parsers/windmill-sql-datatype-parser-wasm/package-lock.json
generated
Normal file
6
backend/parsers/windmill-sql-datatype-parser-wasm/package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "windmill-parser-wasm",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "windmill-sql-datatype-parser-wasm",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
40
backend/parsers/windmill-sql-datatype-parser-wasm/src/lib.rs
Normal file
40
backend/parsers/windmill-sql-datatype-parser-wasm/src/lib.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use windmill_parser_sql::Typ;
|
||||
|
||||
fn to_str(typ: Typ) -> String {
|
||||
match typ {
|
||||
Typ::Str(_) => "str".to_string(),
|
||||
Typ::Int => "int".to_string(),
|
||||
Typ::Float => "float".to_string(),
|
||||
Typ::Bool => "bool".to_string(),
|
||||
Typ::List(t) => format!("list-{}", to_str(*t)),
|
||||
Typ::Bytes => "bytes".to_string(),
|
||||
Typ::Datetime => "datetime".to_string(),
|
||||
_ => "unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_sql(typ: &str) -> String {
|
||||
to_str(windmill_parser_sql::parse_pg_typ(typ))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_mysql(typ: &str) -> String {
|
||||
to_str(windmill_parser_sql::parse_mysql_typ(typ))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_bigquery(typ: &str) -> String {
|
||||
to_str(windmill_parser_sql::parse_bigquery_typ(typ))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_snowflake(typ: &str) -> String {
|
||||
to_str(windmill_parser_sql::parse_snowflake_typ(typ))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_mssql(typ: &str) -> String {
|
||||
to_str(windmill_parser_sql::parse_mssql_typ(typ))
|
||||
}
|
||||
@@ -927,7 +927,7 @@ async fn handle_zombie_flows(
|
||||
r#"
|
||||
SELECT *
|
||||
FROM queue
|
||||
WHERE running = true AND suspend = 0 AND scheduled_for <= now() AND (job_kind = 'flow' OR job_kind = 'flowpreview')
|
||||
WHERE running = true AND suspend = 0 AND suspend_until IS null AND scheduled_for <= now() AND (job_kind = 'flow' OR job_kind = 'flowpreview')
|
||||
AND last_ping IS NOT NULL AND last_ping < NOW() - ($1 || ' seconds')::interval
|
||||
"#,
|
||||
).bind(FLOW_ZOMBIE_TRANSITION_TIMEOUT.as_str())
|
||||
|
||||
@@ -1026,7 +1026,7 @@ async fn listen_for_uuid_on(
|
||||
|
||||
async fn completed_job(uuid: Uuid, db: &Pool<Postgres>) -> CompletedJob {
|
||||
|
||||
sqlx::query_as::<_, CompletedJob>("SELECT * FROM completed_job WHERE id = $1").bind(uuid)
|
||||
sqlx::query_as::<_, CompletedJob>("SELECT *, result->'wm_labels' as labels FROM completed_job WHERE id = $1").bind(uuid)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -3164,6 +3164,7 @@ async fn run_deployed_relative_imports(db: &Pool<Postgres>, script_content: Stri
|
||||
deployment_message: None,
|
||||
concurrency_key: None,
|
||||
visible_to_runner_only: None,
|
||||
no_main_func: None,
|
||||
},
|
||||
).await.unwrap();
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ tower-http.workspace = true
|
||||
hyper.workspace = true
|
||||
itertools.workspace = true
|
||||
reqwest.workspace = true
|
||||
reqwest_11.workspace = true
|
||||
serde.workspace = true
|
||||
sqlx.workspace = true
|
||||
async-oauth2.workspace = true
|
||||
|
||||
@@ -5,5 +5,7 @@ npx @redocly/openapi-cli@latest bundle openapi.yaml > openapi-bundled.yaml
|
||||
|
||||
|
||||
npx @redocly/openapi-cli@latest bundle openapi-bundled.yaml --ext yaml -d > openapi-deref.yaml
|
||||
npx @redocly/openapi-cli@latest bundle openapi-bundled.yaml --ext json > openapi-deref.json
|
||||
|
||||
|
||||
rm openapi-bundled.yaml
|
||||
|
||||
16622
backend/windmill-api/openapi-deref.json
Normal file
16622
backend/windmill-api/openapi-deref.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.303.2
|
||||
version: 1.321.3
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -3401,6 +3401,13 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: hide_without_main
|
||||
description: |
|
||||
(default false)
|
||||
hide the scripts without an exported main function
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: is_template
|
||||
description: |
|
||||
(default regardless)
|
||||
@@ -3890,6 +3897,7 @@ paths:
|
||||
type: integer
|
||||
- $ref: "#/components/parameters/ParentJob"
|
||||
- $ref: "#/components/parameters/WorkerTag"
|
||||
- $ref: "#/components/parameters/CacheTtl"
|
||||
- $ref: "#/components/parameters/NewJobId"
|
||||
- name: invisible_to_owner
|
||||
description: make the run invisible to the the script owner (default false)
|
||||
@@ -3953,6 +3961,7 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
- $ref: "#/components/parameters/ParentJob"
|
||||
- $ref: "#/components/parameters/WorkerTag"
|
||||
- $ref: "#/components/parameters/CacheTtl"
|
||||
- $ref: "#/components/parameters/NewJobId"
|
||||
- $ref: "#/components/parameters/IncludeHeader"
|
||||
- $ref: "#/components/parameters/QueueLimit"
|
||||
@@ -3982,6 +3991,7 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
- $ref: "#/components/parameters/ParentJob"
|
||||
- $ref: "#/components/parameters/WorkerTag"
|
||||
- $ref: "#/components/parameters/CacheTtl"
|
||||
- $ref: "#/components/parameters/NewJobId"
|
||||
- $ref: "#/components/parameters/IncludeHeader"
|
||||
- $ref: "#/components/parameters/QueueLimit"
|
||||
@@ -4897,6 +4907,8 @@ paths:
|
||||
- language
|
||||
force_viewer_static_fields:
|
||||
type: object
|
||||
force_viewer_one_of_fields:
|
||||
type: object
|
||||
required:
|
||||
- args
|
||||
- component
|
||||
@@ -5039,6 +5051,7 @@ paths:
|
||||
type: integer
|
||||
- $ref: "#/components/parameters/ParentJob"
|
||||
- $ref: "#/components/parameters/WorkerTag"
|
||||
- $ref: "#/components/parameters/CacheTtl"
|
||||
- $ref: "#/components/parameters/NewJobId"
|
||||
- $ref: "#/components/parameters/IncludeHeader"
|
||||
- name: invisible_to_owner
|
||||
@@ -5230,11 +5243,18 @@ paths:
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- $ref: "#/components/parameters/Tag"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- name: all_workspaces
|
||||
description: get jobs from all workspaces (only valid if request come from the `admins` workspace)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: is_not_schedule
|
||||
description: is not a scheduled job
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: All queued jobs
|
||||
@@ -5320,6 +5340,7 @@ paths:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/OrderDesc"
|
||||
- $ref: "#/components/parameters/CreatedBy"
|
||||
- $ref: "#/components/parameters/Label"
|
||||
- $ref: "#/components/parameters/ParentJob"
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
@@ -5332,6 +5353,8 @@ paths:
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- $ref: "#/components/parameters/Tag"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -5347,6 +5370,11 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: is_not_schedule
|
||||
description: is not a scheduled job
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: All completed jobs
|
||||
@@ -5366,6 +5394,7 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/CreatedBy"
|
||||
- $ref: "#/components/parameters/Label"
|
||||
- $ref: "#/components/parameters/ParentJob"
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
@@ -5381,6 +5410,8 @@ paths:
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/Tag"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -5406,6 +5437,11 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: is_not_schedule
|
||||
description: is not a scheduled job
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: All jobs
|
||||
@@ -5524,6 +5560,26 @@ paths:
|
||||
flow_status:
|
||||
$ref: "#/components/schemas/WorkflowStatusRecord"
|
||||
|
||||
/w/{workspace}/jobs_u/get_log_file/{path}:
|
||||
get:
|
||||
summary: get log file from object store
|
||||
operationId: getLogFileFromStore
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: path
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: job log
|
||||
content:
|
||||
text/plain:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/jobs_u/get_flow_debug_info/{id}:
|
||||
get:
|
||||
summary: get flow debug info
|
||||
@@ -6190,6 +6246,7 @@ paths:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- name: path
|
||||
description: filter by path
|
||||
in: query
|
||||
@@ -7994,6 +8051,12 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
Label:
|
||||
name: label
|
||||
description: mask to filter exact matching job's label (job labels are completed jobs with as a result an object containing a string in the array at key 'wm_labels')
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
ParentJob:
|
||||
name: parent_job
|
||||
description:
|
||||
@@ -8009,6 +8072,12 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
CacheTtl:
|
||||
name: cache_ttl
|
||||
description: Override the cache time to live (in seconds). Can not be used to disable caching, only override with a new cache ttl
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
NewJobId:
|
||||
name: job_id
|
||||
description:
|
||||
@@ -8319,6 +8388,8 @@ components:
|
||||
type: boolean
|
||||
visible_to_runner_only:
|
||||
type: boolean
|
||||
no_main_func:
|
||||
type: boolean
|
||||
required:
|
||||
- hash
|
||||
- path
|
||||
@@ -8334,6 +8405,7 @@ components:
|
||||
- language
|
||||
- kind
|
||||
- starred
|
||||
- no_main_func
|
||||
|
||||
NewScript:
|
||||
type: object
|
||||
@@ -8407,6 +8479,8 @@ components:
|
||||
type: string
|
||||
visible_to_runner_only:
|
||||
type: boolean
|
||||
no_main_func:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- summary
|
||||
@@ -8708,6 +8782,10 @@ components:
|
||||
type: string
|
||||
priority:
|
||||
type: integer
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- created_by
|
||||
@@ -8725,15 +8803,21 @@ components:
|
||||
- tag
|
||||
|
||||
Job:
|
||||
allOf:
|
||||
- oneOf:
|
||||
oneOf:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/CompletedJob"
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [CompletedJob]
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/QueuedJob"
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [CompletedJob, QueuedJob]
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [QueuedJob]
|
||||
discriminator:
|
||||
propertyName: type
|
||||
|
||||
@@ -9703,6 +9787,11 @@ components:
|
||||
- $ref: "../../openflow.openapi.yaml#/components/schemas/OpenFlow"
|
||||
- $ref: "#/components/schemas/FlowMetadata"
|
||||
|
||||
ExtraPerms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
|
||||
FlowMetadata:
|
||||
type: object
|
||||
properties:
|
||||
@@ -9718,9 +9807,7 @@ components:
|
||||
archived:
|
||||
type: boolean
|
||||
extra_perms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
$ref: "#/components/schemas/ExtraPerms"
|
||||
starred:
|
||||
type: boolean
|
||||
draft_only:
|
||||
@@ -9803,6 +9890,10 @@ components:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
triggerables_v2:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
execution_mode:
|
||||
type: string
|
||||
enum: [viewer, publisher, anonymous]
|
||||
@@ -9894,7 +9985,8 @@ components:
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
value: {}
|
||||
value:
|
||||
type: object
|
||||
policy:
|
||||
$ref: "#/components/schemas/Policy"
|
||||
execution_mode:
|
||||
@@ -10119,6 +10211,8 @@ components:
|
||||
type: string
|
||||
use_individual_branch:
|
||||
type: boolean
|
||||
group_by_folder:
|
||||
type: boolean
|
||||
exclude_types_override:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -143,6 +143,7 @@ pub struct AppHistoryUpdate {
|
||||
}
|
||||
|
||||
pub type StaticFields = HashMap<String, Box<RawValue>>;
|
||||
pub type OneOfFields = HashMap<String, Vec<Box<RawValue>>>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -152,6 +153,12 @@ pub enum ExecutionMode {
|
||||
Viewer,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PolicyTriggerableInputs {
|
||||
static_inputs: StaticFields,
|
||||
one_of_inputs: OneOfFields,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Policy {
|
||||
pub on_behalf_of: Option<String>,
|
||||
@@ -160,7 +167,10 @@ pub struct Policy {
|
||||
// - script/<path>
|
||||
// - flow/<path>
|
||||
// - rawscript/<sha256>
|
||||
pub triggerables: HashMap<String, StaticFields>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub triggerables: Option<HashMap<String, StaticFields>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub triggerables_v2: Option<HashMap<String, PolicyTriggerableInputs>>,
|
||||
pub execution_mode: ExecutionMode,
|
||||
}
|
||||
|
||||
@@ -934,6 +944,7 @@ pub struct ExecuteApp {
|
||||
pub raw_code: Option<RawCode>,
|
||||
// if set, the app is executed as viewer with the given static fields
|
||||
pub force_viewer_static_fields: Option<StaticFields>,
|
||||
pub force_viewer_one_of_fields: Option<OneOfFields>,
|
||||
}
|
||||
|
||||
fn digest(code: &str) -> String {
|
||||
@@ -966,39 +977,56 @@ async fn execute_component(
|
||||
|
||||
let path = path.to_path();
|
||||
|
||||
let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields {
|
||||
let mut hm = HashMap::new();
|
||||
let policy = match payload.clone() {
|
||||
ExecuteApp {
|
||||
force_viewer_static_fields: Some(static_fields),
|
||||
force_viewer_one_of_fields: Some(one_of_fields),
|
||||
..
|
||||
} => {
|
||||
let mut hm = HashMap::new();
|
||||
|
||||
if let Some(path) = payload.path.clone() {
|
||||
hm.insert(format!("{}:{path}", payload.component), static_fields);
|
||||
} else {
|
||||
hm.insert(
|
||||
format!(
|
||||
"{}:{}",
|
||||
payload.component,
|
||||
digest(payload.raw_code.clone().unwrap().content.as_str())
|
||||
),
|
||||
static_fields,
|
||||
);
|
||||
if let Some(path) = payload.path.clone() {
|
||||
hm.insert(
|
||||
format!("{}:{path}", payload.component),
|
||||
PolicyTriggerableInputs {
|
||||
static_inputs: static_fields,
|
||||
one_of_inputs: one_of_fields,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
hm.insert(
|
||||
format!(
|
||||
"{}:{}",
|
||||
payload.component,
|
||||
digest(payload.raw_code.clone().unwrap().content.as_str())
|
||||
),
|
||||
PolicyTriggerableInputs {
|
||||
static_inputs: static_fields,
|
||||
one_of_inputs: one_of_fields,
|
||||
},
|
||||
);
|
||||
}
|
||||
Policy {
|
||||
execution_mode: ExecutionMode::Viewer,
|
||||
triggerables: None,
|
||||
triggerables_v2: Some(hm),
|
||||
on_behalf_of: None,
|
||||
on_behalf_of_email: None,
|
||||
}
|
||||
}
|
||||
Policy {
|
||||
execution_mode: ExecutionMode::Viewer,
|
||||
triggerables: hm,
|
||||
on_behalf_of: None,
|
||||
on_behalf_of_email: None,
|
||||
_ => {
|
||||
let policy_o = sqlx::query_scalar!(
|
||||
"SELECT policy from app WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
let policy = not_found_if_none(policy_o, "App", path)?;
|
||||
|
||||
serde_json::from_value::<Policy>(policy).map_err(to_anyhow)?
|
||||
}
|
||||
} else {
|
||||
let policy_o = sqlx::query_scalar!(
|
||||
"SELECT policy from app WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
let policy = not_found_if_none(policy_o, "App", path)?;
|
||||
|
||||
serde_json::from_value::<Policy>(policy).map_err(to_anyhow)?
|
||||
};
|
||||
|
||||
let (username, permissioned_as, email) = match policy.execution_mode {
|
||||
@@ -1133,22 +1161,106 @@ fn build_args(
|
||||
path: String,
|
||||
args: HashMap<String, Box<RawValue>>,
|
||||
) -> Result<PushArgs<HashMap<String, Box<RawValue>>>> {
|
||||
// disallow var and res access in args coming from the user for security reasons
|
||||
let mut safe_args: HashMap<String, Box<RawValue>> = args.clone();
|
||||
|
||||
let key = format!("{}:{}", component, &path);
|
||||
let (static_inputs, one_of_inputs) = match policy {
|
||||
Policy { triggerables_v2: Some(t), .. } => {
|
||||
let PolicyTriggerableInputs { static_inputs, one_of_inputs } = t
|
||||
.get(&key)
|
||||
.or_else(|| t.get(&path))
|
||||
.map(|x| x.clone())
|
||||
.or_else(|| {
|
||||
if matches!(policy.execution_mode, ExecutionMode::Viewer) {
|
||||
Some(PolicyTriggerableInputs {
|
||||
static_inputs: HashMap::new(),
|
||||
one_of_inputs: HashMap::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(format!("path {} is not allowed in the app policy", path))
|
||||
})?;
|
||||
|
||||
(static_inputs, one_of_inputs)
|
||||
}
|
||||
Policy { triggerables: Some(t), .. } => {
|
||||
let static_inputs = t
|
||||
.get(&key)
|
||||
.or_else(|| t.get(&path))
|
||||
.map(|x| x.clone())
|
||||
.or_else(|| {
|
||||
if matches!(policy.execution_mode, ExecutionMode::Viewer) {
|
||||
Some(HashMap::new())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(format!("path {} is not allowed in the app policy", path))
|
||||
})?;
|
||||
|
||||
(static_inputs, HashMap::new())
|
||||
}
|
||||
_ => Err(Error::BadRequest(format!(
|
||||
"Policy is missing triggerables for {}",
|
||||
key
|
||||
)))?,
|
||||
};
|
||||
|
||||
let mut args = args.clone();
|
||||
let mut safe_args = HashMap::<String, Box<RawValue>>::new();
|
||||
|
||||
for (k, v) in one_of_inputs {
|
||||
if let Some(arg_val) = args.get(&k) {
|
||||
let arg_str = arg_val.get();
|
||||
|
||||
let options_str_vec = v.iter().map(|x| x.get()).collect::<Vec<&str>>();
|
||||
if options_str_vec.contains(&arg_str) {
|
||||
safe_args.insert(k.to_string(), arg_val.clone());
|
||||
args.remove(&k);
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if multiselect
|
||||
if let Ok(args_str_vec) = serde_json::from_str::<Vec<Box<RawValue>>>(arg_val.get()) {
|
||||
if args_str_vec
|
||||
.iter()
|
||||
.all(|x| options_str_vec.contains(&x.get()))
|
||||
{
|
||||
safe_args.insert(k.to_string(), arg_val.clone());
|
||||
args.remove(&k);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return Err(Error::BadRequest(format!(
|
||||
"argument {} with value {} must be one of [{}]",
|
||||
k,
|
||||
arg_str,
|
||||
options_str_vec.join(",")
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
for (k, v) in args {
|
||||
let args_str = serde_json::to_string(&v).unwrap_or_else(|_| "".to_string());
|
||||
if args_str.contains("$var:") || args_str.contains("$res:") {
|
||||
let arg_str = serde_json::to_string(&v).unwrap_or_else(|_| "".to_string());
|
||||
|
||||
if !arg_str.contains("$var:") && !arg_str.contains("$res:") {
|
||||
safe_args.insert(k.to_string(), v);
|
||||
} else {
|
||||
safe_args.insert(
|
||||
k.to_string(),
|
||||
RawValue::from_string(
|
||||
args_str
|
||||
arg_str
|
||||
.replace(
|
||||
"$var:",
|
||||
"The following variable has been ommited for security reasons: ",
|
||||
"The following variable has been omitted for security reasons: ",
|
||||
)
|
||||
.replace(
|
||||
"$res:",
|
||||
"The following resource has been ommited for security reasons: ",
|
||||
"The following resource has been omitted for security reasons: ",
|
||||
),
|
||||
)
|
||||
.map_err(|e| {
|
||||
@@ -1160,24 +1272,8 @@ fn build_args(
|
||||
);
|
||||
}
|
||||
}
|
||||
let key = format!("{}:{}", component, &path);
|
||||
let static_args = policy
|
||||
.triggerables
|
||||
.get(&key)
|
||||
.or_else(|| policy.triggerables.get(&path))
|
||||
.map(|x| x.clone())
|
||||
.or_else(|| {
|
||||
if matches!(policy.execution_mode, ExecutionMode::Viewer) {
|
||||
Some(HashMap::new())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(format!("path {} is not allowed in the app policy", path))
|
||||
})?;
|
||||
let mut extra = HashMap::new();
|
||||
for (k, v) in static_args {
|
||||
for (k, v) in static_inputs {
|
||||
extra.insert(k.to_string(), v.to_owned());
|
||||
}
|
||||
Ok(PushArgs { extra, args: sqlx::types::Json(safe_args) })
|
||||
|
||||
@@ -136,6 +136,15 @@ impl Migrate for CustomMigrator {
|
||||
migration.version,
|
||||
migration.description
|
||||
);
|
||||
if migration.version == 20240424083501 {
|
||||
tracing::info!("Special migration to add index concurrently on job labels 2");
|
||||
sqlx::query!(
|
||||
"DROP INDEX CONCURRENTLY IF EXISTS labeled_jobs_on_jobs"
|
||||
).execute(&mut *self.inner).await?;
|
||||
sqlx::query!(
|
||||
"CREATE INDEX CONCURRENTLY labeled_jobs_on_jobs ON completed_job USING GIN ((result -> 'wm_labels')) WHERE result ? 'wm_label';"
|
||||
).execute(&mut *self.inner).await?;
|
||||
}
|
||||
let r = self.inner.apply(migration).await;
|
||||
tracing::info!("Finished applying migration {}", migration.version);
|
||||
r
|
||||
|
||||
@@ -985,6 +985,7 @@ mod tests {
|
||||
cache_ttl: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
concurrency_key: None,
|
||||
};
|
||||
let expect = serde_json::json!({
|
||||
"modules": [
|
||||
|
||||
@@ -478,13 +478,22 @@ async fn delete_folder(
|
||||
|
||||
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM folder WHERE name = $1 AND workspace_id = $2",
|
||||
let del = sqlx::query_scalar!(
|
||||
"DELETE FROM folder WHERE name = $1 AND workspace_id = $2 RETURNING 1",
|
||||
name,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
if del.is_none() {
|
||||
return Err(windmill_common::error::Error::NotAuthorized(format!(
|
||||
"Not authorized to delete folder {}",
|
||||
name
|
||||
)));
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed.username,
|
||||
|
||||
@@ -251,6 +251,7 @@ pub fn global_service() -> Router {
|
||||
get(get_completed_job_result_maybe),
|
||||
)
|
||||
.route("/getupdate/:id", get(get_job_update))
|
||||
.route("/get_log_file/*file_path", get(get_log_file))
|
||||
.route("/queue/cancel/:id", post(cancel_job_api))
|
||||
.route(
|
||||
"/queue/cancel_persistent/*script_path",
|
||||
@@ -584,6 +585,7 @@ fn generate_get_job_query(no_logs: bool, table: &str) -> String {
|
||||
result,
|
||||
deleted,
|
||||
is_skipped,
|
||||
result->'wm_labels' as labels,
|
||||
CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result"
|
||||
} else {
|
||||
"scheduled_for,
|
||||
@@ -778,6 +780,8 @@ pub struct ListableCompletedJob {
|
||||
pub tag: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub priority: Option<i16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub labels: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -792,6 +796,7 @@ pub struct RunJobQuery {
|
||||
job_id: Option<Uuid>,
|
||||
tag: Option<String>,
|
||||
timeout: Option<i32>,
|
||||
cache_ttl: Option<i32>,
|
||||
}
|
||||
|
||||
impl RunJobQuery {
|
||||
@@ -837,6 +842,7 @@ pub struct ListQueueQuery {
|
||||
pub all_workspaces: Option<bool>,
|
||||
pub is_flow_step: Option<bool>,
|
||||
pub has_null_parent: Option<bool>,
|
||||
pub is_not_schedule: Option<bool>,
|
||||
}
|
||||
|
||||
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
|
||||
@@ -941,6 +947,10 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
sqlb.and_where_le("scheduled_for", "now()");
|
||||
}
|
||||
|
||||
if lq.is_not_schedule.unwrap_or(false) {
|
||||
sqlb.and_where("schedule_path IS null");
|
||||
}
|
||||
|
||||
sqlb
|
||||
}
|
||||
|
||||
@@ -1011,49 +1021,62 @@ async fn cancel_all(
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut jobs = sqlx::query!(
|
||||
"UPDATE queue SET canceled = true, canceled_by = $2, scheduled_for = now(), suspend = 0 WHERE scheduled_for < now() AND workspace_id = $1 AND schedule_path IS NULL RETURNING id, running, is_flow_step",
|
||||
let jobs = sqlx::query!(
|
||||
"SELECT id, running, is_flow_step FROM queue WHERE scheduled_for < now() AND workspace_id = $1 AND schedule_path IS NULL",
|
||||
w_id,
|
||||
authed.username
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
let username = authed.username;
|
||||
let mut uuids = vec![];
|
||||
for j in jobs.iter() {
|
||||
if !j.running && !j.is_flow_step.unwrap_or(false) {
|
||||
let e = serde_json::json!({"message": format!("Job canceled: cancel_all by {username}"), "name": "Canceled", "reason": "cancel_all", "canceler": username});
|
||||
let job_running = get_queued_job(&j.id, &w_id, &db).await?;
|
||||
let r = sqlx::query!(
|
||||
"UPDATE queue SET canceled = true, canceled_by = $1, scheduled_for = now(), suspend = 0 WHERE id = $2 RETURNING 1 as one",
|
||||
username,
|
||||
j.id,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await;
|
||||
|
||||
if let Some(job_running) = job_running {
|
||||
append_logs(
|
||||
j.id,
|
||||
w_id.clone(),
|
||||
format!("canceled by {username}: cancel_all"),
|
||||
db.clone(),
|
||||
)
|
||||
.await;
|
||||
let add_job = add_completed_job_error(
|
||||
&db,
|
||||
&job_running,
|
||||
job_running.mem_peak.unwrap_or(0),
|
||||
Some(CanceledBy {
|
||||
username: Some(username.to_string()),
|
||||
reason: Some("cancel_all".to_string()),
|
||||
}),
|
||||
e,
|
||||
rsmq.clone(),
|
||||
"server",
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = add_job {
|
||||
tracing::error!("Failed to add canceled job: {}", e);
|
||||
if r.as_ref().is_ok_and(|x| x.is_some()) {
|
||||
uuids.push(j.id);
|
||||
|
||||
if !j.running && !j.is_flow_step.unwrap_or(false) {
|
||||
let e = serde_json::json!({"message": format!("Job canceled: cancel_all by {username}"), "name": "Canceled", "reason": "cancel_all", "canceler": username});
|
||||
let job_running = get_queued_job(&j.id, &w_id, &db).await?;
|
||||
|
||||
if let Some(job_running) = job_running {
|
||||
append_logs(
|
||||
j.id,
|
||||
w_id.clone(),
|
||||
format!("canceled by {username}: cancel_all"),
|
||||
db.clone(),
|
||||
)
|
||||
.await;
|
||||
let add_job = add_completed_job_error(
|
||||
&db,
|
||||
&job_running,
|
||||
job_running.mem_peak.unwrap_or(0),
|
||||
Some(CanceledBy {
|
||||
username: Some(username.to_string()),
|
||||
reason: Some("cancel_all".to_string()),
|
||||
}),
|
||||
e,
|
||||
rsmq.clone(),
|
||||
"server",
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = add_job {
|
||||
tracing::error!("Failed to add canceled job: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Failed to cancel job: {:?} {:?}", j.id, r.err());
|
||||
}
|
||||
}
|
||||
let uuids = jobs.iter_mut().map(|j| j.id).collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(uuids))
|
||||
}
|
||||
@@ -1156,13 +1179,14 @@ async fn list_jobs(
|
||||
"null as concurrent_limit",
|
||||
"null as concurrency_time_window_s",
|
||||
"priority",
|
||||
"result->'wm_labels' as labels",
|
||||
],
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let sql = if lq.success.is_none() {
|
||||
let sql = if lq.success.is_none() && lq.label.is_none() {
|
||||
let sqlq = list_queue_jobs_query(
|
||||
&w_id,
|
||||
&ListQueueQuery {
|
||||
@@ -1188,6 +1212,7 @@ async fn list_jobs(
|
||||
all_workspaces: lq.all_workspaces,
|
||||
is_flow_step: lq.is_flow_step,
|
||||
has_null_parent: lq.has_null_parent,
|
||||
is_not_schedule: lq.is_not_schedule,
|
||||
},
|
||||
&[
|
||||
"'QueuedJob' as typ",
|
||||
@@ -1221,6 +1246,7 @@ async fn list_jobs(
|
||||
"concurrent_limit",
|
||||
"concurrency_time_window_s",
|
||||
"priority",
|
||||
"null as labels",
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1953,6 +1979,7 @@ struct UnifiedJob {
|
||||
concurrent_limit: Option<i32>,
|
||||
concurrency_time_window_s: Option<i32>,
|
||||
priority: Option<i16>,
|
||||
labels: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl<'a> From<UnifiedJob> for Job {
|
||||
@@ -1990,6 +2017,7 @@ impl<'a> From<UnifiedJob> for Job {
|
||||
mem_peak: uj.mem_peak,
|
||||
tag: uj.tag,
|
||||
priority: uj.priority,
|
||||
labels: uj.labels,
|
||||
}),
|
||||
"QueuedJob" => Job::QueuedJob(QueuedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
@@ -2270,7 +2298,7 @@ pub async fn restart_flow(
|
||||
check_license_key_valid().await?;
|
||||
|
||||
let completed_job = sqlx::query_as::<_, CompletedJob>(
|
||||
"SELECT * from completed_job WHERE id = $1 and workspace_id = $2",
|
||||
"SELECT *, result->'wm_labels' as labels from completed_job WHERE id = $1 and workspace_id = $2",
|
||||
)
|
||||
.bind(job_id)
|
||||
.bind(&w_id)
|
||||
@@ -2888,13 +2916,16 @@ pub async fn run_wait_result_script_by_hash(
|
||||
tag,
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
cache_ttl,
|
||||
mut cache_ttl,
|
||||
language,
|
||||
dedicated_worker,
|
||||
priority,
|
||||
delete_after_use,
|
||||
timeout,
|
||||
) = get_path_tag_limits_cache_for_hash(&db, &w_id, hash).await?;
|
||||
if let Some(run_query_cache_ttl) = run_query.cache_ttl {
|
||||
cache_ttl = Some(run_query_cache_ttl);
|
||||
}
|
||||
check_scopes(&authed, || format!("run:script/{path}"))?;
|
||||
|
||||
let tag = run_query.tag.clone().or(tag);
|
||||
@@ -3409,7 +3440,7 @@ pub async fn run_job_by_hash(
|
||||
tag,
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
cache_ttl,
|
||||
mut cache_ttl,
|
||||
language,
|
||||
dedicated_worker,
|
||||
priority,
|
||||
@@ -3417,7 +3448,9 @@ pub async fn run_job_by_hash(
|
||||
timeout,
|
||||
) = get_path_tag_limits_cache_for_hash(&db, &w_id, hash).await?;
|
||||
check_scopes(&authed, || format!("run:script/{path}"))?;
|
||||
|
||||
if let Some(run_query_cache_ttl) = run_query.cache_ttl {
|
||||
cache_ttl = Some(run_query_cache_ttl);
|
||||
}
|
||||
let scheduled_for = run_query.get_scheduled_for(&db).await?;
|
||||
let tag = run_query.tag.clone().or(tag);
|
||||
|
||||
@@ -3478,6 +3511,84 @@ pub struct JobUpdate {
|
||||
pub flow_status: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
// #[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
// async fn get_logs_from_store(
|
||||
// log_offset: i32,
|
||||
// logs: &str,
|
||||
// log_file_index: Option<Vec<String>>,
|
||||
// ) -> Option<error::Result<Body>> {
|
||||
// if log_offset > 0 {
|
||||
// if let Some(file_index) = log_file_index {
|
||||
// tracing::debug!("Getting logs from store: {file_index:?}");
|
||||
// if let Some(os) = OBJECT_STORE_CACHE_SETTINGS.read().await.clone() {
|
||||
// tracing::debug!("object store client present, streaming from there");
|
||||
|
||||
// let logs = logs.to_string();
|
||||
// let stream = async_stream::stream! {
|
||||
// for file_p in file_index {
|
||||
// let file_p_2 = file_p.clone();
|
||||
// let file = os.get(&object_store::path::Path::from(file_p)).await;
|
||||
// if let Ok(file) = file {
|
||||
// if let Ok(bytes) = file.bytes().await {
|
||||
// yield Ok(bytes::Bytes::from(bytes)) as object_store::Result<bytes::Bytes>;
|
||||
// }
|
||||
// } else {
|
||||
// tracing::debug!("error getting file from store: {file_p_2}: {}", file.err().unwrap());
|
||||
// }
|
||||
// }
|
||||
|
||||
// yield Ok(bytes::Bytes::from(logs))
|
||||
// };
|
||||
// return Some(Ok(Body::from_stream(stream)));
|
||||
// } else {
|
||||
// tracing::debug!("object store client not present, cannot stream logs from store");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return None;
|
||||
// }
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::Result<Response> {
|
||||
if let Some(os) = OBJECT_STORE_CACHE_SETTINGS.read().await.clone() {
|
||||
let file = os
|
||||
.get(&object_store::path::Path::from(format!("logs/{file_p}")))
|
||||
.await;
|
||||
if let Ok(file) = file {
|
||||
if let Ok(bytes) = file.bytes().await {
|
||||
use axum::http::header;
|
||||
let res = Response::builder()
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body(Body::from(bytes::Bytes::from(bytes)))
|
||||
.unwrap();
|
||||
return Ok(res);
|
||||
} else {
|
||||
return Err(error::Error::InternalErr(format!(
|
||||
"Error getting bytes from file: {}",
|
||||
file_p
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err(error::Error::NotFound(format!(
|
||||
"File not found: {}",
|
||||
file_p
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err(error::Error::InternalErr(
|
||||
"Object store client not present, cannot stream logs from store".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::Result<Response> {
|
||||
return Err(error::Error::NotFound(format!(
|
||||
"Get log file is an EE feature: {}",
|
||||
file_p
|
||||
)));
|
||||
}
|
||||
|
||||
async fn get_job_update(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, job_id)): Path<(String, Uuid)>,
|
||||
@@ -3634,6 +3745,17 @@ fn list_completed_jobs_query(
|
||||
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
|
||||
}
|
||||
|
||||
if let Some(label) = &lq.label {
|
||||
let mut wh = format!("result->'wm_labels' ? ");
|
||||
wh.push_str(&format!("'{}'", &label.replace("'", "''")));
|
||||
sqlb.and_where(&wh);
|
||||
sqlb.and_where("result ? 'wm_labels'");
|
||||
}
|
||||
|
||||
if lq.is_not_schedule.unwrap_or(false) {
|
||||
sqlb.and_where("schedule_path IS null");
|
||||
}
|
||||
|
||||
sqlb
|
||||
}
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -3665,6 +3787,8 @@ pub struct ListCompletedQuery {
|
||||
pub scheduled_for_before_now: Option<bool>,
|
||||
pub all_workspaces: Option<bool>,
|
||||
pub has_null_parent: Option<bool>,
|
||||
pub label: Option<String>,
|
||||
pub is_not_schedule: Option<bool>,
|
||||
}
|
||||
|
||||
async fn list_completed_jobs(
|
||||
@@ -3712,6 +3836,7 @@ async fn list_completed_jobs(
|
||||
"mem_peak",
|
||||
"tag",
|
||||
"priority",
|
||||
"result->'wm_labels' as labels",
|
||||
"'CompletedJob' as type",
|
||||
],
|
||||
)
|
||||
@@ -3729,7 +3854,7 @@ async fn get_completed_job<'a>(
|
||||
let job_o = sqlx::query("SELECT id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
|
||||
CASE WHEN args is null or pg_column_size(args) < 2000000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
|
||||
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
|
||||
raw_lock, email, visible_to_owner, mem_peak, tag, priority FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
raw_lock, email, visible_to_owner, mem_peak, tag, priority, result->'wm_labels' as labels FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
.bind(id)
|
||||
.bind(w_id)
|
||||
.fetch_optional(&db)
|
||||
|
||||
@@ -107,11 +107,6 @@ lazy_static::lazy_static! {
|
||||
.danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())
|
||||
.build().unwrap();
|
||||
|
||||
pub static ref HTTP_CLIENT_11: reqwest_11::Client = reqwest_11::ClientBuilder::new()
|
||||
.user_agent("windmill/beta")
|
||||
.danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())
|
||||
.build().unwrap();
|
||||
|
||||
pub static ref OAUTH_CLIENTS: Arc<RwLock<AllClients>> = Arc::new(RwLock::new(AllClients {
|
||||
logins: HashMap::new(),
|
||||
connects: HashMap::new(),
|
||||
@@ -284,7 +279,8 @@ pub async fn run_server(
|
||||
.route("/version", get(git_v))
|
||||
.route("/uptodate", get(is_up_to_date))
|
||||
.route("/ee_license", get(ee_license))
|
||||
.route("/openapi.yaml", get(openapi)),
|
||||
.route("/openapi.yaml", get(openapi))
|
||||
.route("/openapi.json", get(openapi_json)),
|
||||
)
|
||||
.fallback(static_assets::static_handler)
|
||||
.layer(middleware_stack);
|
||||
@@ -372,6 +368,10 @@ async fn openapi() -> &'static str {
|
||||
include_str!("../openapi-deref.yaml")
|
||||
}
|
||||
|
||||
async fn openapi_json() -> &'static str {
|
||||
include_str!("../openapi-deref.json")
|
||||
}
|
||||
|
||||
pub async fn migrate_db(db: &DB) -> anyhow::Result<()> {
|
||||
db::migrate(db).await?;
|
||||
Ok(())
|
||||
|
||||
@@ -316,6 +316,7 @@ pub struct ListScheduleQuery {
|
||||
pub per_page: Option<usize>,
|
||||
pub path: Option<String>,
|
||||
pub is_flow: Option<bool>,
|
||||
pub args: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_schedule(
|
||||
@@ -339,6 +340,9 @@ async fn list_schedule(
|
||||
if let Some(is_flow) = lsq.is_flow {
|
||||
sqlb.and_where_eq("is_flow", "?".bind(&is_flow));
|
||||
}
|
||||
if let Some(args) = &lsq.args {
|
||||
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
|
||||
}
|
||||
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
|
||||
let rows = sqlx::query_as::<_, Schedule>(&sql)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -577,14 +581,37 @@ async fn delete_schedule(
|
||||
let path = path.to_path();
|
||||
|
||||
clear_schedule(&mut tx, path, &w_id).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
if exists.is_none() {
|
||||
return Err(windmill_common::error::Error::NotFound(format!(
|
||||
"Schedule {} not found",
|
||||
path
|
||||
)));
|
||||
}
|
||||
|
||||
let del = sqlx::query_scalar!(
|
||||
"DELETE FROM schedule WHERE path = $1 AND workspace_id = $2 RETURNING 1",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
if del.is_none() {
|
||||
return Err(windmill_common::error::Error::NotAuthorized(format!(
|
||||
"Not authorized to delete schedule {}",
|
||||
path
|
||||
)));
|
||||
}
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
@@ -773,7 +800,7 @@ pub async fn clear_schedule<'c>(
|
||||
w_id: &str,
|
||||
) -> Result<()> {
|
||||
sqlx::query!(
|
||||
"DELETE FROM queue WHERE schedule_path = $1 AND running = false AND workspace_id = $2",
|
||||
"DELETE FROM queue WHERE schedule_path = $1 AND running = false AND workspace_id = $2 AND is_flow_step = false",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
|
||||
@@ -91,6 +91,8 @@ pub struct ScriptWDraft {
|
||||
pub concurrency_key: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub visible_to_runner_only: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
@@ -192,7 +194,8 @@ async fn list_scripts(
|
||||
"tag",
|
||||
"draft.path IS NOT NULL as has_draft",
|
||||
"draft_only",
|
||||
"ws_error_handler_muted"
|
||||
"ws_error_handler_muted",
|
||||
"no_main_func",
|
||||
])
|
||||
.left()
|
||||
.join("favorite")
|
||||
@@ -212,6 +215,10 @@ async fn list_scripts(
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
|
||||
if authed.is_operator || lq.hide_without_main.unwrap_or(false) {
|
||||
sqlb.and_where("o.no_main_func IS NOT TRUE");
|
||||
}
|
||||
|
||||
if lq.show_archived.unwrap_or(false) {
|
||||
sqlb.and_where_eq(
|
||||
"o.created_at",
|
||||
@@ -475,8 +482,8 @@ async fn create_script(
|
||||
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
|
||||
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, \
|
||||
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
|
||||
delete_after_use, timeout, concurrency_key, visible_to_runner_only) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)",
|
||||
delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)",
|
||||
&w_id,
|
||||
&hash.0,
|
||||
ns.path,
|
||||
@@ -505,6 +512,7 @@ async fn create_script(
|
||||
ns.timeout,
|
||||
ns.concurrency_key,
|
||||
ns.visible_to_runner_only,
|
||||
ns.no_main_func
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -724,7 +732,7 @@ async fn get_script_by_path_w_draft(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let script_o = sqlx::query_as::<_, ScriptWDraft>(
|
||||
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only FROM script LEFT JOIN draft ON
|
||||
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func FROM script LEFT JOIN draft ON
|
||||
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
|
||||
WHERE script.path = $1 AND script.workspace_id = $2 \
|
||||
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \
|
||||
|
||||
@@ -1337,6 +1337,25 @@ async fn accept_invite(
|
||||
.await?;
|
||||
|
||||
if let Some(r) = r {
|
||||
let already_in_workspace = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
|
||||
&nu.workspace_id,
|
||||
&email,
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if already_in_workspace {
|
||||
tx.commit().await?;
|
||||
return Ok((
|
||||
StatusCode::CREATED,
|
||||
format!(
|
||||
"user {} accepted invite to workspace {}",
|
||||
&email, nu.workspace_id
|
||||
),
|
||||
));
|
||||
}
|
||||
let username;
|
||||
(tx, username) = add_user_to_workspace(
|
||||
&nu.workspace_id,
|
||||
|
||||
@@ -722,6 +722,16 @@ async fn edit_auto_invite(
|
||||
|
||||
for user in users_to_auto_add.as_ref().unwrap() {
|
||||
auto_add_user(&user.email, &w_id, &operator, &mut tx).await?;
|
||||
send_email_if_possible(
|
||||
&format!("Added to Windmill's workspace: {w_id}"),
|
||||
&format!(
|
||||
"You have been granted access to Windmill's workspace {w_id} by {email}.
|
||||
|
||||
Access the workspace at {}/?workspace={w_id}",
|
||||
BASE_URL.read().await.clone()
|
||||
),
|
||||
&user.email,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
sqlx::query!(
|
||||
@@ -1936,6 +1946,22 @@ async fn invite_user(
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let already_in_workspace = sqlx::query_scalar!(
|
||||
"SELECT EXISTS (SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
|
||||
&w_id,
|
||||
nu.email
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if already_in_workspace {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"user with email {} already exists in workspace {}",
|
||||
nu.email, w_id
|
||||
)));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_invite
|
||||
(workspace_id, email, is_admin, operator)
|
||||
@@ -2046,6 +2072,14 @@ async fn add_user(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2",
|
||||
&w_id,
|
||||
nu.email
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query_as!(
|
||||
Group,
|
||||
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
|
||||
@@ -2183,6 +2217,8 @@ struct ScriptMetadata {
|
||||
pub restart_unless_cancelled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub visible_to_runner_only: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn is_none_or_false(val: &Option<bool>) -> bool {
|
||||
@@ -2459,6 +2495,7 @@ async fn tarball_workspace(
|
||||
delete_after_use: script.delete_after_use,
|
||||
restart_unless_cancelled: script.restart_unless_cancelled,
|
||||
visible_to_runner_only: script.visible_to_runner_only,
|
||||
no_main_func: script.no_main_func,
|
||||
};
|
||||
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
|
||||
archive
|
||||
|
||||
@@ -105,6 +105,8 @@ pub struct FlowValue {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
// Priority at the flow level
|
||||
pub priority: Option<i16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub concurrency_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
|
||||
@@ -243,6 +243,8 @@ pub struct CompletedJob {
|
||||
pub tag: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub priority: Option<i16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub labels: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl CompletedJob {
|
||||
|
||||
@@ -175,6 +175,8 @@ pub struct Script {
|
||||
pub concurrency_key: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub visible_to_runner_only: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
@@ -194,6 +196,8 @@ pub struct ListableScript {
|
||||
pub draft_only: Option<bool>,
|
||||
pub has_deploy_errors: bool,
|
||||
pub ws_error_handler_muted: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -251,6 +255,7 @@ pub struct NewScript {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub concurrency_key: Option<String>,
|
||||
pub visible_to_runner_only: Option<bool>,
|
||||
pub no_main_func: Option<bool>,
|
||||
}
|
||||
|
||||
fn lock_deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
@@ -320,6 +325,7 @@ pub struct ListScriptQuery {
|
||||
pub is_template: Option<bool>,
|
||||
pub kinds: Option<String>,
|
||||
pub starred_only: Option<bool>,
|
||||
pub hide_without_main: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn to_i64(s: &str) -> crate::error::Result<i64> {
|
||||
|
||||
@@ -53,7 +53,13 @@ pub fn initialize_tracing() {
|
||||
match json_fmt {
|
||||
true => ts_base.with(json_layer().flatten_event(true)).init(),
|
||||
false => ts_base
|
||||
.with(compact_layer().with_ansi(style.to_lowercase() != "never"))
|
||||
.with(
|
||||
compact_layer()
|
||||
.with_ansi(style.to_lowercase() != "never")
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_target(false),
|
||||
)
|
||||
.init(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,5 +28,6 @@ pub struct GitRepositorySettings {
|
||||
pub script_path: String,
|
||||
pub git_repo_resource_path: String,
|
||||
pub use_individual_branch: Option<bool>,
|
||||
pub group_by_folder: Option<bool>,
|
||||
pub exclude_types_override: Option<Vec<ObjectType>>,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ pub mod git_sync_ee;
|
||||
pub use git_sync_ee::handle_deployment_metadata;
|
||||
pub type DB = Pool<Postgres>;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DeployedObject {
|
||||
Script { hash: ScriptHash, path: String, parent_path: Option<String> },
|
||||
Flow { path: String, parent_path: Option<String> },
|
||||
@@ -47,7 +47,7 @@ impl DeployedObject {
|
||||
|
||||
pub fn get_ignore_regex_filter(&self) -> bool {
|
||||
match self {
|
||||
DeployedObject::User { .. } | DeployedObject::Group { .. } => true,
|
||||
Self::User { .. } | Self::Group { .. } | Self::ResourceType { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ use windmill_common::{
|
||||
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
|
||||
schedule::Schedule,
|
||||
scripts::{ScriptHash, ScriptLang},
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SYNC_EMAIL},
|
||||
worker::{to_raw_value, DEFAULT_TAGS_PER_WORKSPACE, NO_LOGS, WORKER_CONFIG},
|
||||
DB, METRICS_ENABLED,
|
||||
};
|
||||
@@ -612,8 +612,10 @@ pub async fn add_completed_job<
|
||||
}
|
||||
} else {
|
||||
if queued_job.schedule_path.is_some() && queued_job.script_path.is_some() {
|
||||
(skip_downstream_error_handlers, tx) = apply_schedule_handlers(
|
||||
tx,
|
||||
let schedule_handlers_tx: QueueTransaction<'_, R> =
|
||||
(rsmq.clone(), db.begin().await?).into();
|
||||
match apply_schedule_handlers(
|
||||
schedule_handlers_tx,
|
||||
db,
|
||||
queued_job.schedule_path.as_ref().unwrap(),
|
||||
queued_job.script_path.as_ref().unwrap(),
|
||||
@@ -624,21 +626,30 @@ pub async fn add_completed_job<
|
||||
queued_job.started_at.unwrap_or(chrono::Utc::now()),
|
||||
queued_job.priority,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if !queued_job.is_flow()
|
||||
&& queued_job.schedule_path.is_some()
|
||||
&& queued_job.script_path.is_some()
|
||||
{
|
||||
// script only
|
||||
tx = handle_maybe_scheduled_job(
|
||||
tx,
|
||||
db,
|
||||
queued_job.schedule_path.as_ref().unwrap(),
|
||||
queued_job.script_path.as_ref().unwrap(),
|
||||
&queued_job.workspace_id,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
Ok((skip, mut schedule_handlers_tx)) => {
|
||||
skip_downstream_error_handlers = skip;
|
||||
|
||||
if !queued_job.is_flow() {
|
||||
// script only
|
||||
schedule_handlers_tx = handle_maybe_scheduled_job(
|
||||
schedule_handlers_tx,
|
||||
db,
|
||||
queued_job.schedule_path.as_ref().unwrap(),
|
||||
queued_job.script_path.as_ref().unwrap(),
|
||||
&queued_job.workspace_id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
schedule_handlers_tx.commit().await?;
|
||||
}
|
||||
Err(err) => {
|
||||
skip_downstream_error_handlers = true;
|
||||
tracing::error!("Could not apply schedule handlers with error: {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if queued_job.concurrent_limit.is_some() {
|
||||
@@ -1220,13 +1231,13 @@ async fn apply_schedule_handlers<
|
||||
}
|
||||
Err(err) => {
|
||||
sqlx::query!(
|
||||
"UPDATE schedule SET enabled = false, error = $1 WHERE workspace_id = $2 AND path = $3",
|
||||
format!("Could not trigger error handler: {err}"),
|
||||
&schedule.workspace_id,
|
||||
&schedule.path
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
"UPDATE schedule SET enabled = false, error = $1 WHERE workspace_id = $2 AND path = $3",
|
||||
format!("Could not trigger error handler: {err}"),
|
||||
&schedule.workspace_id,
|
||||
&schedule.path
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
tracing::warn!(
|
||||
"Could not trigger error handler for {}: {}",
|
||||
schedule_path,
|
||||
@@ -1592,11 +1603,11 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
|
||||
))
|
||||
})?;
|
||||
|
||||
let min_started_at = sqlx::query_scalar!(
|
||||
let min_started_at = sqlx::query!(
|
||||
"SELECT COALESCE((SELECT MIN(started_at) as min_started_at
|
||||
FROM queue
|
||||
WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $2 AND canceled = false
|
||||
GROUP BY script_path), $3)",
|
||||
GROUP BY script_path), $3) as min_started_at, now() AS now",
|
||||
job_script_path,
|
||||
&pulled_job.workspace_id,
|
||||
completed_count.max_ended_at
|
||||
@@ -1651,18 +1662,24 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
|
||||
.await?;
|
||||
|
||||
// optimal scheduling is: 'older_job_in_concurrency_time_window_started_timestamp + script_avg_duration + concurrency_time_window_s'
|
||||
let estimated_next_schedule_timestamp = min_started_at.unwrap_or(pulled_job.scheduled_for)
|
||||
let estimated_next_schedule_timestamp = ((min_started_at
|
||||
.min_started_at
|
||||
.unwrap_or(min_started_at.now.unwrap()))
|
||||
+ Duration::try_seconds(avg_script_duration.map(i64::from).unwrap_or(0))
|
||||
.unwrap_or_default()
|
||||
.max(Duration::try_seconds(5).unwrap_or_default())
|
||||
+ Duration::try_seconds(i64::from(job_custom_concurrency_time_window_s))
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_default()
|
||||
.max(Duration::try_seconds(5).unwrap_or_default()))
|
||||
.max(min_started_at.now.unwrap() + Duration::try_seconds(10).unwrap_or_default());
|
||||
|
||||
tracing::info!("Job '{}' from path '{}' with concurrency key '{}' has reached its concurrency limit of {} jobs run in the last {} seconds. This job will be re-queued for next execution at {}",
|
||||
job_uuid, job_script_path, job_custom_concurrent_limit, job_concurrency_key, job_custom_concurrency_time_window_s, estimated_next_schedule_timestamp);
|
||||
job_uuid, job_script_path, job_concurrency_key, job_custom_concurrent_limit, job_custom_concurrency_time_window_s, estimated_next_schedule_timestamp);
|
||||
|
||||
let job_log_event = format!(
|
||||
"\nRe-scheduled job to {estimated_next_schedule_timestamp} due to concurrency limits with key {job_concurrency_key} and limit {job_custom_concurrent_limit} in the last {job_custom_concurrency_time_window_s} seconds",
|
||||
);
|
||||
let _ = append_logs(job_uuid, pulled_job.workspace_id, job_log_event, db);
|
||||
let _ = append_logs(job_uuid, pulled_job.workspace_id, job_log_event, db).await;
|
||||
if rsmq.is_some() {
|
||||
// if let Some(ref mut rsmq) = tx.rsmq {
|
||||
// if using redis, only one message at a time can be poped from the queue. Process only this message and move to the next elligible job
|
||||
@@ -1884,50 +1901,63 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
|
||||
}
|
||||
|
||||
async fn concurrency_key(db: &Pool<Postgres>, queued_job: &QueuedJob) -> String {
|
||||
if queued_job.is_flow() {
|
||||
// custom concurrency keys are not yet supported for flows
|
||||
queued_job.full_path_with_workspace()
|
||||
} else {
|
||||
let concurrency_key = sqlx::query_scalar!(
|
||||
"SELECT concurrency_key FROM script WHERE hash = $1",
|
||||
queued_job.script_hash.unwrap_or(ScriptHash(0)).0
|
||||
let r = if queued_job.is_flow() {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT value->>'concurrency_key' FROM flow WHERE path = $1 AND workspace_id = $2",
|
||||
queued_job.script_path,
|
||||
queued_job.workspace_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await;
|
||||
match concurrency_key {
|
||||
Ok(Some(custom_concurrency_key)) => {
|
||||
let workspaced =
|
||||
custom_concurrency_key.replace("$workspace", queued_job.workspace_id.as_str());
|
||||
if RE_ARG_TAG.is_match(&workspaced) {
|
||||
let mut interpolated = workspaced.clone();
|
||||
for cap in RE_ARG_TAG.captures_iter(&workspaced) {
|
||||
let arg_name = cap.get(1).unwrap().as_str();
|
||||
let arg_value = match queued_job.args.as_ref() {
|
||||
Some(Json(args_map_json)) => match args_map_json.get(arg_name) {
|
||||
Some(arg_value_raw) => {
|
||||
serde_json::to_string(arg_value_raw).unwrap_or_default()
|
||||
}
|
||||
None => "".to_string(),
|
||||
},
|
||||
.await
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT concurrency_key FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
queued_job.script_hash.unwrap_or(ScriptHash(0)).0,
|
||||
queued_job.workspace_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
};
|
||||
process_custom_concurrency_key(queued_job, r).await
|
||||
}
|
||||
|
||||
async fn process_custom_concurrency_key(
|
||||
queued_job: &QueuedJob,
|
||||
concurrency_key: Result<Option<String>, sqlx::Error>,
|
||||
) -> String {
|
||||
match concurrency_key {
|
||||
Ok(Some(custom_concurrency_key)) => {
|
||||
let workspaced =
|
||||
custom_concurrency_key.replace("$workspace", queued_job.workspace_id.as_str());
|
||||
if RE_ARG_TAG.is_match(&workspaced) {
|
||||
let mut interpolated = workspaced.clone();
|
||||
for cap in RE_ARG_TAG.captures_iter(&workspaced) {
|
||||
let arg_name = cap.get(1).unwrap().as_str();
|
||||
let arg_value = match queued_job.args.as_ref() {
|
||||
Some(Json(args_map_json)) => match args_map_json.get(arg_name) {
|
||||
Some(arg_value_raw) => {
|
||||
serde_json::to_string(arg_value_raw).unwrap_or_default()
|
||||
}
|
||||
None => "".to_string(),
|
||||
};
|
||||
interpolated = interpolated
|
||||
.replace(format!("$args[{}]", arg_name).as_str(), arg_value.as_str());
|
||||
}
|
||||
interpolated
|
||||
} else {
|
||||
workspaced
|
||||
},
|
||||
None => "".to_string(),
|
||||
};
|
||||
interpolated = interpolated
|
||||
.replace(format!("$args[{}]", arg_name).as_str(), arg_value.as_str());
|
||||
}
|
||||
interpolated
|
||||
} else {
|
||||
workspaced
|
||||
}
|
||||
Ok(None) => queued_job.full_path_with_workspace(),
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
"Unable to retrieve concurrency key for script {:?} | {:?}",
|
||||
queued_job.script_path,
|
||||
queued_job.script_hash
|
||||
);
|
||||
queued_job.full_path_with_workspace()
|
||||
}
|
||||
}
|
||||
Ok(None) => queued_job.full_path_with_workspace(),
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
"Unable to retrieve concurrency key for script {:?} | {:?}",
|
||||
queued_job.script_path,
|
||||
queued_job.script_hash
|
||||
);
|
||||
queued_job.full_path_with_workspace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2046,7 +2076,7 @@ async fn get_result_by_id_from_original_flow(
|
||||
json_path: Option<String>,
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
let flow_job = sqlx::query_as::<_, CompletedJob>(
|
||||
"SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2",
|
||||
"SELECT *, null as labels FROM completed_job WHERE id = $1 AND workspace_id = $2",
|
||||
)
|
||||
.bind(completed_flow_id)
|
||||
.bind(w_id)
|
||||
@@ -2641,26 +2671,70 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_super_admin {
|
||||
let user_usage = if let Some(user_usage) = user_usage {
|
||||
user_usage
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT usage.usage + 1 FROM usage
|
||||
WHERE is_workspace IS FALSE AND
|
||||
month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
|
||||
AND id = $1",
|
||||
if email != ERROR_HANDLER_USER_EMAIL
|
||||
&& email != "worker@windmill.dev"
|
||||
&& email != SUPERADMIN_SECRET_EMAIL
|
||||
&& email != SUPERADMIN_SYNC_EMAIL
|
||||
&& email != SUPERADMIN_NOTIFICATION_EMAIL
|
||||
{
|
||||
let user_usage = if let Some(user_usage) = user_usage {
|
||||
user_usage
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT usage.usage + 1 FROM usage
|
||||
WHERE is_workspace IS FALSE AND
|
||||
month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
|
||||
AND id = $1",
|
||||
email
|
||||
)
|
||||
.fetch_optional(_db)
|
||||
.await?
|
||||
.flatten()
|
||||
.unwrap_or(1)
|
||||
};
|
||||
|
||||
if user_usage > MAX_FREE_EXECS
|
||||
&& !matches!(job_payload, JobPayload::Dependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::FlowDependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::AppDependencies { .. })
|
||||
{
|
||||
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(_db)
|
||||
.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_optional(_db)
|
||||
.fetch_one(_db)
|
||||
.await?
|
||||
.flatten()
|
||||
.unwrap_or(1)
|
||||
};
|
||||
.unwrap_or(0);
|
||||
|
||||
let workspace_usage = if let Some(workspace_usage) = workspace_usage {
|
||||
workspace_usage
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
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."
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if workspace_id != "demo" {
|
||||
let workspace_usage = if let Some(workspace_usage) = workspace_usage {
|
||||
workspace_usage
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT usage.usage + 1 FROM usage
|
||||
WHERE is_workspace IS TRUE AND
|
||||
month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
|
||||
@@ -2671,79 +2745,45 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
.await?
|
||||
.flatten()
|
||||
.unwrap_or(1)
|
||||
};
|
||||
};
|
||||
|
||||
if user_usage > MAX_FREE_EXECS
|
||||
&& !matches!(job_payload, JobPayload::Dependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::FlowDependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::AppDependencies { .. })
|
||||
{
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"User {email} has exceeded the free usage limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
if workspace_id != "demo"
|
||||
&& workspace_usage > MAX_FREE_EXECS
|
||||
&& !matches!(job_payload, JobPayload::Dependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::FlowDependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::AppDependencies { .. })
|
||||
{
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Workspace {workspace_id} 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(_db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
if workspace_usage > MAX_FREE_EXECS
|
||||
&& !matches!(job_payload, JobPayload::Dependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::FlowDependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::AppDependencies { .. })
|
||||
{
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Workspace {workspace_id} has exceeded the free usage limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
|
||||
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 in_queue_workspace = sqlx::query_scalar!(
|
||||
"SELECT COUNT(id) FROM queue WHERE workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let in_queue_workspace = sqlx::query_scalar!(
|
||||
"SELECT COUNT(id) FROM queue WHERE workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
if in_queue_workspace > MAX_FREE_EXECS.into() {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Workspace {workspace_id} has exceeded the jobs in queue limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
|
||||
if in_queue_workspace > MAX_FREE_EXECS.into() {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Workspace {workspace_id} has exceeded the jobs in queue limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
let concurrent_runs_workspace = sqlx::query_scalar!(
|
||||
"SELECT COUNT(id) FROM queue WHERE running = true AND workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let concurrent_runs = sqlx::query_scalar!(
|
||||
"SELECT COUNT(id) FROM queue WHERE running = true AND email = $1",
|
||||
email
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.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."
|
||||
)));
|
||||
}
|
||||
|
||||
let concurrent_runs_workspace = sqlx::query_scalar!(
|
||||
"SELECT COUNT(id) FROM queue WHERE running = true AND workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
if concurrent_runs_workspace > MAX_FREE_CONCURRENT_RUNS.into() {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Workspace {workspace_id} has exceeded the concurrent runs limit of {MAX_FREE_CONCURRENT_RUNS} that applies outside of premium workspaces."
|
||||
)));
|
||||
if concurrent_runs_workspace > MAX_FREE_CONCURRENT_RUNS.into() {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Workspace {workspace_id} has exceeded the concurrent runs limit of {MAX_FREE_CONCURRENT_RUNS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3000,6 +3040,7 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
skip_expr: None,
|
||||
cache_ttl: cache_ttl.map(|val| val as u32),
|
||||
early_return: None,
|
||||
concurrency_key: None,
|
||||
priority: priority,
|
||||
};
|
||||
(
|
||||
@@ -3428,7 +3469,7 @@ async fn restarted_flows_resolution(
|
||||
Error,
|
||||
> {
|
||||
let completed_job = sqlx::query_as::<_, CompletedJob>(
|
||||
"SELECT * FROM completed_job WHERE id = $1 and workspace_id = $2",
|
||||
"SELECT *, null as labels FROM completed_job WHERE id = $1 and workspace_id = $2",
|
||||
)
|
||||
.bind(completed_flow_id)
|
||||
.bind(workspace_id)
|
||||
|
||||
@@ -14,7 +14,7 @@ prometheus = ["dep:prometheus", "windmill-common/prometheus"]
|
||||
enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util", "dep:openidconnect"]
|
||||
benchmark = ["windmill-queue/benchmark"]
|
||||
flamegraph = []
|
||||
parquet = ["windmill-common/parquet", "dep:object_store", "dep:tar", "dep:crc"]
|
||||
parquet = ["windmill-common/parquet", "dep:object_store", "dep:tar"]
|
||||
flow_testing = []
|
||||
|
||||
[dependencies]
|
||||
@@ -80,7 +80,6 @@ tokio-util = { workspace = true, optional = true }
|
||||
openidconnect = { workspace = true, optional = true}
|
||||
tar = { workspace = true, optional = true}
|
||||
object_store = { workspace = true, optional = true}
|
||||
crc = { workspace = true, optional = true}
|
||||
|
||||
[build-dependencies]
|
||||
deno_fetch.workspace = true
|
||||
|
||||
@@ -453,6 +453,26 @@ async fn get_mem_peak(pid: Option<u32>, nsjail: bool) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sizeof_val(v: &serde_json::Value) -> usize {
|
||||
std::mem::size_of::<serde_json::Value>()
|
||||
+ match v {
|
||||
serde_json::Value::Null => 0,
|
||||
serde_json::Value::Bool(_) => 0,
|
||||
serde_json::Value::Number(_) => 4, // Incorrect if arbitrary_precision is enabled. oh well
|
||||
serde_json::Value::String(s) => s.capacity(),
|
||||
serde_json::Value::Array(a) => a.iter().map(sizeof_val).sum(),
|
||||
serde_json::Value::Object(o) => o
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
std::mem::size_of::<String>()
|
||||
+ k.capacity()
|
||||
+ sizeof_val(v)
|
||||
+ std::mem::size_of::<usize>() * 3
|
||||
})
|
||||
.sum(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_future_with_polling_update_job_poller<Fut, T>(
|
||||
job_id: Uuid,
|
||||
timeout: Option<i32>,
|
||||
@@ -645,12 +665,20 @@ async fn compact_logs(
|
||||
let (excess_prev_logs, current_logs) = if extra_split {
|
||||
let split_idx = nlogs
|
||||
.char_indices()
|
||||
.nth(excess_size_modulo)
|
||||
.nth(excess_size)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0);
|
||||
let (excess_prev_logs, current_logs) = nlogs.split_at(split_idx);
|
||||
// tracing::error!(
|
||||
// "{:?} {:?} {} {}",
|
||||
// excess_prev_logs.lines().last(),
|
||||
// current_logs.lines().next(),
|
||||
// split_idx,
|
||||
// excess_size_modulo
|
||||
// );
|
||||
(excess_prev_logs, current_logs.to_string())
|
||||
} else {
|
||||
// tracing::error!("{:?}", nlogs.lines().last());
|
||||
("", nlogs.to_string())
|
||||
};
|
||||
|
||||
@@ -668,7 +696,7 @@ async fn compact_logs(
|
||||
|
||||
let mut new_current_logs = match compact_kind {
|
||||
CompactLogs::NoS3 => format!("[windmill] worker {worker_name}: Logs length has exceeded a threshold\n[windmill] Previous logs have been saved to disk at {path}, add object storage in the instance settings to save it on distributed storage and allow direct download from Windmill\n"),
|
||||
CompactLogs::S3 => format!("[windmill] worker {worker_name}: Logs length has exceeded a threshold\n[windmill] Previous logs have been saved to object storage at {path}\n[windmill] Download logs in expanded drawer to get full logs.\n"),
|
||||
CompactLogs::S3 => format!("[windmill] Previous logs have been saved to object storage at {path}\n"),
|
||||
CompactLogs::NotEE => format!("[windmill] worker {worker_name}: Logs length has exceeded a threshold\n[windmill] Previous logs have been saved to disk at {path}\n[windmill] Upgrade to EE and add object storage to save it persistentely on distributed storage and allow direct download from Windmill\n"),
|
||||
};
|
||||
new_current_logs.push_str(¤t_logs);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use crate::{PIP_CACHE_DIR, ROOT_CACHE_DIR};
|
||||
use crate::PIP_CACHE_DIR;
|
||||
|
||||
// #[cfg(feature = "enterprise")]
|
||||
// use rand::Rng;
|
||||
@@ -72,45 +72,57 @@ pub async fn build_tar_and_push(
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
|
||||
pub async fn attempt_fetch_bytes(
|
||||
client: Arc<dyn ObjectStore>,
|
||||
path: &str,
|
||||
) -> error::Result<bytes::Bytes> {
|
||||
use object_store::path::Path;
|
||||
|
||||
let object = client.get(&Path::from(path.clone())).await;
|
||||
if let Err(e) = object {
|
||||
tracing::info!(
|
||||
"Failed to pull bytes from object store at path {path}. Error: {:?}",
|
||||
e
|
||||
);
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"Failed to pull bytes from object store: {path}"
|
||||
)));
|
||||
}
|
||||
|
||||
let bytes = object.unwrap().bytes().await;
|
||||
if bytes.is_err() {
|
||||
tracing::info!(
|
||||
"Failed to read bytes from object store: {path}. Error: {:?}",
|
||||
bytes.err()
|
||||
);
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"Failed to read bytes from object store: {path}"
|
||||
)));
|
||||
}
|
||||
let bytes = bytes.unwrap();
|
||||
|
||||
tracing::info!("{path} len: {}", bytes.len());
|
||||
|
||||
if bytes.len() == 0 {
|
||||
tracing::info!("object {path} not found in bucket, bytes empty",);
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"object {path} does not exist in bucket"
|
||||
)));
|
||||
}
|
||||
|
||||
return Ok(bytes);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub async fn pull_from_tar(client: Arc<dyn ObjectStore>, folder: String) -> error::Result<()> {
|
||||
use object_store::path::Path;
|
||||
use tokio::fs::metadata;
|
||||
let folder_name = folder.split("/").last().unwrap();
|
||||
|
||||
tracing::info!("Attempting to pull piptar {folder_name} from bucket");
|
||||
|
||||
let start = Instant::now();
|
||||
let tar_path = format!("tar/pip/{folder_name}.tar");
|
||||
let bytes = attempt_fetch_bytes(client, &tar_path).await?;
|
||||
|
||||
let object = client
|
||||
.get(&Path::from(format!("tar/pip/{folder_name}.tar")))
|
||||
.await;
|
||||
if let Err(e) = object {
|
||||
tracing::info!("Failed to pull tar from s3: {tar_path}. Error: {:?}", e);
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"Failed to pull tar from s3: {tar_path}"
|
||||
)));
|
||||
}
|
||||
|
||||
let bytes = object.unwrap().bytes().await.unwrap();
|
||||
tracing::info!(
|
||||
"{tar_path} checksum: {}, len: {}",
|
||||
X25.checksum(&bytes),
|
||||
bytes.len()
|
||||
);
|
||||
|
||||
if bytes.len() == 0 {
|
||||
tracing::info!(
|
||||
"piptar {folder_name} not found in bucket. Took {:?}ms",
|
||||
start.elapsed().as_millis()
|
||||
);
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"tar {folder_name} does not exist in bucket"
|
||||
)));
|
||||
}
|
||||
// tracing::info!("B: {target} {folder}");
|
||||
|
||||
extract_pip_tar(bytes, &folder).await.map_err(|e| {
|
||||
|
||||
@@ -33,6 +33,111 @@ lazy_static::lazy_static! {
|
||||
static ref GO_PATH: String = std::env::var("GO_PATH").unwrap_or_else(|_| "/usr/bin/go".to_string());
|
||||
}
|
||||
|
||||
pub async fn save_cache(
|
||||
bin_path: &str,
|
||||
job_dir: &str,
|
||||
_hash: &str,
|
||||
job: &QueuedJob,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
) -> windmill_common::error::Result<()> {
|
||||
let job_main_path = format!("{job_dir}/main");
|
||||
let mut _cached_to_s3 = false;
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
{
|
||||
use object_store::path::Path;
|
||||
|
||||
let hash_path = hash_to_os_path(_hash);
|
||||
if let Err(e) = os
|
||||
.put(
|
||||
&Path::from(hash_path.clone()),
|
||||
bytes::Bytes::from(std::fs::read(&job_main_path)?),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to put go bin to object store: {hash_path}. Error: {:?}",
|
||||
e
|
||||
);
|
||||
} else {
|
||||
_cached_to_s3 = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !*CLOUD_HOSTED {
|
||||
tokio::fs::copy(&job_main_path, bin_path).await?;
|
||||
append_logs(
|
||||
job.id.clone(),
|
||||
job.workspace_id.to_string(),
|
||||
format!(
|
||||
"\nwrite cached binary: {} (backed by object store: {_cached_to_s3})\n",
|
||||
bin_path
|
||||
),
|
||||
db,
|
||||
)
|
||||
.await;
|
||||
} else if _cached_to_s3 {
|
||||
append_logs(
|
||||
job.id.clone(),
|
||||
job.workspace_id.to_string(),
|
||||
format!("write cached binary to object store {}\n", bin_path),
|
||||
db,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
async fn write_binary_file(main_path: &str, byts: &mut bytes::Bytes) -> error::Result<()> {
|
||||
use std::fs::Permissions;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let mut file = File::create(main_path).await?;
|
||||
file.write_buf(byts).await?;
|
||||
file.set_permissions(Permissions::from_mode(0o755)).await?;
|
||||
file.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
fn hash_to_os_path(hash: &str) -> String {
|
||||
format!("gobin/{hash}")
|
||||
}
|
||||
|
||||
async fn load_cache(bin_path: &str, _hash: &str) -> (bool, String) {
|
||||
if tokio::fs::metadata(&bin_path).await.is_ok() {
|
||||
(true, format!("loaded bin from local cache: {}\n", bin_path))
|
||||
} else {
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
{
|
||||
use crate::global_cache::attempt_fetch_bytes;
|
||||
|
||||
if let Ok(mut x) = attempt_fetch_bytes(os, &hash_to_os_path(_hash)).await {
|
||||
if let Err(e) = write_binary_file(bin_path, &mut x).await {
|
||||
tracing::error!("could not write binary file: {e:?}");
|
||||
return (
|
||||
false,
|
||||
"error writing binary file from object store".to_string(),
|
||||
);
|
||||
}
|
||||
tracing::info!("loaded bin from object store {}", bin_path);
|
||||
return (true, format!("loaded bin from object store {}", bin_path));
|
||||
}
|
||||
}
|
||||
(false, "".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn handle_go_job(
|
||||
mem_peak: &mut i32,
|
||||
@@ -50,21 +155,19 @@ pub async fn handle_go_job(
|
||||
) -> Result<Box<RawValue>, Error> {
|
||||
//go does not like executing modules at temp root
|
||||
let job_dir = &format!("{job_dir}/go");
|
||||
let bin_path = format!(
|
||||
"{}/{}",
|
||||
GO_BIN_CACHE_DIR,
|
||||
calculate_hash(&format!(
|
||||
"{}{}",
|
||||
inner_content,
|
||||
requirements_o
|
||||
.as_ref()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default()
|
||||
))
|
||||
);
|
||||
let bin_exists = !*CLOUD_HOSTED && tokio::fs::metadata(&bin_path).await.is_ok();
|
||||
let hash = calculate_hash(&format!(
|
||||
"{}{}v2",
|
||||
inner_content,
|
||||
requirements_o
|
||||
.as_ref()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default()
|
||||
));
|
||||
let bin_path = format!("{}/{hash}", GO_BIN_CACHE_DIR,);
|
||||
|
||||
let (skip_go_mod, skip_tidy) = if bin_exists {
|
||||
let (cache, cache_logs) = load_cache(&bin_path, &hash).await;
|
||||
|
||||
let (skip_go_mod, skip_tidy) = if cache {
|
||||
create_dir(job_dir).await?;
|
||||
(true, true)
|
||||
} else if let Some(requirements) = requirements_o {
|
||||
@@ -73,8 +176,8 @@ pub async fn handle_go_job(
|
||||
(false, false)
|
||||
};
|
||||
|
||||
if !bin_exists {
|
||||
let logs1 = "\n\n--- GO DEPENDENCIES SETUP ---\n".to_string();
|
||||
let cache_logs = if !cache {
|
||||
let logs1 = format!("{cache_logs}\n\n--- GO DEPENDENCIES SETUP ---\n");
|
||||
append_logs(job.id.clone(), job.workspace_id.to_string(), logs1, db).await;
|
||||
|
||||
install_go_dependencies(
|
||||
@@ -92,9 +195,6 @@ pub async fn handle_go_job(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let logs2 = "\n\n--- GO CODE EXECUTION ---\n".to_string();
|
||||
append_logs(job.id.clone(), job.workspace_id.to_string(), logs2, db).await;
|
||||
|
||||
create_args_and_out_file(client, job, job_dir, db).await?;
|
||||
{
|
||||
let sig = windmill_parser_go::parse_go_sig(&inner_content)?;
|
||||
@@ -209,33 +309,24 @@ func Run(req Req) (interface{{}}, error){{
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !*CLOUD_HOSTED {
|
||||
create_dir(&bin_path).await?;
|
||||
let target = format!("{bin_path}/main");
|
||||
tokio::fs::copy(format!("{job_dir}/main"), &target).await?;
|
||||
append_logs(
|
||||
job.id.clone(),
|
||||
job.workspace_id.to_string(),
|
||||
format!("write cached binary: {}\n", bin_path),
|
||||
db,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = save_cache(&bin_path, &job_dir, &hash, &job, db).await {
|
||||
tracing::error!("could not save {bin_path} to go cache: {e:?}");
|
||||
}
|
||||
"".to_string()
|
||||
} else {
|
||||
let path = format!("{bin_path}/main");
|
||||
let mut logs2 = "".to_string();
|
||||
logs2.push_str(&format!("found cached binary: {path}\n"));
|
||||
let target = format!("{job_dir}/main");
|
||||
tokio::fs::symlink(&path, &target).await.map_err(|e| {
|
||||
tokio::fs::symlink(&bin_path, &target).await.map_err(|e| {
|
||||
Error::ExecutionErr(format!(
|
||||
"could not copy cached binary from {path} to {job_dir}/main: {e:?}"
|
||||
"could not copy cached binary from {bin_path} to {job_dir}/main: {e:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
logs2.push_str("\n\n--- GO CODE EXECUTION ---\n");
|
||||
append_logs(job.id.clone(), job.workspace_id.to_string(), logs2, db).await;
|
||||
create_args_and_out_file(client, job, job_dir, db).await?;
|
||||
}
|
||||
cache_logs
|
||||
};
|
||||
|
||||
let logs2 = format!("{cache_logs}\n\n--- GO CODE EXECUTION ---\n");
|
||||
append_logs(job.id.clone(), job.workspace_id.to_string(), logs2, db).await;
|
||||
|
||||
let client = &client.get_authed().await;
|
||||
|
||||
@@ -303,6 +394,13 @@ func Run(req Req) (interface{{}}, error){{
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if cache && *CLOUD_HOSTED {
|
||||
//do not keep the binary in the cache if it is cloud hosted to avoid filling up the disk
|
||||
if let Err(e) = tokio::fs::remove_file(&bin_path).await {
|
||||
tracing::error!("could not remove {bin_path} from go cache: {e:?}");
|
||||
}
|
||||
}
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
||||
use futures::TryFutureExt;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde_json::value::RawValue;
|
||||
use serde_json::{Map, Value};
|
||||
@@ -12,7 +13,7 @@ use windmill_common::error::{self, Error};
|
||||
use windmill_common::worker::to_raw_value;
|
||||
use windmill_common::{error::to_anyhow, jobs::QueuedJob};
|
||||
use windmill_parser_sql::{parse_db_resource, parse_mssql_sig};
|
||||
use windmill_queue::CanceledBy;
|
||||
use windmill_queue::{append_logs, CanceledBy};
|
||||
|
||||
use crate::common::{build_args_values, run_future_with_polling_update_job_poller};
|
||||
use crate::AuthedClientBackgroundTask;
|
||||
@@ -27,6 +28,10 @@ struct MssqlDatabase {
|
||||
instance_name: Option<String>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE_MSSQL_READONLY_INTENT: Regex = Regex::new(r#"(?mi)^-- ApplicationIntent=ReadOnly *(?:\r|\n|$)"#).unwrap();
|
||||
}
|
||||
|
||||
pub async fn do_mssql(
|
||||
job: &QueuedJob,
|
||||
client: &AuthedClientBackgroundTask,
|
||||
@@ -74,6 +79,14 @@ pub async fn do_mssql(
|
||||
config.port(port);
|
||||
}
|
||||
|
||||
let readonly_intent = RE_MSSQL_READONLY_INTENT.is_match(query);
|
||||
config.readonly(readonly_intent);
|
||||
|
||||
if readonly_intent {
|
||||
let logs = format!("\nSetting ApplicationIntent to ReadOnly");
|
||||
append_logs(job.id, job.workspace_id.clone(), logs, db).await;
|
||||
}
|
||||
|
||||
// Using SQL Server authentication.
|
||||
config.authentication(AuthMethod::sql_server(database.user, database.password));
|
||||
config.trust_cert(); // on production, it is not a good idea to do this
|
||||
|
||||
@@ -145,7 +145,16 @@ pub async fn do_mysql(
|
||||
Value::Number(n) if n.is_u64() && arg_t == "uint" => {
|
||||
mysql_async::Value::UInt(n.as_u64().unwrap())
|
||||
}
|
||||
Value::Number(n) if n.is_f64() && arg_t == "real" => n.as_f64().into(),
|
||||
Value::Number(n)
|
||||
if n.is_f64() && (arg_t == "real" || arg_t == "dec" || arg_t == "fixed") =>
|
||||
{
|
||||
n.as_f64().unwrap().into()
|
||||
}
|
||||
Value::Number(n)
|
||||
if n.is_i64() && (arg_t == "real" || arg_t == "dec" || arg_t == "fixed") =>
|
||||
{
|
||||
(n.as_i64().unwrap() as f64).into()
|
||||
}
|
||||
value @ _ => {
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
"Unsupported type in query: {:?} and signature {arg_t:?}",
|
||||
@@ -272,6 +281,9 @@ fn convert_mysql_value_to_json(v: mysql_async::Value, c: ColumnType) -> serde_js
|
||||
ColumnType::MYSQL_TYPE_FLOAT | ColumnType::MYSQL_TYPE_DOUBLE => {
|
||||
conversion_error(f64::from_value_opt(v))
|
||||
}
|
||||
ColumnType::MYSQL_TYPE_DECIMAL | ColumnType::MYSQL_TYPE_NEWDECIMAL => {
|
||||
conversion_error(rust_decimal::Decimal::from_value_opt(v))
|
||||
}
|
||||
ColumnType::MYSQL_TYPE_TINY
|
||||
| ColumnType::MYSQL_TYPE_SHORT
|
||||
| ColumnType::MYSQL_TYPE_LONG
|
||||
|
||||
@@ -31,8 +31,8 @@ use windmill_parser::Typ;
|
||||
use windmill_parser_sql::{parse_db_resource, parse_pgsql_sig};
|
||||
use windmill_queue::CanceledBy;
|
||||
|
||||
use crate::common::{build_args_values, run_future_with_polling_update_job_poller};
|
||||
use crate::AuthedClientBackgroundTask;
|
||||
use crate::common::{build_args_values, run_future_with_polling_update_job_poller, sizeof_val};
|
||||
use crate::{AuthedClientBackgroundTask, MAX_RESULT_SIZE};
|
||||
use bytes::{Buf, BytesMut};
|
||||
use lazy_static::lazy_static;
|
||||
use urlencoding::encode;
|
||||
@@ -230,15 +230,32 @@ pub async fn do_postgresql(
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|x: Row| postgres_row_to_json_value(x))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut siz = 0;
|
||||
let mut res: Vec<serde_json::Value> = vec![];
|
||||
for row in rows.into_iter() {
|
||||
let r = postgres_row_to_json_value(row);
|
||||
if let Ok(v) = r.as_ref() {
|
||||
let size = sizeof_val(v);
|
||||
siz += size;
|
||||
}
|
||||
if *CLOUD_HOSTED && siz > MAX_RESULT_SIZE * 4 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Query result too large for cloud (size = {} > {})",
|
||||
siz,
|
||||
MAX_RESULT_SIZE & 4
|
||||
));
|
||||
}
|
||||
if let Ok(v) = r {
|
||||
res.push(v);
|
||||
} else {
|
||||
return Err(to_anyhow(r.err().unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
Ok((res, siz))
|
||||
};
|
||||
|
||||
let result = run_future_with_polling_update_job_poller(
|
||||
let (result, size) = run_future_with_polling_update_job_poller(
|
||||
job.id,
|
||||
job.timeout,
|
||||
db,
|
||||
@@ -250,6 +267,8 @@ pub async fn do_postgresql(
|
||||
)
|
||||
.await?;
|
||||
|
||||
*mem_peak = size as i32;
|
||||
|
||||
RUNNING.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
if let Some(handle) = handle {
|
||||
@@ -323,6 +342,7 @@ enum PgType {
|
||||
Timestamp(chrono::NaiveDateTime),
|
||||
None(Option<bool>),
|
||||
Array(Vec<PgType>),
|
||||
Json(serde_json::Value),
|
||||
}
|
||||
|
||||
impl ToSql for PgType {
|
||||
@@ -348,6 +368,7 @@ impl ToSql for PgType {
|
||||
PgType::Timestamp(ref val) => val.to_sql(ty, out),
|
||||
PgType::None(ref val) => val.to_sql(ty, out),
|
||||
PgType::Array(ref val) => val.to_sql(ty, out),
|
||||
PgType::Json(ref val) => val.to_sql(ty, out),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,7 +431,7 @@ fn convert_val(value: &Value, arg_t: &String, typ: &Typ) -> windmill_common::err
|
||||
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%3fZ").unwrap_or_default();
|
||||
Ok(PgType::Date(date))
|
||||
}
|
||||
Value::String(s) if arg_t == "time" => {
|
||||
Value::String(s) if arg_t == "time" || arg_t == "timetz" => {
|
||||
let time =
|
||||
chrono::NaiveTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%3fZ").unwrap_or_default();
|
||||
Ok(PgType::Time(time))
|
||||
@@ -420,6 +441,7 @@ fn convert_val(value: &Value, arg_t: &String, typ: &Typ) -> windmill_common::err
|
||||
.unwrap_or_default();
|
||||
Ok(PgType::Timestamp(datetime))
|
||||
}
|
||||
Value::Object(_) => Ok(PgType::Json(value.clone())),
|
||||
Value::String(s) => Ok(PgType::String(s.clone())),
|
||||
_ => Err(Error::ExecutionErr(format!(
|
||||
"Unsupported type in query: {:?} and signature {arg_t:?}",
|
||||
|
||||
@@ -391,8 +391,6 @@ mount {{
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job.id,
|
||||
workspace_id = %job.workspace_id,
|
||||
"started python code execution {}",
|
||||
job.id
|
||||
@@ -614,7 +612,7 @@ async fn replace_pip_secret(
|
||||
}
|
||||
let secret = get_secret_value_as_admin(db, w_id, variable).await?;
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
worker = %worker_name,
|
||||
job_id = %job_id,
|
||||
workspace_id = %w_id,
|
||||
"found secret variable in pip requirements: {}",
|
||||
@@ -878,16 +876,12 @@ pub async fn handle_python_reqs(
|
||||
append_logs(job_id.clone(), w_id.to_string(), logs1, db).await;
|
||||
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job_id,
|
||||
workspace_id = %w_id,
|
||||
"started setup python dependencies"
|
||||
);
|
||||
|
||||
let child = if !*DISABLE_NSJAIL {
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job_id,
|
||||
workspace_id = %w_id,
|
||||
"starting nsjail"
|
||||
);
|
||||
@@ -992,8 +986,6 @@ pub async fn handle_python_reqs(
|
||||
)
|
||||
.await;
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job_id,
|
||||
workspace_id = %w_id,
|
||||
is_ok = child.is_ok(),
|
||||
"finished setting up python dependencies {}",
|
||||
|
||||
@@ -13,6 +13,7 @@ use prometheus::{
|
||||
core::{AtomicI64, GenericGauge},
|
||||
IntCounter,
|
||||
};
|
||||
use tracing::Instrument;
|
||||
#[cfg(feature = "prometheus")]
|
||||
use windmill_common::METRICS_DEBUG_ENABLED;
|
||||
#[cfg(feature = "prometheus")]
|
||||
@@ -88,8 +89,8 @@ use crate::{
|
||||
bash_executor::{handle_bash_job, handle_powershell_job, ANSI_ESCAPE_RE},
|
||||
bun_executor::{gen_lockfile, get_trusted_deps, handle_bun_job},
|
||||
common::{
|
||||
build_args_map, get_cached_resource_value_if_valid, hash_args, read_result, save_in_cache,
|
||||
write_file, NO_LOGS_AT_ALL, SLOW_LOGS,
|
||||
build_args_map, get_cached_resource_value_if_valid, get_reserved_variables, hash_args,
|
||||
read_result, save_in_cache, write_file, NO_LOGS_AT_ALL, SLOW_LOGS,
|
||||
},
|
||||
deno_executor::{generate_deno_lock, handle_deno_job},
|
||||
go_executor::{handle_go_job, install_go_dependencies},
|
||||
@@ -584,6 +585,7 @@ impl JobCompletedSender {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "worker", level = "info", skip_all, fields(worker = %worker_name))]
|
||||
pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
|
||||
db: &Pool<Postgres>,
|
||||
worker_instance: &str,
|
||||
@@ -608,7 +610,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
let start_time = Instant::now();
|
||||
|
||||
let worker_dir = format!("{TMP_DIR}/{worker_name}");
|
||||
tracing::debug!(worker_dir = %worker_dir, worker_name = %worker_name, "Creating worker dir");
|
||||
tracing::debug!(worker_dir = %worker_dir, "Creating worker dir");
|
||||
|
||||
if let Some(ref netrc) = *NETRC {
|
||||
tracing::info!("Writing netrc at {}/.netrc", HOME_ENV.as_str());
|
||||
@@ -1022,7 +1024,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
let worker_name2 = worker_name.clone();
|
||||
let killpill_tx2 = killpill_tx.clone();
|
||||
let job_completed_sender = job_completed_tx.0.clone();
|
||||
let send_result = tokio::spawn(async move {
|
||||
let send_result = tokio::spawn((async move {
|
||||
while let Some(sr) = job_completed_rx.recv().await {
|
||||
match sr {
|
||||
SendResult::JobCompleted(jc) => {
|
||||
@@ -1162,6 +1164,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
token,
|
||||
} => {
|
||||
// let r;
|
||||
tracing::info!(parent_flow = %flow, "updating flow status");
|
||||
if let Err(e) = update_flow_status_after_job_completion(
|
||||
&db2,
|
||||
&AuthedClient {
|
||||
@@ -1185,7 +1188,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error updating flow status after job completion: {e}");
|
||||
tracing::error!("Error updating flow status after job completion for {flow} on {worker_name2}: {e}");
|
||||
}
|
||||
}
|
||||
SendResult::Kill => {
|
||||
@@ -1199,7 +1202,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
tracing::info!("finished processing all completed jobs");
|
||||
});
|
||||
}).instrument(tracing::Span::current()));
|
||||
|
||||
let mut last_executed_job: Option<Instant> = None;
|
||||
let mut last_checked_suspended = Instant::now();
|
||||
@@ -1213,7 +1216,11 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
let vacuum_shift = rand::thread_rng().gen_range(0..VACUUM_PERIOD);
|
||||
|
||||
IS_READY.store(true, Ordering::Relaxed);
|
||||
tracing::info!(worker = %worker_name, "listening for jobs, WORKER_GROUP: {}, config: {:?}", *WORKER_GROUP, WORKER_CONFIG.read().await);
|
||||
tracing::info!(
|
||||
"listening for jobs, WORKER_GROUP: {}, config: {:?}",
|
||||
*WORKER_GROUP,
|
||||
WORKER_CONFIG.read().await
|
||||
);
|
||||
|
||||
// (dedi_path, dedicated_worker_tx, dedicated_worker_handle)
|
||||
// Option<Sender<Arc<QueuedJob>>>,
|
||||
@@ -1337,6 +1344,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
None
|
||||
}
|
||||
};
|
||||
let mut suspend_first_success = false;
|
||||
|
||||
loop {
|
||||
#[cfg(feature = "benchmark")]
|
||||
@@ -1376,24 +1384,27 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
tracing::error!("failed to update worker ping, exiting: {}", e);
|
||||
killpill_tx.send(()).unwrap_or_default();
|
||||
}
|
||||
tracing::debug!("set last ping");
|
||||
tracing::info!("updating last ping");
|
||||
|
||||
last_ping = Instant::now();
|
||||
}
|
||||
|
||||
if (jobs_executed as u32 + vacuum_shift) % VACUUM_PERIOD == 0 {
|
||||
let db2 = db.clone();
|
||||
let worker_name2 = worker_name.clone();
|
||||
tokio::task::spawn(async move {
|
||||
tracing::info!(worker = %worker_name2, "vacuuming queue and completed_job");
|
||||
if let Err(e) = sqlx::query!("VACUUM (skip_locked) queue")
|
||||
.execute(&db2)
|
||||
.await
|
||||
{
|
||||
tracing::error!(worker = %worker_name2, "failed to vacuum queue: {}", e);
|
||||
}
|
||||
tracing::info!(worker = %worker_name2, "vacuumed queue and completed_job");
|
||||
});
|
||||
let current_span = tracing::Span::current();
|
||||
tokio::task::spawn(
|
||||
(async move {
|
||||
tracing::info!("vacuuming queue and completed_job");
|
||||
if let Err(e) = sqlx::query!("VACUUM (skip_locked) queue")
|
||||
.execute(&db2)
|
||||
.await
|
||||
{
|
||||
tracing::error!("failed to vacuum queue: {}", e);
|
||||
}
|
||||
tracing::info!("vacuumed queue and completed_job");
|
||||
})
|
||||
.instrument(current_span),
|
||||
);
|
||||
jobs_executed += 1;
|
||||
}
|
||||
|
||||
@@ -1426,19 +1437,20 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
r
|
||||
}
|
||||
},
|
||||
(job, timer) = async {
|
||||
(job, timer, suspend_first) = async {
|
||||
let pull_time = Instant::now();
|
||||
let suspend_first = if last_checked_suspended.elapsed().as_secs() > 3 {
|
||||
let suspend_first = if suspend_first_success || last_checked_suspended.elapsed().as_secs() > 3 {
|
||||
last_checked_suspended = Instant::now();
|
||||
true
|
||||
} else { false };
|
||||
pull(&db, rsmq.clone(), suspend_first).map(|x| (x, pull_time)).await
|
||||
pull(&db, rsmq.clone(), suspend_first).map(|x| (x, pull_time, suspend_first)).await
|
||||
} => {
|
||||
add_time!(timing, loop_start, "post pull");
|
||||
// tracing::debug!("pulled job: {:?}", job.as_ref().ok().and_then(|x| x.as_ref().map(|y| y.id)));
|
||||
let duration_pull_s = timer.elapsed().as_secs_f64();
|
||||
let err_pull = job.is_ok();
|
||||
let empty = job.as_ref().is_ok_and(|x| x.is_none());
|
||||
suspend_first_success = suspend_first && !empty;
|
||||
if !agent_mode && duration_pull_s > 0.5 {
|
||||
tracing::warn!("pull took more than 0.5s ({duration_pull_s}), this is a sign that the database is VERY undersized for this load. empty: {empty}, err: {err_pull}");
|
||||
#[cfg(feature = "prometheus")]
|
||||
@@ -1496,7 +1508,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
last_executed_job = None;
|
||||
jobs_executed += 1;
|
||||
|
||||
tracing::debug!(worker = %worker_name, "started handling of job {}", job.id);
|
||||
tracing::debug!("started handling of job {}", job.id);
|
||||
|
||||
if matches!(job.job_kind, JobKind::Script | JobKind::Preview) {
|
||||
if !dedicated_workers.is_empty() {
|
||||
@@ -1608,10 +1620,12 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
|
||||
if job.id == Uuid::nil() {
|
||||
tracing::info!(worker = %worker_name, "running warmup job");
|
||||
tracing::info!("running warmup job");
|
||||
} else {
|
||||
tracing::info!(worker = %worker_name, workspace_id = %job.workspace_id, id = %job.id, root_id = %job_root, "fetched job {}, root job: {}", job.id, job_root);
|
||||
}
|
||||
tracing::info!(workspace_id = %job.workspace_id, job_id = %job.id, root_id = %job_root, "fetched job {}, root job: {}", job.id, job_root);
|
||||
} // Here we can't remove the job id, but maybe with the
|
||||
// fields macro we can make a job id that only appears when
|
||||
// the job is defined?
|
||||
|
||||
let job_dir = format!("{worker_dir}/{}", job.id);
|
||||
|
||||
@@ -1736,7 +1750,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
if let Some(secs) = *EXIT_AFTER_NO_JOB_FOR_SECS {
|
||||
if let Some(lj) = last_executed_job {
|
||||
if lj.elapsed().as_secs() > secs {
|
||||
tracing::info!(worker = %worker_name, "no job for {} seconds, exiting", secs);
|
||||
tracing::info!("no job for {} seconds, exiting", secs);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -1765,7 +1779,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(worker = %worker_name, "Failed to pull jobs: {}", err);
|
||||
tracing::error!("Failed to pull jobs: {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1794,7 +1808,6 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
|
||||
send_result.await.expect("send result failed");
|
||||
tracing::info!("worker {} exited", worker_name);
|
||||
println!("worker {} exited", worker_name);
|
||||
}
|
||||
|
||||
type DedicatedWorker = (String, Sender<Arc<QueuedJob>>, Option<JoinHandle<()>>);
|
||||
@@ -2215,6 +2228,7 @@ async fn queue_init_bash_maybe<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
// logs: String,
|
||||
// ) -> error::Result<()> {
|
||||
|
||||
#[tracing::instrument(name = "completed_job", level = "info", skip_all, fields(job_id = %job.id))]
|
||||
pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
JobCompleted { job, result, mem_peak, success, cached_res_path, canceled_by, .. }: JobCompleted,
|
||||
client: &AuthedClient,
|
||||
@@ -2259,6 +2273,7 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
|
||||
let timer = _worker_flow_transition_duration
|
||||
.as_ref()
|
||||
.map(|x| x.start_timer());
|
||||
tracing::info!(parent_flow = %parent_job, subflow = %job.id, "updating flow status (2)");
|
||||
update_flow_status_after_job_completion(
|
||||
db,
|
||||
client,
|
||||
@@ -2296,6 +2311,7 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
|
||||
.await?;
|
||||
if job.is_flow_step {
|
||||
if let Some(parent_job) = job.parent_job {
|
||||
tracing::error!(parent_flow = %parent_job, subflow = %job.id, "process completed job error, updating flow status");
|
||||
update_flow_status_after_job_completion(
|
||||
db,
|
||||
client,
|
||||
@@ -2354,6 +2370,8 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
|
||||
// *barrier = None;
|
||||
// tracing::debug!("leader worker done waiting for");
|
||||
// }
|
||||
|
||||
#[tracing::instrument(name = "job_error", level = "info", skip_all, fields(job_id = %job.id))]
|
||||
pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
db: &Pool<Postgres>,
|
||||
client: &AuthedClient,
|
||||
@@ -2409,6 +2427,7 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
|
||||
};
|
||||
|
||||
let wrapped_error = WrappedError { error: err.clone() };
|
||||
tracing::error!(parent_flow = %flow, subflow = %job_status_to_update, "handle job error, updating flow status: {err:?}");
|
||||
let updated_flow = update_flow_status_after_job_completion(
|
||||
db,
|
||||
client,
|
||||
@@ -2548,7 +2567,7 @@ pub struct PreviousResult<'a> {
|
||||
pub previous_result: Option<&'a RawValue>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[tracing::instrument(name = "job", level = "info", skip_all, fields(job_id = %job.id))]
|
||||
async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
job: Arc<QueuedJob>,
|
||||
db: &DB,
|
||||
@@ -2725,8 +2744,6 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
worker = %worker_name,
|
||||
job_id = %job.id,
|
||||
workspace_id = %job.workspace_id,
|
||||
"handling job {}",
|
||||
job.id
|
||||
@@ -3186,9 +3203,16 @@ async fn handle_code_execution_job(
|
||||
db,
|
||||
)
|
||||
.await;
|
||||
|
||||
let reserved_variables = get_reserved_variables(job, &client.get_token().await, db).await?;
|
||||
|
||||
let code = format!(
|
||||
"const BASE_URL = '{base_internal_url}';\nconst WM_TOKEN = '{}';\n{}",
|
||||
&client.get_token().await,
|
||||
"const BASE_URL = '{base_internal_url}';\nconst BASE_INTERNAL_URL = '{base_internal_url}';\n{}\n{}",
|
||||
reserved_variables
|
||||
.iter()
|
||||
.map(|(k, v)| format!("const {} = '{}';\n", k, v))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"),
|
||||
inner_content
|
||||
);
|
||||
let (result, ts_logs) =
|
||||
@@ -3204,8 +3228,6 @@ async fn handle_code_execution_job(
|
||||
.unwrap_or_else(|| "NO_LANG".to_string());
|
||||
|
||||
tracing::debug!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job.id,
|
||||
workspace_id = %job.workspace_id,
|
||||
"started {} job {}",
|
||||
&lang_str,
|
||||
@@ -3340,8 +3362,6 @@ mount {{
|
||||
_ => panic!("unreachable, language is not supported: {language:#?}"),
|
||||
};
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job.id,
|
||||
workspace_id = %job.workspace_id,
|
||||
is_ok = result.is_ok(),
|
||||
"finished {} job {}",
|
||||
@@ -3925,6 +3945,14 @@ async fn lock_modules_app(
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let mut logs = "".to_string();
|
||||
if v.get("lock")
|
||||
.is_some_and(|x| !x.as_str().unwrap().trim().is_empty())
|
||||
{
|
||||
logs.push_str(
|
||||
"Found already locked inline script. Skipping lock...\n",
|
||||
);
|
||||
return Ok(Value::Object(m.clone()));
|
||||
}
|
||||
logs.push_str("Found lockable inline script. Generating lock...\n");
|
||||
let new_lock = capture_dependency_job(
|
||||
&job.id,
|
||||
|
||||
@@ -116,6 +116,7 @@ pub async fn update_flow_status_after_job_completion<
|
||||
{
|
||||
Ok(j) => j,
|
||||
Err(e) => {
|
||||
tracing::error!("Error while updating flow status of {} after completion of {}, updating flow status again with error: {e}", nrec.flow,&nrec.job_id_for_status);
|
||||
update_flow_status_after_job_completion_internal(
|
||||
db,
|
||||
client,
|
||||
@@ -322,8 +323,17 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
flow
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.await.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error while fetching iterator index: {e}"
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| Error::InternalErr(format!("requiring an index in InProgress")))?;
|
||||
tracing::info!(
|
||||
"parallel iteration {job_id_for_status} of flow {flow} update nindex: {nindex} len: {len}",
|
||||
nindex = nindex,
|
||||
len = itered.len()
|
||||
);
|
||||
(nindex, itered.len() as i32)
|
||||
}
|
||||
(_, Some(BranchAllStatus { len, .. })) => {
|
||||
@@ -336,7 +346,12 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
flow
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error while fetching branchall index: {e}"
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| Error::InternalErr(format!("requiring an index in InProgress")))?;
|
||||
(nindex, *len as i32)
|
||||
}
|
||||
@@ -351,7 +366,12 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
jobs.as_slice()
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error while fetching sucess from completed_jobs: {e}"
|
||||
))
|
||||
})?
|
||||
.into_iter()
|
||||
.all(|x| x)
|
||||
{
|
||||
@@ -375,25 +395,49 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
let r = sqlx::query_scalar!(
|
||||
"DELETE FROM parallel_monitor_lock WHERE parent_flow_id = $1 RETURNING last_ping",
|
||||
flow,
|
||||
).fetch_optional(db).await?;
|
||||
).fetch_optional(db).await.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error while deleting parallel_monitor_lock: {e}"
|
||||
))
|
||||
})?;
|
||||
if r.is_some() {
|
||||
tracing::info!(
|
||||
"parallel flow has removed lock on its parent, last ping was {:?}",
|
||||
r.unwrap()
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
"parallel iteration {job_id_for_status} of flow {flow} has finished",
|
||||
);
|
||||
|
||||
(true, Some(new_status))
|
||||
} else {
|
||||
tx.commit().await?;
|
||||
|
||||
if parallelism.is_some() {
|
||||
sqlx::query!(
|
||||
"UPDATE queue SET suspend = suspend - 1 WHERE parent_job = $1",
|
||||
// this ensure that the lock is taken in the same order and thus avoid deadlocks
|
||||
let ids = sqlx::query_scalar!(
|
||||
"SELECT id FROM queue WHERE parent_job = $1 AND suspend > 0 ORDER by suspend",
|
||||
flow
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error while locking jobs to decrease parallelism of: {e}"))
|
||||
})?;
|
||||
for id in ids {
|
||||
sqlx::query!(
|
||||
"UPDATE queue SET suspend = suspend - 1 WHERE id = $1 AND suspend > 0",
|
||||
id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error decreasing suspend for {id}: {e}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -403,13 +447,18 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
flow
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error while setting last ping to null: {e}"))
|
||||
})?;
|
||||
|
||||
let r = sqlx::query_scalar!(
|
||||
"DELETE FROM parallel_monitor_lock WHERE parent_flow_id = $1 and job_id = $2 RETURNING last_ping",
|
||||
flow,
|
||||
job_id_for_status
|
||||
).fetch_optional(db).await?;
|
||||
).fetch_optional(db).await.map_err(|e| {
|
||||
Error::InternalErr(format!("error while removing parallel_monitor_lock: {e}"))
|
||||
})?;
|
||||
if r.is_some() {
|
||||
tracing::info!(
|
||||
"parallel flow has removed lock on its parent, last ping was {:?}",
|
||||
@@ -476,7 +525,10 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
old_status.step
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error while getting retry fromn step: {e}"))
|
||||
})?
|
||||
.flatten();
|
||||
|
||||
let retry = retry
|
||||
@@ -510,7 +562,10 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
flow
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error while setting flow index for {flow}: {e}"))
|
||||
})?;
|
||||
old_status.step + 1
|
||||
} else {
|
||||
old_status.step
|
||||
@@ -528,7 +583,11 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
flow
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
.await.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error while fetching failure module: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE queue
|
||||
@@ -541,7 +600,12 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
flow
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error while setting flow status in failure step: {e}"
|
||||
))
|
||||
})?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE queue
|
||||
@@ -552,7 +616,10 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
flow
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error while setting new flow status: {e}"))
|
||||
})?;
|
||||
|
||||
if let Some(job_result) = new_status.job_result() {
|
||||
sqlx::query!(
|
||||
@@ -564,7 +631,11 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
flow
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
.await.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error while setting leaf jobs: {e}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -676,7 +747,10 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
&_cleanup_module.flow_jobs_to_clean,
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error while cleaning up completed_job: {e}"))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
if flow_job.canceled {
|
||||
@@ -795,6 +869,8 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
}
|
||||
|
||||
if let Some(parent_job) = flow_job.parent_job {
|
||||
tracing::info!(subflow_id = %flow_job.id, parent_id = %parent_job, "subflow is finished, updating parent flow status");
|
||||
|
||||
return Ok(Some(RecUpdateFlowStatusAfterJobCompletion {
|
||||
flow: parent_job,
|
||||
job_id_for_status: flow,
|
||||
@@ -2531,6 +2607,7 @@ async fn compute_next_flow_transform(
|
||||
cache_ttl: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
concurrency_key: None,
|
||||
},
|
||||
path: Some(format!("{}/forloop", flow_job.script_path())),
|
||||
restarted_from: None,
|
||||
@@ -2629,6 +2706,7 @@ async fn compute_next_flow_transform(
|
||||
cache_ttl: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
concurrency_key: None,
|
||||
},
|
||||
path: Some(format!(
|
||||
"{}/branchone-{}",
|
||||
@@ -2680,6 +2758,7 @@ async fn compute_next_flow_transform(
|
||||
cache_ttl: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
concurrency_key: None,
|
||||
},
|
||||
path: Some(format!(
|
||||
"{}/branchall-{}",
|
||||
@@ -2747,6 +2826,7 @@ async fn compute_next_flow_transform(
|
||||
cache_ttl: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
concurrency_key: None,
|
||||
},
|
||||
path: Some(format!(
|
||||
"{}/branchall-{}",
|
||||
@@ -2819,6 +2899,7 @@ async fn next_loop_iteration(
|
||||
cache_ttl: None,
|
||||
priority: None,
|
||||
early_return: None,
|
||||
concurrency_key: None,
|
||||
},
|
||||
path: inner_path,
|
||||
restarted_from: None,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.303.2";
|
||||
export const VERSION = "v1.321.3";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
87
cli/apps.ts
87
cli/apps.ts
@@ -5,15 +5,13 @@ import {
|
||||
colors,
|
||||
Command,
|
||||
ListableApp,
|
||||
log,
|
||||
Policy,
|
||||
SEP,
|
||||
Table,
|
||||
yamlParse,
|
||||
} from "./deps.ts";
|
||||
import {
|
||||
GlobalOptions,
|
||||
isSuperset,
|
||||
parseFromFile,
|
||||
removeType,
|
||||
} from "./types.ts";
|
||||
import { GlobalOptions, isSuperset } from "./types.ts";
|
||||
|
||||
export interface AppFile {
|
||||
value: any;
|
||||
@@ -21,14 +19,20 @@ export interface AppFile {
|
||||
policy: Policy;
|
||||
}
|
||||
|
||||
const alreadySynced: string[] = [];
|
||||
|
||||
export async function pushApp(
|
||||
workspace: string,
|
||||
filePath: string,
|
||||
app: AppFile | undefined,
|
||||
newApp: AppFile,
|
||||
remotePath: string,
|
||||
localPath: string,
|
||||
message?: string
|
||||
): Promise<void> {
|
||||
const remotePath = removeType(filePath, "app");
|
||||
if (alreadySynced.includes(localPath)) {
|
||||
return;
|
||||
}
|
||||
alreadySynced.push(localPath);
|
||||
|
||||
let app: any = undefined;
|
||||
// deleting old app if it exists in raw mode
|
||||
try {
|
||||
app = await AppService.getAppByPath({
|
||||
@@ -39,28 +43,61 @@ export async function pushApp(
|
||||
//ignore
|
||||
}
|
||||
|
||||
if (app) {
|
||||
if (isSuperset(newApp, app)) {
|
||||
if (!localPath.endsWith(SEP)) {
|
||||
localPath += SEP;
|
||||
}
|
||||
const localAppRaw = await Deno.readTextFile(localPath + "app.yaml");
|
||||
const localApp = yamlParse(localAppRaw) as AppFile;
|
||||
|
||||
function replaceInlineScripts(rec: any) {
|
||||
if (!rec) {
|
||||
return;
|
||||
}
|
||||
if (typeof rec == "object") {
|
||||
return Object.entries(rec).flatMap(([k, v]) => {
|
||||
if (k == "inlineScript" && typeof v == "object") {
|
||||
const o: Record<string, any> = v as any;
|
||||
if (o["content"] && o["content"].startsWith("!inline")) {
|
||||
const basePath = localPath + o["content"].split(" ")[1];
|
||||
o["content"] = Deno.readTextFileSync(basePath);
|
||||
}
|
||||
if (o["lock"] && o["lock"].startsWith("!inline")) {
|
||||
const basePath = localPath + o["lock"].split(" ")[1];
|
||||
o["lock"] = Deno.readTextFileSync(basePath);
|
||||
}
|
||||
} else {
|
||||
replaceInlineScripts(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
replaceInlineScripts(localApp.value);
|
||||
|
||||
if (app) {
|
||||
if (isSuperset(localApp, app)) {
|
||||
log.info(colors.green(`App ${remotePath} is up to date`));
|
||||
return;
|
||||
}
|
||||
log.info(colors.bold.yellow(`Updating app ${remotePath}...`));
|
||||
await AppService.updateApp({
|
||||
workspace,
|
||||
path: remotePath.replaceAll("\\", "/"),
|
||||
requestBody: {
|
||||
deployment_message: message,
|
||||
...newApp,
|
||||
...localApp,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log(colors.yellow.bold("Creating new app..."));
|
||||
log.info(colors.yellow.bold("Creating new app..."));
|
||||
|
||||
console.log(message);
|
||||
await AppService.createApp({
|
||||
workspace,
|
||||
requestBody: {
|
||||
path: remotePath.replaceAll("\\", "/"),
|
||||
deployment_message: message,
|
||||
...newApp,
|
||||
...localApp,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -94,32 +131,22 @@ async function list(opts: GlobalOptions) {
|
||||
.render();
|
||||
}
|
||||
|
||||
async function push(opts: GlobalOptions, filePath: string) {
|
||||
const remotePath = filePath.split(".")[0];
|
||||
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
if (!validatePath(remotePath)) {
|
||||
return;
|
||||
}
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
await pushApp(
|
||||
workspace.workspaceId,
|
||||
filePath,
|
||||
undefined,
|
||||
parseFromFile(filePath)
|
||||
);
|
||||
=======
|
||||
await pushApp(workspace.workspaceId, filePath, parseFromFile(filePath));
|
||||
>>>>>>> Stashed changes
|
||||
console.log(colors.bold.underline.green("App pushed"));
|
||||
await pushApp(workspace.workspaceId, remotePath, filePath);
|
||||
log.info(colors.bold.underline.green("Flow pushed"));
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("app related commands")
|
||||
.action(list as any)
|
||||
.command("push", "push a local app ")
|
||||
.arguments("<file_path:file>")
|
||||
.arguments("<file_path:string> <remote_path:string>")
|
||||
.action(push as any);
|
||||
|
||||
export default command;
|
||||
|
||||
54
cli/deps.ts
54
cli/deps.ts
@@ -1,37 +1,42 @@
|
||||
// windmill
|
||||
export { setClient } from "https://deno.land/x/windmill@v1.291.4/mod.ts";
|
||||
export * from "https://deno.land/x/windmill@v1.291.4/windmill-api/index.ts";
|
||||
export { SEP } from "https://deno.land/std@0.201.0/path/separator.ts";
|
||||
export * from "npm:windmill-client@1.319.1";
|
||||
|
||||
// cliffy
|
||||
export { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts";
|
||||
export { Table } from "https://deno.land/x/cliffy@v1.0.0-rc.3/table/table.ts";
|
||||
export { colors } from "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/colors.ts";
|
||||
export { Secret } from "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/secret.ts";
|
||||
export { Select } from "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/select.ts";
|
||||
export { Confirm } from "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/confirm.ts";
|
||||
export { Input } from "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/input.ts";
|
||||
export { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts";
|
||||
export { Table } from "https://deno.land/x/cliffy@v1.0.0-rc.4/table/table.ts";
|
||||
export { colors } from "https://deno.land/x/cliffy@v1.0.0-rc.4/ansi/colors.ts";
|
||||
export { Secret } from "https://deno.land/x/cliffy@v1.0.0-rc.4/prompt/secret.ts";
|
||||
export { Select } from "https://deno.land/x/cliffy@v1.0.0-rc.4/prompt/select.ts";
|
||||
export { Confirm } from "https://deno.land/x/cliffy@v1.0.0-rc.4/prompt/confirm.ts";
|
||||
export { Input } from "https://deno.land/x/cliffy@v1.0.0-rc.4/prompt/input.ts";
|
||||
export {
|
||||
DenoLandProvider,
|
||||
UpgradeCommand,
|
||||
} from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/upgrade/mod.ts";
|
||||
export { CompletionsCommand } from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/mod.ts";
|
||||
} from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/mod.ts";
|
||||
export { CompletionsCommand } from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/mod.ts";
|
||||
// std
|
||||
export * as path from "https://deno.land/std@0.184.0/path/mod.ts";
|
||||
export { ensureDir } from "https://deno.land/std@0.184.0/fs/ensure_dir.ts";
|
||||
export * as path from "https://deno.land/std@0.207.0/path/mod.ts";
|
||||
export { ensureDir } from "https://deno.land/std@0.207.0/fs/ensure_dir.ts";
|
||||
export {
|
||||
copy,
|
||||
readAll,
|
||||
readerFromStreamReader,
|
||||
} from "https://deno.land/std@0.184.0/streams/mod.ts";
|
||||
} from "https://deno.land/std@0.207.0/streams/mod.ts";
|
||||
export { SEP } from "https://deno.land/std@0.207.0/path/separator.ts";
|
||||
export { DelimiterStream } from "https://deno.land/std@0.207.0/streams/mod.ts";
|
||||
export { iterateReader } from "https://deno.land/std@0.207.0/streams/iterate_reader.ts";
|
||||
export { writeAllSync } from "https://deno.land/std@0.207.0/streams/mod.ts";
|
||||
export { encodeHex } from "https://deno.land/std@0.207.0/encoding/hex.ts";
|
||||
export * as log from "https://deno.land/std@0.207.0/log/mod.ts";
|
||||
export {
|
||||
stringify as yamlStringify,
|
||||
parse as yamlParse,
|
||||
} from "https://deno.land/std@0.207.0/yaml/mod.ts";
|
||||
|
||||
// other
|
||||
export { Application, Router } from "https://deno.land/x/oak@v12.5.0/mod.ts";
|
||||
|
||||
export { getPort } from "https://deno.land/x/getport@v2.1.2/mod.ts";
|
||||
|
||||
export { DelimiterStream } from "https://deno.land/std@0.184.0/streams/mod.ts";
|
||||
export { iterateReader } from "https://deno.land/std@0.184.0/streams/iterate_reader.ts";
|
||||
export { writeAllSync } from "https://deno.land/std@0.184.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.
|
||||
@@ -42,11 +47,6 @@ export { default as microdiff } from "https://deno.land/x/microdiff@v1.3.1/index
|
||||
export { default as objectHash } from "https://deno.land/x/object_hash@2.0.3.1/mod.ts";
|
||||
export { minimatch } from "npm:minimatch";
|
||||
export { default as JSZip } from "npm:jszip@3.7.1";
|
||||
export * as log from "https://deno.land/std@0.186.0/log/mod.ts";
|
||||
export {
|
||||
stringify as yamlStringify,
|
||||
parse as yamlParse,
|
||||
} from "https://deno.land/std@0.184.0/yaml/mod.ts";
|
||||
|
||||
export { open } from "https://deno.land/x/open@v0.0.5/index.ts";
|
||||
export { encodeHex } from "https://deno.land/std@0.207.0/encoding/hex.ts";
|
||||
export { default as gitignore_parser } from "npm:gitignore-parser";
|
||||
|
||||
44
cli/flow.ts
44
cli/flow.ts
@@ -27,13 +27,13 @@ const alreadySynced: string[] = [];
|
||||
export async function pushFlow(
|
||||
workspace: string,
|
||||
remotePath: string,
|
||||
localFlowPath: string,
|
||||
localPath: string,
|
||||
message?: string
|
||||
): Promise<void> {
|
||||
if (alreadySynced.includes(localFlowPath)) {
|
||||
if (alreadySynced.includes(localPath)) {
|
||||
return;
|
||||
}
|
||||
alreadySynced.push(localFlowPath);
|
||||
alreadySynced.push(localPath);
|
||||
let flow: Flow | undefined = undefined;
|
||||
try {
|
||||
flow = await FlowService.getFlowByPath({
|
||||
@@ -44,17 +44,31 @@ export async function pushFlow(
|
||||
// flow doesn't exist
|
||||
}
|
||||
|
||||
if (!localFlowPath.endsWith(SEP)) {
|
||||
localFlowPath += SEP;
|
||||
if (!localPath.endsWith(SEP)) {
|
||||
localPath += SEP;
|
||||
}
|
||||
const localFlowRaw = await Deno.readTextFile(localFlowPath + "flow.yaml");
|
||||
const localFlowRaw = await Deno.readTextFile(localPath + "flow.yaml");
|
||||
const localFlow = yamlParse(localFlowRaw) as FlowFile;
|
||||
|
||||
function replaceInlineScripts(modules: FlowModule[]) {
|
||||
modules.forEach((m) => {
|
||||
if (m.value.type == "rawscript") {
|
||||
const path = m.value.content.split(" ")[1];
|
||||
m.value.content = Deno.readTextFileSync(localFlowPath + path);
|
||||
m.value.content = Deno.readTextFileSync(localPath + path);
|
||||
const lock = m.value.lock;
|
||||
|
||||
if (
|
||||
lock &&
|
||||
typeof lock == "string" &&
|
||||
lock.trimStart().startsWith("!inline ")
|
||||
) {
|
||||
const path = lock.split(" ")[1];
|
||||
try {
|
||||
m.value.lock = Deno.readTextFileSync(localPath + path);
|
||||
} catch {
|
||||
log.error(`Lock file ${path} not found`);
|
||||
}
|
||||
}
|
||||
} else if (m.value.type == "forloopflow") {
|
||||
replaceInlineScripts(m.value.modules);
|
||||
} else if (m.value.type == "branchall") {
|
||||
@@ -194,12 +208,15 @@ async function run(
|
||||
log.info(jobInfo.result ?? {});
|
||||
}
|
||||
|
||||
export function bootstrap(opts: GlobalOptions & {summary: string, description: string}, flowPath: string) {
|
||||
export function bootstrap(
|
||||
opts: GlobalOptions & { summary: string; description: string },
|
||||
flowPath: string
|
||||
) {
|
||||
if (!validatePath(flowPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const flowDirFullPath = `${flowPath}.flow`
|
||||
const flowDirFullPath = `${flowPath}.flow`;
|
||||
Deno.mkdirSync(flowDirFullPath, { recursive: false });
|
||||
|
||||
const newFlowDefinition = defaultFlowDefinition();
|
||||
@@ -210,13 +227,12 @@ export function bootstrap(opts: GlobalOptions & {summary: string, description: s
|
||||
newFlowDefinition.description = opts.description;
|
||||
}
|
||||
|
||||
const newFlowDefinitionYaml = yamlStringify(newFlowDefinition as Record<string, any>);
|
||||
const newFlowDefinitionYaml = yamlStringify(
|
||||
newFlowDefinition as Record<string, any>
|
||||
);
|
||||
|
||||
const flowYamlPath = `${flowDirFullPath}/flow.yaml`;
|
||||
Deno.writeTextFile(
|
||||
flowYamlPath,
|
||||
newFlowDefinitionYaml,
|
||||
{ createNew: true });
|
||||
Deno.writeTextFile(flowYamlPath, newFlowDefinitionYaml, { createNew: true });
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
|
||||
@@ -17,16 +17,16 @@ async function pull(opts: GlobalOptions) {
|
||||
|
||||
const userInfo = await requireLogin(opts);
|
||||
|
||||
const uid = await SettingService.getGlobal({
|
||||
const uid = (await SettingService.getGlobal({
|
||||
key: "uid",
|
||||
});
|
||||
})) as string;
|
||||
|
||||
const hubBaseUrl =
|
||||
(await SettingService.getGlobal({
|
||||
key: "hubBaseUrl",
|
||||
})) ?? "https://hub.windmill.dev";
|
||||
|
||||
const headers = {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/json",
|
||||
"X-email": userInfo.email,
|
||||
};
|
||||
@@ -99,8 +99,7 @@ async function pull(opts: GlobalOptions) {
|
||||
workspace.workspaceId,
|
||||
x.name + ".resource-type.json",
|
||||
undefined,
|
||||
x,
|
||||
true
|
||||
x
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ addEventListener("error", (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
export const VERSION = "v1.303.2";
|
||||
export const VERSION = "v1.321.3";
|
||||
|
||||
let command: any = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -178,6 +178,16 @@ async function updateScriptLock(
|
||||
metadataContent: Record<string, any>,
|
||||
rawDeps: string | undefined
|
||||
): Promise<void> {
|
||||
if (
|
||||
!(
|
||||
language == "bun" ||
|
||||
language == "python3" ||
|
||||
language == "go" ||
|
||||
language == "deno"
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// generate the script lock running a dependency job in Windmill and update it inplace
|
||||
// TODO: update this once the client is released
|
||||
const rawResponse = await fetch(
|
||||
@@ -214,7 +224,9 @@ async function updateScriptLock(
|
||||
)}`
|
||||
);
|
||||
}
|
||||
metadataContent.lock = lock;
|
||||
const lockPath = remotePath + ".script.lock";
|
||||
await Deno.writeTextFile(lockPath, lock);
|
||||
metadataContent.lock = "!inline " + lockPath;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to generate lockfile. Status was: ${rawResponse.statusText}, ${responseText}, ${e}`
|
||||
@@ -446,6 +458,11 @@ export async function parseMetadataFile(
|
||||
metadataFilePath = scriptPath + ".script.yaml";
|
||||
await Deno.stat(metadataFilePath);
|
||||
const payload: any = yamlParse(await Deno.readTextFile(metadataFilePath));
|
||||
if (payload?.["lock"].startsWith("!inline ")) {
|
||||
payload["lock"] = await Deno.readTextFile(
|
||||
payload["lock"].split(" ")[1]
|
||||
);
|
||||
}
|
||||
if (Array.isArray(payload?.["lock"])) {
|
||||
payload["lock"] = payload["lock"].join("\n");
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ export async function downloadZip(
|
||||
includeSettings?: boolean,
|
||||
defaultTs?: "bun" | "deno"
|
||||
): Promise<JSZip | undefined> {
|
||||
const requestHeaders: HeadersInit = new Headers();
|
||||
const requestHeaders: HeadersInit & { set(x: string, y: string): void } =
|
||||
new Headers();
|
||||
requestHeaders.set("Authorization", "Bearer " + workspace.token);
|
||||
requestHeaders.set("Content-Type", "application/octet-stream");
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { colors, Command } from "./deps.ts";
|
||||
import { colors, Command, log } from "./deps.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
|
||||
async function stub(_opts: GlobalOptions, _dir?: string) {
|
||||
function stub(_opts: GlobalOptions, _dir?: string) {
|
||||
log.info(
|
||||
colors.red.underline(
|
||||
'Push is deprecated. Use "sync push --raw" instead. See <TODO_LINK_HERE> for more information.'
|
||||
|
||||
@@ -83,7 +83,11 @@ export async function handleScriptMetadata(
|
||||
message: string | undefined,
|
||||
globalDeps: GlobalDeps
|
||||
): Promise<boolean> {
|
||||
if (path.endsWith(".script.json") || path.endsWith(".script.yaml")) {
|
||||
if (
|
||||
path.endsWith(".script.json") ||
|
||||
path.endsWith(".script.yaml") ||
|
||||
path.endsWith(".script.lock")
|
||||
) {
|
||||
const contentPath = await findContentFile(path);
|
||||
return handleFile(
|
||||
contentPath,
|
||||
@@ -166,7 +170,9 @@ export async function handleFile(
|
||||
Boolean(typed.restart_unless_cancelled) ==
|
||||
Boolean(remote.restart_unless_cancelled) &&
|
||||
Boolean(typed.visible_to_runner_only) ==
|
||||
Boolean(remote.visible_to_runner_only))
|
||||
Boolean(remote.visible_to_runner_only) &&
|
||||
Boolean(typed.no_main_func) == Boolean(remote.no_main_func) &&
|
||||
typed.priority == Boolean(remote.priority))
|
||||
) {
|
||||
log.info(colors.green(`Script ${remotePath} is up to date`));
|
||||
return true;
|
||||
@@ -181,7 +187,7 @@ export async function handleFile(
|
||||
requestBody: {
|
||||
content,
|
||||
description: typed?.description ?? "",
|
||||
language: language as NewScript.language,
|
||||
language: language,
|
||||
path: remotePath.replaceAll("\\", "/"),
|
||||
summary: typed?.summary ?? "",
|
||||
kind: typed?.kind,
|
||||
@@ -197,6 +203,8 @@ export async function handleFile(
|
||||
deployment_message: message,
|
||||
restart_unless_cancelled: typed?.restart_unless_cancelled,
|
||||
visible_to_runner_only: typed?.visible_to_runner_only,
|
||||
no_main_func: typed?.no_main_func,
|
||||
priority: typed?.priority,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -209,7 +217,7 @@ export async function handleFile(
|
||||
requestBody: {
|
||||
content,
|
||||
description: typed?.description ?? "",
|
||||
language: language as NewScript.language,
|
||||
language: language,
|
||||
path: remotePath.replaceAll("\\", "/"),
|
||||
summary: typed?.summary ?? "",
|
||||
kind: typed?.kind,
|
||||
@@ -225,6 +233,8 @@ export async function handleFile(
|
||||
deployment_message: message,
|
||||
restart_unless_cancelled: typed?.restart_unless_cancelled,
|
||||
visible_to_runner_only: typed?.visible_to_runner_only,
|
||||
no_main_func: typed?.no_main_func,
|
||||
priority: typed?.priority,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -236,6 +246,8 @@ export async function handleFile(
|
||||
export async function findContentFile(filePath: string) {
|
||||
const candidates = filePath.endsWith("script.json")
|
||||
? exts.map((x) => filePath.replace(".script.json", x))
|
||||
: filePath.endsWith("script.lock")
|
||||
? exts.map((x) => filePath.replace(".script.lock", x))
|
||||
: exts.map((x) => filePath.replace(".script.yaml", x));
|
||||
|
||||
const validCandidates = (
|
||||
@@ -634,7 +646,8 @@ async function generateMetadata(
|
||||
return (
|
||||
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
|
||||
ignore(p, isD) ||
|
||||
p.includes(".flow/")
|
||||
p.includes(".flow/") ||
|
||||
p.includes(".app/")
|
||||
);
|
||||
},
|
||||
false,
|
||||
|
||||
253
cli/sync.ts
253
cli/sync.ts
@@ -108,17 +108,19 @@ export const yamlOptions = {
|
||||
};
|
||||
|
||||
function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
|
||||
function _internal_file(p: string, f: JSZip.JSZipObject): DynFSElement {
|
||||
const isFlow = p.endsWith("flow.json");
|
||||
function transformPath() {
|
||||
if (isFlow) {
|
||||
return p.replace("flow.json", "flow");
|
||||
} else {
|
||||
return useYaml && p.endsWith(".json")
|
||||
? p.replaceAll(".json", ".yaml")
|
||||
: p;
|
||||
}
|
||||
}
|
||||
async function _internal_file(
|
||||
p: string,
|
||||
f: JSZip.JSZipObject
|
||||
): Promise<DynFSElement[]> {
|
||||
const kind: "flow" | "app" | "script" | "other" = p.endsWith("flow.json")
|
||||
? "flow"
|
||||
: p.endsWith("app.json")
|
||||
? "app"
|
||||
: p.endsWith("script.json")
|
||||
? "script"
|
||||
: "other";
|
||||
|
||||
const isJson = p.endsWith(".json");
|
||||
|
||||
interface InlineScript {
|
||||
path: string;
|
||||
@@ -129,8 +131,8 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
|
||||
const seen_names = new Set<string>();
|
||||
function assignPath(
|
||||
summary: string | undefined,
|
||||
language: RawScript.language
|
||||
): string {
|
||||
language: RawScript["language"]
|
||||
): [string, string] {
|
||||
let name;
|
||||
|
||||
const INLINE_SCRIPT = "inline_script";
|
||||
@@ -163,27 +165,43 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
|
||||
else if (language == "graphql") ext = "gql";
|
||||
else if (language == "bun") ext = "bun.ts";
|
||||
else if (language == "nativets") ext = "native.ts";
|
||||
else if (language == "frontend") ext = "frontend.js";
|
||||
else ext = "no_ext";
|
||||
|
||||
return `${name}.inline_script.${ext}`;
|
||||
return [`${name}.inline_script.`, ext];
|
||||
}
|
||||
|
||||
function extractInlineScripts(modules: FlowModule[]): InlineScript[] {
|
||||
function extractInlineScriptsForFlows(
|
||||
modules: FlowModule[]
|
||||
): InlineScript[] {
|
||||
return modules.flatMap((m) => {
|
||||
if (m.value.type == "rawscript") {
|
||||
const path = assignPath(m.summary, m.value.language);
|
||||
const [basePath, ext] = assignPath(m.summary, m.value.language);
|
||||
const path = basePath + ext;
|
||||
const content = m.value.content;
|
||||
const r = [{ path: path, content: content }];
|
||||
m.value.content = "!inline " + path;
|
||||
return [{ path: path, content: content }];
|
||||
const lock = m.value.lock;
|
||||
if (lock) {
|
||||
const lockPath = basePath + "lock";
|
||||
m.value.lock = "!inline " + lockPath;
|
||||
r.push({ path: lockPath, content: lock });
|
||||
}
|
||||
return r;
|
||||
} else if (m.value.type == "forloopflow") {
|
||||
return extractInlineScripts(m.value.modules);
|
||||
return extractInlineScriptsForFlows(m.value.modules);
|
||||
} else if (m.value.type == "branchall") {
|
||||
return m.value.branches.flatMap((b) =>
|
||||
extractInlineScripts(b.modules)
|
||||
extractInlineScriptsForFlows(b.modules)
|
||||
);
|
||||
} else if (m.value.type == "whileloopflow") {
|
||||
return extractInlineScriptsForFlows(m.value.modules);
|
||||
} else if (m.value.type == "branchone") {
|
||||
return [
|
||||
...m.value.branches.flatMap((b) => extractInlineScripts(b.modules)),
|
||||
...extractInlineScripts(m.value.default),
|
||||
...m.value.branches.flatMap((b) =>
|
||||
extractInlineScriptsForFlows(b.modules)
|
||||
),
|
||||
...extractInlineScriptsForFlows(m.value.default),
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
@@ -191,47 +209,147 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
|
||||
});
|
||||
}
|
||||
|
||||
const flowPath = transformPath();
|
||||
return {
|
||||
isDirectory: isFlow,
|
||||
path: flowPath,
|
||||
async *getChildren(): AsyncIterable<DynFSElement> {
|
||||
if (isFlow) {
|
||||
const flow: OpenFlow = JSON.parse(await f.async("text"));
|
||||
const inlineScripts = extractInlineScripts(flow.value.modules);
|
||||
for (const s of inlineScripts) {
|
||||
function extractInlineScriptsForApps(rec: any): InlineScript[] {
|
||||
if (!rec) {
|
||||
return [];
|
||||
}
|
||||
if (typeof rec == "object") {
|
||||
return Object.entries(rec).flatMap(([k, v]) => {
|
||||
if (k == "inlineScript" && typeof v == "object") {
|
||||
const o: Record<string, any> = v as any;
|
||||
const name = rec["name"];
|
||||
const [basePath, ext] = assignPath(name, o["language"]);
|
||||
const r = [];
|
||||
if (o["content"]) {
|
||||
const content = o["content"];
|
||||
o["content"] = "!inline " + basePath + ext;
|
||||
r.push({
|
||||
path: basePath + ext,
|
||||
content: content,
|
||||
});
|
||||
}
|
||||
if (o["lock"]) {
|
||||
const lock = o["lock"];
|
||||
o["lock"] = "!inline " + basePath + "lock";
|
||||
r.push({
|
||||
path: basePath + "lock",
|
||||
content: lock,
|
||||
});
|
||||
}
|
||||
return r;
|
||||
} else {
|
||||
return extractInlineScriptsForApps(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function transformPath() {
|
||||
if (kind == "flow") {
|
||||
return p.replace("flow.json", "flow");
|
||||
} else if (kind == "app") {
|
||||
return p.replace("app.json", "app");
|
||||
} else {
|
||||
return useYaml && isJson ? p.replaceAll(".json", ".yaml") : p;
|
||||
}
|
||||
}
|
||||
|
||||
const finalPath = transformPath();
|
||||
const r = [
|
||||
{
|
||||
isDirectory: kind == "flow" || kind == "app",
|
||||
path: finalPath,
|
||||
async *getChildren(): AsyncIterable<DynFSElement> {
|
||||
if (kind == "flow") {
|
||||
const flow: OpenFlow = JSON.parse(await f.async("text"));
|
||||
const inlineScripts = extractInlineScriptsForFlows(
|
||||
flow.value.modules
|
||||
);
|
||||
for (const s of inlineScripts) {
|
||||
yield {
|
||||
isDirectory: false,
|
||||
path: path.join(finalPath, s.path),
|
||||
async *getChildren() {},
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return s.content;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
yield {
|
||||
isDirectory: false,
|
||||
path: path.join(flowPath, s.path),
|
||||
path: path.join(finalPath, "flow.yaml"),
|
||||
async *getChildren() {},
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return s.content;
|
||||
return yamlStringify(flow, yamlOptions);
|
||||
},
|
||||
};
|
||||
} else if (kind == "app") {
|
||||
const app = JSON.parse(await f.async("text"));
|
||||
const inlineScripts = extractInlineScriptsForApps(app?.["value"]);
|
||||
for (const s of inlineScripts) {
|
||||
yield {
|
||||
isDirectory: false,
|
||||
path: path.join(finalPath, s.path),
|
||||
async *getChildren() {},
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return s.content;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
yield {
|
||||
isDirectory: false,
|
||||
path: path.join(finalPath, "app.yaml"),
|
||||
async *getChildren() {},
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return yamlStringify(app, yamlOptions);
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
yield {
|
||||
isDirectory: false,
|
||||
path: path.join(flowPath, "flow.yaml"),
|
||||
async *getChildren() {},
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return yamlStringify(flow, yamlOptions);
|
||||
},
|
||||
};
|
||||
}
|
||||
async getContentText(): Promise<string> {
|
||||
const content = await f.async("text");
|
||||
|
||||
if (kind == "script") {
|
||||
const parsed = JSON.parse(content);
|
||||
if (parsed["lock"]) {
|
||||
parsed["lock"] = "!inline " + removeSuffix(p, ".json") + ".lock";
|
||||
}
|
||||
return useYaml
|
||||
? yamlStringify(parsed, yamlOptions)
|
||||
: JSON.stringify(parsed, null, 2);
|
||||
}
|
||||
|
||||
return useYaml && isJson
|
||||
? yamlStringify(JSON.parse(content), yamlOptions)
|
||||
: content;
|
||||
},
|
||||
},
|
||||
// async getContentBytes(): Promise<Uint8Array> {
|
||||
// return await f.async("uint8array");
|
||||
// },
|
||||
async getContentText(): Promise<string> {
|
||||
const content = await f.async("text");
|
||||
return useYaml && p.endsWith(".json")
|
||||
? yamlStringify(JSON.parse(content), yamlOptions)
|
||||
: content;
|
||||
},
|
||||
};
|
||||
];
|
||||
if (kind == "script") {
|
||||
const content = await f.async("text");
|
||||
const parsed = JSON.parse(content);
|
||||
const lock = parsed["lock"];
|
||||
if (lock) {
|
||||
r.push({
|
||||
isDirectory: false,
|
||||
path: removeSuffix(finalPath, ".json") + ".lock",
|
||||
async *getChildren() {},
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return lock;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
function _internal_folder(p: string, zip: JSZip): DynFSElement {
|
||||
return {
|
||||
@@ -245,7 +363,10 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
|
||||
const e = zip.folder(file.name)!;
|
||||
yield _internal_folder(totalPath, e);
|
||||
} else {
|
||||
yield _internal_file(totalPath, file);
|
||||
const fs = await _internal_file(totalPath, file);
|
||||
for (const f of fs) {
|
||||
yield f;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -339,9 +460,19 @@ export async function elementsToMap(
|
||||
if (skips.skipResources && path.endsWith(".resource" + ext)) continue;
|
||||
|
||||
if (
|
||||
!["json", "yaml", "go", "sh", "ts", "py", "sql", "gql", "ps1"].includes(
|
||||
path.split(".").pop() ?? ""
|
||||
)
|
||||
![
|
||||
"json",
|
||||
"yaml",
|
||||
"go",
|
||||
"sh",
|
||||
"ts",
|
||||
"py",
|
||||
"sql",
|
||||
"gql",
|
||||
"ps1",
|
||||
"js",
|
||||
"lock",
|
||||
].includes(path.split(".").pop() ?? "")
|
||||
)
|
||||
continue;
|
||||
const content = await entry.getContentText();
|
||||
@@ -730,7 +861,7 @@ async function pull(opts: GlobalOptions & SyncOptions) {
|
||||
showConflict(conflict.path, conflict.local, conflict.change.after);
|
||||
}
|
||||
log.info(
|
||||
colors.red(`Please resolve theses conflicts manually by either:
|
||||
colors.red(`Please resolve these conflicts manually by either:
|
||||
- reverting the content back to its remote (\`wmill pull\` and refuse to preserve local when prompted)
|
||||
- pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes
|
||||
`)
|
||||
@@ -925,7 +1056,8 @@ async function push(opts: GlobalOptions & SyncOptions) {
|
||||
} else if (change.name === "added") {
|
||||
if (
|
||||
change.path.endsWith(".script.json") ||
|
||||
change.path.endsWith(".script.yaml")
|
||||
change.path.endsWith(".script.yaml") ||
|
||||
change.path.endsWith(".lock")
|
||||
) {
|
||||
continue;
|
||||
} else if (
|
||||
@@ -958,6 +1090,9 @@ async function push(opts: GlobalOptions & SyncOptions) {
|
||||
await Deno.writeTextFile(stateTarget, change.content);
|
||||
}
|
||||
} else if (change.name === "deleted") {
|
||||
if (change.path.endsWith(".lock")) {
|
||||
continue;
|
||||
}
|
||||
const typ = getTypeStrFromPath(change.path);
|
||||
|
||||
if (typ == "script") {
|
||||
@@ -1005,7 +1140,7 @@ async function push(opts: GlobalOptions & SyncOptions) {
|
||||
case "app":
|
||||
await AppService.deleteApp({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".app.json"),
|
||||
path: removeSuffix(change.path, ".app/app.json"),
|
||||
});
|
||||
break;
|
||||
case "schedule":
|
||||
|
||||
@@ -111,7 +111,8 @@ export async function pushObj(
|
||||
const typeEnding = getTypeStrFromPath(p);
|
||||
|
||||
if (typeEnding === "app") {
|
||||
await pushApp(workspace, p, befObj, newObj, message);
|
||||
const appName = p.split(".app" + path.sep)[0];
|
||||
await pushApp(workspace, appName, appName + ".app", message);
|
||||
} else if (typeEnding === "folder") {
|
||||
await pushFolder(workspace, p, befObj, newObj);
|
||||
} else if (typeEnding === "variable") {
|
||||
@@ -171,6 +172,9 @@ export function getTypeStrFromPath(
|
||||
if (p.includes(".flow" + path.sep)) {
|
||||
return "flow";
|
||||
}
|
||||
if (p.includes(".app" + path.sep)) {
|
||||
return "app";
|
||||
}
|
||||
const parsed = path.parse(p);
|
||||
if (
|
||||
parsed.ext == ".go" ||
|
||||
@@ -179,7 +183,8 @@ export function getTypeStrFromPath(
|
||||
parsed.ext == ".py" ||
|
||||
parsed.ext == ".sql" ||
|
||||
parsed.ext == ".gql" ||
|
||||
parsed.ext == ".ps1"
|
||||
parsed.ext == ".ps1" ||
|
||||
parsed.ext == ".js"
|
||||
) {
|
||||
return "script";
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user