Compare commits
3 Commits
v1.324.2
...
rf/monacoU
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f46459844 | ||
|
|
f82dfa7e97 | ||
|
|
a605d79f21 |
1
.github/change-versions-mac.sh
vendored
1
.github/change-versions-mac.sh
vendored
@@ -16,7 +16,6 @@ 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,12 +12,10 @@ 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
|
||||
|
||||
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
@@ -136,7 +136,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud
|
||||
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
${{ steps.meta-ee-public.outputs.tags }}
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud
|
||||
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect
|
||||
PYTHON_IMAGE=python:3.12.2-slim-bookworm
|
||||
tags: |
|
||||
${{ steps.meta-ee-public-py312.outputs.tags }}
|
||||
|
||||
16
.github/workflows/gallery_on_release.yml
vendored
16
.github/workflows/gallery_on_release.yml
vendored
@@ -1,16 +0,0 @@
|
||||
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
16
.github/workflows/jsr_on_release.yml
vendored
@@ -1,16 +0,0 @@
|
||||
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
|
||||
1046
CHANGELOG.md
1046
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
20
Dockerfile
20
Dockerfile
@@ -12,14 +12,12 @@ RUN apt-get -y update \
|
||||
|
||||
RUN rustup component add rustfmt
|
||||
|
||||
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
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install cargo-chef
|
||||
|
||||
WORKDIR /windmill
|
||||
|
||||
ENV SQLX_OFFLINE=true
|
||||
# ENV CARGO_INCREMENTAL=1
|
||||
ENV CARGO_INCREMENTAL=1
|
||||
|
||||
FROM node:20-alpine as frontend
|
||||
|
||||
@@ -48,9 +46,7 @@ FROM rust_base AS planner
|
||||
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
|
||||
COPY ./backend ./
|
||||
|
||||
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
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM rust_base AS builder
|
||||
ARG features=""
|
||||
@@ -59,9 +55,7 @@ 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 --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
|
||||
RUN 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 ./
|
||||
@@ -70,9 +64,7 @@ COPY --from=frontend /frontend /frontend
|
||||
COPY --from=frontend /backend/windmill-api/openapi-deref.yaml ./windmill-api/openapi-deref.yaml
|
||||
COPY .git/ .git/
|
||||
|
||||
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"
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
|
||||
|
||||
|
||||
FROM ${DEBIAN_IMAGE} as downloader
|
||||
@@ -171,7 +163,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.7 /usr/local/bin/bun /usr/bin/bun
|
||||
COPY --from=oven/bun:1.1.0 /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/ powershell-client/ are
|
||||
snippets upon demand. The files under python-client/ deno-client/ go-client/ are
|
||||
Apache 2.0 Licensed.
|
||||
|
||||
The openapi files, including the OpenFlow spec is Apache 2.0 Licensed.
|
||||
|
||||
38
README.md
38
README.md
@@ -352,44 +352,6 @@ you to have it being synced automatically everyday.
|
||||
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
|
||||
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
|
||||
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
|
||||
| DISABLE_RESPONSE_LOGS | false | Disable response logs | Server |
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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, no_main_func, codebase) 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, $30)",
|
||||
"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)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -66,12 +66,10 @@
|
||||
"Bool",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar"
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8e7ff45c5378c3a3406ba94dc0653afa5d28c072203617c598caefaaf1bafcfb"
|
||||
"hash": "020d33ed5d47350b456783fd548422ea8dcf2d786d0e9fa849754db82c9fa378"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, running, is_flow_step FROM queue WHERE scheduled_for < now() AND workspace_id = $1 AND schedule_path IS NULL",
|
||||
"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",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -21,7 +21,8 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
@@ -30,5 +31,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "caeb49629b8673c1f1c84a6e40c3e2d2c3bc3fdbde530a0a6b6fd68a22b867c3"
|
||||
"hash": "18699cb0eca25b6bde05d81571dfdea8cafd0043634f61b0f652a93767c9c30a"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2",
|
||||
"query": "DELETE FROM folder WHERE name = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,5 +11,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0d7ba88a9810e434aa00fd63bbf416cbe222f2c67ccc8aa92e651c2bea4c2d7b"
|
||||
"hash": "26e4ec75366d1e46a98710f29066b40e66a802f98eeabbb3ae5bebe3aeb6b3f8"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
15
backend/.sqlx/query-61e9662fe42506131222412ab3de48cf6485dea10aa3a2f97c0fd6322a0cb17f.json
generated
Normal file
15
backend/.sqlx/query-61e9662fe42506131222412ab3de48cf6485dea10aa3a2f97c0fd6322a0cb17f.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "61e9662fe42506131222412ab3de48cf6485dea10aa3a2f97c0fd6322a0cb17f"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DROP INDEX CONCURRENTLY IF EXISTS labeled_jobs_on_jobs",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6f12be65a4fe3eb39292164363f557de9cef7017dcfbcd40370b849a288c52e3"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -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": "94ff696b4d3904e3823ef637fa8f1f0d0bdac01040c81b31514326417eb58cee"
|
||||
"hash": "7d93eb90163516718c85f28f8f05093133c5cbc96414000ac364583114d0ce77"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"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,16 +1,11 @@
|
||||
{
|
||||
"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) as min_started_at, now() AS now",
|
||||
"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)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "min_started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "now",
|
||||
"name": "coalesce",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
@@ -22,9 +17,8 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3901cce744c9b246b661c817e068bdb3b1ab504ff8070fcccf6c909ad75f1f6d"
|
||||
"hash": "abc7c72dfe9b01cde6f5b206300ed33e3d15b16bce2510160166a66fb7598e61"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false AND workspace_id = $2 AND is_flow_step = false",
|
||||
"query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,5 +11,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8d4235984f27d8b939ffd5c660d5b57dc38dd3b2643361ed3b7cdcd1534d2e21"
|
||||
"hash": "ade89de6e8527c543b182229f1febeb2513ad58b03ab526df148582264fb3a44"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT args FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT args FROM input WHERE id = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "args",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "bbd5f968d7b62a55a7ebf7b98cfd411678ce544fdb9472d8d223235fb2818aaf"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
976
backend/Cargo.lock
generated
976
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.324.2"
|
||||
version = "1.306.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -24,7 +24,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.324.2"
|
||||
version = "1.306.2"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -50,7 +50,6 @@ parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/p
|
||||
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus"]
|
||||
flow_testing = ["windmill-worker/flow_testing"]
|
||||
openidconnect = ["windmill-api/openidconnect"]
|
||||
cloud = ["windmill-queue/cloud"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -109,7 +108,7 @@ windmill-parser-sql = { path = "./parsers/windmill-parser-sql" }
|
||||
windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
|
||||
windmill-api-client = { path = "./windmill-api-client" }
|
||||
|
||||
axum = { version = "^0.7", features = ["multipart"] }
|
||||
axum = { version = "^0.7" }
|
||||
headers = "^0"
|
||||
hyper = { version = "^1", features = ["full"] }
|
||||
tokio = { version = "^1", features = ["full", "tracing"] }
|
||||
@@ -225,7 +224,7 @@ tokenizers = "0.14.1"
|
||||
candle-core = "0.3.0"
|
||||
candle-transformers = "0.3.0"
|
||||
candle-nn = "0.3.0"
|
||||
tiberius = { git = "https://github.com/prisma/tiberius", rev = "8f66a699dfa041e7b5f736c7e94f92c945453c9e", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"]}
|
||||
tiberius = { version = "0.12.2", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"] }
|
||||
pin-project = "1"
|
||||
indexmap = { version = "2.2.5", features = ["serde"]}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
644b6f49f087790a728a7a0a82525997b1742738
|
||||
66d9cbb158ab9a5869a45ba253bf57f2cbb5ecb6
|
||||
@@ -1 +0,0 @@
|
||||
-- Add down migration script here
|
||||
@@ -1,228 +0,0 @@
|
||||
|
||||
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[])));
|
||||
@@ -1 +0,0 @@
|
||||
-- Add down migration script here
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
-- Add down migration script here
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE script ADD COLUMN no_main_func BOOLEAN;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add down migration script here
|
||||
DROP INDEX IF EXISTS labeled_jobs_on_completed_jobs;
|
||||
@@ -1 +0,0 @@
|
||||
-- Add up migration script here
|
||||
@@ -1 +0,0 @@
|
||||
-- Add down migration script here
|
||||
@@ -1 +0,0 @@
|
||||
-- Add up migration script here
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE script DROP COLUMN codebase;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Add up migration script here
|
||||
|
||||
ALTER TABLE script ADD COLUMN codebase VARCHAR(255);
|
||||
@@ -141,16 +141,5 @@
|
||||
"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,9 +50,6 @@ 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",
|
||||
"github" => "PyGithub",
|
||||
};
|
||||
|
||||
fn replace_import(x: String) -> String {
|
||||
@@ -64,7 +61,7 @@ fn replace_import(x: String) -> String {
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"^\#\s?(\S+)\s*$").unwrap();
|
||||
static ref RE: Regex = Regex::new(r"^\#\s?(\S+)$").unwrap();
|
||||
}
|
||||
|
||||
fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<String> {
|
||||
|
||||
@@ -292,30 +292,22 @@ pub fn parse_pg_typ(typ: &str) -> Typ {
|
||||
Typ::List(Box::new(base_typ))
|
||||
} else {
|
||||
match typ {
|
||||
"varchar" | "character varying" => Typ::Str(None),
|
||||
"varchar" => Typ::Str(None),
|
||||
"text" => Typ::Str(None),
|
||||
"int" | "integer" | "int4" => Typ::Int,
|
||||
"int" => Typ::Int,
|
||||
"bigint" => Typ::Int,
|
||||
"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,
|
||||
"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,
|
||||
"oid" => Typ::Int,
|
||||
"date"
|
||||
| "time"
|
||||
| "timetz"
|
||||
| "time with time zone"
|
||||
| "time without time zone"
|
||||
| "timestamp"
|
||||
| "timestamptz"
|
||||
| "timestamp with time zone"
|
||||
| "timestamp without time zone" => Typ::Datetime,
|
||||
"date" | "time" | "timestamp" | "timestamptz" => Typ::Datetime,
|
||||
_ => Typ::Str(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"collaborators": [
|
||||
"Ruben Fiszel <ruben@windmill.dev>"
|
||||
],
|
||||
"version": "1.318.0",
|
||||
"version": "1.286.2",
|
||||
"files": [
|
||||
"windmill_parser_wasm_bg.wasm",
|
||||
"windmill_parser_wasm.js",
|
||||
|
||||
@@ -561,6 +561,10 @@ 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);
|
||||
};
|
||||
@@ -612,10 +616,6 @@ 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.
@@ -18,11 +18,11 @@ use tokio::fs::DirBuilder;
|
||||
use windmill_api::HTTP_CLIENT;
|
||||
use windmill_common::{
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
|
||||
CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, ENV_SETTINGS,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CUSTOM_TAGS_SETTING,
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING,
|
||||
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
|
||||
NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
|
||||
},
|
||||
@@ -47,11 +47,10 @@ use windmill_worker::{
|
||||
use crate::monitor::{
|
||||
initial_load, load_keep_job_dir, load_require_preexisting_user, load_tag_per_workspace_enabled,
|
||||
monitor_db, monitor_pool, reload_base_url_setting, reload_bunfig_install_scopes_setting,
|
||||
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
|
||||
reload_hub_base_url_setting, reload_job_default_timeout_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_pip_index_url_setting,
|
||||
reload_retention_period_setting, reload_scim_token_setting, reload_server_config,
|
||||
reload_worker_config,
|
||||
reload_extra_pip_index_url_setting, reload_hub_base_url_setting,
|
||||
reload_job_default_timeout_setting, reload_license_key, reload_npm_config_registry_setting,
|
||||
reload_pip_index_url_setting, reload_retention_period_setting, reload_scim_token_setting,
|
||||
reload_server_config, reload_worker_config,
|
||||
};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -504,11 +503,6 @@ Windmill Community Edition {GIT_VERSION}
|
||||
tracing::error!(error = %e, "Could not reload hub base url setting");
|
||||
}
|
||||
},
|
||||
CRITICAL_ERROR_CHANNELS_SETTING => {
|
||||
if let Err(e) = reload_critical_error_channels_setting(&db).await {
|
||||
tracing::error!(error = %e, "Could not reload critical error emails setting");
|
||||
}
|
||||
},
|
||||
a @_ => {
|
||||
tracing::info!("Unrecognized Global Setting Change Payload: {:?}", a);
|
||||
}
|
||||
|
||||
@@ -22,17 +22,15 @@ use windmill_api::{
|
||||
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN,
|
||||
};
|
||||
use windmill_common::{
|
||||
ee::CriticalErrorChannel,
|
||||
error,
|
||||
flow_status::FlowStatusModule,
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
|
||||
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
|
||||
},
|
||||
jobs::QueuedJob,
|
||||
oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH,
|
||||
@@ -42,8 +40,7 @@ use windmill_common::{
|
||||
load_worker_config, reload_custom_tags_setting, DEFAULT_TAGS_PER_WORKSPACE, SERVER_CONFIG,
|
||||
WORKER_CONFIG,
|
||||
},
|
||||
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL,
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
BASE_URL, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
};
|
||||
use windmill_queue::cancel_job;
|
||||
use windmill_worker::{
|
||||
@@ -146,10 +143,6 @@ pub async fn initial_load(
|
||||
tracing::error!("Error reloading hub base url: {:?}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = reload_critical_error_channels_setting(&db).await {
|
||||
tracing::error!("Could not reload critical error emails setting: {:?}", e);
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
if !_is_agent {
|
||||
reload_s3_cache_setting(&db).await;
|
||||
@@ -1081,27 +1074,3 @@ pub async fn reload_hub_base_url_setting(db: &DB, server_mode: bool) -> error::R
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_critical_error_channels_setting(db: &DB) -> error::Result<()> {
|
||||
let critical_error_channels =
|
||||
load_value_from_global_settings(db, CRITICAL_ERROR_CHANNELS_SETTING).await?;
|
||||
|
||||
let critical_error_channels = if let Some(q) = critical_error_channels {
|
||||
if let Ok(v) = serde_json::from_value::<Vec<CriticalErrorChannel>>(q.clone()) {
|
||||
v
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Could not parse critical_error_emails setting as an array of channels, found: {:#?}",
|
||||
&q
|
||||
);
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let mut l = CRITICAL_ERROR_CHANNELS.write().await;
|
||||
*l = critical_error_channels;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ if [ "$REVERT" == "YES" ]; then
|
||||
ce_file="${ee_file/${EE_CODE_DIR}/.}"
|
||||
ce_file="${root_dirpath}/backend/${ce_file}"
|
||||
if [ "$REVERT_PREVIOUS" == "YES" ]; then
|
||||
git checkout HEAD@{15} ${ce_file} || true
|
||||
git checkout HEAD@{5} ${ce_file} || true
|
||||
else
|
||||
git restore --staged ${ce_file} || true
|
||||
git restore ${ce_file} || true
|
||||
|
||||
@@ -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 *, result->'wm_labels' as labels FROM completed_job WHERE id = $1").bind(uuid)
|
||||
sqlx::query_as::<_, CompletedJob>("SELECT * FROM completed_job WHERE id = $1").bind(uuid)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -1663,7 +1663,6 @@ func main(derp string) (string, error) {
|
||||
.to_owned();
|
||||
|
||||
let result = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
lock: None,
|
||||
@@ -1695,7 +1694,6 @@ echo "hello $msg"
|
||||
.to_owned();
|
||||
|
||||
let job = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
lock: None,
|
||||
@@ -1724,7 +1722,6 @@ def main():
|
||||
.to_owned();
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
@@ -1759,7 +1756,6 @@ def main():
|
||||
.to_owned();
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
@@ -1793,7 +1789,6 @@ def main():
|
||||
.to_owned();
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: None,
|
||||
language: ScriptLang::Python3,
|
||||
@@ -3169,8 +3164,6 @@ 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,
|
||||
codebase: None
|
||||
},
|
||||
).await.unwrap();
|
||||
|
||||
@@ -3218,7 +3211,6 @@ async fn run_preview_relative_imports(db: &Pool<Postgres>, script_content: Strin
|
||||
let db2 = db.clone();
|
||||
in_test_worker(&db, async move {
|
||||
let job = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content: script_content,
|
||||
path: Some("f/system/test_import".to_string()),
|
||||
language,
|
||||
|
||||
@@ -72,6 +72,7 @@ async_zip.workspace = true
|
||||
rsmq_async.workspace = true
|
||||
regex.workspace = true
|
||||
bytes.workspace = true
|
||||
mail-send.workspace = true
|
||||
samael = { workspace = true, optional = true }
|
||||
async-recursion.workspace = true
|
||||
rsa.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.324.2
|
||||
version: 1.306.2
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -3401,13 +3401,6 @@ 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)
|
||||
@@ -4907,8 +4900,6 @@ paths:
|
||||
- language
|
||||
force_viewer_static_fields:
|
||||
type: object
|
||||
force_viewer_one_of_fields:
|
||||
type: object
|
||||
required:
|
||||
- args
|
||||
- component
|
||||
@@ -5250,11 +5241,6 @@ 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 queued jobs
|
||||
@@ -5340,7 +5326,6 @@ 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"
|
||||
@@ -5370,11 +5355,6 @@ 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
|
||||
@@ -5394,7 +5374,6 @@ 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"
|
||||
@@ -5437,11 +5416,6 @@ 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
|
||||
@@ -5560,26 +5534,6 @@ 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
|
||||
@@ -7271,26 +7225,6 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/Input"
|
||||
|
||||
/w/{workspace}/inputs/{jobOrInputId}/args:
|
||||
get:
|
||||
summary: Get args from history or saved input
|
||||
operationId: getArgsFromHistoryOrSavedInput
|
||||
tags:
|
||||
- input
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: jobOrInputId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: args
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/inputs/list:
|
||||
get:
|
||||
summary: List saved Inputs for a Runnable
|
||||
@@ -8071,12 +8005,6 @@ 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:
|
||||
@@ -8408,11 +8336,6 @@ components:
|
||||
type: boolean
|
||||
visible_to_runner_only:
|
||||
type: boolean
|
||||
no_main_func:
|
||||
type: boolean
|
||||
codebase:
|
||||
type: string
|
||||
|
||||
required:
|
||||
- hash
|
||||
- path
|
||||
@@ -8428,8 +8351,6 @@ components:
|
||||
- language
|
||||
- kind
|
||||
- starred
|
||||
- no_main_func
|
||||
|
||||
|
||||
NewScript:
|
||||
type: object
|
||||
@@ -8503,10 +8424,6 @@ components:
|
||||
type: string
|
||||
visible_to_runner_only:
|
||||
type: boolean
|
||||
no_main_func:
|
||||
type: boolean
|
||||
codebase:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- summary
|
||||
@@ -8808,10 +8725,6 @@ components:
|
||||
type: string
|
||||
priority:
|
||||
type: integer
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- created_by
|
||||
@@ -8829,21 +8742,15 @@ components:
|
||||
- tag
|
||||
|
||||
Job:
|
||||
oneOf:
|
||||
- allOf:
|
||||
allOf:
|
||||
- oneOf:
|
||||
- $ref: "#/components/schemas/CompletedJob"
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [CompletedJob]
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/QueuedJob"
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [QueuedJob]
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [CompletedJob, QueuedJob]
|
||||
discriminator:
|
||||
propertyName: type
|
||||
|
||||
@@ -9813,11 +9720,6 @@ components:
|
||||
- $ref: "../../openflow.openapi.yaml#/components/schemas/OpenFlow"
|
||||
- $ref: "#/components/schemas/FlowMetadata"
|
||||
|
||||
ExtraPerms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
|
||||
FlowMetadata:
|
||||
type: object
|
||||
properties:
|
||||
@@ -9833,7 +9735,9 @@ components:
|
||||
archived:
|
||||
type: boolean
|
||||
extra_perms:
|
||||
$ref: "#/components/schemas/ExtraPerms"
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
starred:
|
||||
type: boolean
|
||||
draft_only:
|
||||
@@ -9916,10 +9820,6 @@ components:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
triggerables_v2:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
execution_mode:
|
||||
type: string
|
||||
enum: [viewer, publisher, anonymous]
|
||||
@@ -10011,8 +9911,7 @@ components:
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
value:
|
||||
type: object
|
||||
value: {}
|
||||
policy:
|
||||
$ref: "#/components/schemas/Policy"
|
||||
execution_mode:
|
||||
|
||||
@@ -143,7 +143,6 @@ 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")]
|
||||
@@ -153,12 +152,6 @@ 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>,
|
||||
@@ -167,10 +160,7 @@ pub struct Policy {
|
||||
// - script/<path>
|
||||
// - flow/<path>
|
||||
// - rawscript/<sha256>
|
||||
#[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 triggerables: HashMap<String, StaticFields>,
|
||||
pub execution_mode: ExecutionMode,
|
||||
}
|
||||
|
||||
@@ -944,7 +934,6 @@ 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 {
|
||||
@@ -977,56 +966,39 @@ async fn execute_component(
|
||||
|
||||
let path = path.to_path();
|
||||
|
||||
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();
|
||||
let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields {
|
||||
let mut hm = HashMap::new();
|
||||
|
||||
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,
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
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)?
|
||||
Policy {
|
||||
execution_mode: ExecutionMode::Viewer,
|
||||
triggerables: hm,
|
||||
on_behalf_of: None,
|
||||
on_behalf_of_email: None,
|
||||
}
|
||||
} 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 {
|
||||
@@ -1161,106 +1133,22 @@ fn build_args(
|
||||
path: String,
|
||||
args: HashMap<String, Box<RawValue>>,
|
||||
) -> Result<PushArgs<HashMap<String, Box<RawValue>>>> {
|
||||
|
||||
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(",")
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// disallow var and res access in args coming from the user for security reasons
|
||||
let mut safe_args: HashMap<String, Box<RawValue>> = args.clone();
|
||||
for (k, v) in args {
|
||||
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 {
|
||||
let args_str = serde_json::to_string(&v).unwrap_or_else(|_| "".to_string());
|
||||
if args_str.contains("$var:") || args_str.contains("$res:") {
|
||||
safe_args.insert(
|
||||
k.to_string(),
|
||||
RawValue::from_string(
|
||||
arg_str
|
||||
args_str
|
||||
.replace(
|
||||
"$var:",
|
||||
"The following variable has been omitted for security reasons: ",
|
||||
"The following variable has been ommited for security reasons: ",
|
||||
)
|
||||
.replace(
|
||||
"$res:",
|
||||
"The following resource has been omitted for security reasons: ",
|
||||
"The following resource has been ommited for security reasons: ",
|
||||
),
|
||||
)
|
||||
.map_err(|e| {
|
||||
@@ -1272,8 +1160,24 @@ 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_inputs {
|
||||
for (k, v) in static_args {
|
||||
extra.insert(k.to_string(), v.to_owned());
|
||||
}
|
||||
Ok(PushArgs { extra, args: sqlx::types::Json(safe_args) })
|
||||
|
||||
@@ -136,15 +136,6 @@ 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
|
||||
|
||||
@@ -543,7 +543,7 @@ async fn update_flow(
|
||||
clear_schedule(tx.transaction_mut(), &schedule.path, &w_id).await?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,7 +955,7 @@ mod tests {
|
||||
continue_on_error: None,
|
||||
},
|
||||
],
|
||||
failure_module: Some(Box::new(FlowModule {
|
||||
failure_module: Some(FlowModule {
|
||||
id: "d".to_string(),
|
||||
value: FlowModuleValue::Script {
|
||||
path: "test".to_string(),
|
||||
@@ -977,7 +977,7 @@ mod tests {
|
||||
priority: None,
|
||||
delete_after_use: None,
|
||||
continue_on_error: None,
|
||||
})),
|
||||
}),
|
||||
same_worker: false,
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
|
||||
@@ -271,7 +271,7 @@ async fn update_folder(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Json(mut ng): Json<UpdateFolder>,
|
||||
Json(ng): Json<UpdateFolder>,
|
||||
) -> Result<String> {
|
||||
use sql_builder::prelude::*;
|
||||
|
||||
@@ -282,25 +282,6 @@ async fn update_folder(
|
||||
if let Some(display_name) = ng.display_name {
|
||||
sqlb.set("display_name", "?".bind(&display_name));
|
||||
}
|
||||
|
||||
if !authed.is_admin {
|
||||
let prefixed_username = format!("u/{}", authed.username);
|
||||
if ng.owners.as_ref().is_some_and(|x| {
|
||||
!x.contains(&prefixed_username)
|
||||
&& !authed.groups.iter().any(|g| x.contains(&format!("g/{g}")))
|
||||
}) {
|
||||
ng.owners.as_mut().unwrap().push(prefixed_username.clone());
|
||||
if ng.extra_perms.is_none() {
|
||||
ng.extra_perms = Some(serde_json::Value::Object(serde_json::Map::new()));
|
||||
}
|
||||
ng.extra_perms
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(prefixed_username, serde_json::json!(true));
|
||||
}
|
||||
}
|
||||
if let Some(owners) = ng.owners {
|
||||
sqlb.set(
|
||||
"owners",
|
||||
@@ -329,16 +310,9 @@ async fn update_folder(
|
||||
.sql()
|
||||
.map_err(|e| error::Error::InternalErr(e.to_string()))?;
|
||||
let nfolder = sqlx::query_as::<_, Folder>(&sql)
|
||||
.fetch_optional(&mut *tx)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let nfolder = nfolder.ok_or_else(|| {
|
||||
windmill_common::error::Error::NotAuthorized(format!(
|
||||
"You are not an owner of {} and hence cannot modify it",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
|
||||
if let Some(extra_perms) = nfolder.extra_perms.as_object() {
|
||||
for o in nfolder.owners {
|
||||
if !extra_perms
|
||||
@@ -504,22 +478,13 @@ async fn delete_folder(
|
||||
|
||||
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
|
||||
|
||||
let del = sqlx::query_scalar!(
|
||||
"DELETE FROM folder WHERE name = $1 AND workspace_id = $2 RETURNING 1",
|
||||
sqlx::query!(
|
||||
"DELETE FROM folder WHERE name = $1 AND workspace_id = $2",
|
||||
name,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
if del.is_none() {
|
||||
return Err(windmill_common::error::Error::NotAuthorized(format!(
|
||||
"Not authorized to delete folder {}",
|
||||
name
|
||||
)));
|
||||
}
|
||||
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed.username,
|
||||
|
||||
@@ -25,7 +25,7 @@ use windmill_common::{
|
||||
error::JsonResult,
|
||||
jobs::JobKind,
|
||||
scripts::to_i64,
|
||||
utils::{not_found_if_none, paginate, Pagination},
|
||||
utils::{paginate, Pagination},
|
||||
};
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
@@ -34,10 +34,6 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/create", post(create_input))
|
||||
.route("/update", post(update_input))
|
||||
.route("/delete/:id", post(delete_input))
|
||||
.route(
|
||||
"/:job_or_input_id/args",
|
||||
get(get_args_from_history_or_saved_input),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
|
||||
@@ -127,7 +123,7 @@ async fn get_input_history(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let sql = &format!(
|
||||
"select id, created_at, created_by, CASE WHEN args is null or pg_column_size(args) < 40000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, success from completed_job \
|
||||
"select id, created_at, created_by, args, success from completed_job \
|
||||
where {} = $1 and job_kind = $2 and workspace_id = $3 \
|
||||
order by created_at desc limit $4 offset $5",
|
||||
r.runnable_type.column_name()
|
||||
@@ -171,27 +167,6 @@ async fn get_input_history(
|
||||
Ok(Json(inputs))
|
||||
}
|
||||
|
||||
async fn get_args_from_history_or_saved_input(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, job_or_input_id)): Path<(String, Uuid)>,
|
||||
) -> JsonResult<Option<Value>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let result_o = sqlx::query_scalar!(
|
||||
"SELECT args FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT args FROM input WHERE id = $1 AND workspace_id = $2",
|
||||
job_or_input_id,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let result = not_found_if_none(result_o, "Input args", job_or_input_id.to_string())?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
async fn list_saved_inputs(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -204,7 +179,7 @@ async fn list_saved_inputs(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let rows = sqlx::query_as::<_, InputRow>(
|
||||
"select id, workspace_id, runnable_id, runnable_type, name, CASE WHEN pg_column_size(args) < 40000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, created_at, created_by, is_public from input \
|
||||
"select * from input \
|
||||
where runnable_id = $1 and runnable_type = $2 and workspace_id = $3 \
|
||||
and (is_public IS true OR created_by = $4) \
|
||||
order by created_at desc limit $5 offset $6",
|
||||
|
||||
@@ -19,9 +19,6 @@ use windmill_common::jobs::{
|
||||
format_completed_job_result, format_result, CompletedJobWithFormattedResult, FormattedResult,
|
||||
ENTRYPOINT_OVERRIDE,
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
|
||||
use windmill_common::variables::get_workspace_key;
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
@@ -166,7 +163,6 @@ pub fn workspaced_service() -> Router {
|
||||
.layer(cors.clone()),
|
||||
)
|
||||
.route("/run/preview", post(run_preview_script))
|
||||
.route("/run/preview_bundle", post(run_bundle_preview_script))
|
||||
.route("/add_batch_jobs/:n", post(add_batch_jobs))
|
||||
.route("/run/preview_flow", post(run_preview_flow_job))
|
||||
.route(
|
||||
@@ -255,7 +251,6 @@ 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",
|
||||
@@ -589,7 +584,6 @@ 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,
|
||||
@@ -784,8 +778,6 @@ 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)]
|
||||
@@ -846,7 +838,6 @@ 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 {
|
||||
@@ -951,10 +942,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -1025,62 +1012,49 @@ async fn cancel_all(
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
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",
|
||||
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",
|
||||
w_id,
|
||||
authed.username
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
let username = authed.username;
|
||||
let mut uuids = vec![];
|
||||
for j in jobs.iter() {
|
||||
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 !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 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);
|
||||
}
|
||||
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))
|
||||
}
|
||||
@@ -1183,14 +1157,13 @@ 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() && lq.label.is_none() {
|
||||
let sql = if lq.success.is_none() {
|
||||
let sqlq = list_queue_jobs_query(
|
||||
&w_id,
|
||||
&ListQueueQuery {
|
||||
@@ -1216,7 +1189,6 @@ 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",
|
||||
@@ -1250,7 +1222,6 @@ async fn list_jobs(
|
||||
"concurrent_limit",
|
||||
"concurrency_time_window_s",
|
||||
"priority",
|
||||
"null as labels",
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1983,7 +1954,6 @@ 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 {
|
||||
@@ -2021,7 +1991,6 @@ 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,
|
||||
@@ -2081,9 +2050,7 @@ enum PreviewKind {
|
||||
Identity,
|
||||
Http,
|
||||
Noop,
|
||||
Bundle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Preview {
|
||||
content: Option<String>,
|
||||
@@ -2304,7 +2271,7 @@ pub async fn restart_flow(
|
||||
check_license_key_valid().await?;
|
||||
|
||||
let completed_job = sqlx::query_as::<_, CompletedJob>(
|
||||
"SELECT *, result->'wm_labels' as labels from completed_job WHERE id = $1 and workspace_id = $2",
|
||||
"SELECT * from completed_job WHERE id = $1 and workspace_id = $2",
|
||||
)
|
||||
.bind(job_id)
|
||||
.bind(&w_id)
|
||||
@@ -2426,7 +2393,6 @@ pub async fn run_workflow_as_code(
|
||||
let (job_payload, tag, _delete_after_use, timeout) = match job.job_kind {
|
||||
JobKind::Preview => (
|
||||
JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content: job.raw_code.unwrap_or_default(),
|
||||
path: job.script_path,
|
||||
language: job.language.unwrap_or_else(|| ScriptLang::Deno),
|
||||
@@ -3090,7 +3056,6 @@ async fn run_preview_script(
|
||||
Some(PreviewKind::Identity) => JobPayload::Identity,
|
||||
Some(PreviewKind::Noop) => JobPayload::Noop,
|
||||
_ => JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content: preview.content.unwrap_or_default(),
|
||||
path: preview.path,
|
||||
language: preview.language.unwrap_or(ScriptLang::Deno),
|
||||
@@ -3125,136 +3090,6 @@ async fn run_preview_script(
|
||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
async fn run_bundle_preview_script(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
mut multipart: axum::extract::Multipart,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
check_license_key_valid().await?;
|
||||
|
||||
check_scopes(&authed, || format!("runscript"))?;
|
||||
if authed.is_operator {
|
||||
return Err(error::Error::NotAuthorized(
|
||||
"Operators cannot run preview jobs for security reasons".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut job_id = None;
|
||||
let mut tx = None;
|
||||
let mut uploaded = false;
|
||||
while let Some(field) = multipart.next_field().await.unwrap() {
|
||||
let name = field.name().unwrap().to_string();
|
||||
let data = field.bytes().await.unwrap();
|
||||
if name == "preview" {
|
||||
let preview: Preview = serde_json::from_slice(&data).map_err(to_anyhow)?;
|
||||
|
||||
let scheduled_for = run_query.get_scheduled_for(&db).await?;
|
||||
let tag = run_query.tag.clone().or(preview.tag.clone());
|
||||
check_tag_available_for_workspace(&w_id, &tag).await?;
|
||||
let ltx =
|
||||
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq.clone());
|
||||
|
||||
let args = preview.args.unwrap_or_default();
|
||||
|
||||
// hmap.insert("")
|
||||
let (uuid, ntx) = push(
|
||||
&db,
|
||||
ltx,
|
||||
&w_id,
|
||||
match preview.kind {
|
||||
Some(PreviewKind::Identity) => JobPayload::Identity,
|
||||
Some(PreviewKind::Noop) => JobPayload::Noop,
|
||||
_ => JobPayload::Code(RawCode {
|
||||
hash: Some(PREVIEW_IS_CODEBASE_HASH),
|
||||
content: preview.content.unwrap_or_default(),
|
||||
path: preview.path,
|
||||
language: preview.language.unwrap_or(ScriptLang::Deno),
|
||||
lock: preview.lock,
|
||||
concurrent_limit: None, // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here
|
||||
concurrency_time_window_s: None, // TODO(gbouv): same as above
|
||||
cache_ttl: None,
|
||||
dedicated_worker: preview.dedicated_worker,
|
||||
}),
|
||||
},
|
||||
args,
|
||||
authed.display_username(),
|
||||
&authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
scheduled_for,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
run_query.job_id,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
true,
|
||||
tag,
|
||||
run_query.timeout,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
job_id = Some(uuid);
|
||||
tx = Some(ntx);
|
||||
}
|
||||
if name == "file" {
|
||||
let id = job_id
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(
|
||||
"script need to be passed first in the multipart upload".to_string(),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
uploaded = true;
|
||||
|
||||
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
{
|
||||
let path = windmill_common::s3_helpers::bundle(&w_id, &id);
|
||||
if let Err(e) = os
|
||||
.put(&object_store::path::Path::from(path.clone()), data)
|
||||
.await
|
||||
{
|
||||
tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e);
|
||||
return Err(Error::ExecutionErr(format!("Failed to put {path} to s3")));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string()));
|
||||
}
|
||||
}
|
||||
// println!("Length of `{}` is {} bytes", name, data.len());
|
||||
}
|
||||
if !uploaded {
|
||||
return Err(Error::BadRequest("No file uploaded".to_string()));
|
||||
}
|
||||
if job_id.is_none() {
|
||||
return Err(Error::BadRequest(
|
||||
"No script found in the uploaded file".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
tx.unwrap().commit().await?;
|
||||
|
||||
Ok((StatusCode::CREATED, job_id.unwrap().to_string()))
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
async fn run_bundle_preview_script() -> error::Result<(StatusCode, String)> {
|
||||
return Err(Error::BadRequest(
|
||||
"bundle preview is an ee feature".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RunDependenciesRequest {
|
||||
pub raw_scripts: Vec<RawScriptForDependencies>,
|
||||
@@ -3649,84 +3484,6 @@ 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)>,
|
||||
@@ -3883,17 +3640,6 @@ 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)]
|
||||
@@ -3925,8 +3671,6 @@ 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(
|
||||
@@ -3974,7 +3718,6 @@ async fn list_completed_jobs(
|
||||
"mem_peak",
|
||||
"tag",
|
||||
"priority",
|
||||
"result->'wm_labels' as labels",
|
||||
"'CompletedJob' as type",
|
||||
],
|
||||
)
|
||||
@@ -3992,7 +3735,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, result->'wm_labels' as labels FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
raw_lock, email, visible_to_owner, mem_peak, tag, priority FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
.bind(id)
|
||||
.bind(w_id)
|
||||
.fetch_optional(&db)
|
||||
|
||||
@@ -25,7 +25,6 @@ use db::DB;
|
||||
use git_version::git_version;
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use tokio::sync::RwLock;
|
||||
use tower::ServiceBuilder;
|
||||
@@ -105,8 +104,6 @@ lazy_static::lazy_static! {
|
||||
|
||||
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||
.user_agent("windmill/beta")
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())
|
||||
.build().unwrap();
|
||||
|
||||
@@ -144,12 +141,14 @@ pub async fn run_server(
|
||||
));
|
||||
let argon2 = Arc::new(Argon2::default());
|
||||
|
||||
let disable_response_logs = std::env::var("DISABLE_RESPONSE_LOGS")
|
||||
.ok()
|
||||
.map(|x| x == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
let middleware_stack = ServiceBuilder::new()
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.on_response(MyOnResponse {})
|
||||
.make_span_with(MyMakeSpan {})
|
||||
.on_request(())
|
||||
.on_failure(MyOnFailure {}),
|
||||
)
|
||||
.layer(Extension(db.clone()))
|
||||
.layer(Extension(rsmq))
|
||||
.layer(Extension(user_db))
|
||||
@@ -286,18 +285,6 @@ pub async fn run_server(
|
||||
.fallback(static_assets::static_handler)
|
||||
.layer(middleware_stack);
|
||||
|
||||
let app = if disable_response_logs {
|
||||
app
|
||||
} else {
|
||||
app.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.on_response(MyOnResponse {})
|
||||
.make_span_with(MyMakeSpan {})
|
||||
.on_request(())
|
||||
.on_failure(MyOnFailure {}),
|
||||
)
|
||||
};
|
||||
|
||||
let instance_name = rd_string(5);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
@@ -333,7 +320,6 @@ async fn is_up_to_date() -> Result<String, AppError> {
|
||||
let error_reading_version = || anyhow::anyhow!("Error reading latest released version");
|
||||
let version = HTTP_CLIENT
|
||||
.get("https://api.github.com/repos/windmill-labs/windmill/releases/latest")
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await
|
||||
.context("Impossible to reach api.github")?
|
||||
|
||||
@@ -221,7 +221,7 @@ async fn create_schedule(
|
||||
.await?;
|
||||
|
||||
if ns.enabled.unwrap_or(true) {
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -303,7 +303,7 @@ async fn edit_schedule(
|
||||
.await?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -512,7 +512,7 @@ pub async fn set_enabled(
|
||||
.await?;
|
||||
|
||||
if payload.enabled {
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -560,7 +560,7 @@ pub async fn set_enabled(
|
||||
// .await?;
|
||||
|
||||
// if payload.enabled {
|
||||
// tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
// tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
// }
|
||||
// tx.commit().await?;
|
||||
|
||||
@@ -581,37 +581,14 @@ async fn delete_schedule(
|
||||
let path = path.to_path();
|
||||
|
||||
clear_schedule(&mut tx, path, &w_id).await?;
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.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
|
||||
)));
|
||||
}
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
@@ -800,7 +777,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 AND is_flow_step = false",
|
||||
"DELETE FROM queue WHERE schedule_path = $1 AND running = false AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
|
||||
@@ -13,9 +13,6 @@ use crate::{
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
HTTP_CLIENT,
|
||||
};
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use axum::extract::Multipart;
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
response::IntoResponse,
|
||||
@@ -34,10 +31,6 @@ use std::{
|
||||
};
|
||||
use windmill_audit::audit_ee::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use windmill_common::error::to_anyhow;
|
||||
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, JsonResult, Result},
|
||||
@@ -98,8 +91,6 @@ 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 {
|
||||
@@ -123,7 +114,6 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/list", get(list_scripts))
|
||||
.route("/list_search", get(list_search_scripts))
|
||||
.route("/create", post(create_script))
|
||||
.route("/create_snapshot", post(create_snapshot_script))
|
||||
.route("/archive/p/*path", post(archive_script_by_path))
|
||||
.route("/get/draft/*path", get(get_script_by_path_w_draft))
|
||||
.route("/get/p/*path", get(get_script_by_path))
|
||||
@@ -202,9 +192,7 @@ async fn list_scripts(
|
||||
"tag",
|
||||
"draft.path IS NOT NULL as has_draft",
|
||||
"draft_only",
|
||||
"ws_error_handler_muted",
|
||||
"no_main_func",
|
||||
"codebase IS NOT NULL as use_codebase"
|
||||
"ws_error_handler_muted"
|
||||
])
|
||||
.left()
|
||||
.join("favorite")
|
||||
@@ -224,10 +212,6 @@ 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",
|
||||
@@ -321,82 +305,6 @@ fn hash_script(ns: &NewScript) -> i64 {
|
||||
dh.finish() as i64
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
async fn create_snapshot_script() -> Result<(StatusCode, String)> {
|
||||
Err(Error::BadRequest("Upgrade to EE to use bundle".to_string()))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
async fn create_snapshot_script(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
let mut script_hash = None;
|
||||
let mut tx = None;
|
||||
let mut uploaded = false;
|
||||
while let Some(field) = multipart.next_field().await.unwrap() {
|
||||
let name = field.name().unwrap().to_string();
|
||||
let data = field.bytes().await.unwrap();
|
||||
if name == "script" {
|
||||
let ns = Some(serde_json::from_slice(&data).map_err(to_anyhow)?);
|
||||
let (new_hash, ntx) = create_script_internal(
|
||||
ns.unwrap(),
|
||||
w_id.clone(),
|
||||
authed.clone(),
|
||||
db.clone(),
|
||||
rsmq.clone(),
|
||||
user_db.clone(),
|
||||
webhook.clone(),
|
||||
)
|
||||
.await?;
|
||||
script_hash = Some(new_hash.to_string());
|
||||
tx = Some(ntx);
|
||||
}
|
||||
if name == "file" {
|
||||
let hash = script_hash.as_ref().ok_or_else(|| {
|
||||
Error::BadRequest(
|
||||
"script need to be passed first in the multipart upload".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
uploaded = true;
|
||||
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
{
|
||||
let path = windmill_common::s3_helpers::bundle(&w_id, &hash);
|
||||
if let Err(e) = os
|
||||
.put(&object_store::path::Path::from(path.clone()), data)
|
||||
.await
|
||||
{
|
||||
tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e);
|
||||
return Err(Error::ExecutionErr(format!("Failed to put {path} to s3")));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string()));
|
||||
}
|
||||
}
|
||||
// println!("Length of `{}` is {} bytes", name, data.len());
|
||||
}
|
||||
if !uploaded {
|
||||
return Err(Error::BadRequest("No file uploaded".to_string()));
|
||||
}
|
||||
if script_hash.is_none() {
|
||||
return Err(Error::BadRequest(
|
||||
"No script found in the uploaded file".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
tx.unwrap().commit().await?;
|
||||
return Ok((StatusCode::CREATED, format!("{}", script_hash.unwrap())));
|
||||
}
|
||||
|
||||
async fn create_script(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -406,24 +314,6 @@ async fn create_script(
|
||||
Path(w_id): Path<String>,
|
||||
Json(ns): Json<NewScript>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
let (hash, tx) = create_script_internal(ns, w_id, authed, db, rsmq, user_db, webhook).await?;
|
||||
tx.commit().await?;
|
||||
Ok((StatusCode::CREATED, format!("{}", hash)))
|
||||
}
|
||||
|
||||
async fn create_script_internal<'c>(
|
||||
ns: NewScript,
|
||||
w_id: String,
|
||||
authed: ApiAuthed,
|
||||
db: sqlx::Pool<Postgres>,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
user_db: UserDB,
|
||||
webhook: WebhookShared,
|
||||
) -> Result<(
|
||||
ScriptHash,
|
||||
QueueTransaction<'c, rsmq_async::MultiplexedRsmq>,
|
||||
)> {
|
||||
let codebase = ns.codebase.as_ref();
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if ns.ws_error_handler_muted.is_some_and(|val| val) {
|
||||
return Err(Error::BadRequest(
|
||||
@@ -431,10 +321,12 @@ async fn create_script_internal<'c>(
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let script_path = ns.path.clone();
|
||||
let hash = ScriptHash(hash_script(&ns));
|
||||
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
|
||||
let mut tx: QueueTransaction<'_, _> = (rsmq.clone(), user_db.begin(&authed).await?).into();
|
||||
|
||||
if sqlx::query_scalar!(
|
||||
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
hash.0,
|
||||
@@ -450,6 +342,7 @@ async fn create_script_internal<'c>(
|
||||
.to_owned(),
|
||||
));
|
||||
};
|
||||
|
||||
let clashing_script = sqlx::query_as::<_, Script>(
|
||||
"SELECT * FROM script WHERE path = $1 AND archived = false AND workspace_id = $2",
|
||||
)
|
||||
@@ -457,6 +350,7 @@ async fn create_script_internal<'c>(
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
|
||||
struct ParentInfo {
|
||||
p_hashes: Vec<i64>,
|
||||
perms: serde_json::Value,
|
||||
@@ -549,11 +443,13 @@ async fn create_script_internal<'c>(
|
||||
r
|
||||
}
|
||||
}?;
|
||||
|
||||
let p_hashes = parent_hashes_and_perms.as_ref().map(|v| &v.p_hashes[..]);
|
||||
let extra_perms = parent_hashes_and_perms
|
||||
.as_ref()
|
||||
.map(|v| v.perms.clone())
|
||||
.unwrap_or(json!({}));
|
||||
|
||||
let lock = if !(ns.language == ScriptLang::Python3
|
||||
|| ns.language == ScriptLang::Go
|
||||
|| ns.language == ScriptLang::Bun
|
||||
@@ -564,20 +460,23 @@ async fn create_script_internal<'c>(
|
||||
ns.lock
|
||||
.and_then(|e| if e.is_empty() { None } else { Some(e) })
|
||||
};
|
||||
|
||||
let needs_lock_gen = lock.is_none();
|
||||
|
||||
let envs = ns.envs.as_ref().map(|x| x.as_slice());
|
||||
let envs = if ns.envs.is_none() || ns.envs.as_ref().unwrap().is_empty() {
|
||||
None
|
||||
} else {
|
||||
envs
|
||||
};
|
||||
//::text::json is to ensure we use serde_json with preserve order
|
||||
sqlx::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, codebase) \
|
||||
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, $30)",
|
||||
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)",
|
||||
&w_id,
|
||||
&hash.0,
|
||||
ns.path,
|
||||
@@ -606,11 +505,10 @@ async fn create_script_internal<'c>(
|
||||
ns.timeout,
|
||||
ns.concurrency_key,
|
||||
ns.visible_to_runner_only,
|
||||
ns.no_main_func,
|
||||
codebase
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
let p_path_opt = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone());
|
||||
if let Some(ref p_path) = p_path_opt {
|
||||
sqlx::query!(
|
||||
@@ -648,7 +546,7 @@ async fn create_script_internal<'c>(
|
||||
clear_schedule(tx.transaction_mut(), &schedule.path, &w_id).await?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -660,6 +558,7 @@ async fn create_script_internal<'c>(
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {
|
||||
audit_log(
|
||||
&mut tx,
|
||||
@@ -705,6 +604,7 @@ async fn create_script_internal<'c>(
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let permissioned_as = username_to_permissioned_as(&authed.username);
|
||||
if needs_lock_gen {
|
||||
let tag = if ns.dedicated_worker.is_some_and(|x| x) {
|
||||
@@ -753,7 +653,7 @@ async fn create_script_internal<'c>(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok((hash, new_tx))
|
||||
new_tx.commit().await?;
|
||||
} else {
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
@@ -770,8 +670,10 @@ async fn create_script_internal<'c>(
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
Ok((hash, tx))
|
||||
tx.commit().await?;
|
||||
}
|
||||
|
||||
Ok((StatusCode::CREATED, format!("{}", hash)))
|
||||
}
|
||||
|
||||
pub async fn get_hub_script_by_path(
|
||||
@@ -822,7 +724,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, no_main_func 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 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 \
|
||||
|
||||
@@ -21,17 +21,15 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
|
||||
use mail_send::{mail_builder::MessageBuilder, SmtpClientBuilder};
|
||||
use serde::Deserialize;
|
||||
use tokio::time::timeout;
|
||||
use windmill_common::{
|
||||
error::{self, JsonResult, Result},
|
||||
error::{self, to_anyhow, JsonResult, Result},
|
||||
global_settings::{AUTOMATE_USERNAME_CREATION_SETTING, ENV_SETTINGS, HUB_BASE_URL_SETTING},
|
||||
server::Smtp,
|
||||
utils::send_email,
|
||||
};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::error::to_anyhow;
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
#[warn(unused_mut)]
|
||||
let r = Router::new()
|
||||
@@ -69,17 +67,34 @@ pub async fn test_email(
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let smtp = test_email.smtp;
|
||||
let to = test_email.to;
|
||||
|
||||
let client_timeout = Duration::from_secs(3);
|
||||
send_email(
|
||||
"Test email from Windmill",
|
||||
"Test email content",
|
||||
vec![to],
|
||||
smtp,
|
||||
Some(client_timeout),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut client = SmtpClientBuilder::new(smtp.host, smtp.port)
|
||||
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
|
||||
if std::env::var("ACCEPT_INVALID_CERTS").is_ok() {
|
||||
client = client.allow_invalid_certs();
|
||||
}
|
||||
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
|
||||
if !username.is_empty() {
|
||||
client.credentials((username, password))
|
||||
} else {
|
||||
client
|
||||
}
|
||||
} else {
|
||||
client
|
||||
};
|
||||
let message = MessageBuilder::new()
|
||||
.from(("Windmill", smtp.from.as_str()))
|
||||
.to(to.clone())
|
||||
.subject("Test email from Windmill")
|
||||
.text_body("Test email content");
|
||||
let dur = Duration::from_secs(3);
|
||||
timeout(dur, client.connect())
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
.map_err(to_anyhow)?
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
tracing::info!("Sent test email to {to}");
|
||||
Ok("Sent test email".to_string())
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ use axum::{
|
||||
};
|
||||
use hyper::{header::LOCATION, StatusCode};
|
||||
use lazy_static::lazy_static;
|
||||
use mail_send::mail_builder::MessageBuilder;
|
||||
use mail_send::SmtpClientBuilder;
|
||||
use quick_cache::sync::Cache;
|
||||
use rand::rngs::OsRng;
|
||||
use regex::Regex;
|
||||
@@ -44,11 +46,10 @@ use windmill_audit::audit_ee::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING;
|
||||
use windmill_common::users::truncate_token;
|
||||
use windmill_common::utils::send_email;
|
||||
use windmill_common::worker::{CLOUD_HOSTED, SERVER_CONFIG};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{self, Error, JsonResult, Result},
|
||||
error::{self, to_anyhow, Error, JsonResult, Result},
|
||||
users::SUPERADMIN_SECRET_EMAIL,
|
||||
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
|
||||
};
|
||||
@@ -1336,25 +1337,6 @@ 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,
|
||||
@@ -1823,15 +1805,41 @@ pub fn send_email_if_possible(subject: &str, content: &str, to: &str) {
|
||||
let content = content.to_string();
|
||||
let to = to.to_string();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = send_email_if_possible_intern(&subject, &content, to.clone()).await {
|
||||
tracing::error!("Failed to send email to {}: {}", to, e);
|
||||
if let Err(e) = send_email_if_possible_intern(&subject, &content, &to).await {
|
||||
tracing::error!("Failed to send email to {}: {}", &to, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn send_email_if_possible_intern(subject: &str, content: &str, to: String) -> Result<()> {
|
||||
pub async fn send_email_if_possible_intern(subject: &str, content: &str, to: &str) -> Result<()> {
|
||||
if let Some(smtp) = SERVER_CONFIG.read().await.smtp.clone() {
|
||||
send_email(subject, content, vec![to], smtp, None).await?;
|
||||
let mut client = SmtpClientBuilder::new(smtp.host, smtp.port)
|
||||
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
|
||||
if std::env::var("ACCEPT_INVALID_CERTS").is_ok() {
|
||||
client = client.allow_invalid_certs();
|
||||
}
|
||||
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
|
||||
if !username.is_empty() {
|
||||
client.credentials((username, password))
|
||||
} else {
|
||||
client
|
||||
}
|
||||
} else {
|
||||
client
|
||||
};
|
||||
let message = MessageBuilder::new()
|
||||
.from(("Windmill", smtp.from.as_str()))
|
||||
.to(to)
|
||||
.subject(subject)
|
||||
.text_body(content);
|
||||
client
|
||||
.connect()
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
tracing::info!("Sent email to {to}: {subject}");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ impl WebhookShared {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<WebhookPayload>();
|
||||
let _process = tokio::spawn(async move {
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
// TODO: investigate pool timeouts and such if TCP load is high
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
|
||||
@@ -53,6 +53,7 @@ use windmill_common::{
|
||||
variables::ExportableListableVariable,
|
||||
};
|
||||
use windmill_git_sync::handle_deployment_metadata;
|
||||
use windmill_queue::QueueTransaction;
|
||||
|
||||
use crate::oauth2_ee::InstanceEvent;
|
||||
use crate::variables::{decrypt, encrypt};
|
||||
@@ -487,25 +488,25 @@ async fn run_slack_message_test_job(
|
||||
json!(format!("$res:{WORKSPACE_SLACK_BOT_TOKEN_PATH}")),
|
||||
);
|
||||
|
||||
let uuid = windmill_queue::push_error_handler(
|
||||
let tx: QueueTransaction<'_, _> = (rsmq.clone(), db.begin().await?).into();
|
||||
let (uuid, tx) = windmill_queue::handle_on_failure(
|
||||
&db,
|
||||
rsmq,
|
||||
tx,
|
||||
Uuid::parse_str("00000000-0000-0000-0000-000000000000")?,
|
||||
None,
|
||||
Some("slack_message_test".to_string()),
|
||||
"slack_message_test",
|
||||
"slack_message_test",
|
||||
false,
|
||||
w_id.as_str(),
|
||||
&format!("script/{}", req.hub_script_path.as_str()),
|
||||
sqlx::types::Json(&fake_result),
|
||||
None,
|
||||
Some(Utc::now()),
|
||||
0,
|
||||
Utc::now(),
|
||||
Some(json!(extra_args)),
|
||||
authed.email.as_str(),
|
||||
false,
|
||||
false,
|
||||
None, // Note: we could mark it as high priority to return result quickly to the user
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(RunSlackMessageTestJobResponse {
|
||||
job_uuid: uuid.to_string(),
|
||||
@@ -721,16 +722,6 @@ 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!(
|
||||
@@ -1945,22 +1936,6 @@ 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)
|
||||
@@ -2071,14 +2046,6 @@ 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",
|
||||
@@ -2216,10 +2183,6 @@ 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>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub codebase: Option<String>,
|
||||
}
|
||||
|
||||
pub fn is_none_or_false(val: &Option<bool>) -> bool {
|
||||
@@ -2496,8 +2459,6 @@ 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,
|
||||
codebase: script.codebase,
|
||||
};
|
||||
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
|
||||
archive
|
||||
|
||||
@@ -10,7 +10,7 @@ enterprise = []
|
||||
prometheus = ["dep:prometheus"]
|
||||
flamegraph = ["dep:tracing-flame"]
|
||||
loki = ["dep:tracing-loki"]
|
||||
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts", "dep:bytes"]
|
||||
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts"]
|
||||
|
||||
[lib]
|
||||
name = "windmill_common"
|
||||
@@ -46,6 +46,4 @@ object_store = { workspace = true, optional = true }
|
||||
prometheus = { workspace = true, optional = true }
|
||||
aws-config = { workspace = true, optional = true }
|
||||
aws-sdk-sts = { workspace = true, optional = true }
|
||||
indexmap.workspace = true
|
||||
bytes = { workspace = true, optional = true }
|
||||
mail-send.workspace = true
|
||||
indexmap.workspace = true
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::ee::LicensePlan::Community;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -19,11 +18,3 @@ pub async fn get_license_plan() -> LicensePlan {
|
||||
// Implementation is not open source
|
||||
return Community;
|
||||
}
|
||||
|
||||
pub async fn trigger_critical_error_channels(_x: String) {
|
||||
//not open-source
|
||||
()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum CriticalErrorChannel {}
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::response::Response;
|
||||
use axum::{response::IntoResponse, response::Json};
|
||||
use axum::{
|
||||
response::IntoResponse,
|
||||
response::Json,
|
||||
};
|
||||
|
||||
use hyper::StatusCode;
|
||||
use sqlx::migrate::MigrateError;
|
||||
@@ -42,8 +45,6 @@ pub enum Error {
|
||||
SqlErr(#[from] sqlx::Error),
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
#[error("Quota exceeded: {0}")]
|
||||
QuotaExceeded(String),
|
||||
#[error("Internal: {0}")]
|
||||
InternalErr(String),
|
||||
#[error("Hexadecimal decoding error: {0}")]
|
||||
@@ -58,8 +59,6 @@ pub enum Error {
|
||||
JsonErr(serde_json::Value),
|
||||
#[error("{0}")]
|
||||
OpenAIError(String),
|
||||
#[error("{0}")]
|
||||
AlreadyCompleted(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
@@ -82,10 +81,9 @@ impl IntoResponse for Error {
|
||||
Self::NotFound(_) => axum::http::StatusCode::NOT_FOUND,
|
||||
Self::NotAuthorized(_) => axum::http::StatusCode::UNAUTHORIZED,
|
||||
Self::RequireAdmin(_) => axum::http::StatusCode::FORBIDDEN,
|
||||
Self::SqlErr(_)
|
||||
| Self::BadRequest(_)
|
||||
| Self::OpenAIError(_)
|
||||
| Self::QuotaExceeded(_) => axum::http::StatusCode::BAD_REQUEST,
|
||||
Self::SqlErr(_) | Self::BadRequest(_) | Self::OpenAIError(_) => {
|
||||
axum::http::StatusCode::BAD_REQUEST
|
||||
}
|
||||
_ => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
|
||||
@@ -13,19 +13,16 @@
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub async fn get_ip() -> anyhow::Result<String> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = tokio::time::sleep(Duration::from_secs(10)) => {
|
||||
return Err(anyhow::anyhow!("Expected to get ip under 10s"))
|
||||
},
|
||||
ip = reqwest::ClientBuilder::new()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.timeout(Duration::from_secs(5))
|
||||
use reqwest::Result;
|
||||
|
||||
pub async fn get_ip() -> Result<String> {
|
||||
reqwest::ClientBuilder::new()
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build()?
|
||||
.get("https://hub.windmill.dev/getip")
|
||||
.send() => Ok(ip?
|
||||
.error_for_status()?
|
||||
.text().await?),
|
||||
}
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn is_retry_default(v: &RetryStatus) -> bool {
|
||||
pub struct FlowStatus {
|
||||
pub step: i32,
|
||||
pub modules: Vec<FlowStatusModule>,
|
||||
pub failure_module: Box<FlowStatusModuleWParent>,
|
||||
pub failure_module: FlowStatusModuleWParent,
|
||||
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
#[serde(default)]
|
||||
@@ -227,7 +227,7 @@ impl FlowStatus {
|
||||
.iter()
|
||||
.map(|m| FlowStatusModule::WaitingForPriorSteps { id: m.id.clone() })
|
||||
.collect(),
|
||||
failure_module: Box::new(FlowStatusModuleWParent {
|
||||
failure_module: FlowStatusModuleWParent {
|
||||
parent_module: None,
|
||||
module_status: FlowStatusModule::WaitingForPriorSteps {
|
||||
id: f
|
||||
@@ -236,7 +236,7 @@ impl FlowStatus {
|
||||
.map(|x| x.id.clone())
|
||||
.unwrap_or_else(|| "failure".to_string()),
|
||||
},
|
||||
}),
|
||||
},
|
||||
cleanup_module: FlowCleanupModule { flow_jobs_to_clean: vec![] },
|
||||
retry: RetryStatus { fail_count: 0, failed_jobs: vec![] },
|
||||
restarted_from: None,
|
||||
|
||||
@@ -87,7 +87,7 @@ pub struct FlowValue {
|
||||
pub modules: Vec<FlowModule>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub failure_module: Option<Box<FlowModule>>,
|
||||
pub failure_module: Option<FlowModule>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "is_default")]
|
||||
pub same_worker: bool,
|
||||
|
||||
@@ -24,7 +24,6 @@ pub const OBJECT_STORE_CACHE_CONFIG_SETTING: &str = "object_store_cache_config";
|
||||
|
||||
pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation";
|
||||
pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";
|
||||
pub const CRITICAL_ERROR_CHANNELS_SETTING: &str = "critical_error_channels";
|
||||
|
||||
pub const ENV_SETTINGS: [&str; 50] = [
|
||||
"DISABLE_NSJAIL",
|
||||
|
||||
@@ -243,8 +243,6 @@ 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 {
|
||||
@@ -345,7 +343,6 @@ pub enum JobPayload {
|
||||
pub struct RawCode {
|
||||
pub content: String,
|
||||
pub path: Option<String>,
|
||||
pub hash: Option<i64>,
|
||||
pub language: ScriptLang,
|
||||
pub lock: Option<String>,
|
||||
pub concurrent_limit: Option<i32>,
|
||||
|
||||
@@ -11,7 +11,6 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use ee::CriticalErrorChannel;
|
||||
use error::Error;
|
||||
use scripts::ScriptLang;
|
||||
use sqlx::{Pool, Postgres};
|
||||
@@ -45,7 +44,7 @@ pub mod workspaces;
|
||||
pub mod tracing_init;
|
||||
|
||||
pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
|
||||
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 5;
|
||||
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 4;
|
||||
|
||||
pub const DEFAULT_HUB_BASE_URL: &str = "https://hub.windmill.dev";
|
||||
|
||||
@@ -74,9 +73,6 @@ lazy_static::lazy_static! {
|
||||
pub static ref IS_READY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
pub static ref HUB_BASE_URL: Arc<RwLock<String>> = Arc::new(RwLock::new(DEFAULT_HUB_BASE_URL.to_string()));
|
||||
|
||||
|
||||
pub static ref CRITICAL_ERROR_CHANNELS: Arc<RwLock<Vec<CriticalErrorChannel>>> = Arc::new(RwLock::new(vec![]));
|
||||
}
|
||||
|
||||
pub async fn shutdown_signal(
|
||||
|
||||
@@ -177,48 +177,6 @@ pub fn build_object_store_client(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
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)).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(feature = "parquet")]
|
||||
use aws_config::{default_provider::credentials::DefaultCredentialsChain, Region};
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -450,7 +408,3 @@ impl CredentialProvider for AwsCredentialAdapter {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bundle(w_id: &str, hash: &str) -> String {
|
||||
format!("script_bundle/{}/{}", w_id, hash)
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@ impl Display for ScriptKind {
|
||||
}
|
||||
}
|
||||
|
||||
pub const PREVIEW_IS_CODEBASE_HASH: i64 = -42;
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
pub struct Script {
|
||||
pub workspace_id: String,
|
||||
@@ -177,10 +175,6 @@ 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>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub codebase: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
@@ -200,14 +194,6 @@ 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>,
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub use_codebase: bool,
|
||||
}
|
||||
|
||||
fn is_false(x: &bool) -> bool {
|
||||
return !x;
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -265,8 +251,6 @@ 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>,
|
||||
pub codebase: Option<String>,
|
||||
}
|
||||
|
||||
fn lock_deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
@@ -336,7 +320,6 @@ 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,13 +53,7 @@ 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_file(true)
|
||||
.with_line_number(true)
|
||||
.with_target(false),
|
||||
)
|
||||
.with(compact_layer().with_ansi(style.to_lowercase() != "never"))
|
||||
.init(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,11 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::ee::trigger_critical_error_channels;
|
||||
use crate::ee::LICENSE_KEY_ID;
|
||||
use crate::error::{to_anyhow, Error, Result};
|
||||
use crate::global_settings::UNIQUE_ID_SETTING;
|
||||
use crate::server::Smtp;
|
||||
use crate::DB;
|
||||
use git_version::git_version;
|
||||
use mail_send::mail_builder::MessageBuilder;
|
||||
use mail_send::SmtpClientBuilder;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -90,20 +85,12 @@ pub async fn query_elems_from_hub(
|
||||
url: &str,
|
||||
query_params: Option<Vec<(&str, String)>>,
|
||||
db: &DB,
|
||||
) -> Result<(
|
||||
reqwest::StatusCode,
|
||||
reqwest::header::HeaderMap,
|
||||
axum::body::Body,
|
||||
)> {
|
||||
) -> Result<(reqwest::StatusCode, reqwest::header::HeaderMap, axum::body::Body)> {
|
||||
let response = http_get_from_hub(http_client, url, false, query_params, db).await?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
Ok((
|
||||
status,
|
||||
response.headers().clone(),
|
||||
axum::body::Body::from_stream(response.bytes_stream()),
|
||||
))
|
||||
Ok((status, response.headers().clone(), axum::body::Body::from_stream(response.bytes_stream())))
|
||||
}
|
||||
|
||||
pub async fn http_get_from_hub(
|
||||
@@ -180,61 +167,3 @@ pub enum Mode {
|
||||
Server,
|
||||
Standalone,
|
||||
}
|
||||
|
||||
pub async fn send_email(
|
||||
subject: &str,
|
||||
content: &str,
|
||||
to: Vec<String>,
|
||||
smtp: Smtp,
|
||||
client_timeout: Option<tokio::time::Duration>,
|
||||
) -> Result<()> {
|
||||
let mut client = SmtpClientBuilder::new(smtp.host, smtp.port)
|
||||
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
|
||||
if std::env::var("ACCEPT_INVALID_CERTS").is_ok() {
|
||||
client = client.allow_invalid_certs();
|
||||
}
|
||||
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
|
||||
if !username.is_empty() {
|
||||
client.credentials((username, password))
|
||||
} else {
|
||||
client
|
||||
}
|
||||
} else {
|
||||
client
|
||||
};
|
||||
let message = MessageBuilder::new()
|
||||
.from(("Windmill", smtp.from.as_str()))
|
||||
.to(to.clone())
|
||||
.subject(subject)
|
||||
.text_body(content);
|
||||
|
||||
match client_timeout {
|
||||
Some(timeout) => {
|
||||
tokio::time::timeout(timeout, client.connect())
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
.map_err(to_anyhow)?
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
}
|
||||
None => {
|
||||
client
|
||||
.connect()
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
}
|
||||
}
|
||||
tracing::info!("Sent email to {:#?}: {subject}", to);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub async fn report_critical_error(error_message: String) -> () {
|
||||
tracing::error!("CRITICAL ERROR: {error_message}");
|
||||
#[cfg(feature = "enterprise")]
|
||||
trigger_critical_error_channels(error_message).await;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use magic_crypt::{MagicCrypt256, MagicCryptTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, Transaction};
|
||||
|
||||
use crate::{worker::WORKER_GROUP, BASE_URL, DB};
|
||||
use crate::{BASE_URL, DB};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SECRET_SALT: Option<String> = std::env::var("SECRET_SALT").ok();
|
||||
@@ -300,12 +300,6 @@ pub async fn get_reserved_variables(
|
||||
description: "OIDC JWT token (EE only)".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_WORKER_GROUP".to_string(),
|
||||
value: WORKER_GROUP.clone(),
|
||||
description: "name of the worker group the job is running on".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
].into_iter().chain( sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT name, value FROM workspace_env WHERE workspace_id = $1",
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@ path = "src/lib.rs"
|
||||
[features]
|
||||
default = []
|
||||
enterprise = ["windmill-common/enterprise"]
|
||||
cloud = []
|
||||
benchmark = []
|
||||
prometheus = ["dep:prometheus"]
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,9 @@ use windmill_common::{
|
||||
pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
db: &DB,
|
||||
mut tx: QueueTransaction<'c, R>,
|
||||
schedule: &Schedule,
|
||||
schedule: Schedule,
|
||||
) -> Result<QueueTransaction<'c, R>> {
|
||||
let sched = cron::Schedule::from_str(schedule.schedule.as_ref())
|
||||
let sched = cron::Schedule::from_str(&schedule.schedule)
|
||||
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
||||
|
||||
let tz = chrono_tz::Tz::from_str(&schedule.timezone)
|
||||
@@ -68,9 +68,9 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
|
||||
let mut args: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
|
||||
|
||||
if let Some(args_v) = &schedule.args {
|
||||
if let Some(args_v) = schedule.args {
|
||||
if let serde_json::Value::Object(args_m) = args_v {
|
||||
args = args_m.clone()
|
||||
args = args_m
|
||||
} else {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
"args of scripts needs to be dict".to_string(),
|
||||
@@ -90,7 +90,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
.map(|x| (x.tag, x.dedicated_worker))
|
||||
.unwrap_or_else(|| (None, None));
|
||||
(
|
||||
JobPayload::Flow { path: schedule.script_path.clone(), dedicated_worker },
|
||||
JobPayload::Flow { path: schedule.script_path, dedicated_worker },
|
||||
tag,
|
||||
None,
|
||||
)
|
||||
@@ -113,8 +113,8 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
.await?;
|
||||
|
||||
if schedule.retry.is_some() {
|
||||
let parsed_retry = serde_json::from_value::<Retry>(schedule.retry.clone().unwrap())
|
||||
.map_err(|err| {
|
||||
let parsed_retry =
|
||||
serde_json::from_value::<Retry>(schedule.retry.unwrap()).map_err(|err| {
|
||||
error::Error::InternalErr(format!(
|
||||
"Unable to parse retry information from schedule: {}",
|
||||
err.to_string(),
|
||||
@@ -127,7 +127,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
// if retry is set, we wrap the script into a one step flow with a retry on the module
|
||||
(
|
||||
JobPayload::SingleScriptFlow {
|
||||
path: schedule.script_path.clone(),
|
||||
path: schedule.script_path,
|
||||
hash: hash,
|
||||
retry: parsed_retry,
|
||||
args: static_args,
|
||||
@@ -144,7 +144,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
(
|
||||
JobPayload::ScriptHash {
|
||||
hash,
|
||||
path: schedule.script_path.clone(),
|
||||
path: schedule.script_path,
|
||||
concurrent_limit: concurrent_limit,
|
||||
concurrency_time_window_s: concurrency_time_window_s,
|
||||
cache_ttl: cache_ttl,
|
||||
@@ -153,7 +153,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
priority,
|
||||
},
|
||||
if schedule.tag.as_ref().is_some_and(|x| x != "") {
|
||||
schedule.tag.clone()
|
||||
schedule.tag
|
||||
} else {
|
||||
tag
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ const p = {
|
||||
const cdir = resolve("./");
|
||||
const cdirNoPrivate = cdir.replace(/^\/private/, ""); // for macos
|
||||
const filter = new RegExp(
|
||||
`^(?!\\.\/main\\.ts)(?!${cdir}\/main\\.ts)(?!(?:/private)?${cdirNoPrivate}\/wrapper\\.mjs).*\\.ts$`
|
||||
`^(?!\\.\/main\\.ts)(?!${cdir}\/main\\.ts)(?!(?:/private)?${cdirNoPrivate}\/wrapper\\.ts).*\\.ts$`
|
||||
);
|
||||
build.onResolve({ filter }, (args) => {
|
||||
const file_path =
|
||||
@@ -21,7 +21,7 @@ if (!bo.success) {
|
||||
content.replaceAll("__require", "require")
|
||||
);
|
||||
|
||||
const dependencies = {};
|
||||
const dependencies: Record<string, string[]> = {};
|
||||
for (const i of imports) {
|
||||
let [_, name, version] = i.path.match(captureVersion) ?? [];
|
||||
if (name == undefined) {
|
||||
@@ -46,7 +46,7 @@ if (!bo.success) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const resolvedDeps = {};
|
||||
const resolvedDeps: Record<string, string> = {};
|
||||
for (const i in dependencies) {
|
||||
const versions = dependencies[i];
|
||||
resolvedDeps[i] =
|
||||
@@ -61,7 +61,7 @@ if (!bo.success) {
|
||||
JSON.stringify({ dependencies: resolvedDeps }, null, 2)
|
||||
);
|
||||
|
||||
function reduceIntersect(versions, name) {
|
||||
function reduceIntersect(versions: string[], name: string): string {
|
||||
console.log(
|
||||
`multiple versions detected for ${name}: ${versions.join(", ")}`
|
||||
);
|
||||
@@ -66,7 +66,7 @@ mount {
|
||||
src: "{JOB_DIR}/package.json"
|
||||
dst: "/tmp/{LANG}/package.json"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
mandatory: true
|
||||
}
|
||||
|
||||
mount {
|
||||
@@ -77,6 +77,13 @@ mount {
|
||||
}
|
||||
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/wrapper.ts"
|
||||
dst: "/tmp/{LANG}/wrapper.ts"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/wrapper.mjs"
|
||||
dst: "/tmp/{LANG}/wrapper.mjs"
|
||||
@@ -107,13 +114,6 @@ mount {
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/main.js"
|
||||
dst: "/tmp/{LANG}/main.js"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/args.json"
|
||||
|
||||
@@ -35,15 +35,11 @@ use windmill_common::{
|
||||
error::{self, to_anyhow, Result},
|
||||
jobs::QueuedJob,
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use windmill_common::s3_helpers::attempt_fetch_bytes;
|
||||
|
||||
use windmill_parser::Typ;
|
||||
|
||||
const RELATIVE_BUN_LOADER: &str = include_str!("../loader.bun.js");
|
||||
const RELATIVE_BUN_LOADER: &str = include_str!("../loader.bun.ts");
|
||||
|
||||
const RELATIVE_BUN_BUILDER: &str = include_str!("../loader_builder.bun.js");
|
||||
const RELATIVE_BUN_BUILDER: &str = include_str!("../loader_builder.bun.ts");
|
||||
|
||||
const NSJAIL_CONFIG_RUN_BUN_CONTENT: &str = include_str!("../nsjail/run.bun.config.proto");
|
||||
|
||||
@@ -80,7 +76,7 @@ pub async fn gen_lockfile(
|
||||
} else {
|
||||
let _ = write_file(
|
||||
&job_dir,
|
||||
"build.js",
|
||||
"build.ts",
|
||||
&format!(
|
||||
r#"
|
||||
{}
|
||||
@@ -104,7 +100,7 @@ pub async fn gen_lockfile(
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(common_bun_proc_envs.clone())
|
||||
.args(vec!["run", "build.js"])
|
||||
.args(vec!["run", "build.ts"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let child_process = start_child_process(child_cmd, &*BUN_PATH).await?;
|
||||
@@ -342,158 +338,9 @@ fn get_annotation(inner_content: &str) -> Annotations {
|
||||
Annotations { npm_mode, nodejs_mode }
|
||||
}
|
||||
|
||||
pub async fn build_loader(
|
||||
job_dir: &str,
|
||||
base_internal_url: &str,
|
||||
token: &str,
|
||||
w_id: &str,
|
||||
current_path: &str,
|
||||
nodejs_mode: bool,
|
||||
) -> Result<()> {
|
||||
let loader = RELATIVE_BUN_LOADER
|
||||
.replace("W_ID", w_id)
|
||||
.replace("BASE_INTERNAL_URL", base_internal_url)
|
||||
.replace("TOKEN", token)
|
||||
.replace("CURRENT_PATH", current_path)
|
||||
.replace("RAW_GET_ENDPOINT", "raw_unpinned");
|
||||
if nodejs_mode {
|
||||
write_file(
|
||||
&job_dir,
|
||||
"node_builder.ts",
|
||||
&format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
import {{ readdir }} from "node:fs/promises";
|
||||
|
||||
let fileNames = []
|
||||
try {{
|
||||
fileNames = await readdir("{job_dir}/node_modules")
|
||||
}} catch (e) {{
|
||||
|
||||
}}
|
||||
|
||||
const bo = await Bun.build({{
|
||||
entrypoints: ["{job_dir}/wrapper.mjs"],
|
||||
outdir: "./",
|
||||
target: "node",
|
||||
plugins: [p],
|
||||
external: fileNames,
|
||||
}});
|
||||
|
||||
if (!bo.success) {{
|
||||
bo.logs.forEach((l) => console.log(l));
|
||||
process.exit(1);
|
||||
}}
|
||||
"#,
|
||||
loader
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
write_file(
|
||||
&job_dir,
|
||||
"loader.bun.js",
|
||||
&format!(
|
||||
r#"
|
||||
import {{ plugin }} from "bun";
|
||||
|
||||
{}
|
||||
|
||||
plugin(p)
|
||||
"#,
|
||||
loader
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn generate_wrapper_mjs(
|
||||
job_dir: &str,
|
||||
w_id: &str,
|
||||
job_id: &Uuid,
|
||||
worker_name: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
timeout: Option<i32>,
|
||||
mem_peak: &mut i32,
|
||||
canceled_by: &mut Option<CanceledBy>,
|
||||
common_bun_proc_envs: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
let mut child = Command::new(&*BUN_PATH);
|
||||
child
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(common_bun_proc_envs.clone())
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.args(vec!["run", "node_builder.ts"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let child_process = start_child_process(child, &*BUN_PATH).await?;
|
||||
handle_child(
|
||||
job_id,
|
||||
db,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
child_process,
|
||||
false,
|
||||
worker_name,
|
||||
w_id,
|
||||
"bun build",
|
||||
timeout,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::rename(
|
||||
format!("{job_dir}/wrapper.js"),
|
||||
format!("{job_dir}/wrapper.mjs"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| error::Error::InternalErr(format!("Could not move wrapper to mjs: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub async fn pull_codebase(w_id: &str, id: &str, job_dir: &str) -> Result<()> {
|
||||
let path = windmill_common::s3_helpers::bundle(&w_id, &id);
|
||||
let bun_cache_path = format!("{}/{}", BUN_CACHE_DIR, path);
|
||||
let dst = format!("{job_dir}/main.js");
|
||||
let dirs_splitted = bun_cache_path.split("/").collect_vec();
|
||||
tokio::fs::create_dir_all(dirs_splitted[..dirs_splitted.len() - 1].join("/")).await?;
|
||||
if tokio::fs::metadata(&bun_cache_path).await.is_ok() {
|
||||
tracing::info!("loading {bun_cache_path} from cache");
|
||||
tokio::fs::symlink(&bun_cache_path, dst).await?;
|
||||
} else if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
{
|
||||
let bytes = attempt_fetch_bytes(os, &path).await?;
|
||||
if *windmill_common::worker::CLOUD_HOSTED {
|
||||
tokio::fs::write(dst, &bytes).await?;
|
||||
} else {
|
||||
tokio::fs::write(&bun_cache_path, &bytes).await?;
|
||||
tokio::fs::symlink(bun_cache_path, dst).await?;
|
||||
}
|
||||
|
||||
// extract_tar(bytes, job_dir).await?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
pub async fn pull_codebase(_w_id: &str, _id: &str, _job_dir: &str) -> Result<()> {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
"codebase is an EE feature".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn handle_bun_job(
|
||||
requirements_o: Option<String>,
|
||||
codebase: Option<String>,
|
||||
mem_peak: &mut i32,
|
||||
canceled_by: &mut Option<CanceledBy>,
|
||||
job: &QueuedJob,
|
||||
@@ -506,20 +353,13 @@ pub async fn handle_bun_job(
|
||||
envs: HashMap<String, String>,
|
||||
shared_mount: &str,
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
if !codebase.is_some() {
|
||||
let _ = write_file(job_dir, "main.ts", inner_content).await?;
|
||||
} else {
|
||||
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#).await?;
|
||||
}
|
||||
let _ = write_file(job_dir, "main.ts", inner_content).await?;
|
||||
|
||||
let common_bun_proc_envs: HashMap<String, String> =
|
||||
get_common_bun_proc_envs(&base_internal_url).await;
|
||||
|
||||
let mut annotation = get_annotation(inner_content);
|
||||
let annotation = get_annotation(inner_content);
|
||||
|
||||
if codebase.is_some() {
|
||||
annotation.nodejs_mode = true
|
||||
}
|
||||
let main_override = get_main_override(job.args.as_ref());
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
@@ -529,9 +369,7 @@ pub async fn handle_bun_job(
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(codebase) = codebase.as_ref() {
|
||||
pull_codebase(&job.workspace_id, codebase, job_dir).await?;
|
||||
} else if let Some(reqs) = requirements_o {
|
||||
if let Some(reqs) = requirements_o {
|
||||
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
|
||||
if splitted.len() != 2 {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
@@ -597,11 +435,10 @@ pub async fn handle_bun_job(
|
||||
// }
|
||||
}
|
||||
|
||||
let _ = write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?).await?;
|
||||
let main_code = remove_pinned_imports(inner_content)?;
|
||||
let _ = write_file(job_dir, "main.ts", &main_code).await?;
|
||||
|
||||
let init_logs = if codebase.is_some() {
|
||||
"\n\n--- NODE SNAPSHOT EXECUTION ---\n".to_string()
|
||||
} else if annotation.nodejs_mode {
|
||||
let init_logs = if annotation.nodejs_mode {
|
||||
"\n\n--- NODE CODE EXECUTION ---\n".to_string()
|
||||
} else {
|
||||
"\n\n--- BUN CODE EXECUTION ---\n".to_string()
|
||||
@@ -632,17 +469,11 @@ pub async fn handle_bun_job(
|
||||
// we cannot use Bun.read and Bun.write because it results in an EBADF error on cloud
|
||||
let main_name = main_override.unwrap_or("main".to_string());
|
||||
|
||||
let main_import = if codebase.is_some() {
|
||||
"./main.js"
|
||||
} else {
|
||||
"./main.ts"
|
||||
};
|
||||
|
||||
let wrapper_content: String = format!(
|
||||
r#"
|
||||
import {{ {main_name} }} from "{main_import}";
|
||||
import {{ {main_name} }} from "./main.ts";
|
||||
|
||||
import * as fs from "fs/promises";
|
||||
const fs = require('fs/promises');
|
||||
|
||||
const args = await fs.readFile('args.json', {{ encoding: 'utf8' }}).then(JSON.parse)
|
||||
.then(({{ {spread} }}) => [ {spread} ])
|
||||
@@ -653,7 +484,7 @@ BigInt.prototype.toJSON = function () {{
|
||||
|
||||
{dates}
|
||||
async function run() {{
|
||||
let res = await {main_name}(...args);
|
||||
let res: any = await {main_name}(...args);
|
||||
const res_json = JSON.stringify(res ?? null, (key, value) => typeof value === 'undefined' ? null : value);
|
||||
await fs.writeFile("result.json", res_json);
|
||||
process.exit(0);
|
||||
@@ -671,7 +502,7 @@ try {{
|
||||
}}
|
||||
"#,
|
||||
);
|
||||
write_file(job_dir, "wrapper.mjs", &wrapper_content).await?;
|
||||
write_file(job_dir, "wrapper.ts", &wrapper_content).await?;
|
||||
Ok(()) as error::Result<()>
|
||||
};
|
||||
|
||||
@@ -689,19 +520,65 @@ try {{
|
||||
Ok(reserved_variables) as error::Result<HashMap<String, String>>
|
||||
};
|
||||
|
||||
let write_loader_f = async {
|
||||
if !codebase.is_some() {
|
||||
build_loader(
|
||||
job_dir,
|
||||
base_internal_url,
|
||||
&client.get_token().await,
|
||||
&job.workspace_id,
|
||||
&job.script_path(),
|
||||
annotation.nodejs_mode,
|
||||
let loader = RELATIVE_BUN_LOADER
|
||||
.replace("W_ID", &job.workspace_id)
|
||||
.replace("BASE_INTERNAL_URL", base_internal_url)
|
||||
.replace("TOKEN", &client.get_token().await)
|
||||
.replace("CURRENT_PATH", job.script_path())
|
||||
.replace("RAW_GET_ENDPOINT", "raw_unpinned");
|
||||
let write_loader_f = async move {
|
||||
if annotation.nodejs_mode {
|
||||
write_file(
|
||||
&job_dir,
|
||||
"node_builder.ts",
|
||||
&format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
import {{ readdir }} from "node:fs/promises";
|
||||
|
||||
let fileNames = []
|
||||
try {{
|
||||
fileNames = await readdir("{job_dir}/node_modules")
|
||||
}} catch (e) {{
|
||||
|
||||
}}
|
||||
|
||||
const bo = await Bun.build({{
|
||||
entrypoints: ["{job_dir}/wrapper.ts"],
|
||||
outdir: "./",
|
||||
target: "node",
|
||||
plugins: [p],
|
||||
external: fileNames,
|
||||
}});
|
||||
|
||||
if (!bo.success) {{
|
||||
bo.logs.forEach((l) => console.log(l));
|
||||
process.exit(1);
|
||||
}}
|
||||
"#,
|
||||
loader
|
||||
),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
Ok(()) as error::Result<()>
|
||||
} else {
|
||||
Ok(())
|
||||
write_file(
|
||||
&job_dir,
|
||||
"loader.bun.ts",
|
||||
&format!(
|
||||
r#"
|
||||
import {{ plugin }} from "bun";
|
||||
|
||||
{}
|
||||
|
||||
plugin(p)
|
||||
"#,
|
||||
loader
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(()) as error::Result<()>
|
||||
}
|
||||
};
|
||||
|
||||
@@ -711,19 +588,37 @@ try {{
|
||||
write_loader_f
|
||||
)?;
|
||||
|
||||
if annotation.nodejs_mode && !codebase.is_some() {
|
||||
generate_wrapper_mjs(
|
||||
job_dir,
|
||||
&job.workspace_id,
|
||||
if annotation.nodejs_mode {
|
||||
let mut child = Command::new(&*BUN_PATH);
|
||||
child
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(common_bun_proc_envs.clone())
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.args(vec!["run", "node_builder.ts"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let child_process = start_child_process(child, &*BUN_PATH).await?;
|
||||
handle_child(
|
||||
&job.id,
|
||||
worker_name,
|
||||
db,
|
||||
job.timeout,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
&common_bun_proc_envs,
|
||||
child_process,
|
||||
false,
|
||||
worker_name,
|
||||
&job.workspace_id,
|
||||
"bun build",
|
||||
job.timeout,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::rename(
|
||||
format!("{job_dir}/wrapper.js"),
|
||||
format!("{job_dir}/wrapper.mjs"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| error::Error::InternalErr(format!("Could not move wrapper to mjs: {e}")))?;
|
||||
}
|
||||
|
||||
//do not cache local dependencies
|
||||
@@ -764,18 +659,8 @@ try {{
|
||||
"run.config.proto",
|
||||
"--",
|
||||
&NODE_PATH,
|
||||
"--experimental-default-type=module",
|
||||
"/tmp/nodejs/wrapper.mjs",
|
||||
]
|
||||
} else if codebase.is_some() {
|
||||
vec![
|
||||
"--config",
|
||||
"run.config.proto",
|
||||
"--",
|
||||
&BUN_PATH,
|
||||
"run",
|
||||
"/tmp/bun/wrapper.mjs",
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
"--config",
|
||||
@@ -786,8 +671,8 @@ try {{
|
||||
"-i",
|
||||
"--prefer-offline",
|
||||
"-r",
|
||||
"/tmp/bun/loader.bun.js",
|
||||
"/tmp/bun/wrapper.mjs",
|
||||
"/tmp/bun/loader.bun.ts",
|
||||
"/tmp/bun/wrapper.ts",
|
||||
]
|
||||
};
|
||||
nsjail_cmd
|
||||
@@ -812,33 +697,28 @@ try {{
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(common_bun_proc_envs)
|
||||
.args(vec!["--experimental-default-type=module", &script_path])
|
||||
.args(vec![&script_path])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
bun_cmd
|
||||
} else {
|
||||
let script_path = format!("{job_dir}/wrapper.mjs");
|
||||
let script_path = format!("{job_dir}/wrapper.ts");
|
||||
|
||||
let mut bun_cmd = Command::new(&*BUN_PATH);
|
||||
let args = if codebase.is_some() {
|
||||
vec!["run", &script_path]
|
||||
} else {
|
||||
vec![
|
||||
"run",
|
||||
"-i",
|
||||
"--prefer-offline",
|
||||
"-r",
|
||||
"./loader.bun.js",
|
||||
&script_path,
|
||||
]
|
||||
};
|
||||
bun_cmd
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(common_bun_proc_envs)
|
||||
.args(args)
|
||||
.args(vec![
|
||||
"run",
|
||||
"-i",
|
||||
"--prefer-offline",
|
||||
"-r",
|
||||
"./loader.bun.ts",
|
||||
&script_path,
|
||||
])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
bun_cmd
|
||||
@@ -900,7 +780,6 @@ use std::sync::Arc;
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn start_worker(
|
||||
requirements_o: Option<String>,
|
||||
codebase: Option<String>,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
inner_content: &str,
|
||||
base_internal_url: &str,
|
||||
@@ -917,21 +796,10 @@ pub async fn start_worker(
|
||||
let mut logs = "".to_string();
|
||||
let mut mem_peak: i32 = 0;
|
||||
let mut canceled_by: Option<CanceledBy> = None;
|
||||
tracing::info!("Starting worker {w_id};{script_path} (codebase: {codebase:?}");
|
||||
if !codebase.is_some() {
|
||||
let _ = write_file(job_dir, "main.ts", inner_content).await?;
|
||||
} else {
|
||||
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#).await?;
|
||||
}
|
||||
|
||||
let _ = write_file(job_dir, "main.ts", inner_content).await?;
|
||||
let common_bun_proc_envs: HashMap<String, String> =
|
||||
get_common_bun_proc_envs(&base_internal_url).await;
|
||||
|
||||
let mut annotation = get_annotation(inner_content);
|
||||
|
||||
//TODO: remove this when bun dedicated workers work without issues
|
||||
annotation.nodejs_mode = true;
|
||||
|
||||
let context = variables::get_reserved_variables(
|
||||
db,
|
||||
w_id,
|
||||
@@ -950,10 +818,8 @@ pub async fn start_worker(
|
||||
)
|
||||
.await;
|
||||
let context_envs = build_envs_map(context.to_vec()).await;
|
||||
|
||||
if let Some(codebase) = codebase.as_ref() {
|
||||
pull_codebase(w_id, codebase, job_dir).await?;
|
||||
} else if let Some(reqs) = requirements_o {
|
||||
let annotation = get_annotation(inner_content);
|
||||
if let Some(reqs) = requirements_o {
|
||||
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
|
||||
if splitted.len() != 2 {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
@@ -1036,23 +902,9 @@ pub async fn start_worker(
|
||||
let spread = args.into_iter().map(|x| x.name).join(",");
|
||||
// logs.push_str(format!("infer args: {:?}\n", start.elapsed().as_micros()).as_str());
|
||||
// we cannot use Bun.read and Bun.write because it results in an EBADF error on cloud
|
||||
|
||||
let is_debug = std::env::var("RUST_LOG").is_ok_and(|x| x == "windmill=debug");
|
||||
let print_lines = if is_debug {
|
||||
r#"console.log(line);"#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let main_import = if codebase.is_some() {
|
||||
"./main.js"
|
||||
} else {
|
||||
"./main.ts"
|
||||
};
|
||||
let wrapper_content: String = format!(
|
||||
r#"
|
||||
import {{ main }} from "{main_import}";
|
||||
import {{ createInterface }} from "node:readline"
|
||||
import {{ main }} from "./main.ts";
|
||||
|
||||
BigInt.prototype.toJSON = function () {{
|
||||
return this.toString();
|
||||
@@ -1060,100 +912,77 @@ BigInt.prototype.toJSON = function () {{
|
||||
|
||||
{dates}
|
||||
|
||||
console.log('start');
|
||||
let stdout = Bun.stdout.writer();
|
||||
stdout.write('start\n');
|
||||
|
||||
for await (const line of createInterface({{ input: process.stdin }})) {{
|
||||
{print_lines}
|
||||
|
||||
if (line === "end") {{
|
||||
process.exit(0);
|
||||
for await (const chunk of Bun.stdin.stream()) {{
|
||||
const lines = Buffer.from(chunk).toString();
|
||||
let exit = false;
|
||||
for (const line of lines.trim().split("\n")) {{
|
||||
if (line === "end") {{
|
||||
exit = true;
|
||||
break;
|
||||
}}
|
||||
try {{
|
||||
let {{ {spread} }} = JSON.parse(line)
|
||||
let res: any = await main(...[ {spread} ]);
|
||||
stdout.write("wm_res[success]:" + JSON.stringify(res ?? null, (key, value) => typeof value === 'undefined' ? null : value) + '\n');
|
||||
}} catch (e) {{
|
||||
stdout.write("wm_res[error]:" + JSON.stringify({{ message: e.message, name: e.name, stack: e.stack, line: line }}) + '\n');
|
||||
}}
|
||||
stdout.flush();
|
||||
}}
|
||||
try {{
|
||||
let {{ {spread} }} = JSON.parse(line)
|
||||
let res = await main(...[ {spread} ]);
|
||||
console.log("wm_res[success]:" + JSON.stringify(res ?? null, (key, value) => typeof value === 'undefined' ? null : value));
|
||||
}} catch (e) {{
|
||||
console.log("wm_res[error]:" + JSON.stringify({{ message: e.message, name: e.name, stack: e.stack, line: line }}));
|
||||
if (exit) {{
|
||||
break;
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
);
|
||||
write_file(job_dir, "wrapper.mjs", &wrapper_content).await?;
|
||||
write_file(job_dir, "wrapper.ts", &wrapper_content).await?;
|
||||
}
|
||||
|
||||
if !codebase.is_some() {
|
||||
build_loader(
|
||||
job_dir,
|
||||
base_internal_url,
|
||||
token,
|
||||
w_id,
|
||||
script_path,
|
||||
annotation.nodejs_mode,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let _ = write_file(
|
||||
&job_dir,
|
||||
"loader.bun.ts",
|
||||
&format!(
|
||||
r#"
|
||||
import {{ plugin }} from "bun";
|
||||
|
||||
if annotation.nodejs_mode && !codebase.is_some() {
|
||||
generate_wrapper_mjs(
|
||||
job_dir,
|
||||
w_id,
|
||||
&Uuid::nil(),
|
||||
worker_name,
|
||||
db,
|
||||
None,
|
||||
&mut mem_peak,
|
||||
&mut canceled_by,
|
||||
&common_bun_proc_envs,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
{}
|
||||
|
||||
if annotation.nodejs_mode {
|
||||
let script_path = format!("{job_dir}/wrapper.mjs");
|
||||
plugin(p)
|
||||
"#,
|
||||
RELATIVE_BUN_LOADER
|
||||
.replace("W_ID", &w_id)
|
||||
.replace("BASE_INTERNAL_URL", base_internal_url)
|
||||
.replace("TOKEN", token)
|
||||
.replace("CURRENT_PATH", script_path)
|
||||
.replace("RAW_GET_ENDPOINT", "raw_unpinned")
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
handle_dedicated_process(
|
||||
&*NODE_PATH,
|
||||
job_dir,
|
||||
context_envs,
|
||||
envs,
|
||||
context,
|
||||
common_bun_proc_envs,
|
||||
vec!["--experimental-default-type=module", &script_path],
|
||||
killpill_rx,
|
||||
job_completed_tx,
|
||||
token,
|
||||
jobs_rx,
|
||||
worker_name,
|
||||
db,
|
||||
&script_path,
|
||||
"nodejs",
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
handle_dedicated_process(
|
||||
&*BUN_PATH,
|
||||
job_dir,
|
||||
context_envs,
|
||||
envs,
|
||||
context,
|
||||
common_bun_proc_envs,
|
||||
vec![
|
||||
"run",
|
||||
"-i",
|
||||
"--prefer-offline",
|
||||
"-r",
|
||||
"./loader.bun.js",
|
||||
&format!("{job_dir}/wrapper.mjs"),
|
||||
],
|
||||
killpill_rx,
|
||||
job_completed_tx,
|
||||
token,
|
||||
jobs_rx,
|
||||
worker_name,
|
||||
db,
|
||||
script_path,
|
||||
"bun",
|
||||
)
|
||||
.await
|
||||
}
|
||||
handle_dedicated_process(
|
||||
&*BUN_PATH,
|
||||
job_dir,
|
||||
context_envs,
|
||||
envs,
|
||||
context,
|
||||
common_bun_proc_envs,
|
||||
vec![
|
||||
"run",
|
||||
"-i",
|
||||
"--prefer-offline",
|
||||
"-r",
|
||||
"./loader.bun.ts",
|
||||
&format!("{job_dir}/wrapper.ts"),
|
||||
],
|
||||
killpill_rx,
|
||||
job_completed_tx,
|
||||
token,
|
||||
jobs_rx,
|
||||
worker_name,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -453,26 +453,6 @@ 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>,
|
||||
@@ -482,7 +462,7 @@ pub async fn run_future_with_polling_update_job_poller<Fut, T>(
|
||||
result_f: Fut,
|
||||
worker_name: &str,
|
||||
w_id: &str,
|
||||
) -> error::Result<T>
|
||||
) -> anyhow::Result<T>
|
||||
where
|
||||
Fut: Future<Output = anyhow::Result<T>>,
|
||||
{
|
||||
@@ -514,22 +494,12 @@ where
|
||||
tracing::error!("Query timeout: {}", e);
|
||||
Error::ExecutionErr(format!("Query timeout after (>{}s)", timeout_ms/1000))
|
||||
})?,
|
||||
ex = update_job, if job_id != Uuid::nil() => {
|
||||
match ex {
|
||||
UpdateJobPollingExit::Done => Err(Error::ExecutionErr("Job cancelled".to_string())).map_err(to_anyhow)?,
|
||||
UpdateJobPollingExit::AlreadyCompleted => Err(Error::AlreadyCompleted("Job already completed".to_string())).map_err(to_anyhow)?,
|
||||
}
|
||||
}
|
||||
_ = update_job, if job_id != Uuid::nil() => Err(Error::ExecutionErr("Job cancelled".to_string())).map_err(to_anyhow)?,
|
||||
}?;
|
||||
drop(tx);
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub enum UpdateJobPollingExit {
|
||||
Done,
|
||||
AlreadyCompleted,
|
||||
}
|
||||
|
||||
pub async fn update_job_poller<F, Fut>(
|
||||
job_id: Uuid,
|
||||
db: &DB,
|
||||
@@ -539,14 +509,13 @@ pub async fn update_job_poller<F, Fut>(
|
||||
worker_name: &str,
|
||||
w_id: &str,
|
||||
mut rx: broadcast::Receiver<()>,
|
||||
) -> UpdateJobPollingExit
|
||||
where
|
||||
) where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = i32>,
|
||||
{
|
||||
let update_job_interval = Duration::from_millis(500);
|
||||
if job_id == Uuid::nil() {
|
||||
return UpdateJobPollingExit::Done;
|
||||
return;
|
||||
}
|
||||
let db = db.clone();
|
||||
|
||||
@@ -604,36 +573,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let (canceled, canceled_by, canceled_reason, already_completed) = sqlx::query_as::<_, (bool, Option<String>, Option<String>, bool)>("UPDATE queue SET mem_peak = $1, last_ping = now() WHERE id = $2 RETURNING canceled, canceled_by, canceled_reason, false")
|
||||
let (canceled, canceled_by, canceled_reason) = sqlx::query_as::<_, (bool, Option<String>, Option<String>)>("UPDATE queue SET mem_peak = $1, last_ping = now() WHERE id = $2 RETURNING canceled, canceled_by, canceled_reason")
|
||||
.bind(*mem_peak)
|
||||
.bind(job_id)
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::error!(%e, "error updating job {job_id}: {e}");
|
||||
Some((false, None, None, false))
|
||||
Some((false, None, None))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// if the job is not in queue, it can only be in the completed_job so it is already complete
|
||||
(false, None, None, true)
|
||||
});
|
||||
if already_completed {
|
||||
return UpdateJobPollingExit::AlreadyCompleted
|
||||
}
|
||||
.unwrap_or((false, None, None));
|
||||
if canceled {
|
||||
canceled_by_ref.replace(CanceledBy {
|
||||
username: canceled_by.clone(),
|
||||
reason: canceled_reason.clone(),
|
||||
});
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
tracing::info!("job {job_id} finished");
|
||||
|
||||
UpdateJobPollingExit::Done
|
||||
}
|
||||
|
||||
pub enum CompactLogs {
|
||||
@@ -684,20 +645,12 @@ async fn compact_logs(
|
||||
let (excess_prev_logs, current_logs) = if extra_split {
|
||||
let split_idx = nlogs
|
||||
.char_indices()
|
||||
.nth(excess_size)
|
||||
.nth(excess_size_modulo)
|
||||
.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())
|
||||
};
|
||||
|
||||
@@ -715,7 +668,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] Previous logs have been saved to object storage at {path}\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::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);
|
||||
@@ -916,7 +869,6 @@ pub async fn handle_child(
|
||||
TooManyLogs,
|
||||
Timeout,
|
||||
Cancelled,
|
||||
AlreadyCompleted,
|
||||
}
|
||||
|
||||
let (timeout_duration, timeout_warn_msg) =
|
||||
@@ -934,10 +886,7 @@ pub async fn handle_child(
|
||||
result = child.wait() => return result.map(Ok),
|
||||
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
|
||||
_ = sleep(timeout_duration) => KillReason::Timeout,
|
||||
ex = update_job, if job_id != Uuid::nil() => match ex {
|
||||
UpdateJobPollingExit::Done => KillReason::Cancelled,
|
||||
UpdateJobPollingExit::AlreadyCompleted => KillReason::AlreadyCompleted,
|
||||
},
|
||||
_ = update_job, if job_id != Uuid::nil() => KillReason::Cancelled,
|
||||
};
|
||||
tx.send(()).expect("rx should never be dropped");
|
||||
drop(tx);
|
||||
@@ -1183,7 +1132,7 @@ pub async fn resolve_job_timeout(
|
||||
custom_timeout_secs: Option<i32>,
|
||||
) -> (Duration, Option<String>) {
|
||||
let mut warn_msg: Option<String> = None;
|
||||
#[cfg(feature = "cloud")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
let cloud_premium_workspace = *CLOUD_HOSTED
|
||||
&& sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", _w_id)
|
||||
.fetch_one(_db)
|
||||
@@ -1192,7 +1141,7 @@ pub async fn resolve_job_timeout(
|
||||
tracing::error!(%e, "error getting premium workspace for job {_job_id}: {e}");
|
||||
})
|
||||
.unwrap_or(false);
|
||||
#[cfg(not(feature = "cloud"))]
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
let cloud_premium_workspace = false;
|
||||
|
||||
// compute global max timeout
|
||||
|
||||
@@ -58,8 +58,6 @@ pub async fn handle_dedicated_process(
|
||||
mut jobs_rx: Receiver<Arc<QueuedJob>>,
|
||||
worker_name: &str,
|
||||
db: &DB,
|
||||
script_path: &str,
|
||||
mode: &str,
|
||||
) -> std::result::Result<(), error::Error> {
|
||||
//do not cache local dependencies
|
||||
let mut child = {
|
||||
@@ -116,7 +114,7 @@ pub async fn handle_dedicated_process(
|
||||
// let mut j = 0;
|
||||
let mut alive = true;
|
||||
|
||||
let init_log = format!("dedicated worker {mode}: {worker_name}\n\n");
|
||||
let init_log = format!("dedicated worker: {worker_name}\n\n");
|
||||
let mut logs = init_log.clone();
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -127,11 +125,9 @@ pub async fn handle_dedicated_process(
|
||||
if let Err(e) = write_stdin(&mut stdin, "end").await {
|
||||
tracing::info!("Could not write end message to stdin: {e:?}")
|
||||
}
|
||||
stdin.flush().await.context("stdin flush")?;
|
||||
},
|
||||
line = err_reader.next_line() => {
|
||||
if let Some(line) = line.expect("line is ok") {
|
||||
tracing::debug!("stderr dedicated worker: {line}");
|
||||
logs.push_str("[stderr] ");
|
||||
logs.push_str(&line);
|
||||
logs.push_str("\n");
|
||||
@@ -148,10 +144,9 @@ pub async fn handle_dedicated_process(
|
||||
tracing::info!("dedicated worker process started");
|
||||
continue;
|
||||
}
|
||||
tracing::debug!("processed job: |{line}|");
|
||||
tracing::debug!("processed job: {line}");
|
||||
if line.starts_with("wm_res[") {
|
||||
let job: Arc<QueuedJob> = jobs.pop_front().expect("pop");
|
||||
tracing::info!("job completed on dedicated worker {script_path}: {}", job.id);
|
||||
match serde_json::from_str::<Box<serde_json::value::RawValue>>(&line.replace("wm_res[success]:", "").replace("wm_res[error]:", "")) {
|
||||
Ok(result) => {
|
||||
append_logs(job.id, job.workspace_id.clone(), logs.clone(), db).await;
|
||||
@@ -179,9 +174,8 @@ pub async fn handle_dedicated_process(
|
||||
job = conditional_polling(jobs_rx.recv(), alive && jobs.len() < MAX_BUFFERED_DEDICATED_JOBS) => {
|
||||
// i += 1;
|
||||
if let Some(job) = job {
|
||||
tracing::debug!("received job");
|
||||
jobs.push_back(job.clone());
|
||||
tracing::info!("received job and adding to queue on dedicated worker for {script_path}: {} (queue_size: {})", job.id, jobs.len());
|
||||
|
||||
// write_stdin(&mut stdin, &serde_json::to_string(&job.args.unwrap_or_else(|| serde_json::json!({"x": job.id}))).expect("serialize")).await?;
|
||||
write_stdin(&mut stdin, &serde_json::to_string(&job.args).expect("serialize")).await?;
|
||||
stdin.flush().await.context("stdin flush")?;
|
||||
|
||||
@@ -527,8 +527,6 @@ for await (const chunk of Deno.stdin.readable) {{
|
||||
jobs_rx,
|
||||
worker_name,
|
||||
db,
|
||||
script_path,
|
||||
"deno",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -73,19 +73,47 @@ pub async fn build_tar_and_push(
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub async fn pull_from_tar(client: Arc<dyn ObjectStore>, folder: String) -> error::Result<()> {
|
||||
use windmill_common::s3_helpers::attempt_fetch_bytes;
|
||||
|
||||
use object_store::path::Path;
|
||||
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(tar_path.clone())).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;
|
||||
if bytes.is_err() {
|
||||
tracing::info!(
|
||||
"Failed to read tar from s3: {tar_path}. Error: {:?}",
|
||||
bytes.err()
|
||||
);
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"Failed to read tar from s3: {tar_path}"
|
||||
)));
|
||||
}
|
||||
let bytes = bytes.unwrap();
|
||||
tracing::info!("{tar_path} len: {}", 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_tar(bytes, &folder).await.map_err(|e| {
|
||||
extract_pip_tar(bytes, &folder).await.map_err(|e| {
|
||||
tracing::error!("Failed to extract piptar {folder_name}. Error: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
@@ -99,7 +127,7 @@ pub async fn pull_from_tar(client: Arc<dyn ObjectStore>, folder: String) -> erro
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub async fn extract_tar(tar: bytes::Bytes, folder: &str) -> error::Result<()> {
|
||||
pub async fn extract_pip_tar(tar: bytes::Bytes, folder: &str) -> error::Result<()> {
|
||||
use bytes::Buf;
|
||||
use tokio::fs::{self};
|
||||
|
||||
@@ -109,14 +137,14 @@ pub async fn extract_tar(tar: bytes::Bytes, folder: &str) -> error::Result<()> {
|
||||
let mut ar = tar::Archive::new(tar.reader());
|
||||
|
||||
if let Err(e) = ar.unpack(folder) {
|
||||
tracing::info!("Failed to untar to {folder}. Error: {:?}", e);
|
||||
tracing::info!("Failed to untar piptar. Error: {:?}", e);
|
||||
fs::remove_dir_all(&folder).await?;
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"Failed to untar piptar {folder}"
|
||||
)));
|
||||
}
|
||||
tracing::info!(
|
||||
"Finished extracting tar to {folder}. Took {}ms",
|
||||
"Finished extracting pip tar {folder}. Took {}ms",
|
||||
start.elapsed().as_millis(),
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -33,111 +33,6 @@ 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 windmill_common::s3_helpers::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,
|
||||
@@ -155,19 +50,21 @@ 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 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 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 (cache, cache_logs) = load_cache(&bin_path, &hash).await;
|
||||
|
||||
let (skip_go_mod, skip_tidy) = if cache {
|
||||
let (skip_go_mod, skip_tidy) = if bin_exists {
|
||||
create_dir(job_dir).await?;
|
||||
(true, true)
|
||||
} else if let Some(requirements) = requirements_o {
|
||||
@@ -176,8 +73,8 @@ pub async fn handle_go_job(
|
||||
(false, false)
|
||||
};
|
||||
|
||||
let cache_logs = if !cache {
|
||||
let logs1 = format!("{cache_logs}\n\n--- GO DEPENDENCIES SETUP ---\n");
|
||||
if !bin_exists {
|
||||
let logs1 = "\n\n--- GO DEPENDENCIES SETUP ---\n".to_string();
|
||||
append_logs(job.id.clone(), job.workspace_id.to_string(), logs1, db).await;
|
||||
|
||||
install_go_dependencies(
|
||||
@@ -195,6 +92,9 @@ 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)?;
|
||||
@@ -309,24 +209,33 @@ func Run(req Req) (interface{{}}, error){{
|
||||
)
|
||||
.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:?}");
|
||||
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;
|
||||
}
|
||||
"".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(&bin_path, &target).await.map_err(|e| {
|
||||
tokio::fs::symlink(&path, &target).await.map_err(|e| {
|
||||
Error::ExecutionErr(format!(
|
||||
"could not copy cached binary from {bin_path} to {job_dir}/main: {e:?}"
|
||||
"could not copy cached binary from {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;
|
||||
|
||||
@@ -394,13 +303,6 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -139,18 +139,14 @@ pub async fn eval_timeout(
|
||||
}
|
||||
}
|
||||
|
||||
let p_ids = by_id.as_ref().map(|x| {
|
||||
[
|
||||
format!("results.{}", x.previous_id),
|
||||
format!("results?.{}", x.previous_id),
|
||||
format!("results[\"{}\"]", x.previous_id),
|
||||
format!("results?.[\"{}\"]", x.previous_id),
|
||||
]
|
||||
});
|
||||
let p_id = by_id.as_ref().map(|x| format!("results.{}", x.previous_id));
|
||||
let p_id2 = by_id
|
||||
.as_ref()
|
||||
.map(|x| format!("results[\"{}\"]", x.previous_id));
|
||||
|
||||
if p_ids.is_some()
|
||||
if p_id.is_some()
|
||||
&& transform_context.contains_key("previous_result")
|
||||
&& p_ids.as_ref().unwrap().iter().any(|x| x == &expr)
|
||||
&& &expr == p_id.as_ref().unwrap()
|
||||
{
|
||||
// tracing::error!("PREVIOUS_RESULT");
|
||||
return Ok(transform_context
|
||||
@@ -160,6 +156,17 @@ pub async fn eval_timeout(
|
||||
.clone());
|
||||
}
|
||||
|
||||
if p_id2.is_some()
|
||||
&& transform_context.contains_key("previous_result")
|
||||
&& &expr == p_id2.as_ref().unwrap()
|
||||
{
|
||||
return Ok(transform_context
|
||||
.get("previous_result")
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.clone());
|
||||
}
|
||||
|
||||
if by_id.is_some() && authed_client.is_some() {
|
||||
if let Some(x) = RE_FULL
|
||||
.captures(&expr)
|
||||
@@ -220,10 +227,10 @@ pub async fn eval_timeout(
|
||||
.collect_vec();
|
||||
|
||||
if !context_keys.contains(&"previous_result".to_string())
|
||||
&& (p_ids.is_some() && p_ids.as_ref().unwrap().iter().any(|x| expr.contains(x)))
|
||||
&& (p_id.is_some() && expr.contains(p_id.as_ref().unwrap()))
|
||||
|| expr.contains("error")
|
||||
|| (p_id2.is_some() && expr.contains(p_id2.as_ref().unwrap()))
|
||||
{
|
||||
// tracing::error!("PREVIOUS_RESULT");
|
||||
context_keys.push("previous_result".to_string());
|
||||
}
|
||||
let has_flow_input = expr.contains("flow_input");
|
||||
@@ -767,7 +774,6 @@ fn op_get_static_args(op_state: Rc<RefCell<OpState>>) -> Vec<Option<String>> {
|
||||
|
||||
#[op2(fast)]
|
||||
fn op_log(op_state: Rc<RefCell<OpState>>, #[string] log: &str) {
|
||||
// tracing::error!("log: |{}|", log);
|
||||
op_state
|
||||
.borrow_mut()
|
||||
.borrow_mut::<LogString>()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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};
|
||||
@@ -13,7 +12,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::{append_logs, CanceledBy};
|
||||
use windmill_queue::CanceledBy;
|
||||
|
||||
use crate::common::{build_args_values, run_future_with_polling_update_job_poller};
|
||||
use crate::AuthedClientBackgroundTask;
|
||||
@@ -28,10 +27,6 @@ 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,
|
||||
@@ -79,14 +74,6 @@ 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,16 +145,7 @@ 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" || 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::Number(n) if n.is_f64() && arg_t == "real" => n.as_f64().into(),
|
||||
value @ _ => {
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
"Unsupported type in query: {:?} and signature {arg_t:?}",
|
||||
@@ -281,9 +272,6 @@ 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, sizeof_val};
|
||||
use crate::{AuthedClientBackgroundTask, MAX_RESULT_SIZE};
|
||||
use crate::common::{build_args_values, run_future_with_polling_update_job_poller};
|
||||
use crate::AuthedClientBackgroundTask;
|
||||
use bytes::{Buf, BytesMut};
|
||||
use lazy_static::lazy_static;
|
||||
use urlencoding::encode;
|
||||
@@ -230,32 +230,15 @@ pub async fn do_postgresql(
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|x: Row| postgres_row_to_json_value(x))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok((res, siz))
|
||||
Ok(result)
|
||||
};
|
||||
|
||||
let (result, size) = run_future_with_polling_update_job_poller(
|
||||
let result = run_future_with_polling_update_job_poller(
|
||||
job.id,
|
||||
job.timeout,
|
||||
db,
|
||||
@@ -267,8 +250,6 @@ pub async fn do_postgresql(
|
||||
)
|
||||
.await?;
|
||||
|
||||
*mem_peak = size as i32;
|
||||
|
||||
RUNNING.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
if let Some(handle) = handle {
|
||||
@@ -342,7 +323,6 @@ enum PgType {
|
||||
Timestamp(chrono::NaiveDateTime),
|
||||
None(Option<bool>),
|
||||
Array(Vec<PgType>),
|
||||
Json(serde_json::Value),
|
||||
}
|
||||
|
||||
impl ToSql for PgType {
|
||||
@@ -368,7 +348,6 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,7 +410,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" || arg_t == "timetz" => {
|
||||
Value::String(s) if arg_t == "time" => {
|
||||
let time =
|
||||
chrono::NaiveTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%3fZ").unwrap_or_default();
|
||||
Ok(PgType::Time(time))
|
||||
@@ -441,7 +420,6 @@ 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,6 +391,8 @@ mount {{
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job.id,
|
||||
workspace_id = %job.workspace_id,
|
||||
"started python code execution {}",
|
||||
job.id
|
||||
@@ -472,13 +474,7 @@ async fn prepare_wrapper(
|
||||
|
||||
let relative_imports = RELATIVE_IMPORT_REGEX.is_match(&inner_content);
|
||||
|
||||
let script_path_splitted = script_path.split("/").map(|x| {
|
||||
if x.starts_with(|x: char| x.is_ascii_digit()) {
|
||||
format!("_{}", x)
|
||||
} else {
|
||||
x.to_string()
|
||||
}
|
||||
});
|
||||
let script_path_splitted = script_path.split("/");
|
||||
let dirs_full = script_path_splitted
|
||||
.clone()
|
||||
.take(script_path_splitted.clone().count() - 1)
|
||||
@@ -503,6 +499,12 @@ async fn prepare_wrapper(
|
||||
let module_dir = format!("{}/{}", job_dir, dirs);
|
||||
tokio::fs::create_dir_all(format!("{module_dir}/")).await?;
|
||||
|
||||
let last = if last.starts_with(|x: char| x.is_ascii_digit()) {
|
||||
format!("_{}", last)
|
||||
} else {
|
||||
last
|
||||
};
|
||||
|
||||
let _ = write_file(&module_dir, &format!("{last}.py"), inner_content).await?;
|
||||
if relative_imports {
|
||||
let _ = write_file(job_dir, "loader.py", RELATIVE_PYTHON_LOADER).await?;
|
||||
@@ -612,7 +614,7 @@ async fn replace_pip_secret(
|
||||
}
|
||||
let secret = get_secret_value_as_admin(db, w_id, variable).await?;
|
||||
tracing::info!(
|
||||
worker = %worker_name,
|
||||
worker_name = %worker_name,
|
||||
job_id = %job_id,
|
||||
workspace_id = %w_id,
|
||||
"found secret variable in pip requirements: {}",
|
||||
@@ -876,12 +878,16 @@ 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"
|
||||
);
|
||||
@@ -986,6 +992,8 @@ 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 {}",
|
||||
@@ -1178,10 +1186,6 @@ for line in sys.stdin:
|
||||
proc_envs.insert("PYTHONPATH".to_string(), additional_python_paths_folders);
|
||||
proc_envs.insert("PATH".to_string(), PATH_ENV.to_string());
|
||||
proc_envs.insert("TZ".to_string(), TZ_ENV.to_string());
|
||||
proc_envs.insert(
|
||||
"BASE_INTERNAL_URL".to_string(),
|
||||
base_internal_url.to_string(),
|
||||
);
|
||||
proc_envs.insert("BASE_URL".to_string(), base_internal_url.to_string());
|
||||
handle_dedicated_process(
|
||||
&*PYTHON_PATH,
|
||||
@@ -1197,8 +1201,6 @@ for line in sys.stdin:
|
||||
jobs_rx,
|
||||
worker_name,
|
||||
db,
|
||||
script_path,
|
||||
"python",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ globalThis.URLSearchParams = url.URLSearchParams;
|
||||
globalThis.Headers = headers.Headers;
|
||||
globalThis.FileReader = fileReader.FileReader;
|
||||
globalThis.console = new console.Console((msg, level) =>
|
||||
globalThis.Deno.core.ops.op_log(msg)
|
||||
globalThis.Deno.core.ops.op_log([msg])
|
||||
);
|
||||
// Object.assign(globalThis, {
|
||||
// console: nonEnumerable(
|
||||
|
||||
@@ -13,7 +13,6 @@ use prometheus::{
|
||||
core::{AtomicI64, GenericGauge},
|
||||
IntCounter,
|
||||
};
|
||||
use tracing::Instrument;
|
||||
#[cfg(feature = "prometheus")]
|
||||
use windmill_common::METRICS_DEBUG_ENABLED;
|
||||
#[cfg(feature = "prometheus")]
|
||||
@@ -41,7 +40,7 @@ use windmill_common::{
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue},
|
||||
get_latest_deployed_hash_for_path,
|
||||
jobs::{JobKind, JobPayload, QueuedJob},
|
||||
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang, PREVIEW_IS_CODEBASE_HASH},
|
||||
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SYNC_EMAIL},
|
||||
utils::{rd_string, StripPath},
|
||||
worker::{
|
||||
@@ -89,8 +88,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, get_reserved_variables, hash_args,
|
||||
read_result, save_in_cache, write_file, NO_LOGS_AT_ALL, SLOW_LOGS,
|
||||
build_args_map, get_cached_resource_value_if_valid, 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},
|
||||
@@ -198,7 +197,6 @@ pub const DENO_CACHE_DIR_NPM: &str = concatcp!(ROOT_CACHE_DIR, "deno/npm");
|
||||
|
||||
pub const GO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "go");
|
||||
pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun");
|
||||
|
||||
pub const HUB_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub");
|
||||
pub const GO_BIN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "gobin");
|
||||
pub const POWERSHELL_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "powershell");
|
||||
@@ -586,7 +584,6 @@ 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,
|
||||
@@ -611,7 +608,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, "Creating worker dir");
|
||||
tracing::debug!(worker_dir = %worker_dir, worker_name = %worker_name, "Creating worker dir");
|
||||
|
||||
if let Some(ref netrc) = *NETRC {
|
||||
tracing::info!("Writing netrc at {}/.netrc", HOME_ENV.as_str());
|
||||
@@ -1025,7 +1022,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) => {
|
||||
@@ -1039,7 +1036,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
let same_worker_tx2 = same_worker_tx2.clone();
|
||||
let rsmq2 = rsmq2.clone();
|
||||
let worker_name = worker_name2.clone();
|
||||
if matches!(jc.job.job_kind, JobKind::Noop) {
|
||||
if matches!(jc.job.job_kind, JobKind::Noop) || is_dedicated_worker {
|
||||
thread_count.fetch_add(1, Ordering::SeqCst);
|
||||
let thread_count = thread_count.clone();
|
||||
|
||||
@@ -1131,19 +1128,15 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
.await
|
||||
.expect("update config to trigger restart of all dedicated workers at that config");
|
||||
killpill_tx.send(()).unwrap_or_default();
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let is_init_script_and_failure =
|
||||
!jc.success && jc.job.tag.as_str() == INIT_SCRIPT_TAG;
|
||||
let is_dependency_job = matches!(
|
||||
jc.job.job_kind,
|
||||
JobKind::Dependencies | JobKind::FlowDependencies);
|
||||
handle_receive_completed_job(
|
||||
jc,
|
||||
base_internal_url2,
|
||||
db2.clone(),
|
||||
db2,
|
||||
worker_dir2,
|
||||
same_worker_tx2,
|
||||
rsmq2,
|
||||
@@ -1157,15 +1150,6 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
tracing::error!("init script errored, exiting");
|
||||
killpill_tx2.send(()).unwrap_or_default();
|
||||
}
|
||||
if is_dependency_job && is_dedicated_worker {
|
||||
tracing::error!("Dedicated worker executed a dependency job, a new script has been deployed. Exiting expecting to be restarted.");
|
||||
sqlx::query!("UPDATE config SET config = config WHERE name = $1", format!("worker__{}", *WORKER_GROUP))
|
||||
.execute(&db2)
|
||||
.await
|
||||
.expect("update config to trigger restart of all dedicated workers at that config");
|
||||
killpill_tx2.send(()).unwrap_or_default();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
SendResult::UpdateFlow {
|
||||
@@ -1216,7 +1200,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();
|
||||
@@ -1230,11 +1214,7 @@ 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!(
|
||||
"listening for jobs, WORKER_GROUP: {}, config: {:?}",
|
||||
*WORKER_GROUP,
|
||||
WORKER_CONFIG.read().await
|
||||
);
|
||||
tracing::info!(worker = %worker_name, "listening for jobs, WORKER_GROUP: {}, config: {:?}", *WORKER_GROUP, WORKER_CONFIG.read().await);
|
||||
|
||||
// (dedi_path, dedicated_worker_tx, dedicated_worker_handle)
|
||||
// Option<Sender<Arc<QueuedJob>>>,
|
||||
@@ -1398,27 +1378,24 @@ 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::info!("updating last ping");
|
||||
tracing::debug!("set last ping");
|
||||
|
||||
last_ping = Instant::now();
|
||||
}
|
||||
|
||||
if (jobs_executed as u32 + vacuum_shift) % VACUUM_PERIOD == 0 {
|
||||
let db2 = db.clone();
|
||||
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),
|
||||
);
|
||||
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");
|
||||
});
|
||||
jobs_executed += 1;
|
||||
}
|
||||
|
||||
@@ -1522,7 +1499,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
last_executed_job = None;
|
||||
jobs_executed += 1;
|
||||
|
||||
tracing::debug!("started handling of job {}", job.id);
|
||||
tracing::debug!(worker = %worker_name, "started handling of job {}", job.id);
|
||||
|
||||
if matches!(job.job_kind, JobKind::Script | JobKind::Preview) {
|
||||
if !dedicated_workers.is_empty() {
|
||||
@@ -1634,12 +1611,10 @@ 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!("running warmup job");
|
||||
tracing::info!(worker = %worker_name, "running warmup job");
|
||||
} else {
|
||||
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?
|
||||
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);
|
||||
}
|
||||
|
||||
let job_dir = format!("{worker_dir}/{}", job.id);
|
||||
|
||||
@@ -1685,7 +1660,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
let tag = job.tag.clone();
|
||||
|
||||
let arc_job = Arc::new(job);
|
||||
if let Err(err) = handle_queued_job(
|
||||
if let Some(err) = handle_queued_job(
|
||||
arc_job.clone(),
|
||||
db,
|
||||
&authed_client,
|
||||
@@ -1700,6 +1675,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
worker_code_execution_duration.clone(),
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
{
|
||||
let is_init_script = arc_job.tag.as_str() == INIT_SCRIPT_TAG;
|
||||
handle_job_error(
|
||||
@@ -1763,7 +1739,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!("no job for {} seconds, exiting", secs);
|
||||
tracing::info!(worker = %worker_name, "no job for {} seconds, exiting", secs);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -1792,7 +1768,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to pull jobs: {}", err);
|
||||
tracing::error!(worker = %worker_name, "Failed to pull jobs: {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1821,6 +1797,7 @@ 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<()>>);
|
||||
@@ -2039,21 +2016,18 @@ async fn spawn_dedicated_worker(
|
||||
SpawnWorker::RawScript { path, .. } => path.to_string(),
|
||||
SpawnWorker::Script { path, .. } => path.to_string(),
|
||||
};
|
||||
|
||||
let path2 = path.clone();
|
||||
let w_id = w_id.to_string();
|
||||
|
||||
let (content, lock, language, envs, codebase) = match sw {
|
||||
let (content, lock, language, envs) = match sw {
|
||||
SpawnWorker::Script { path, hash } => {
|
||||
let q = if let Some(hash) = hash {
|
||||
get_script_content_by_hash(&hash, &w_id, &db).await.map(
|
||||
|r: ContentReqLangEnvs| {
|
||||
Some((r.content, r.lockfile, r.language, r.envs, r.codebase))
|
||||
},
|
||||
|r: ContentReqLangEnvs| Some((r.content, r.lockfile, r.language, r.envs)),
|
||||
)
|
||||
} else {
|
||||
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>, bool, Option<ScriptHash>)>(
|
||||
"SELECT content, lock, language, envs, codebase IS NOT NULL, hash FROM script WHERE path = $1 AND workspace_id = $2 AND
|
||||
sqlx::query_as::<_, (String, Option<String>, Option<ScriptLang>, Option<Vec<String>>)>(
|
||||
"SELECT content, lock, language, envs FROM script WHERE path = $1 AND workspace_id = $2 AND
|
||||
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
|
||||
deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
|
||||
)
|
||||
@@ -2062,7 +2036,6 @@ async fn spawn_dedicated_worker(
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("expected content and lock: {e}")))
|
||||
.map(|x| x.map(|y| (y.0, y.1, y.2, y.3, if y.4 { y.5.map(|z| z.to_string()) } else { None })))
|
||||
};
|
||||
if let Ok(q) = q {
|
||||
if let Some(wp) = q {
|
||||
@@ -2081,9 +2054,7 @@ async fn spawn_dedicated_worker(
|
||||
return None;
|
||||
}
|
||||
}
|
||||
SpawnWorker::RawScript { content, lock, lang, .. } => {
|
||||
(content, lock, Some(lang), None, None)
|
||||
}
|
||||
SpawnWorker::RawScript { content, lock, lang, .. } => (content, lock, Some(lang), None),
|
||||
};
|
||||
|
||||
match language {
|
||||
@@ -2138,7 +2109,6 @@ async fn spawn_dedicated_worker(
|
||||
Some(ScriptLang::Bun) => {
|
||||
crate::bun_executor::start_worker(
|
||||
lock,
|
||||
codebase,
|
||||
&db,
|
||||
&content,
|
||||
&base_internal_url,
|
||||
@@ -2175,9 +2145,6 @@ async fn spawn_dedicated_worker(
|
||||
} {
|
||||
tracing::error!("error in dedicated worker: {:?}", e);
|
||||
};
|
||||
if let Err(e) = killpill_tx.clone().send(()) {
|
||||
tracing::error!("failed to send final killpill to dedicated worker: {:?}", e);
|
||||
}
|
||||
});
|
||||
return Some((node_id.unwrap_or(path2), dedicated_worker_tx, Some(handle)));
|
||||
// (Some(dedi_path), Some(dedicated_worker_tx), Some(handle))
|
||||
@@ -2197,7 +2164,6 @@ async fn queue_init_bash_maybe<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
tx,
|
||||
"admins",
|
||||
windmill_common::jobs::JobPayload::Code(windmill_common::jobs::RawCode {
|
||||
hash: None,
|
||||
content: content.clone(),
|
||||
path: Some(format!("init_script_{worker_name}")),
|
||||
language: ScriptLang::Bash,
|
||||
@@ -2252,7 +2218,6 @@ 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,
|
||||
@@ -2394,8 +2359,6 @@ 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,
|
||||
@@ -2591,7 +2554,7 @@ pub struct PreviousResult<'a> {
|
||||
pub previous_result: Option<&'a RawValue>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "job", level = "info", skip_all, fields(job_id = %job.id))]
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
job: Arc<QueuedJob>,
|
||||
db: &DB,
|
||||
@@ -2768,6 +2731,8 @@ 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
|
||||
@@ -2852,14 +2817,6 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
if job.as_ref().workspace_id == "" {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if result
|
||||
.as_ref()
|
||||
.is_err_and(|err| matches!(err, &Error::AlreadyCompleted(_)))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
process_result(
|
||||
job,
|
||||
result,
|
||||
@@ -3004,7 +2961,6 @@ struct ContentReqLangEnvs {
|
||||
lockfile: Option<String>,
|
||||
language: Option<ScriptLang>,
|
||||
envs: Option<Vec<String>>,
|
||||
codebase: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_hub_script_content_and_requirements(
|
||||
@@ -3041,7 +2997,6 @@ async fn get_hub_script_content_and_requirements(
|
||||
lockfile: script.lockfile,
|
||||
language: Some(script.language),
|
||||
envs: None,
|
||||
codebase: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3074,27 +3029,16 @@ async fn get_script_content_by_hash(
|
||||
Option<String>,
|
||||
Option<ScriptLang>,
|
||||
Option<Vec<String>>,
|
||||
bool,
|
||||
),
|
||||
>(
|
||||
"SELECT content, lock, language, envs, codebase IS NOT NULL FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
"SELECT content, lock, language, envs FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
)
|
||||
.bind(script_hash.0)
|
||||
.bind(w_id)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::InternalErr(format!("expected content and lock")))?;
|
||||
Ok(ContentReqLangEnvs {
|
||||
content: r.0,
|
||||
lockfile: r.1,
|
||||
language: r.2,
|
||||
envs: r.3,
|
||||
codebase: if r.4 {
|
||||
Some(script_hash.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
Ok(ContentReqLangEnvs { content: r.0, lockfile: r.1, language: r.2, envs: r.3 })
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
@@ -3110,48 +3054,35 @@ async fn handle_code_execution_job(
|
||||
worker_name: &str,
|
||||
column_order: &mut Option<Vec<String>>,
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
let ContentReqLangEnvs {
|
||||
content: inner_content,
|
||||
lockfile: requirements_o,
|
||||
language,
|
||||
envs,
|
||||
codebase,
|
||||
} = match job.job_kind {
|
||||
JobKind::Preview => ContentReqLangEnvs {
|
||||
content: job
|
||||
.raw_code
|
||||
.clone()
|
||||
.unwrap_or_else(|| "no raw code".to_owned()),
|
||||
lockfile: job.raw_lock.clone(),
|
||||
language: job.language.to_owned(),
|
||||
envs: None,
|
||||
codebase: if job
|
||||
.script_hash
|
||||
.is_some_and(|y| y.0 == PREVIEW_IS_CODEBASE_HASH)
|
||||
{
|
||||
Some(job.id.to_string())
|
||||
} else {
|
||||
None
|
||||
let ContentReqLangEnvs { content: inner_content, lockfile: requirements_o, language, envs } =
|
||||
match job.job_kind {
|
||||
JobKind::Preview => ContentReqLangEnvs {
|
||||
content: job
|
||||
.raw_code
|
||||
.clone()
|
||||
.unwrap_or_else(|| "no raw code".to_owned()),
|
||||
lockfile: job.raw_lock.clone(),
|
||||
language: job.language.to_owned(),
|
||||
envs: None,
|
||||
},
|
||||
},
|
||||
JobKind::Script_Hub => {
|
||||
get_hub_script_content_and_requirements(job.script_path.clone(), db).await?
|
||||
}
|
||||
JobKind::Script => {
|
||||
get_script_content_by_hash(
|
||||
&job.script_hash.unwrap_or(ScriptHash(0)),
|
||||
&job.workspace_id,
|
||||
db,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
JobKind::DeploymentCallback => {
|
||||
get_script_content_by_path(job.script_path.clone(), &job.workspace_id, db).await?
|
||||
}
|
||||
_ => unreachable!(
|
||||
"handle_code_execution_job should never be reachable with a non-code execution job"
|
||||
),
|
||||
};
|
||||
JobKind::Script_Hub => {
|
||||
get_hub_script_content_and_requirements(job.script_path.clone(), db).await?
|
||||
}
|
||||
JobKind::Script => {
|
||||
get_script_content_by_hash(
|
||||
&job.script_hash.unwrap_or(ScriptHash(0)),
|
||||
&job.workspace_id,
|
||||
db,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
JobKind::DeploymentCallback => {
|
||||
get_script_content_by_path(job.script_path.clone(), &job.workspace_id, db).await?
|
||||
}
|
||||
_ => unreachable!(
|
||||
"handle_code_execution_job should never be reachable with a non-code execution job"
|
||||
),
|
||||
};
|
||||
|
||||
if language == Some(ScriptLang::Postgresql) {
|
||||
return do_postgresql(
|
||||
@@ -3261,16 +3192,9 @@ 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 BASE_INTERNAL_URL = '{base_internal_url}';\n{}\n{}",
|
||||
reserved_variables
|
||||
.iter()
|
||||
.map(|(k, v)| format!("const {} = '{}';\n", k, v))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"),
|
||||
"const BASE_URL = '{base_internal_url}';\nconst WM_TOKEN = '{}';\n{}",
|
||||
&client.get_token().await,
|
||||
inner_content
|
||||
);
|
||||
let (result, ts_logs) =
|
||||
@@ -3286,6 +3210,8 @@ 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,
|
||||
@@ -3354,7 +3280,6 @@ mount {{
|
||||
Some(ScriptLang::Bun) => {
|
||||
handle_bun_job(
|
||||
requirements_o,
|
||||
codebase,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
job,
|
||||
@@ -3421,6 +3346,8 @@ 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 {}",
|
||||
@@ -4004,14 +3931,6 @@ 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,
|
||||
|
||||
@@ -42,7 +42,6 @@ use windmill_common::{
|
||||
},
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform, Retry, Suspend},
|
||||
};
|
||||
use windmill_queue::schedule::get_schedule_opt;
|
||||
use windmill_queue::{
|
||||
add_completed_job, add_completed_job_error, append_logs, get_queued_job,
|
||||
handle_maybe_scheduled_job, CanceledBy, PushIsolationLevel, WrappedError,
|
||||
@@ -672,7 +671,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
.root_job
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
tracing::info!(id = %flow_job.id, root_id = %job_root, "update flow status");
|
||||
tracing::info!(id = %flow_job.id, root_id = %job_root, worker_name = %worker_name, "update flow status");
|
||||
|
||||
let module = get_module(&flow_job, module_index);
|
||||
|
||||
@@ -870,7 +869,7 @@ 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");
|
||||
tracing::info!(subflow_id = %flow_job.id, parent_id = %parent_job, worker_name = %worker_name, "subflow is finished, updating parent flow status");
|
||||
|
||||
return Ok(Some(RecUpdateFlowStatusAfterJobCompletion {
|
||||
flow: parent_job,
|
||||
@@ -932,7 +931,7 @@ fn get_module(flow_job: &QueuedJob, module_index: Option<usize>) -> Option<FlowM
|
||||
if let Some(module) = raw_flow.modules.get(i) {
|
||||
Some(module.clone())
|
||||
} else {
|
||||
raw_flow.failure_module.map(|x| *x.clone())
|
||||
raw_flow.failure_module
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -1202,37 +1201,23 @@ pub async fn handle_flow<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
&& flow_job.script_path.is_some()
|
||||
&& status.step == 0
|
||||
{
|
||||
let mut tx: QueueTransaction<'_, R> = (rsmq.clone(), db.begin().await?).into();
|
||||
let tx: QueueTransaction<'_, R> = (rsmq.clone(), db.begin().await?).into();
|
||||
|
||||
let schedule_path = flow_job.schedule_path.as_ref().unwrap();
|
||||
|
||||
let schedule =
|
||||
get_schedule_opt(tx.transaction_mut(), &flow_job.workspace_id, schedule_path).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
if let Some(schedule) = schedule {
|
||||
if let Err(err) = handle_maybe_scheduled_job(
|
||||
rsmq.clone(),
|
||||
db,
|
||||
flow_job,
|
||||
&schedule,
|
||||
flow_job.script_path.as_ref().unwrap(),
|
||||
&flow_job.workspace_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
match err {
|
||||
Error::QuotaExceeded(_) => return Err(err.into()),
|
||||
// scheduling next job failed and could not disable schedule => make zombie job to retry
|
||||
_ => return Ok(()),
|
||||
}
|
||||
};
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Schedule {schedule_path} in {} not found. Impossible to schedule again",
|
||||
&flow_job.workspace_id
|
||||
);
|
||||
match handle_maybe_scheduled_job(
|
||||
tx,
|
||||
db,
|
||||
flow_job.schedule_path.as_ref().unwrap(),
|
||||
flow_job.script_path.as_ref().unwrap(),
|
||||
&flow_job.workspace_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tx) => {
|
||||
tx.commit().await?;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error during handle_maybe_scheduled_job: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1689,7 +1674,7 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
let mut module: &FlowModule = flow
|
||||
.modules
|
||||
.get(i)
|
||||
.or_else(|| flow.failure_module.as_deref())
|
||||
.or_else(|| flow.failure_module.as_ref())
|
||||
.with_context(|| format!("no module at index {}", status.step))?;
|
||||
|
||||
let current_id = &module.id;
|
||||
@@ -3124,7 +3109,6 @@ fn raw_script_to_payload(
|
||||
) -> JobPayloadWithTag {
|
||||
JobPayloadWithTag {
|
||||
payload: JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
path,
|
||||
content: content.clone(),
|
||||
language: language.clone(),
|
||||
|
||||
@@ -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.324.2";
|
||||
export const VERSION = "v1.306.2";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
85
cli/apps.ts
85
cli/apps.ts
@@ -5,13 +5,15 @@ import {
|
||||
colors,
|
||||
Command,
|
||||
ListableApp,
|
||||
log,
|
||||
Policy,
|
||||
SEP,
|
||||
Table,
|
||||
yamlParse,
|
||||
} from "./deps.ts";
|
||||
import { GlobalOptions, isSuperset } from "./types.ts";
|
||||
import {
|
||||
GlobalOptions,
|
||||
isSuperset,
|
||||
parseFromFile,
|
||||
removeType,
|
||||
} from "./types.ts";
|
||||
|
||||
export interface AppFile {
|
||||
value: any;
|
||||
@@ -19,20 +21,14 @@ export interface AppFile {
|
||||
policy: Policy;
|
||||
}
|
||||
|
||||
const alreadySynced: string[] = [];
|
||||
|
||||
export async function pushApp(
|
||||
workspace: string,
|
||||
remotePath: string,
|
||||
localPath: string,
|
||||
filePath: string,
|
||||
app: AppFile | undefined,
|
||||
newApp: AppFile,
|
||||
message?: string
|
||||
): Promise<void> {
|
||||
if (alreadySynced.includes(localPath)) {
|
||||
return;
|
||||
}
|
||||
alreadySynced.push(localPath);
|
||||
|
||||
let app: any = undefined;
|
||||
const remotePath = removeType(filePath, "app");
|
||||
// deleting old app if it exists in raw mode
|
||||
try {
|
||||
app = await AppService.getAppByPath({
|
||||
@@ -43,61 +39,28 @@ export async function pushApp(
|
||||
//ignore
|
||||
}
|
||||
|
||||
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`));
|
||||
if (isSuperset(newApp, app)) {
|
||||
return;
|
||||
}
|
||||
log.info(colors.bold.yellow(`Updating app ${remotePath}...`));
|
||||
await AppService.updateApp({
|
||||
workspace,
|
||||
path: remotePath.replaceAll("\\", "/"),
|
||||
requestBody: {
|
||||
deployment_message: message,
|
||||
...localApp,
|
||||
...newApp,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
log.info(colors.yellow.bold("Creating new app..."));
|
||||
console.log(colors.yellow.bold("Creating new app..."));
|
||||
|
||||
console.log(message);
|
||||
await AppService.createApp({
|
||||
workspace,
|
||||
requestBody: {
|
||||
path: remotePath.replaceAll("\\", "/"),
|
||||
deployment_message: message,
|
||||
...localApp,
|
||||
...newApp,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -131,22 +94,32 @@ async function list(opts: GlobalOptions) {
|
||||
.render();
|
||||
}
|
||||
|
||||
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
async function push(opts: GlobalOptions, filePath: string) {
|
||||
const remotePath = filePath.split(".")[0];
|
||||
if (!validatePath(remotePath)) {
|
||||
return;
|
||||
}
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await pushApp(workspace.workspaceId, remotePath, filePath);
|
||||
log.info(colors.bold.underline.green("Flow pushed"));
|
||||
<<<<<<< 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"));
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("app related commands")
|
||||
.action(list as any)
|
||||
.command("push", "push a local app ")
|
||||
.arguments("<file_path:string> <remote_path:string>")
|
||||
.arguments("<file_path:file>")
|
||||
.action(push as any);
|
||||
|
||||
export default command;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user