Compare commits

..

3 Commits

Author SHA1 Message Date
Ruben Fiszel
2f46459844 all 2024-04-14 19:37:12 +02:00
Ruben Fiszel
f82dfa7e97 all 2024-04-14 19:37:03 +02:00
Ruben Fiszel
a605d79f21 all 2024-04-14 19:24:54 +02:00
392 changed files with 7537 additions and 15140 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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/

View File

@@ -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.

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View 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"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DROP INDEX CONCURRENTLY IF EXISTS labeled_jobs_on_jobs",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "6f12be65a4fe3eb39292164363f557de9cef7017dcfbcd40370b849a288c52e3"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"]}

View File

@@ -1 +1 @@
644b6f49f087790a728a7a0a82525997b1742738
66d9cbb158ab9a5869a45ba253bf57f2cbb5ecb6

View File

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

View File

@@ -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[])));

View File

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

View File

@@ -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

View File

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

View File

@@ -1,2 +0,0 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN no_main_func BOOLEAN;

View File

@@ -1,2 +0,0 @@
-- Add down migration script here
DROP INDEX IF EXISTS labeled_jobs_on_completed_jobs;

View File

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

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
-- Add down migration script here
ALTER TABLE script DROP COLUMN codebase;

View File

@@ -1,3 +0,0 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN codebase VARCHAR(255);

View File

@@ -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"
]
}
}

View File

@@ -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> {

View File

@@ -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),
}
}

View File

@@ -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",

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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(())
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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:

View File

@@ -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) })

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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",

View File

@@ -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)

View File

@@ -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")?

View File

@@ -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
)

View File

@@ -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 \

View File

@@ -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())
}

View File

@@ -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(());
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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,
};

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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",

View File

@@ -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>,

View File

@@ -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(

View File

@@ -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)
}

View File

@@ -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> {

View File

@@ -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(),
}
}

View File

@@ -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;
}

View File

@@ -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",
)

View File

@@ -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

View File

@@ -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
},

View File

@@ -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 =

View File

@@ -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(", ")}`
);

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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(&current_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

View File

@@ -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")?;

View File

@@ -527,8 +527,6 @@ for await (const chunk of Deno.stdin.readable) {{
jobs_rx,
worker_name,
db,
script_path,
"deno",
)
.await
}

View File

@@ -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(())

View File

@@ -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
}

View File

@@ -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>()

View File

@@ -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

View File

@@ -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

View File

@@ -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:?}",

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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({

View File

@@ -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