Compare commits

..

1 Commits

Author SHA1 Message Date
Ruben Fiszel
649872ab0d all 2024-10-17 15:18:03 +02:00
141 changed files with 1163 additions and 5051 deletions

View File

@@ -40,11 +40,11 @@ RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VER
RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=oven/bun:1.1.31 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.30 /usr/local/bin/bun /usr/bin/bun
ARG TARGETPLATFORM
RUN curl -Lsf https://github.com/denoland/deno/releases/download/v2.0.2/deno-x86_64-unknown-linux-gnu.zip -o deno.zip
RUN curl -Lsf https://github.com/denoland/deno/releases/download/v2.0.0/deno-x86_64-unknown-linux-gnu.zip -o deno.zip
# RUN [ "$TARGETPLATFORM" == "linux/arm64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v2.0.0/deno-aarch64-unknown-linux-gnu.zip -o deno.zip || true
RUN unzip deno.zip && rm deno.zip && mv deno /usr/bin/deno

View File

@@ -26,7 +26,6 @@ permissions: write-all
jobs:
build:
runs-on: ubicloud
if: (github.event_name != 'issue_comment') || (contains(github.event.comment.body, '/buildimage_all') || contains(github.event.comment.body, '/buildimage_base'))
steps:
- uses: actions/checkout@v4
with:
@@ -88,7 +87,6 @@ jobs:
build_ee:
runs-on: ubicloud
if: (github.event_name != 'issue_comment') || (contains(github.event.comment.body, '/buildimage_ee') || contains(github.event.comment.body, '/buildimage_nsjail')) || contains(github.event.comment.body, '/buildimage_all')
steps:
- uses: actions/checkout@v4
with:
@@ -438,7 +436,7 @@ jobs:
build_ee_nsjail:
needs: [build_ee]
runs-on: ubicloud
if: (github.event_name != 'issue_comment') || (github.event_name != 'pull_request') || (contains(github.event.comment.body, '/buildimage_nsjail') || contains(github.event.comment.body, '/buildimage_all'))
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v4
with:

View File

@@ -1,77 +1,5 @@
# Changelog
## [1.411.1](https://github.com/windmill-labs/windmill/compare/v1.411.0...v1.411.1) (2024-10-22)
### Bug Fixes
* update bun to 1.1.32 ([#4568](https://github.com/windmill-labs/windmill/issues/4568)) ([0586446](https://github.com/windmill-labs/windmill/commit/058644667129f0d79ec147aacdda449142ae0ab9))
## [1.411.0](https://github.com/windmill-labs/windmill/compare/v1.410.3...v1.411.0) (2024-10-21)
### Features
* **cli:** encrypt sensitive instance settings ([#4561](https://github.com/windmill-labs/windmill/issues/4561)) ([b8a6a11](https://github.com/windmill-labs/windmill/commit/b8a6a116354b10f5977e54edb365d6711e160538))
### Bug Fixes
* Do not ignore file resources with json file ext ([#4562](https://github.com/windmill-labs/windmill/issues/4562)) ([2079b2e](https://github.com/windmill-labs/windmill/commit/2079b2e7e19aa2fe327f2ae66d1b5eba988b9b0a))
* update bun to 1.1.31 and deno to 2.0.2 ([0d90396](https://github.com/windmill-labs/windmill/commit/0d9039641b3348e75599937188c35dd89a000584))
## [1.410.3](https://github.com/windmill-labs/windmill/compare/v1.410.2...v1.410.3) (2024-10-20)
### Bug Fixes
* **go-client:** reduce runtime dependencies by bumping oai-codeen to v2.4.1 ([87f5c07](https://github.com/windmill-labs/windmill/commit/87f5c078dd63cd3e0ba3e719c2da7b83f1458e2c))
## [1.410.1](https://github.com/windmill-labs/windmill/compare/v1.410.0...v1.410.1) (2024-10-19)
### Bug Fixes
* **cli:** improve wmill init behavior ([26a40d1](https://github.com/windmill-labs/windmill/commit/26a40d19441aa816ee711ce30f3435dddd3542a7))
* **frontend:** improve display of error handlers ([a92a2fd](https://github.com/windmill-labs/windmill/commit/a92a2fd6fd67c11c04554a90b8ae6d7a0dd9067c))
## [1.410.1](https://github.com/windmill-labs/windmill/compare/v1.410.0...v1.410.1) (2024-10-19)
### Bug Fixes
* **cli:** improve wmill init behavior ([26a40d1](https://github.com/windmill-labs/windmill/commit/26a40d19441aa816ee711ce30f3435dddd3542a7))
## [1.410.0](https://github.com/windmill-labs/windmill/compare/v1.409.4...v1.410.0) (2024-10-18)
### Features
* **typescript-bun:** support relative imports without the .ts extension ([248fdc2](https://github.com/windmill-labs/windmill/commit/248fdc24a61aca946902b20b9d8187101a9b7bfa))
* websocket triggers ([#4505](https://github.com/windmill-labs/windmill/issues/4505)) ([8807e99](https://github.com/windmill-labs/windmill/commit/8807e99f06caf3e26c526eefdc83dbc2f7aa93ee))
### Bug Fixes
* cache js static assets by default ([08595c6](https://github.com/windmill-labs/windmill/commit/08595c6f14a89df1559974e8835e63881a1a9601))
* **frontend:** add back script lockfile to script details pae ([549b11d](https://github.com/windmill-labs/windmill/commit/549b11dcfb39a60acd6d3899fce51b31149f8594))
* improve cancelling of jobs on public apps for anonymous users ([d7cf5ea](https://github.com/windmill-labs/windmill/commit/d7cf5ea37db313e78865708bc22e87a1feea9e0d))
## [1.409.4](https://github.com/windmill-labs/windmill/compare/v1.409.3...v1.409.4) (2024-10-17)
### Bug Fixes
* fix flow viewer renderer outside of flow details and editor context ([59a1e67](https://github.com/windmill-labs/windmill/commit/59a1e67465cd21ded5539cb5b829bc0d4e7169ed))
## [1.409.3](https://github.com/windmill-labs/windmill/compare/v1.409.2...v1.409.3) (2024-10-17)
### Bug Fixes
* do not delete primary schedule of script/flow on redeploy even if schedule wasn't loaded ([c3c2fe4](https://github.com/windmill-labs/windmill/commit/c3c2fe462c52f48795a55025414b16f8d3d98fe0))
* **nsjail:** improve memory reading when using nsjail ([b7ad19b](https://github.com/windmill-labs/windmill/commit/b7ad19bb75bd885a254ebac778349c7d88af8326))
## [1.409.2](https://github.com/windmill-labs/windmill/compare/v1.409.1...v1.409.2) (2024-10-16)

View File

@@ -175,9 +175,9 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=builder /frontend/build /static_frontend
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=denoland/deno:2.0.2 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=denoland/deno:2.0.0 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.1.32 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.30 /usr/local/bin/bun /usr/bin/bun
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer

View File

@@ -1,24 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET server_id = $1, last_server_ping = now() WHERE enabled IS TRUE AND workspace_id = $2 AND path = $3 AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') RETURNING true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "02424907504848e983bfa89eec343061932dc5b4b17cf13d5cf8d833aedbe6d5"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET enabled = $1, email = $2, edited_by = $3, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL\n WHERE path = $4 AND workspace_id = $5 RETURNING 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Bool",
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "12a86755706ce030a0a9142da78d035d0c7d361240b60005cc864e4345eb0bc7"
}

View File

@@ -1,24 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM websocket_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Bool",
"Text"
]
},
"nullable": [
null
]
},
"hash": "2e6165543e34216dfaedf6e10729f733cff812e62dea1c1bce8cefd3a3979b14"
}

View File

@@ -1,108 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now()) RETURNING *",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "url",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 11,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "enabled",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "filters",
"type_info": "JsonbArray"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Bool",
"JsonbArray",
"Varchar",
"Varchar"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false,
false
]
},
"hash": "4e9668a46bad9e82baa51422946d373b18b6577198df7545c94bd19be3446775"
}

View File

@@ -1,98 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT *\n FROM websocket_trigger\n WHERE enabled IS TRUE AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "url",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 11,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "enabled",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "filters",
"type_info": "JsonbArray"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false,
false
]
},
"hash": "5303cb9dd5903aa4791ef8e5e5881a50a832e65c8c9632e2e12cd9c2747f2fc7"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE path = $1 AND workspace_id = $2)",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1)",
"describe": {
"columns": [
{
@@ -11,7 +11,6 @@
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
@@ -19,5 +18,5 @@
null
]
},
"hash": "2238aaed46031f71b17c1cabcef935a0d3b2ca6d057f446ed9b02d4386e2ddd9"
"hash": "9ebb9c16948a695a053068d9a1df0152691271ac33d8c82db47be11746ccbcef"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, edited_by = $6, email = $7, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL\n WHERE workspace_id = $8 AND path = $9",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Bool",
"JsonbArray",
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "acbf74cf3302bfcf7615285070d3f8958932bb8a2dda715f1b9152ab44442780"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM websocket_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "d54840373df5da9662ed11ed0a605cddac517bdc06dca0a8be071330338e948a"
}

View File

@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as \"websocket_used!\", EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as \"http_routes_used!\"",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "websocket_used!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "http_routes_used!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null
]
},
"hash": "db24f1f6ef1e26b7de7926bd3f32d1fa673c4970c25ff9ad98f0fdf199801b53"
}

View File

@@ -1,101 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT *\n FROM websocket_trigger\n WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "url",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 11,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "enabled",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "filters",
"type_info": "JsonbArray"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false,
false
]
},
"hash": "f2baee15e6d1fecd6d2d7b39fda1b50a15ec8bd349f1081af54da5dc2f5e3021"
}

View File

@@ -1,25 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET last_server_ping = now(), error = $1 WHERE workspace_id = $2 AND path = $3 AND server_id = $4 AND enabled IS TRUE RETURNING 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "febd70e6d5304ad912c232d3d8fc3f8ce0133d8a1a9e5ca1124eff9a988dc2d7"
}

316
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.411.1"
version = "1.409.2"
authors.workspace = true
edition.workspace = true
@@ -14,7 +14,6 @@ members = [
"./windmill-audit",
"./windmill-git-sync",
"./windmill-indexer",
"./windmill-macros",
"./parsers/windmill-parser",
"./parsers/windmill-parser-ts",
"./parsers/windmill-parser-wasm",
@@ -24,11 +23,11 @@ members = [
"./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-imports",
"./parsers/windmill-sql-datatype-parser-wasm",
"./parsers/windmill-parser-yaml", "windmill-macros",
"./parsers/windmill-parser-yaml",
]
[workspace.package]
version = "1.411.1"
version = "1.409.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -55,7 +54,7 @@ embedding = ["windmill-api/embedding"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet", "windmill-indexer/parquet", "dep:object_store"]
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus"]
flow_testing = ["windmill-worker/flow_testing"]
openidconnect = ["windmill-api/openidconnect"]
openidconnect = ["windmill-api/openidconnect", "windmill-common/openidconnect"]
cloud = ["windmill-queue/cloud", "windmill-worker/cloud"]
jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-ctl"]
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy"]
@@ -117,7 +116,6 @@ windmill-common = { path = "./windmill-common", default-features = false }
windmill-audit = { path = "./windmill-audit" }
windmill-git-sync = { path = "./windmill-git-sync" }
windmill-indexer = {path = "./windmill-indexer"}
windmill-macros = {path = "./windmill-macros"}
windmill-parser = { path = "./parsers/windmill-parser" }
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
windmill-parser-py = { path = "./parsers/windmill-parser-py" }
@@ -219,7 +217,7 @@ serde_derive = "1.0.147"
const_format = { version = "0.2", features = ["rust_1_64", "rust_1_51"] }
dyn-iter = "0.2.0"
rsa = "0.7.2"
async-stripe = { version = "0.39.1", features = [
async-stripe = { version = "0.34.1", features = [
"runtime-tokio-hyper",
"checkout",
"billing",
@@ -283,12 +281,9 @@ triomphe = "^0"
tantivy = "0.22.0"
# Macro-related
proc-macro2 = "1.0"
pulldown-cmark = "0.9"
toml = "0.7"
syn = { version = "2.0.74", features = ["full"] }
quote = "1.0.36"
regex-lite = "0.1.6"
yaml-rust = "0.4.5"
tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] }

View File

@@ -1 +1 @@
d61c163e0a311ecd86d1398aff883eaea8d0b09a
0428068e4fbbd1380a4d8bbaab5c8e7955decdb8

View File

@@ -1,2 +0,0 @@
-- Add down migration script here
DROP TABLE websocket_trigger;

View File

@@ -1,67 +0,0 @@
-- Add up migration script here
CREATE TABLE websocket_trigger (
path VARCHAR(255) NOT NULL,
url VARCHAR(255) NOT NULL,
script_path VARCHAR(255) NOT NULL,
is_flow BOOLEAN NOT NULL,
workspace_id VARCHAR(50) NOT NULL,
edited_by VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
edited_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
extra_perms JSONB NOT NULL DEFAULT '{}',
server_id VARCHAR(50) NULL,
last_server_ping TIMESTAMPTZ NULL,
error TEXT NULL,
enabled BOOLEAN NOT NULL,
filters JSONB[] NOT NULL DEFAULT '{}',
PRIMARY KEY (path, workspace_id)
);
GRANT ALL ON websocket_trigger TO windmill_user;
GRANT ALL ON websocket_trigger TO windmill_admin;
ALTER TABLE websocket_trigger ENABLE ROW LEVEL SECURITY;
CREATE POLICY admin_policy ON websocket_trigger FOR ALL TO windmill_admin USING (true);
CREATE POLICY see_folder_extra_perms_user_select ON websocket_trigger FOR SELECT TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'f' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_insert ON websocket_trigger FOR INSERT TO windmill_user
WITH CHECK (SPLIT_PART(websocket_trigger.path, '/', 1) = 'f' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_update ON websocket_trigger FOR UPDATE TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'f' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_delete ON websocket_trigger FOR DELETE TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'f' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_own ON websocket_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'u' AND SPLIT_PART(websocket_trigger.path, '/', 2) = current_setting('session.user'));
CREATE POLICY see_member ON websocket_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(websocket_trigger.path, '/', 1) = 'g' AND SPLIT_PART(websocket_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
CREATE POLICY see_extra_perms_user_select ON websocket_trigger FOR SELECT TO windmill_user
USING (extra_perms ? CONCAT('u/', current_setting('session.user')));
CREATE POLICY see_extra_perms_user_insert ON websocket_trigger FOR INSERT TO windmill_user
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_update ON websocket_trigger FOR UPDATE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_delete ON websocket_trigger FOR DELETE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_groups_select ON websocket_trigger 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 websocket_trigger 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 websocket_trigger 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 websocket_trigger 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));

View File

@@ -1,16 +0,0 @@
use anyhow::anyhow;
#[cfg(feature = "enterprise")]
use windmill_common::error::{Error, Result};
pub async fn set_license_key(_license_key: String) -> anyhow::Result<()> {
// Implementation is not open source
Err(anyhow!("License cannot be set in Windmill CE"))
}
#[cfg(feature = "enterprise")]
pub async fn verify_license_key() -> Result<()> {
// Implementation is not open source
Err(Error::InternalErr(
"License always invalid in Windmill CE".to_string(),
))
}

1
backend/src/ee.rs Symbolic link
View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/src/ee.rs

View File

@@ -17,7 +17,7 @@ benchmark = []
embedding = ["dep:tinyvector", "dep:hf-hub", "dep:tokenizers", "dep:candle-core", "dep:candle-transformers", "dep:candle-nn"]
parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/parquet"]
prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus"]
openidconnect = ["dep:openidconnect"]
openidconnect = ["dep:openidconnect", "windmill-common/openidconnect"]
tantivy = ["dep:windmill-indexer"]
[dependencies]
@@ -95,7 +95,6 @@ openidconnect = { workspace = true, optional = true}
url = { workspace = true, optional = true}
jsonwebtoken = { workspace = true }
matchit.workspace = true
tokio-tungstenite.workspace = true
pin-project.workspace = true
http.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.411.1
version: 1.409.2
title: Windmill API
contact:
@@ -2162,30 +2162,6 @@ paths:
schema:
type: number
/w/{workspace}/workspaces/used_triggers:
get:
summary: get used triggers
operationId: getUsedTriggers
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: status
content:
application/json:
schema:
type: object
properties:
http_routes_used:
type: boolean
websocket_used:
type: boolean
required:
- http_routes_used
- websocket_used
/w/{workspace}/users/list:
get:
summary: list users
@@ -4116,26 +4092,6 @@ paths:
items:
$ref: "#/components/schemas/ScriptHistory"
/w/{workspace}/scripts/get_latest_version/{path}:
get:
summary: get scripts's latest version (hash)
operationId: getScriptLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- script
responses:
"200":
description: Script version/hash
content:
application/json:
required: false
schema:
$ref: "#/components/schemas/ScriptHistory"
/w/{workspace}/scripts/history_update/h/{hash}/p/{path}:
post:
summary: update history of a script
@@ -4624,25 +4580,6 @@ paths:
items:
$ref: "#/components/schemas/FlowVersion"
/w/{workspace}/flows/get_latest_version/{path}:
get:
summary: get flow's latest version
operationId: getFlowLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- flow
responses:
"200":
description: Flow version
content:
application/json:
required: false
schema:
$ref: "#/components/schemas/FlowVersion"
/w/{workspace}/flows/get/v/{version}/p/{path}:
get:
summary: get flow version
@@ -5197,24 +5134,6 @@ paths:
items:
$ref: "#/components/schemas/AppHistory"
/w/{workspace}/apps/get_latest_version/{path}:
get:
summary: get apps's latest version
operationId: getAppLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- app
responses:
"200":
description: App version
content:
application/json:
required: false
schema:
$ref: "#/components/schemas/AppHistory"
/w/{workspace}/apps/history_update/a/{id}/v/{version}:
post:
summary: update app history
@@ -7233,170 +7152,22 @@ paths:
schema:
type: boolean
/w/{workspace}/websocket_triggers/create:
post:
summary: create websocket trigger
operationId: createWebsocketTrigger
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: new websocket trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewWebsocketTrigger"
responses:
"201":
description: websocket trigger created
content:
text/plain:
schema:
type: string
/w/{workspace}/websocket_triggers/update/{path}:
post:
summary: update websocket trigger
operationId: updateWebsocketTrigger
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: updated trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EditWebsocketTrigger"
responses:
"200":
description: websocket trigger updated
content:
text/plain:
schema:
type: string
/w/{workspace}/websocket_triggers/delete/{path}:
delete:
summary: delete websocket trigger
operationId: deleteWebsocketTrigger
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: websocket trigger deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/websocket_triggers/get/{path}:
/w/{workspace}/http_triggers/used:
get:
summary: get websocket trigger
operationId: getWebsocketTrigger
summary: whether http triggers are used
operationId: used
tags:
- websocket_trigger
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: websocket trigger deleted
content:
application/json:
schema:
$ref: "#/components/schemas/WebsocketTrigger"
/w/{workspace}/websocket_triggers/list:
get:
summary: list websocket triggers
operationId: listWebsocketTriggers
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
required: true
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: path
description: filter by path
in: query
schema:
type: string
- name: is_flow
in: query
schema:
type: boolean
- name: path_start
in: query
schema:
type: string
responses:
"200":
description: websocket trigger list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WebsocketTrigger"
/w/{workspace}/websocket_triggers/exists/{path}:
get:
summary: does websocket trigger exists
operationId: existsWebsocketTrigger
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: websocket trigger exists
description: whether http triggers are used
content:
application/json:
schema:
type: boolean
/w/{workspace}/websocket_triggers/setenabled/{path}:
post:
summary: set enabled websocket trigger
operationId: setWebsocketTriggerEnabled
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: updated websocket trigger enable
required: true
content:
application/json:
schema:
type: object
properties:
enabled:
type: boolean
required:
- enabled
responses:
"200":
description: websocket trigger enabled set
content:
text/plain:
schema:
type: string
/groups/list:
get:
summary: list instance groups
@@ -8224,7 +7995,6 @@ paths:
app,
raw_app,
http_trigger,
websocket_trigger,
]
responses:
"200":
@@ -8262,7 +8032,6 @@ paths:
app,
raw_app,
http_trigger,
websocket_trigger,
]
requestBody:
description: acl to add
@@ -8311,7 +8080,6 @@ paths:
app,
raw_app,
http_trigger,
websocket_trigger,
]
requestBody:
description: acl to add
@@ -10735,7 +10503,6 @@ components:
- "users.delete"
- "users.update"
- "users.login"
- "users.login_failure"
- "users.logout"
- "users.accept_invite"
- "users.decline_invite"
@@ -11446,128 +11213,6 @@ components:
type: number
email_count:
type: number
websocket_count:
type: number
WebsocketTrigger:
type: object
properties:
path:
type: string
edited_by:
type: string
edited_at:
type: string
format: date-time
script_path:
type: string
url:
type: string
is_flow:
type: boolean
extra_perms:
type: object
additionalProperties:
type: boolean
email:
type: string
workspace_id:
type: string
server_id:
type: string
last_server_ping:
type: string
format: date-time
error:
type: string
enabled:
type: boolean
filters:
type: array
items:
type: object
properties:
key:
type: string
value: {}
required:
- key
- value
required:
- path
- edited_by
- edited_at
- script_path
- url
- extra_perms
- is_flow
- email
- workspace_id
- enabled
- filters
NewWebsocketTrigger:
type: object
properties:
path:
type: string
script_path:
type: string
is_flow:
type: boolean
url:
type: string
enabled:
type: boolean
filters:
type: array
items:
type: object
properties:
key:
type: string
value: {}
required:
- key
- value
required:
- path
- script_path
- url
- is_flow
- filters
EditWebsocketTrigger:
type: object
properties:
url:
type: string
path:
type: string
script_path:
type: string
is_flow:
type: boolean
filters:
type: array
items:
type: object
properties:
key:
type: string
value: {}
required:
- key
- value
required:
- path
- script_path
- url
- is_flow
- filters
Group:
type: object

View File

@@ -62,7 +62,6 @@ pub fn workspaced_service() -> Router {
.route("/delete/*path", delete(delete_app))
.route("/create", post(create_app))
.route("/history/p/*path", get(get_app_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route("/history_update/a/:id/v/:version", post(update_app_history))
}
@@ -428,38 +427,6 @@ async fn get_app_history(
return Ok(Json(result));
}
async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<AppHistory>> {
let mut tx = user_db.begin(&authed).await?;
let row = sqlx::query!(
"SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg
FROM app a LEFT JOIN app_version av ON a.id = av.app_id LEFT JOIN deployment_metadata dm ON av.id = dm.app_version
WHERE a.workspace_id = $1 AND a.path = $2
ORDER BY created_at DESC",
w_id,
path.to_path(),
).fetch_optional(&mut *tx).await?;
tx.commit().await?;
if let Some(row) = row {
let result = AppHistory {
app_id: row.app_id,
version: row.version_id,
deployment_msg: row.deployment_msg,
};
return Ok(Json(Some(result)));
} else {
return Ok(Json(None));
}
}
async fn update_app_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,

View File

@@ -1,32 +0,0 @@
use anyhow::anyhow;
#[cfg(feature = "enterprise")]
use std::sync::Arc;
#[cfg(feature = "enterprise")]
use tokio::sync::RwLock;
pub async fn validate_license_key(_license_key: String) -> anyhow::Result<String> {
// Implementation is not open source
Err(anyhow!("License can't be validated in Windmill CE"))
}
#[cfg(feature = "enterprise")]
pub async fn jwt_ext_auth(
_w_id: Option<&String>,
_token: &str,
_external_jwks: Option<Arc<RwLock<ExternalJwks>>>,
) -> anyhow::Result<(crate::db::ApiAuthed, usize)> {
// Implementation is not open source
Err(anyhow!("External JWT auth is not open source"))
}
#[cfg(feature = "enterprise")]
pub struct ExternalJwks;
#[cfg(feature = "enterprise")]
impl ExternalJwks {
pub async fn load() -> Option<Arc<RwLock<Self>>> {
// Implementation is not open source
None
}
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-api/src/ee.rs

View File

@@ -63,7 +63,6 @@ pub fn workspaced_service() -> Router {
.route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths))
.route("/history/p/*path", get(get_flow_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route(
"/history_update/v/:version/p/*path",
post(update_flow_history),
@@ -539,31 +538,6 @@ async fn get_flow_history(
Ok(Json(flows))
}
async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<FlowVersion>> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let version = sqlx::query_as!(
FlowVersion,
"SELECT flow_version.id, flow_version.created_at, deployment_metadata.deployment_msg FROM flow_version
LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version
WHERE flow_version.path = $1 AND flow_version.workspace_id = $2
ORDER BY flow_version.created_at DESC",
path,
w_id
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(version))
}
async fn get_flow_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,

View File

@@ -12,8 +12,10 @@ use std::collections::HashMap;
use tower_http::cors::CorsLayer;
use windmill_audit::{audit_ee::audit_log, ActionKind};
use windmill_common::{
auth::fetch_authed_from_permissioned_as,
db::UserDB,
error::{self, JsonResult},
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};
@@ -25,7 +27,7 @@ use crate::{
run_flow_by_path_inner, run_script_by_path_inner, run_wait_result_flow_by_path_internal,
run_wait_result_script_by_path_internal, RunJobQuery,
},
users::{fetch_api_authed, OptAuthed},
users::OptAuthed,
};
lazy_static::lazy_static! {
@@ -64,6 +66,7 @@ pub fn workspaced_service() -> Router {
.route("/update/*path", post(update_trigger))
.route("/delete/*path", delete(delete_trigger))
.route("/exists/*path", get(exists_trigger))
.route("/used", get(used))
.route("/route_exists", post(exists_route))
}
@@ -343,6 +346,17 @@ async fn delete_trigger(
Ok(format!("HTTP trigger {path} deleted"))
}
async fn used(Extension(db): Extension<DB>, Path(w_id): Path<String>) -> JsonResult<bool> {
let used = sqlx::query_scalar!(
r#"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1)"#,
w_id,
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(used))
}
async fn exists_trigger(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -407,6 +421,28 @@ struct TriggerRoute {
http_method: HttpMethod,
}
async fn fetch_api_authed(
username: String,
email: String,
w_id: &str,
db: &DB,
username_override: String,
) -> error::Result<ApiAuthed> {
let permissioned_as = username_to_permissioned_as(username.as_str());
let authed =
fetch_authed_from_permissioned_as(permissioned_as, email.clone(), w_id, db).await?;
Ok(ApiAuthed {
username: username,
email: email,
is_admin: authed.is_admin,
is_operator: authed.is_operator,
groups: authed.groups,
folders: authed.folders,
scopes: authed.scopes,
username_override: Some(username_override),
})
}
async fn get_http_route_trigger(
route_path: &str,
opt_authed: Option<ApiAuthed>,

View File

@@ -1,5 +0,0 @@
use axum::Router;
pub fn workspaced_service() -> Router {
Router::new()
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-api/src/indexer_ee.rs

View File

@@ -1,5 +0,0 @@
use axum::Router;
pub fn workspaced_service() -> Router {
Router::new()
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-api/src/job_helpers_ee.rs

View File

@@ -70,10 +70,7 @@ use windmill_common::{
oauth2::HmacSha256,
scripts::{ScriptHash, ScriptLang},
users::username_to_permissioned_as,
utils::{
not_found_if_none, now_from_db, paginate, paginate_without_limits, require_admin,
Pagination, StripPath,
},
utils::{not_found_if_none, now_from_db, paginate, paginate_without_limits, require_admin, Pagination, StripPath},
};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
@@ -4196,10 +4193,10 @@ async fn run_dependencies_job(
JsonRawValue::from_string("true".to_string()).unwrap(),
);
if language == ScriptLang::Bun {
let annotation = windmill_common::worker::TypeScriptAnnotations::parse(&raw_code);
let annotation = windmill_common::worker::get_annotation_ts(&raw_code);
hm.insert(
"npm_mode".to_string(),
JsonRawValue::from_string(annotation.npm.to_string()).unwrap(),
JsonRawValue::from_string(annotation.npm_mode.to_string()).unwrap(),
);
}
(PushArgs { extra: Some(hm), args: &ehm }, deps)
@@ -4754,6 +4751,7 @@ async fn get_job_update(
&w_id,
job_id,
"progress_perc"
)
.fetch_optional(&db)
.await?.and_then(|inner| inner)

View File

@@ -39,7 +39,7 @@ use tower_http::{
trace::TraceLayer,
};
use windmill_common::db::UserDB;
use windmill_common::worker::{ALL_TAGS, CLOUD_HOSTED};
use windmill_common::worker::ALL_TAGS;
use windmill_common::{BASE_URL, INSTANCE_NAME};
use crate::scim_ee::has_scim_token;
@@ -87,7 +87,6 @@ mod users;
mod utils;
mod variables;
mod webhook_util;
mod websocket_triggers;
mod workers;
mod workspaces;
@@ -226,7 +225,7 @@ pub async fn run_server(
db: db.clone(),
user_db: user_db,
auth_cache: auth_cache.clone(),
rsmq: rsmq.clone(),
rsmq: rsmq,
base_internal_url: base_internal_url.clone(),
});
if let Err(err) = smtp_server.start_listener_thread(addr).await {
@@ -246,11 +245,6 @@ pub async fn run_server(
}
};
if !*CLOUD_HOSTED {
let ws_killpill_rx = rx.resubscribe();
websocket_triggers::start_websockets(db.clone(), rsmq, ws_killpill_rx).await;
}
// build our application with a route
let app = Router::new()
.nest(
@@ -291,11 +285,7 @@ pub async fn run_server(
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc_ee::workspaced_service())
.nest("/http_triggers", http_triggers::workspaced_service())
.nest(
"/websocket_triggers",
websocket_triggers::workspaced_service(),
),
.nest("/http_triggers", http_triggers::workspaced_service()),
)
.nest("/workspaces", workspaces::global_service())
.nest(

View File

@@ -1,188 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use std::{collections::HashMap, fmt::Debug};
use axum::{routing::get, Json, Router};
use hmac::Mac;
use hyper::HeaderMap;
use itertools::Itertools;
use oauth2::{Client as OClient, *};
use serde::{Deserialize, Serialize};
use sqlx::{Postgres, Transaction};
use windmill_common::more_serde::maybe_number_opt;
use crate::OAUTH_CLIENTS;
use windmill_common::error;
use windmill_common::oauth2::*;
use crate::db::DB;
use std::str;
pub fn global_service() -> Router {
Router::new()
.route("/list_supabase", get(list_supabase))
.route("/list_logins", get(list_logins))
.route("/list_connects", get(list_connects))
}
pub fn workspaced_service() -> Router {
Router::new()
}
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum InstanceEvent {
UserAdded { email: String },
// UserDeleted { email: String },
// UserDeletedWorkspace { workspace: String, email: String },
UserAddedWorkspace { workspace: String, email: String },
UserInvitedWorkspace { workspace: String, email: String },
UserJoinedWorkspace { workspace: String, email: String, username: String },
}
#[derive(Debug, Clone)]
pub struct ClientWithScopes {
_client: OClient,
_scopes: Vec<String>,
_extra_params: Option<HashMap<String, String>>,
_extra_params_callback: Option<HashMap<String, String>>,
_allowed_domains: Option<Vec<String>>,
_userinfo_url: Option<String>,
}
pub type BasicClientsMap = HashMap<String, ClientWithScopes>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OAuthConfig {
auth_url: String,
token_url: String,
userinfo_url: Option<String>,
scopes: Option<Vec<String>>,
extra_params: Option<HashMap<String, String>>,
extra_params_callback: Option<HashMap<String, String>>,
req_body_auth: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OAuthClient {
id: String,
secret: String,
allowed_domains: Option<Vec<String>>,
connect_config: Option<OAuthConfig>,
login_config: Option<OAuthConfig>,
}
#[derive(Debug)]
pub struct AllClients {
pub logins: BasicClientsMap,
pub connects: BasicClientsMap,
pub slack: Option<OClient>,
}
pub fn build_oauth_clients(
_base_url: &str,
_oauths_from_config: Option<HashMap<String, OAuthClient>>,
) -> anyhow::Result<AllClients> {
// Implementation is not open source
return Ok(AllClients {
logins: HashMap::default(),
connects: HashMap::default(),
slack: None,
});
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TokenResponse {
access_token: AccessToken,
#[serde(deserialize_with = "maybe_number_opt")]
#[serde(default)]
expires_in: Option<u64>,
refresh_token: Option<RefreshToken>,
#[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")]
#[serde(serialize_with = "helpers::serialize_space_delimited_vec")]
#[serde(default)]
scope: Option<Vec<Scope>>,
}
#[derive(Serialize)]
struct Logins {
oauth: Vec<String>,
saml: Option<String>,
}
async fn list_logins() -> error::JsonResult<Logins> {
// Implementation is not open source
return Ok(Json(Logins { oauth: vec![], saml: None }));
}
async fn list_connects() -> error::JsonResult<Vec<String>> {
Ok(Json(
(&OAUTH_CLIENTS.read().await.connects)
.keys()
.map(|x| x.to_owned())
.collect_vec(),
))
}
pub async fn _refresh_token<'c>(
_tx: Transaction<'c, Postgres>,
_path: &str,
_w_id: &str,
_id: i32,
_db: &DB,
) -> error::Result<String> {
// Implementation is not open source
Err(error::Error::BadRequest(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
async fn list_supabase(_headers: HeaderMap) -> error::Result<String> {
// Implementation is not open source
Err(error::Error::BadRequest(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
pub async fn check_nb_of_user(db: &DB) -> error::Result<()> {
let nb_users_sso =
sqlx::query_scalar!("SELECT COUNT(*) FROM password WHERE login_type != 'password'",)
.fetch_one(db)
.await?;
if nb_users_sso.unwrap_or(0) >= 10 {
return Err(error::Error::BadRequest(
"You have reached the maximum number of oauth users accounts (10) without an enterprise license"
.to_string(),
));
}
let nb_users = sqlx::query_scalar!("SELECT COUNT(*) FROM password",)
.fetch_one(db)
.await?;
if nb_users.unwrap_or(0) >= 50 {
return Err(error::Error::BadRequest(
"You have reached the maximum number of accounts (50) without an enterprise license"
.to_string(),
));
}
return Ok(());
}
#[derive(Clone, Debug)]
pub struct SlackVerifier {
_mac: HmacSha256,
}
impl SlackVerifier {
pub fn new<S: AsRef<[u8]>>(secret: S) -> anyhow::Result<SlackVerifier> {
HmacSha256::new_from_slice(secret.as_ref())
.map(|mac| SlackVerifier { _mac: mac })
.map_err(|_| anyhow::anyhow!("invalid secret"))
}
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-api/src/oauth2_ee.rs

View File

@@ -1,17 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2023
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use axum::Router;
pub fn global_service() -> Router {
Router::new()
}
pub fn workspaced_service() -> Router {
Router::new()
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-api/src/oidc_ee.rs

View File

@@ -1,25 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2023
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
#![allow(non_snake_case)]
use axum::{routing::post, Router};
pub struct ServiceProviderExt();
pub async fn build_sp_extension() -> anyhow::Result<ServiceProviderExt> {
return Ok(ServiceProviderExt());
}
pub fn global_service() -> Router {
Router::new().route("/acs", post(acs))
}
pub async fn acs() -> String {
// Implementation is not open source as it is a Windmill Enterprise Edition feature
"SAML available only in enterprise version".to_string()
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-api/src/saml_ee.rs

View File

@@ -1,23 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2023
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use axum::{middleware::Next, response::Response, routing::get, Router};
use hyper::Request;
pub fn global_service() -> Router {
Router::new().route("/ee", get(ee))
}
pub async fn ee() -> String {
return "Enterprise Edition".to_string();
}
pub async fn has_scim_token<B>(_request: Request<B>, _next: Next) -> Response {
//Not implemented in open-source version
todo!()
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-api/src/scim_ee.rs

View File

@@ -56,7 +56,7 @@ use windmill_common::{
utils::{
not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath,
},
worker::to_raw_value,
worker::{get_annotation_ts, to_raw_value},
HUB_BASE_URL,
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
@@ -152,7 +152,6 @@ pub fn workspaced_service() -> Router {
post(toggle_workspace_error_handler),
)
.route("/history/p/*path", get(get_script_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route(
"/history_update/h/:hash/p/*path",
post(update_script_history),
@@ -607,8 +606,8 @@ async fn create_script_internal<'c>(
};
let lang = if &ns.language == &ScriptLang::Bun || &ns.language == &ScriptLang::Bunnative {
let anns = windmill_common::worker::TypeScriptAnnotations::parse(&ns.content);
if anns.native {
let anns = get_annotation_ts(&ns.content);
if anns.native_mode {
ScriptLang::Bunnative
} else {
ScriptLang::Bun
@@ -949,38 +948,6 @@ async fn get_script_history(
return Ok(Json(result));
}
async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<ScriptHistory>> {
let mut tx = user_db.begin(&authed).await?;
let row_o = sqlx::query!(
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg
FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash
WHERE s.workspace_id = $1 AND s.path = $2
ORDER by created_at DESC",
w_id,
path.to_path(),
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
if let Some(row) = row_o {
let result = ScriptHistory {
script_hash: ScriptHash(row.hash),
deployment_msg: row.deployment_msg, //
};
return Ok(Json(Some(result)));
} else {
return Ok(Json(None));
}
}
async fn update_script_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,

View File

@@ -1,17 +0,0 @@
use crate::{db::DB, users::AuthCache};
use std::{net::SocketAddr, sync::Arc};
use windmill_common::db::UserDB;
pub struct SmtpServer {
pub auth_cache: Arc<AuthCache>,
pub db: DB,
pub user_db: UserDB,
pub rsmq: Option<rsmq_async::MultiplexedRsmq>,
pub base_internal_url: String,
}
impl SmtpServer {
pub async fn start_listener_thread(self: Arc<Self>, _addr: SocketAddr) -> anyhow::Result<()> {
Err(anyhow::anyhow!("Implementation not open source"))
}
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-api/src/smtp_server_ee.rs

View File

@@ -48,10 +48,7 @@ fn serve_path(path: &str) -> Response<Body> {
let mut res = Response::builder()
.header(header::CONTENT_TYPE, mime.as_ref())
.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*");
if mime.as_ref() == mime::APPLICATION_JAVASCRIPT
|| mime.as_ref() == mime::TEXT_JAVASCRIPT
|| path.ends_with(".wasm")
{
if mime.as_ref() == mime::APPLICATION_JAVASCRIPT || path.ends_with(".wasm") {
res = res.header(header::CACHE_CONTROL, "max-age=31536000");
} else if (mime.type_(), mime.subtype()) == (mime::TEXT, mime::CSS) {
res = res.header(header::CACHE_CONTROL, "max-age=31536000");

View File

@@ -1,7 +0,0 @@
#[cfg(feature = "stripe")]
use axum::Router;
#[cfg(feature = "stripe")]
pub fn add_stripe_routes(router: Router) -> Router {
return router;
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-api/src/stripe_ee.rs

View File

@@ -17,7 +17,6 @@ pub struct TriggersCount {
http_routes_count: i64,
webhook_count: i64,
email_count: i64,
websocket_count: i64,
}
pub(crate) async fn get_triggers_count_internal(
db: &DB,
@@ -54,16 +53,6 @@ pub(crate) async fn get_triggers_count_internal(
.await?
.unwrap_or(0);
let websocket_count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM websocket_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
path,
is_flow,
w_id
)
.fetch_one(db)
.await?
.unwrap_or(0);
let webhook_count = (if is_flow {
sqlx::query_scalar!(
"SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
@@ -104,7 +93,6 @@ pub(crate) async fn get_triggers_count_internal(
http_routes_count,
webhook_count,
email_count,
websocket_count,
}))
}

View File

@@ -48,9 +48,8 @@ use tower_cookies::{Cookie, Cookies};
use tracing::{Instrument, Span};
use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::auth::fetch_authed_from_permissioned_as;
use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING;
use windmill_common::users::{truncate_token, username_to_permissioned_as};
use windmill_common::users::truncate_token;
use windmill_common::utils::{paginate, send_email};
use windmill_common::worker::{CLOUD_HOSTED, SMTP_CONFIG};
use windmill_common::{
@@ -706,17 +705,14 @@ where
}
pub fn get_scope_tags(authed: &ApiAuthed) -> Option<Vec<&str>> {
authed.scopes.as_ref()?.iter().find_map(|s| {
if s.starts_with("if_jobs:filter_tags:") {
Some(
s.trim_start_matches("if_jobs:filter_tags:")
.split(",")
.collect::<Vec<_>>(),
)
} else {
None
}
})
authed
.scopes
.as_ref()?
.iter()
.find_map(|s| match s.split(":").collect::<Vec<_>>().as_slice() {
["if_jobs", "filter_tags", tags] => Some(tags.split(",").collect::<Vec<_>>()),
_ => None,
})
}
#[derive(Clone, Debug)]
@@ -740,28 +736,6 @@ where
}
}
pub async fn fetch_api_authed(
username: String,
email: String,
w_id: &str,
db: &DB,
username_override: String,
) -> error::Result<ApiAuthed> {
let permissioned_as = username_to_permissioned_as(username.as_str());
let authed =
fetch_authed_from_permissioned_as(permissioned_as, email.clone(), w_id, db).await?;
Ok(ApiAuthed {
username: username,
email: email,
is_admin: authed.is_admin,
is_operator: authed.is_operator,
groups: authed.groups,
folders: authed.folders,
scopes: authed.scopes,
username_override: Some(username_override),
})
}
#[derive(FromRow, Serialize)]
pub struct User {
pub workspace_id: String,
@@ -2290,8 +2264,6 @@ async fn login(
) -> Result<String> {
let mut tx = db.begin().await?;
let email = email.to_lowercase();
let audit_author =
AuditAuthor { email: email.clone(), username: email.clone(), username_override: None };
let email_w_h: Option<(String, String, bool, bool)> = sqlx::query_as(
"SELECT email, password_hash, super_admin, first_time_user FROM password WHERE email = $1 AND login_type = \
'password'",
@@ -2307,16 +2279,6 @@ async fn login(
.verify_password(password.as_bytes(), &parsed_hash)
.is_err()
{
audit_log(
&mut *tx,
&audit_author,
"users.login_failure",
ActionKind::Create,
"global",
None,
None,
)
.await?;
Err(Error::BadRequest("Invalid login".to_string()))
} else {
if first_time_user {
@@ -2342,7 +2304,11 @@ async fn login(
audit_log(
&mut *tx,
&audit_author,
&AuditAuthor {
username: email.clone(),
email: email.clone(),
username_override: None,
},
"users.login",
ActionKind::Create,
"global",
@@ -2355,16 +2321,6 @@ async fn login(
Ok(token)
}
} else {
audit_log(
&mut *tx,
&audit_author,
"users.login_failure",
ActionKind::Create,
"global",
None,
None,
)
.await?;
Err(Error::BadRequest("Invalid login".to_string()))
}
}

View File

@@ -1,684 +0,0 @@
use axum::{
extract::{Path, Query},
routing::{delete, get, post},
Extension, Json, Router,
};
use futures::StreamExt;
use http::StatusCode;
use itertools::Itertools;
use rand::seq::SliceRandom;
use serde::{
de::{self, MapAccess, Visitor},
Deserialize, Deserializer, Serialize,
};
use serde_json::Value;
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::prelude::FromRow;
use std::{collections::HashMap, fmt};
use tokio_tungstenite::connect_async;
use windmill_audit::{audit_ee::audit_log, ActionKind};
use windmill_common::{
db::UserDB,
error::{self, JsonResult},
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
INSTANCE_NAME,
};
use windmill_queue::PushArgsOwned;
use crate::{
db::{ApiAuthed, DB},
jobs::{
run_wait_result_flow_by_path_internal, run_wait_result_script_by_path_internal, RunJobQuery,
},
users::fetch_api_authed,
};
pub fn workspaced_service() -> Router {
Router::new()
.route("/create", post(create_websocket_trigger))
.route("/list", get(list_websocket_triggers))
.route("/get/*path", get(get_websocket_trigger))
.route("/update/*path", post(update_websocket_trigger))
.route("/delete/*path", delete(delete_websocket_trigger))
.route("/exists/*path", get(exists_websocket_trigger))
.route("/setenabled/*path", post(set_enabled))
}
#[derive(Deserialize)]
struct NewWebsocketTrigger {
path: String,
url: String,
script_path: String,
is_flow: bool,
enabled: Option<bool>,
filters: Vec<serde_json::Value>,
}
#[derive(FromRow, Serialize, Clone)]
pub struct WebsocketTrigger {
workspace_id: String,
path: String,
url: String,
script_path: String,
is_flow: bool,
edited_by: String,
email: String,
edited_at: chrono::DateTime<chrono::Utc>,
server_id: Option<String>,
last_server_ping: Option<chrono::DateTime<chrono::Utc>>,
extra_perms: serde_json::Value,
error: Option<String>,
enabled: bool,
filters: Vec<serde_json::Value>,
}
#[derive(Deserialize)]
struct EditWebsocketTrigger {
path: String,
url: String,
script_path: String,
is_flow: bool,
filters: Vec<serde_json::Value>,
}
#[derive(Deserialize)]
pub struct ListWebsocketTriggerQuery {
pub page: Option<usize>,
pub per_page: Option<usize>,
pub path: Option<String>,
pub is_flow: Option<bool>,
pub path_start: Option<String>,
}
async fn list_websocket_triggers(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(lst): Query<ListWebsocketTriggerQuery>,
) -> error::JsonResult<Vec<WebsocketTrigger>> {
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page });
let mut sqlb = SqlBuilder::select_from("websocket_trigger")
.field("*")
.order_by("edited_at", true)
.and_where("workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
if let Some(path) = lst.path {
sqlb.and_where_eq("script_path", "?".bind(&path));
}
if let Some(is_flow) = lst.is_flow {
sqlb.and_where_eq("is_flow", "?".bind(&is_flow));
}
if let Some(path_start) = &lst.path_start {
sqlb.and_where_like_left("path", path_start);
}
let sql = sqlb
.sql()
.map_err(|e| error::Error::InternalErr(e.to_string()))?;
let rows = sqlx::query_as::<_, WebsocketTrigger>(&sql)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(rows))
}
async fn get_websocket_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> error::JsonResult<WebsocketTrigger> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
let trigger = sqlx::query_as!(
WebsocketTrigger,
r#"SELECT *
FROM websocket_trigger
WHERE workspace_id = $1 AND path = $2"#,
w_id,
path,
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
let trigger = not_found_if_none(trigger, "Trigger", path)?;
Ok(Json(trigger))
}
async fn create_websocket_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(ct): Json<NewWebsocketTrigger>,
) -> error::Result<(StatusCode, String)> {
if *CLOUD_HOSTED {
return Err(error::Error::BadRequest(
"Websocket triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
));
}
require_admin(authed.is_admin, &authed.username)?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query_as!(
WebsocketTrigger,
"INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now()) RETURNING *",
w_id,
ct.path,
ct.url,
ct.script_path,
ct.is_flow,
ct.enabled.unwrap_or(true),
&ct.filters,
&authed.username,
&authed.email
)
.fetch_one(&mut *tx).await?;
audit_log(
&mut *tx,
&authed,
"websocket_triggers.create",
ActionKind::Create,
&w_id,
Some(ct.path.as_str()),
None,
)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, format!("{}", ct.path)))
}
async fn update_websocket_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ct): Json<EditWebsocketTrigger>,
) -> error::Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
// important to update server_id, last_server_ping and error to NULL to stop current websocket listener
sqlx::query!(
"UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, edited_by = $6, email = $7, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL
WHERE workspace_id = $8 AND path = $9",
ct.url,
ct.script_path,
ct.path,
ct.is_flow,
&ct.filters,
&authed.username,
&authed.email,
w_id,
path,
)
.execute(&mut *tx).await?;
audit_log(
&mut *tx,
&authed,
"websocket_triggers.update",
ActionKind::Create,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
Ok(path.to_string())
}
#[derive(Deserialize)]
pub struct SetEnabled {
pub enabled: bool,
}
pub async fn set_enabled(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<SetEnabled>,
) -> error::Result<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
// important to set server_id, last_server_ping and error to NULL to stop current websocket listener
let one_o = sqlx::query_scalar!(
"UPDATE websocket_trigger SET enabled = $1, email = $2, edited_by = $3, edited_at = now(), server_id = NULL, last_server_ping = NULL, error = NULL
WHERE path = $4 AND workspace_id = $5 RETURNING 1",
payload.enabled,
&authed.email,
&authed.username,
path,
w_id,
).fetch_optional(&mut *tx).await?;
not_found_if_none(one_o.flatten(), "Websocket trigger", path)?;
audit_log(
&mut *tx,
&authed,
"websocket_triggers.setenabled",
ActionKind::Update,
&w_id,
Some(path),
Some([("enabled", payload.enabled.to_string().as_ref())].into()),
)
.await?;
tx.commit().await?;
Ok(format!(
"succesfully updated websocket trigger at path {} to status {}",
path, payload.enabled
))
}
async fn delete_websocket_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM websocket_trigger WHERE workspace_id = $1 AND path = $2",
w_id,
path,
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"websocket_triggers.delete",
ActionKind::Delete,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Websocket trigger {path} deleted"))
}
async fn exists_websocket_trigger(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE path = $1 AND workspace_id = $2)",
path,
w_id,
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
async fn listen_to_unlistened_websockets(
db: &DB,
rsmq: &Option<rsmq_async::MultiplexedRsmq>,
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
) -> () {
match sqlx::query_as!(
WebsocketTrigger,
r#"SELECT *
FROM websocket_trigger
WHERE enabled IS TRUE AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')"#
)
.fetch_all(db)
.await
{
Ok(mut triggers) => {
triggers.shuffle(&mut rand::thread_rng());
for trigger in triggers {
maybe_listen_to_websocket(trigger, db.clone(), rsmq.clone(), killpill_rx.resubscribe()).await;
}
}
Err(err) => {
tracing::error!("Error fetching websocket triggers: {:?}", err);
}
};
}
pub async fn start_websockets(
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
tokio::spawn(async move {
listen_to_unlistened_websockets(&db, &rsmq, &killpill_rx).await;
loop {
tokio::select! {
biased;
_ = killpill_rx.recv() => {
return;
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(15)) => {
listen_to_unlistened_websockets(&db, &rsmq, &killpill_rx).await;
}
}
}
});
}
async fn maybe_listen_to_websocket(
ws_trigger: WebsocketTrigger,
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
match sqlx::query_scalar!(
"UPDATE websocket_trigger SET server_id = $1, last_server_ping = now() WHERE enabled IS TRUE AND workspace_id = $2 AND path = $3 AND (server_id IS NULL OR last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') RETURNING true",
*INSTANCE_NAME,
ws_trigger.workspace_id,
ws_trigger.path,
).fetch_optional(&db).await {
Ok(has_lock) => {
if has_lock.flatten().unwrap_or(false) {
tokio::spawn(listen_to_websocket(ws_trigger, db, rsmq, killpill_rx));
} else {
tracing::info!("Websocket {} already being listened to", ws_trigger.url);
}
},
Err(err) => {
tracing::error!("Error acquiring lock for websocket {}: {:?}", ws_trigger.path, err);
}
};
}
struct SupersetVisitor<'a> {
key: &'a str,
value_to_check: &'a Value,
}
impl<'de, 'a> Visitor<'de> for SupersetVisitor<'a> {
type Value = bool;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a JSON object with a specific key at the top level")
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
while let Some(key) = map.next_key::<String>()? {
if key == self.key {
// Deserialize the value for the key and check if it's a superset
let json_value: Value = map.next_value()?;
tracing::info!("json_value: {:?}", json_value);
tracing::info!("value_to_check: {:?}", self.value_to_check);
return Ok(is_superset(&json_value, self.value_to_check));
} else {
// Skip the value if it's not the one we're interested in
let _ = map.next_value::<de::IgnoredAny>()?;
}
}
// If the key was not found, return false
Ok(false)
}
}
// Function to check if json_value is a superset of value_to_check
fn is_superset(json_value: &Value, value_to_check: &Value) -> bool {
match (json_value, value_to_check) {
(Value::Object(json_map), Value::Object(check_map)) => {
// Check that all keys and values in check_map exist and match in json_map
check_map.iter().all(|(k, v)| {
json_map
.get(k)
.map_or(false, |json_val| is_superset(json_val, v))
})
}
(Value::Array(json_array), Value::Array(check_array)) => {
// Check that all elements in check_array exist in json_array
check_array.iter().all(|check_item| {
json_array
.iter()
.any(|json_item| is_superset(json_item, check_item))
})
}
_ => json_value == value_to_check,
}
}
// A function to deserialize and check if the value at the given key is a superset of a passed value
fn is_value_superset<'a, 'de, D>(
deserializer: D,
key: &'a str,
value_to_check: &'a Value,
) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(SupersetVisitor { key, value_to_check })
}
async fn listen_to_websocket(
ws_trigger: WebsocketTrigger,
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
async fn update_ping(db: DB, ws_trigger: &WebsocketTrigger, error: Option<&str>) -> Option<()> {
match sqlx::query_scalar!(
"UPDATE websocket_trigger SET last_server_ping = now(), error = $1 WHERE workspace_id = $2 AND path = $3 AND server_id = $4 AND enabled IS TRUE RETURNING 1",
error,
ws_trigger.workspace_id,
ws_trigger.path,
*INSTANCE_NAME
).fetch_optional(&db).await {
Ok(updated) => {
if updated.flatten().is_none() {
tracing::info!("Websocket {} changed, disabled, or deleted, stopping...", ws_trigger.url);
return None;
}
},
Err(err) => {
tracing::warn!("Error updating ping of websocket {}: {:?}", ws_trigger.url, err);
}
};
Some(())
}
let url = ws_trigger.url.as_str();
#[derive(Deserialize)]
struct JsonFilter {
key: String,
value: serde_json::Value,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Filter {
JsonFilter(JsonFilter),
}
let filters: Vec<Filter> = ws_trigger
.filters
.iter()
.filter_map(|m| serde_json::from_value(m.clone()).ok())
.collect_vec();
loop {
tokio::select! {
biased;
_ = killpill_rx.recv() => {
return;
},
connection = connect_async(url) => {
match connection {
Ok((ws_stream, _)) => {
tracing::info!("Listening to websocket {}", url);
if let None = update_ping(db.clone(), &ws_trigger, None).await {
return;
}
let mut last_ping = tokio::time::Instant::now();
let (_, mut read) = ws_stream.split();
loop {
tokio::select! {
biased;
_ = killpill_rx.recv() => {
return;
}
msg = read.next() => {
if let Some(msg) = msg {
if last_ping.elapsed() > tokio::time::Duration::from_secs(5) {
if let None = update_ping(db.clone(), &ws_trigger, None).await {
return;
}
last_ping = tokio::time::Instant::now();
}
match msg {
Ok(msg) => {
match msg {
tokio_tungstenite::tungstenite::Message::Text(text) => {
let mut should_handle = true;
for filter in &filters {
match filter {
Filter::JsonFilter(JsonFilter { key, value }) => {
let mut deserializer = serde_json::Deserializer::from_str(text.as_str());
should_handle = match is_value_superset(&mut deserializer, key, &value) {
Ok(filter_match) => {
filter_match
},
Err(err) => {
tracing::warn!("Error deserializing filter for websocket {}: {:?}", url, err);
false
}
};
}
}
if !should_handle {
break;
}
}
if should_handle {
let db_ = db.clone();
let rsmq_ = rsmq.clone();
let ws_trigger_ = ws_trigger.clone();
tokio::spawn(async move {
let url = ws_trigger_.url.clone();
if let Err(err) = run_job(db_, rsmq_, ws_trigger_, text).await {
tracing::error!("Error running job on websocket {}: {:?}", url, err);
};
});
}
},
_ => {}
}
},
Err(err) => {
tracing::error!("Error reading from websocket {}: {:?}", url, err);
}
}
} else {
tracing::error!("Websocket {} closed", url);
if let None =
update_ping(db.clone(), &ws_trigger, Some("Websocket closed")).await
{
return;
}
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
break;
}
},
_ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {
if let None = update_ping(db.clone(), &ws_trigger, None).await {
return;
}
last_ping = tokio::time::Instant::now();
},
}
}
}
Err(err) => {
tracing::error!("Error connecting to websocket {}: {:?}", url, err);
if let None =
update_ping(db.clone(), &ws_trigger, Some(err.to_string().as_str())).await
{
return;
}
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
}
}
}
}
}
}
async fn run_job(
db: DB,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
trigger: WebsocketTrigger,
msg: String,
) -> anyhow::Result<()> {
let args = PushArgsOwned {
args: HashMap::from([("msg".to_string(), to_raw_value(&msg))]),
extra: Some(HashMap::from([(
"wm_trigger".to_string(),
to_raw_value(&serde_json::json!({"kind": "websocket"})),
)])),
};
let label_prefix = Some(format!("ws-{}-", trigger.path));
let authed = fetch_api_authed(
trigger.edited_by.clone(),
trigger.email.clone(),
&trigger.workspace_id,
&db,
"anonymous".to_string(),
)
.await?;
let user_db = UserDB::new(db.clone());
let run_query = RunJobQuery::default();
if trigger.is_flow {
run_wait_result_flow_by_path_internal(
db,
run_query,
StripPath(trigger.script_path.to_owned()),
authed,
rsmq,
user_db,
args,
trigger.workspace_id.clone(),
label_prefix,
)
.await?;
} else {
run_wait_result_script_by_path_internal(
db,
run_query,
StripPath(trigger.script_path.to_owned()),
authed,
rsmq,
user_db,
trigger.workspace_id.clone(),
args,
label_prefix,
)
.await?;
}
Ok(())
}

View File

@@ -116,8 +116,7 @@ pub fn workspaced_service() -> Router {
.route("/get_workspace_name", get(get_workspace_name))
.route("/change_workspace_name", post(change_workspace_name))
.route("/change_workspace_id", post(change_workspace_id))
.route("/usage", get(get_usage))
.route("/used_triggers", get(get_used_triggers));
.route("/usage", get(get_usage));
#[cfg(feature = "stripe")]
{
@@ -1489,30 +1488,6 @@ async fn set_encryption_key(
return Ok(());
}
#[derive(Serialize)]
struct UsedTriggers {
pub websocket_used: bool,
pub http_routes_used: bool,
}
async fn get_used_triggers(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<UsedTriggers> {
let mut tx = user_db.begin(&authed).await?;
let websocket_used = sqlx::query_as!(
UsedTriggers,
r#"SELECT EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as "websocket_used!", EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as "http_routes_used!""#,
w_id,
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(websocket_used))
}
async fn list_workspaces_as_super_admin(
authed: ApiAuthed,
Extension(db): Extension<DB>,

View File

@@ -1,75 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use std::collections::HashMap;
use windmill_common::{
error::{Error, Result},
utils::Pagination,
};
use crate::{ActionKind, AuditLog, ListAuditLogQuery};
use sqlx::{Postgres, Transaction};
#[derive(Clone)]
pub struct AuditAuthor {
pub username: String,
pub email: String,
pub username_override: Option<String>,
}
impl AuditAuthorable for AuditAuthor {
fn email(&self) -> &str {
&self.email
}
fn username(&self) -> &str {
&self.username
}
fn username_override(&self) -> Option<&str> {
self.username_override.as_deref()
}
}
pub trait AuditAuthorable {
fn username(&self) -> &str;
fn email(&self) -> &str;
fn username_override(&self) -> Option<&str>;
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn audit_log<'c, E: sqlx::Executor<'c, Database = Postgres>>(
_db: E,
_author: &impl AuditAuthorable,
mut _operation: &str,
_action_kind: ActionKind,
_w_id: &str,
mut _resource: Option<&str>,
_parameters: Option<HashMap<&str, &str>>,
) -> Result<()> {
// Implementation is not open source as Audit logs is a Windmill Enterprise Edition feature
Ok(())
}
pub async fn list_audit(
_tx: Transaction<'_, Postgres>,
_w_id: String,
_pagination: Pagination,
_lq: ListAuditLogQuery,
) -> Result<Vec<AuditLog>> {
// Implementation is not open source as Audit logs is a Windmill Enterprise Edition feature
return Ok(vec![]);
}
pub async fn get_audit(tx: Transaction<'_, Postgres>, _id: i32, _w_id: &str) -> Result<AuditLog> {
// Implementation is not open source as Audit logs is a Windmill Enterprise Edition feature
tx.commit().await?;
Err(Error::NotFound(
"Audit log not not available in Windmill Community edition".to_string(),
))
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-audit/src/audit_ee.rs

View File

@@ -13,6 +13,7 @@ flamegraph = ["dep:tracing-flame"]
loki = ["dep:tracing-loki"]
benchmark = []
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts"]
openidconnect = ["dep:openidconnect"]
[lib]
name = "windmill_common"
@@ -57,7 +58,7 @@ futures-core.workspace = true
async-stream.workspace = true
const_format.workspace = true
crc.workspace = true
windmill-macros.workspace = true
openidconnect = { workspace = true, optional = true}
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemalloc-ctl = { optional = true, workspace = true }

View File

@@ -1,86 +0,0 @@
#[cfg(feature = "enterprise")]
use crate::db::DB;
use crate::ee::LicensePlan::Community;
#[cfg(feature = "enterprise")]
use crate::error;
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::RwLock;
lazy_static::lazy_static! {
pub static ref LICENSE_KEY_VALID: Arc<RwLock<bool>> = Arc::new(RwLock::new(true));
pub static ref LICENSE_KEY_ID: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref LICENSE_KEY: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
}
pub enum LicensePlan {
Community,
Pro,
Enterprise,
}
pub async fn get_license_plan() -> LicensePlan {
// Implementation is not open source
return Community;
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum CriticalErrorChannel {
Email { email: String },
Slack { slack_channel: String },
}
pub enum CriticalAlertKind {
#[cfg(feature = "enterprise")]
CriticalError,
#[cfg(feature = "enterprise")]
RecoveredCriticalError,
}
#[cfg(feature = "enterprise")]
pub async fn send_critical_alert(
_error_message: String,
_db: &DB,
_kind: CriticalAlertKind,
_channels: Option<Vec<CriticalErrorChannel>>,
) {
}
#[cfg(feature = "enterprise")]
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
// Implementation is not open source
}
#[cfg(feature = "enterprise")]
pub enum RenewReason {
Manual,
Schedule,
OnStart,
}
#[cfg(feature = "enterprise")]
pub async fn renew_license_key(
_http_client: &reqwest::Client,
_db: &crate::db::DB,
_key: Option<String>,
_reason: RenewReason,
) -> String {
// Implementation is not open source
"".to_string()
}
#[cfg(feature = "enterprise")]
pub async fn create_customer_portal_session(
_http_client: &reqwest::Client,
_key: Option<String>,
) -> error::Result<String> {
// Implementation is not open source
Ok("".to_string())
}
#[cfg(feature = "enterprise")]
pub async fn worker_groups_alerts(_db: &DB) {}
#[cfg(feature = "enterprise")]
pub async fn jobs_waiting_alerts(_db: &DB) {}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-common/src/ee.rs

View File

@@ -1,73 +0,0 @@
#[cfg(feature = "enterprise")]
use crate::db::DB;
use crate::ee::LicensePlan::Community;
#[cfg(feature = "enterprise")]
use crate::error;
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::RwLock;
lazy_static::lazy_static! {
pub static ref LICENSE_KEY_VALID: Arc<RwLock<bool>> = Arc::new(RwLock::new(true));
pub static ref LICENSE_KEY_ID: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref LICENSE_KEY: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
}
pub enum LicensePlan {
Community,
Pro,
Enterprise,
}
pub async fn get_license_plan() -> LicensePlan {
// Implementation is not open source
return Community;
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum CriticalErrorChannel {}
pub enum CriticalAlertKind {
#[cfg(feature = "enterprise")]
CriticalError,
#[cfg(feature = "enterprise")]
RecoveredCriticalError,
}
#[cfg(feature = "enterprise")]
pub async fn send_critical_alert(
_error_message: String,
_db: &DB,
_kind: CriticalAlertKind,
_channels: Option<Vec<CriticalErrorChannel>>,
) {
}
#[cfg(feature = "enterprise")]
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
// Implementation is not open source
}
#[cfg(feature = "enterprise")]
pub async fn renew_license_key(
_http_client: &reqwest::Client,
_db: &crate::db::DB,
_key: Option<String>,
_manual: bool,
) -> String {
// Implementation is not open source
"".to_string()
}
#[cfg(feature = "enterprise")]
pub async fn create_customer_portal_session(
_http_client: &reqwest::Client,
_key: Option<String>,
) -> error::Result<String> {
// Implementation is not open source
Ok("".to_string())
}
#[cfg(feature = "enterprise")]
pub async fn worker_groups_alerts(_db: &DB) {}

View File

@@ -1,76 +0,0 @@
#[cfg(feature = "enterprise")]
use crate::db::DB;
use crate::ee::LicensePlan::Community;
#[cfg(feature = "enterprise")]
use crate::error;
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::RwLock;
lazy_static::lazy_static! {
pub static ref LICENSE_KEY_VALID: Arc<RwLock<bool>> = Arc::new(RwLock::new(true));
pub static ref LICENSE_KEY_ID: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref LICENSE_KEY: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
}
pub enum LicensePlan {
Community,
Pro,
Enterprise,
}
pub async fn get_license_plan() -> LicensePlan {
// Implementation is not open source
return Community;
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum CriticalErrorChannel {
Email { email: String },
Slack { slack_channel: String },
}
pub enum CriticalAlertKind {
#[cfg(feature = "enterprise")]
CriticalError,
#[cfg(feature = "enterprise")]
RecoveredCriticalError,
}
#[cfg(feature = "enterprise")]
pub async fn send_critical_alert(
_error_message: String,
_db: &DB,
_kind: CriticalAlertKind,
_channels: Option<Vec<CriticalErrorChannel>>,
) {
}
#[cfg(feature = "enterprise")]
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
// Implementation is not open source
}
#[cfg(feature = "enterprise")]
pub async fn renew_license_key(
_http_client: &reqwest::Client,
_db: &crate::db::DB,
_key: Option<String>,
_manual: bool,
) -> String {
// Implementation is not open source
"".to_string()
}
#[cfg(feature = "enterprise")]
pub async fn create_customer_portal_session(
_http_client: &reqwest::Client,
_key: Option<String>,
) -> error::Result<String> {
// Implementation is not open source
Ok("".to_string())
}
#[cfg(feature = "enterprise")]
pub async fn worker_groups_alerts(_db: &DB) {}

View File

@@ -1,18 +0,0 @@
use std::future::Future;
use crate::{
error::Error,
s3_helpers::{ObjectStoreResource, StorageResourceType},
};
pub async fn get_s3_resource_internal<'c, F, Fut>(
_resource_type: StorageResourceType,
_s3_resource_value_raw: serde_json::Value,
_gen_token: F,
) -> crate::error::Result<ObjectStoreResource>
where
F: FnOnce(String) -> Fut,
Fut: Future<Output = Result<String, Error>> + Send + 'static,
{
todo!()
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-common/src/job_s3_helpers_ee.rs

View File

@@ -640,7 +640,9 @@ pub async fn get_logs_from_store(
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
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! {

View File

@@ -32,6 +32,8 @@ pub mod job_s3_helpers_ee;
pub mod jobs;
pub mod more_serde;
pub mod oauth2;
#[cfg(feature = "openidconnect")]
pub mod oidc_ee;
pub mod s3_helpers;
pub mod auth;

View File

@@ -0,0 +1,170 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2023
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
#[cfg(feature = "openidconnect")]
use anyhow;
#[cfg(feature = "openidconnect")]
use std::process::Command;
#[cfg(feature = "openidconnect")]
use openidconnect::{
core::{CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey},
IssuerUrl, JsonWebKeyId,
};
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
use openidconnect::{
core::{
CoreClaimName, CoreJsonWebKeySet, CoreProviderMetadata, CoreResponseType,
CoreSubjectIdentifierType,
},
AuthUrl, EmptyAdditionalProviderMetadata, JsonWebKeySetUrl, ResponseTypes,
};
#[cfg(feature = "openidconnect")]
use openidconnect::AdditionalClaims;
#[cfg(feature = "openidconnect")]
use crate::db::DB;
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
use axum::extract::Path;
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
use axum::routing::{get, post};
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
use axum::Extension;
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
use axum::Json;
use axum::Router;
use serde::{Deserialize, Serialize};
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
pub async fn generate_id_token<T: AdditionalClaims>(
db: &DB,
claim: T,
audience: String,
identifier: String,
email: Option<String>,
) -> crate::error::Result<String> {
use chrono::{Duration, Utc};
use openidconnect::{
core::{CoreGenderClaim, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm},
Audience, EndUserEmail, IdToken, IdTokenClaims, StandardClaims, SubjectIdentifier,
};
let private_key = get_private_key(&db).await?;
let issue_url = format!("{}/api/oidc/", crate::BASE_URL.read().await.clone());
let id_token = IdToken::<
T,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
CoreJsonWebKeyType,
>::new(
IdTokenClaims::<T, CoreGenderClaim>::new(
// Specify the issuer URL for the OpenID Connect Provider.
IssuerUrl::new(issue_url)
.map_err(|e| anyhow::anyhow!("Failed to generate IssueUrl: {}", e))?,
// The audience is usually a single entry with the client ID of the client for whom
// the ID token is intended. This is a required claim.
vec![Audience::new(audience)],
// The ID token expiration is usually much shorter than that of the access or refresh
// tokens issued to clients.
Utc::now() + Duration::try_hours(48).unwrap(),
// The issue time is usually the current time.
Utc::now(),
// Set the standard claims defined by the OpenID Connect Core spec.
StandardClaims::new(
// Stable subject identifiers are recommended in place of e-mail addresses or other
// potentially unstable identifiers. This is the only required claim.
SubjectIdentifier::new(identifier),
)
// Optional: specify the user's e-mail address. This should only be provided if the
// client has been granted the 'profile' or 'email' scopes.
.set_email(email.map(|x| EndUserEmail::new(x)))
// Optional: specify whether the provider has verified the user's e-mail address.
.set_email_verified(Some(true)),
// OpenID Connect Providers may supply custom claims by providing a struct that
// implements the AdditionalClaims trait. This requires manually using the
// generic IdTokenClaims struct rather than the CoreIdTokenClaims type alias,
// however.
claim,
),
// The private key used for signing the ID token. For confidential clients (those able
// to maintain a client secret), a CoreHmacKey can also be used, in conjunction
// with one of the CoreJwsSigningAlgorithm::HmacSha* signing algorithms. When using an
// HMAC-based signing algorithm, the UTF-8 representation of the client secret should
// be used as the HMAC key.
&CoreRsaPrivateSigningKey::from_pem(
&private_key,
Some(JsonWebKeyId::new("windmill".to_string())),
)
.map_err(|e| anyhow::anyhow!("Invalid private key: {}", e))?,
// Uses the RS256 signature algorithm. This crate supports any RS*, PS*, or HS*
// signature algorithm.
CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,
// When returning the ID token alongside an access token (e.g., in the Authorization Code
// flow), it is recommended to pass the access token here to set the `at_hash` claim
// automatically.
None,
// When returning the ID token alongside an authorization code (e.g., in the implicit
// flow), it is recommended to pass the authorization code here to set the `c_hash` claim
// automatically.
None,
)
.map_err(|e| anyhow::anyhow!("Failed to generate token: {}", e))?;
Ok(id_token.to_string())
}
#[cfg(feature = "openidconnect")]
pub async fn get_private_key(db: &DB) -> anyhow::Result<String> {
let key = sqlx::query_scalar!(
"SELECT value->>'private_key' FROM global_settings WHERE name = 'rsa_keys'",
)
.fetch_optional(db)
.await?
.flatten();
if let Some(key) = key {
return Ok(key);
} else {
let keys = gen_pems(db).await?;
return Ok(keys.private_key);
}
}
#[cfg(feature = "openidconnect")]
async fn gen_pems(db: &DB) -> anyhow::Result<Keys> {
let private_key_cmd = Command::new("openssl")
.arg("genrsa")
.arg("--traditional")
.arg("2048")
.output()
.expect("failed to execute process");
let private_key = String::from_utf8(private_key_cmd.stdout).unwrap();
tracing::debug!("Generated private key: {}", private_key);
let keys = Keys { private_key };
sqlx::query!(
"INSERT INTO global_settings (name, value) VALUES ('rsa_keys', $1)",
serde_json::to_value(&keys).unwrap()
)
.execute(db)
.await?;
Ok(keys)
}
#[derive(Debug, Clone, serde::Serialize)]
struct Keys {
private_key: String,
}

View File

@@ -22,6 +22,7 @@ use tokio::sync::RwLock;
lazy_static::lazy_static! {
pub static ref OBJECT_STORE_CACHE_SETTINGS: Arc<RwLock<Option<Arc<dyn ObjectStore>>>> = Arc::new(RwLock::new(None));
pub static ref OBJECT_STORE_OIDC_SETTINGS: Arc<RwLock<Option<Arc<S3AwsOidcResource>>>> = Arc::new(RwLock::new(None));
}
#[derive(Serialize, Deserialize, Debug)]
@@ -356,18 +357,42 @@ pub enum ObjectStoreSettings {
pub enum ObjectSettings {
S3(S3Settings),
Azure(AzureBlobResource),
AwsOidc(S3AwsOidcResource),
}
#[cfg(feature = "parquet")]
pub async fn build_object_store_from_settings(
settings: ObjectSettings,
) -> error::Result<Arc<dyn ObjectStore>> {
use crate::oidc_ee::generate_id_token;
match settings {
ObjectSettings::S3(s3_settings) => build_s3_client_from_settings(s3_settings).await,
ObjectSettings::Azure(azure_settings) => {
let azure_blob_resource = azure_settings;
build_azure_blob_client(&azure_blob_resource)
}
ObjectSettings::AwsOidc(aws_oidc_settings) => {
#[cfg(feature = "openidconnect")]
{
let token_fn = |audience: String| async move {
generate_id_token(
db,
claim,
aws_oidc_settings.audience,
"windmill_instance",
"instance_storage@windmill.dev",
)
};
todo!()
}
#[cfg(not(feature = "openidconnect"))]
{
return Err(error::Error::InternalErr(
"OpenID Connect is not enabled".to_string(),
));
}
}
}
}

View File

@@ -1,48 +0,0 @@
use sqlx::Postgres;
use crate::{error::Result, scripts::ScriptLang, DB};
pub async fn get_disable_stats_setting(_db: &DB) -> bool {
// stats details are closed source
false
}
pub async fn schedule_stats(_db: &DB, _http_client: &reqwest::Client) -> () {
// stats details are closed source
}
#[derive(Debug, sqlx::FromRow, serde::Serialize)]
struct JobsUsage {
language: Option<ScriptLang>,
total_duration: i64,
count: i64,
}
pub enum SendStatsReason {
Manual,
Schedule,
OnStart,
}
pub async fn send_stats(
_http_client: &reqwest::Client,
_db: &DB,
_skip_job_usage: bool,
_reason: SendStatsReason,
) -> Result<()> {
// stats details are closed source
Ok(())
}
pub struct ActiveUserUsage {
pub author_count: Option<i32>,
pub operator_count: Option<i32>,
}
pub async fn get_user_usage<'c, E: sqlx::Executor<'c, Database = Postgres>>(
_db: E,
) -> Result<ActiveUserUsage> {
let usage = ActiveUserUsage { author_count: None, operator_count: None };
Ok(usage)
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-common/src/stats_ee.rs

View File

@@ -13,7 +13,6 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use tokio::sync::RwLock;
use windmill_macros::annotations;
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, server::Smtp, DB};
@@ -304,25 +303,64 @@ fn parse_file<T: FromStr>(path: &str) -> Option<T> {
.flatten()
}
#[annotations("#")]
pub struct PythonAnnotations {
pub no_cache: bool,
pub no_uv: bool,
}
#[annotations("//")]
pub struct TypeScriptAnnotations {
pub npm: bool,
pub nodejs: bool,
pub native: bool,
pub npm_mode: bool,
pub nodejs_mode: bool,
pub native_mode: bool,
pub nobundling: bool,
}
#[annotations("--")]
pub fn get_annotation_ts(inner_content: &str) -> TypeScriptAnnotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("//"))
.map(|x| x.to_string().replace("//", "").trim().to_string())
.collect_vec();
let nodejs_mode: bool = annotations.contains(&"nodejs".to_string());
let npm_mode: bool = annotations.contains(&"npm".to_string());
let native_mode: bool = annotations.contains(&"native".to_string());
//TODO: remove || npm_mode when bun build is more powerful
let nobundling: bool =
annotations.contains(&"nobundling".to_string()) || nodejs_mode || *DISABLE_BUNDLING;
TypeScriptAnnotations { npm_mode, nodejs_mode, native_mode, nobundling }
}
pub struct PythonAnnotations {
pub no_uv: bool,
pub no_cache: bool,
}
pub fn get_annotation_python(inner_content: &str) -> PythonAnnotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("#"))
.map(|x| x.to_string().replace("#", "").trim().to_string())
.collect_vec();
let no_uv: bool = annotations.contains(&"no_uv".to_string());
let no_cache: bool = annotations.contains(&"no_cache".to_string());
PythonAnnotations { no_uv, no_cache }
}
pub struct SqlAnnotations {
pub return_last_result: bool,
}
pub fn get_sql_annotations(inner_content: &str) -> SqlAnnotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("--"))
.map(|x| x.to_string().replace("--", "").trim().to_string())
.collect_vec();
let return_last_result: bool = annotations.contains(&"return_last_result".to_string());
SqlAnnotations { return_last_result }
}
pub async fn load_cache(bin_path: &str, _remote_path: &str) -> (bool, String) {
if tokio::fs::metadata(&bin_path).await.is_ok() {
(true, format!("loaded from local cache: {}\n", bin_path))

View File

@@ -1,17 +0,0 @@
use windmill_common::error::Result;
use crate::{DeployedObject, DB};
pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send + Clone + 'c>(
_email: &str,
_created_by: &str,
_db: &DB,
_w_id: &str,
_obj: DeployedObject,
_deployment_message: Option<String>,
_rsmq: Option<R>,
_skip_db_insert: bool,
) -> Result<()> {
// Git sync is an enterprise feature and not part of the open-source version
return Ok(());
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-git-sync/src/git_sync_ee.rs

View File

@@ -1,21 +0,0 @@
use anyhow::anyhow;
use sqlx::{Pool, Postgres};
use windmill_common::error::Error;
#[derive(Clone)]
pub struct IndexReader;
#[derive(Clone)]
pub struct IndexWriter;
pub async fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
Err(anyhow!("Cannot initialize index: not in EE").into())
}
pub async fn run_indexer(
_db: Pool<Postgres>,
mut _index_writer: IndexWriter,
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
) {
tracing::error!("Cannot run indexer: not in EE");
}

View File

@@ -0,0 +1 @@
/git/windmill/../windmill-ee-private/windmill-indexer/src/indexer_ee.rs

View File

@@ -1,20 +0,0 @@
[package]
name = "windmill-macros"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
# Dependencies for tests
[dev-dependencies]
# tests/annotation.rs
lazy_static.workspace = true
itertools.workspace = true
regex.workspace = true

View File

@@ -1,95 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Ident, ItemStruct, Lit};
#[proc_macro_attribute]
pub fn annotations(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemStruct);
let name = input.ident.clone();
let fields = input
.fields
.iter()
.map(|f| f.ident.clone().unwrap())
.collect::<Vec<Ident>>();
// Match on the literal to extract the string value
let comm_lit = match parse_macro_input!(attr as Lit) {
Lit::Str(lit_str) => lit_str.value(), // This will give "#" without quotes
_ => panic!("Expected a string literal"),
};
// Generate regex
let mut reg = format!("^{}|", &comm_lit);
{
for field in fields.iter() {
reg.push_str(&(field.to_string()));
reg.push_str("\\b");
}
reg.push_str(r#"|\w+"#);
}
// Example of generated regex:
// ^#
// |ann1\b|ann2\b|ann3\b|ann4\b
// |\w+
TokenStream::from(quote! {
#[derive(Default, Debug)]
#input
impl std::ops::BitOrAssign for #name{
fn bitor_assign(&mut self, rhs: Self) {
// Unfold fields
// Read more: https://docs.rs/quote/latest/quote/macro.quote.html#interpolation
#( self.#fields |= rhs.#fields; )*
}
}
impl #name {
/// Autogenerated by windmill-macros
pub fn parse(inner_content: &str) -> Self{
let mut res = Self::default();
lazy_static::lazy_static! {
static ref RE: regex::Regex = regex::Regex::new(#reg).unwrap();
}
// Create lines stream
let mut lines = inner_content.lines();
'outer: while let Some(line) = lines.next() {
// If comment sign(s) on the right place
let mut comms = false;
// New instance
// We will apply it if in line only annotations
let mut new = Self::default();
'inner: for (i, mat) in RE.find_iter(line).enumerate() {
match mat.as_str(){
#comm_lit if i == 0 => {
comms = true;
continue 'inner;
},
// Will expand into something like:
// "ann1" => new.ann1 = true,
// "ann2" => new.ann2 = true,
// "ann3" => new.ann3 = true,
#( stringify!(#fields) => new.#fields = true, )*
// Non annotations
_ => continue 'outer,
};
}
if !comms {
// We dont want to continue if line does not start with #
return res;
}
// Apply changes
res |= new;
}
res
}
}
})
}

View File

@@ -1,169 +0,0 @@
#[cfg(test)]
mod annotations_tests {
extern crate windmill_macros;
use itertools::Itertools;
use windmill_macros::annotations;
// Previous implementation.
// We have to make sure that new one works the same as old one
fn old(inner_content: &str) -> Annotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("#"))
.map(|x| x.to_string().replace("#", "").trim().to_string())
.collect_vec();
let ann1: bool = annotations.contains(&"ann1".to_string());
let ann2: bool = annotations.contains(&"ann2".to_string());
let ann3: bool = annotations.contains(&"ann3".to_string());
let ann4: bool = annotations.contains(&"ann4".to_string());
let ann5: bool = annotations.contains(&"ann5".to_string());
Annotations { ann1, ann2, ann3, ann4, ann5 }
}
#[annotations("#")]
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct Annotations {
pub ann1: bool,
pub ann2: bool,
pub ann3: bool,
pub ann4: bool,
pub ann5: bool,
}
#[annotations("//")]
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct SlashedAnnotations {
pub ann1: bool,
pub ann2: bool,
pub ann3: bool,
pub ann4: bool,
}
#[annotations("--")]
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct MinusedAnnotations {
pub ann1: bool,
pub ann2: bool,
}
// e.g. rust, TS and JS
#[test]
fn slashed_annotations() {
let cont = "// ann1
// ann2
//ann3";
assert_eq!(
SlashedAnnotations { ann1: true, ann2: true, ann3: true, ann4: false },
SlashedAnnotations::parse(cont)
);
}
// e.g. Haskell, SQL
#[test]
fn minused_annotations() {
let cont = "-- ann1
-- ann2";
assert_eq!(
MinusedAnnotations { ann1: true, ann2: true },
MinusedAnnotations::parse(cont)
);
}
#[test]
fn simple_integration() {
let cont = "# ann1";
let expected = Annotations { ann1: true, ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
#[test]
fn multiline_integration() {
let cont = "# ann2
# ann3
# ann4
# ann5
";
let expected = Annotations {
ann1: false,
ann2: true,
ann3: true,
ann4: true,
ann5: true,
//
};
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
#[test]
fn spacing_integration() {
// First line is ignored and not used
{
let cont = "
# ann2";
let expected = Annotations { ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
// Wrong spacing for ann3
{
let cont = "# ann2
# ann3";
let expected = Annotations { ann2: true, ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
// Drunk but valid spacing
{
let cont = "#ann1
# ann2";
let expected = Annotations { ann2: true, ann1: true, ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
}
#[test]
fn comments_inbetween_integration() {
let cont = "# ann2
# Just comment, has nothing to do with annotations
# Another comment: ann1 ann2 ann3
# ann4 is not valid annotation
# Actual annotation next line:
# ann5
# Should be ignored
# ann3
";
let expected = Annotations { ann2: true, ann5: true, ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
#[test]
fn hash_collision() {
// TODO
}
#[test]
fn non_matching_integration() {
{
let cont = r#" "ann1", ann2 "#;
let expected = Annotations::default();
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
// Empty
{
let cont = "";
let expected = Annotations::default();
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
}
}

View File

@@ -11,7 +11,7 @@ path = "src/lib.rs"
[features]
default = []
prometheus = ["dep:prometheus", "windmill-common/prometheus"]
enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "dep:gcp_auth", "dep:pem", "dep:tiberius", "dep:tokio-util", "dep:openidconnect"]
enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "dep:gcp_auth", "dep:pem", "dep:tiberius", "dep:tokio-util"]
benchmark = ["windmill-queue/benchmark", "windmill-common/benchmark"]
flamegraph = []
parquet = ["windmill-common/parquet", "dep:object_store"]
@@ -85,7 +85,6 @@ reqwest.workspace = true
hex.workspace = true
tiberius = { workspace = true, optional = true }
tokio-util = { workspace = true, optional = true }
openidconnect = { workspace = true, optional = true}
tar.workspace = true
object_store = { workspace = true, optional = true}
convert_case.workspace = true

View File

@@ -8,65 +8,29 @@ const p = {
"localhost",
"127.0.0.1"
);
const w_id = "W_ID";
const current_path = "CURRENT_PATH";
const token = "TOKEN";
const cdir = resolve("./");
const cdirNoPrivate = cdir.replace(/^\/private/, ""); // for macos
const filterResolve = new RegExp(
`^(?!\\.\/main\\.ts)(?!${cdir}\/main\\.ts)(?!(?:/private)?${cdirNoPrivate}\/wrapper\\.mjs).*\\.ts$`
);
let cdirNodeModules = `${cdir}/node_modules/`;
const filterLoad = new RegExp(`^${cdir}\/main\\.ts$`);
const transpiler = new Bun.Transpiler({
loader: "tsx",
});
function replaceRelativeImports(code) {
const imports = transpiler.scanImports(code);
for (const imp of imports) {
if (imp.kind == "import-statement") {
if (imp.path.startsWith(".") && !imp.path.endsWith(".ts")) {
code = code.replaceAll(imp.path, imp.path + ".ts");
}
}
}
return {
contents: code,
};
}
build.onLoad({ filter: filterLoad }, async (args) => {
const code = readFileSync(args.path, "utf8");
return replaceRelativeImports(code);
});
build.onLoad({ filter: /.*\.url$/ }, async (args) => {
const url = readFileSync(args.path, "utf8");
const req = await fetch(url, {
method: "GET",
headers: {
Authorization: "Bearer " + token,
},
});
if (!req.ok) {
throw new Error(
`Failed to find relative import at ${url}`,
req.statusText
);
}
const contents = await req.text();
const contents = await (
await fetch(url, {
method: "GET",
headers: { Authorization: "Bearer TOKEN" },
})
).text();
return {
contents: replaceRelativeImports(contents).contents,
contents,
loader: "tsx",
};
});
build.onResolve({ filter: filterResolve }, (args) => {
const cdir = resolve("./");
const cdirNoPrivate = cdir.replace(/^\/private/, ""); // for macos
const filter = new RegExp(
`^(?!\\.\/main\\.ts)(?!${cdir}\/main\\.ts)(?!(?:/private)?${cdirNoPrivate}\/wrapper\\.mjs).*\\.ts$`
);
let cdirNodeModules = `${cdir}/node_modules/`;
build.onResolve({ filter }, (args) => {
if (args.importer?.startsWith(cdirNodeModules)) {
return undefined;
}
@@ -77,10 +41,9 @@ const p = {
const isRelative = !args.path.startsWith("/");
let endExt = args.path.endsWith(".ts") ? "" : ".ts";
const url = isRelative
? `${base_internal_url}/api/w/${w_id}/scripts/raw_unpinned/p/${file_path}/../${args.path}${endExt}`
: `${base_internal_url}/api/w/${w_id}/scripts/raw_unpinned/p/${args.path}${endExt}`;
? `${base_internal_url}/api/w/${w_id}/scripts/RAW_GET_ENDPOINT/p/${file_path}/../${args.path}`
: `${base_internal_url}/api/w/${w_id}/scripts/RAW_GET_ENDPOINT/p/${args.path}`;
const file = isRelative
? resolve("./" + file_path + "/../" + args.path + ".url")
: resolve("./" + args.path + ".url");

View File

@@ -5,6 +5,7 @@ use futures::{FutureExt, TryFutureExt};
use serde_json::{json, value::RawValue, Value};
use windmill_common::error::to_anyhow;
use windmill_common::jobs::QueuedJob;
use windmill_common::worker::get_sql_annotations;
use windmill_common::{error::Error, worker::to_raw_value};
use windmill_parser_sql::{
parse_bigquery_sig, parse_db_resource, parse_sql_blocks, parse_sql_statement_named_params,
@@ -237,7 +238,7 @@ pub async fn do_bigquery(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let annotations = get_sql_annotations(query);
let service_account = CustomServiceAccount::from_json(&database)
.map_err(|e| Error::ExecutionErr(e.to_string()))?;

View File

@@ -47,7 +47,7 @@ use windmill_common::{
get_latest_hash_for_path,
jobs::{QueuedJob, PREPROCESSOR_FAKE_ENTRYPOINT},
scripts::ScriptLang,
worker::{exists_in_cache, save_cache, write_file},
worker::{exists_in_cache, get_annotation_ts, save_cache, write_file},
DB,
};
@@ -663,7 +663,7 @@ pub async fn prebundle_bun_script(
if exists_in_cache(&local_path, &remote_path).await {
return Ok(());
}
let annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
let annotation = get_annotation_ts(inner_content);
if annotation.nobundling {
return Ok(());
}
@@ -676,9 +676,9 @@ pub async fn prebundle_bun_script(
&token,
w_id,
script_path,
if annotation.nodejs {
if annotation.nodejs_mode {
LoaderMode::NodeBundle
} else if annotation.native {
} else if annotation.native_mode {
LoaderMode::BrowserBundle
} else {
LoaderMode::BunBundle
@@ -800,7 +800,7 @@ pub async fn handle_bun_job(
new_args: &mut Option<HashMap<String, Box<RawValue>>>,
occupancy_metrics: &mut OccupancyMetrics,
) -> error::Result<Box<RawValue>> {
let mut annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
let mut annotation = windmill_common::worker::get_annotation_ts(inner_content);
let (mut has_bundle_cache, cache_logs, local_path, remote_path) =
if requirements_o.is_some() && !annotation.nobundling && codebase.is_none() {
@@ -822,7 +822,7 @@ pub async fn handle_bun_job(
if !codebase.is_some() && !has_bundle_cache {
let _ = write_file(job_dir, "main.ts", inner_content)?;
} else if !annotation.native && codebase.is_none() {
} else if !annotation.native_mode && codebase.is_none() {
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#)?;
};
@@ -830,7 +830,7 @@ pub async fn handle_bun_job(
get_common_bun_proc_envs(Some(&base_internal_url)).await;
if codebase.is_some() {
annotation.nodejs = true
annotation.nodejs_mode = true
}
let (main_override, apply_preprocessor) = match get_main_override(job.args.as_ref()) {
Some(main_override) => {
@@ -844,7 +844,7 @@ pub async fn handle_bun_job(
};
#[cfg(not(feature = "enterprise"))]
if annotation.nodejs || annotation.npm {
if annotation.nodejs_mode || annotation.npm_mode {
return Err(error::Error::ExecutionErr(
"Nodejs / npm mode is an EE feature".to_string(),
));
@@ -875,20 +875,20 @@ pub async fn handle_bun_job(
pull_codebase(&job.workspace_id, codebase, job_dir).await?;
} else if let Some(reqs) = requirements_o.as_ref() {
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
if splitted.len() != 2 && !annotation.npm {
if splitted.len() != 2 && !annotation.npm_mode {
return Err(error::Error::ExecutionErr(
format!("Invalid requirements, expected to find //bun.lockb split pattern in reqs. Found: |{reqs}|")
));
}
let _ = write_file(job_dir, "package.json", &splitted[0])?;
let lockb = if annotation.npm { "" } else { splitted[1] };
let lockb = if annotation.npm_mode { "" } else { splitted[1] };
if lockb != EMPTY_FILE {
let mut skip_install = false;
let mut create_buntar = false;
let mut buntar_path = "".to_string();
if !annotation.npm {
if !annotation.npm_mode {
let _ = write_lockb(&splitted[1], job_dir).await?;
let mut sha_path = sha2::Sha256::new();
@@ -921,7 +921,7 @@ pub async fn handle_bun_job(
job_dir,
worker_name,
common_bun_proc_envs.clone(),
annotation.npm,
annotation.npm_mode,
&mut Some(occupancy_metrics),
)
.await?;
@@ -963,7 +963,7 @@ pub async fn handle_bun_job(
worker_name,
false,
None,
annotation.npm,
annotation.npm_mode,
&mut Some(occupancy_metrics),
)
.await?;
@@ -971,19 +971,19 @@ pub async fn handle_bun_job(
// }
}
let mut init_logs = if annotation.native {
let mut init_logs = if annotation.native_mode {
"\n\n--- NATIVE CODE EXECUTION ---\n".to_string()
} else if has_bundle_cache {
if annotation.nodejs {
if annotation.nodejs_mode {
"\n\n--- NODE BUNDLE SNAPSHOT EXECUTION ---\n".to_string()
} else {
"\n\n--- BUN BUNDLE SNAPSHOT EXECUTION ---\n".to_string()
}
} else if codebase.is_some() {
"\n\n--- NODE CODEBASE SNAPSHOT EXECUTION ---\n".to_string()
} else if annotation.native {
} else if annotation.native_mode {
"\n\n--- NATIVE CODE EXECUTION ---\n".to_string()
} else if annotation.nodejs {
} else if annotation.nodejs_mode {
write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?)?;
"\n\n--- NODE CODE EXECUTION ---\n".to_string()
} else {
@@ -1003,7 +1003,7 @@ pub async fn handle_bun_job(
}
let write_wrapper_f = async {
if !has_bundle_cache && annotation.native {
if !has_bundle_cache && annotation.native_mode {
return Ok(()) as error::Result<()>;
}
// let mut start = Instant::now();
@@ -1126,7 +1126,7 @@ try {{
let reserved_variables_args_out_f = async {
let args_and_out_f = async {
if !annotation.native {
if !annotation.native_mode {
create_args_and_out_file(&client, job, job_dir, db).await?;
}
Ok(()) as Result<()>
@@ -1143,7 +1143,7 @@ try {{
let build_cache = !has_bundle_cache
&& !annotation.nobundling
&& !codebase.is_some()
&& (requirements_o.is_some() || annotation.native);
&& (requirements_o.is_some() || annotation.native_mode);
let write_loader_f = async {
if build_cache {
@@ -1153,9 +1153,9 @@ try {{
&client.get_token().await,
&job.workspace_id,
&job.script_path(),
if annotation.nodejs {
if annotation.nodejs_mode {
LoaderMode::NodeBundle
} else if annotation.native {
} else if annotation.native_mode {
LoaderMode::BrowserBundle
} else {
LoaderMode::BunBundle
@@ -1171,7 +1171,7 @@ try {{
&client.get_token().await,
&job.workspace_id,
&job.script_path(),
if annotation.nodejs {
if annotation.nodejs_mode {
LoaderMode::Node
} else {
LoaderMode::Bun
@@ -1217,7 +1217,7 @@ try {{
}
}
}
if !annotation.native {
if !annotation.native_mode {
let ex_wrapper = read_file_content(&format!("{job_dir}/wrapper.mjs")).await?;
write_file(
job_dir,
@@ -1231,7 +1231,7 @@ try {{
}
fs::remove_file(format!("{job_dir}/main.ts"))?;
has_bundle_cache = true;
} else if annotation.nodejs {
} else if annotation.nodejs_mode {
generate_wrapper_mjs(
job_dir,
&job.workspace_id,
@@ -1247,14 +1247,9 @@ try {{
.await?;
}
}
if annotation.native {
if annotation.native_mode {
#[cfg(not(feature = "deno_core"))]
{
tracing::error!(
r#""deno_core" feature is not activated, but "//native" annotation used. Returning empty value..."#
);
return Ok(to_raw_value("").unwrap());
}
return Ok(to_raw_value("").unwrap());
#[cfg(feature = "deno_core")]
{
@@ -1314,7 +1309,14 @@ try {{
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_BUN_CONTENT
.replace("{LANG}", if annotation.nodejs { "nodejs" } else { "bun" })
.replace(
"{LANG}",
if annotation.nodejs_mode {
"nodejs"
} else {
"bun"
},
)
.replace("{JOB_DIR}", job_dir)
.replace("{CACHE_DIR}", BUN_CACHE_DIR)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
@@ -1322,7 +1324,7 @@ try {{
"{SHARED_MOUNT}",
&shared_mount.replace(
"/tmp/shared",
if annotation.nodejs {
if annotation.nodejs_mode {
"/tmp/nodejs/shared"
} else {
"/tmp/bun/shared"
@@ -1332,7 +1334,7 @@ try {{
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
let args = if annotation.nodejs {
let args = if annotation.nodejs_mode {
vec![
"--config",
"run.config.proto",
@@ -1376,7 +1378,7 @@ try {{
.stderr(Stdio::piped());
start_child_process(nsjail_cmd, NSJAIL_PATH.as_str()).await?
} else {
let cmd = if annotation.nodejs {
let cmd = if annotation.nodejs_mode {
let script_path = format!("{job_dir}/wrapper.mjs");
let mut bun_cmd = Command::new(&*NODE_BIN_PATH);
@@ -1428,7 +1430,7 @@ try {{
start_child_process(
cmd,
if annotation.nodejs {
if annotation.nodejs_mode {
&*NODE_BIN_PATH
} else {
&*BUN_PATH
@@ -1533,10 +1535,10 @@ pub async fn start_worker(
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(Some(&base_internal_url)).await;
let mut annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
let mut annotation = windmill_common::worker::get_annotation_ts(inner_content);
//TODO: remove this when bun dedicated workers work without issues
annotation.nodejs = true;
annotation.nodejs_mode = true;
let context = variables::get_reserved_variables(
db,
@@ -1590,7 +1592,7 @@ pub async fn start_worker(
job_dir,
worker_name,
common_bun_proc_envs.clone(),
annotation.npm,
annotation.npm_mode,
&mut None,
)
.await?;
@@ -1611,7 +1613,7 @@ pub async fn start_worker(
worker_name,
false,
None,
annotation.npm,
annotation.npm_mode,
&mut None,
)
.await?;
@@ -1689,7 +1691,7 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
token,
w_id,
script_path,
if annotation.nodejs {
if annotation.nodejs_mode {
LoaderMode::Node
} else {
LoaderMode::Bun
@@ -1698,7 +1700,7 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
.await?;
}
if annotation.nodejs && !codebase.is_some() {
if annotation.nodejs_mode && !codebase.is_some() {
generate_wrapper_mjs(
job_dir,
w_id,
@@ -1714,7 +1716,7 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
.await?;
}
if annotation.nodejs {
if annotation.nodejs_mode {
let script_path = format!("{job_dir}/wrapper.mjs");
handle_dedicated_process(

View File

@@ -9,7 +9,7 @@ use tokio::net::TcpStream;
use tokio_util::compat::TokioAsyncWriteCompatExt;
use uuid::Uuid;
use windmill_common::error::{self, Error};
use windmill_common::worker::to_raw_value;
use windmill_common::worker::{get_sql_annotations, 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};
@@ -68,7 +68,7 @@ pub async fn do_mssql(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let annotations = get_sql_annotations(query);
let mut config = Config::new();

View File

@@ -13,7 +13,7 @@ use tokio::sync::Mutex;
use windmill_common::{
error::{to_anyhow, Error},
jobs::QueuedJob,
worker::to_raw_value,
worker::{get_sql_annotations, to_raw_value},
};
use windmill_parser_sql::{
parse_db_resource, parse_mysql_sig, parse_sql_blocks, parse_sql_statement_named_params,
@@ -148,7 +148,7 @@ pub async fn do_mysql(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let annotations = get_sql_annotations(query);
let opts = OptsBuilder::default()
.db_name(Some(database.database))

View File

@@ -30,7 +30,7 @@ use tokio_postgres::{
};
use uuid::Uuid;
use windmill_common::error::{self, Error};
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
use windmill_common::worker::{get_sql_annotations, to_raw_value, CLOUD_HOSTED};
use windmill_common::{error::to_anyhow, jobs::QueuedJob};
use windmill_parser::{Arg, Typ};
use windmill_parser_sql::{
@@ -191,7 +191,7 @@ pub async fn do_postgresql(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let annotations = get_sql_annotations(query);
let sslmode = match database.sslmode.as_deref() {
Some("allow") => "prefer".to_string(),

View File

@@ -898,7 +898,7 @@ async fn handle_python_deps(
let requirements = match requirements_o {
Some(r) => r,
None => {
let annotation = windmill_common::worker::PythonAnnotations::parse(inner_content);
let annotation = windmill_common::worker::get_annotation_python(inner_content);
let mut already_visited = vec![];
let requirements = windmill_parser_py_imports::parse_python_imports(

View File

@@ -9,6 +9,7 @@ use serde_json::{json, value::RawValue, Value};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use windmill_common::error::to_anyhow;
use windmill_common::worker::get_sql_annotations;
use windmill_common::jobs::QueuedJob;
use windmill_common::{error::Error, worker::to_raw_value};
@@ -265,7 +266,7 @@ pub async fn do_snowflake(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let annotations = get_sql_annotations(query);
let qualified_username = format!(
"{}.{}",

View File

@@ -12,7 +12,7 @@ use windmill_common::flows::{FlowModule, FlowModuleValue};
use windmill_common::get_latest_deployed_hash_for_path;
use windmill_common::jobs::JobPayload;
use windmill_common::scripts::ScriptHash;
use windmill_common::worker::{to_raw_value, to_raw_value_owned, write_file};
use windmill_common::worker::{get_annotation_ts, to_raw_value, to_raw_value_owned, write_file};
use windmill_common::{
error::{self, to_anyhow},
flows::FlowValue,
@@ -953,10 +953,10 @@ async fn lock_modules<'c>(
}
if language == ScriptLang::Bun || language == ScriptLang::Bunnative {
let anns = windmill_common::worker::TypeScriptAnnotations::parse(&content);
if anns.native && language == ScriptLang::Bun {
let anns = get_annotation_ts(&content);
if anns.native_mode && language == ScriptLang::Bun {
language = ScriptLang::Bunnative;
} else if !anns.native && language == ScriptLang::Bunnative {
} else if !anns.native_mode && language == ScriptLang::Bunnative {
language = ScriptLang::Bun;
};
}
@@ -1003,10 +1003,10 @@ async fn lock_modules<'c>(
fn skip_creating_new_lock(language: &ScriptLang, content: &str) -> bool {
if language == &ScriptLang::Bun || language == &ScriptLang::Bunnative {
let anns = windmill_common::worker::TypeScriptAnnotations::parse(&content);
if anns.native && language == &ScriptLang::Bun {
let anns = get_annotation_ts(&content);
if anns.native_mode && language == &ScriptLang::Bun {
return false;
} else if !anns.native && language == &ScriptLang::Bunnative {
} else if !anns.native_mode && language == &ScriptLang::Bunnative {
return false;
};
}
@@ -1077,13 +1077,11 @@ async fn lock_modules_app(
match new_lock {
Ok(new_lock) => {
append_logs(&job.id, &job.workspace_id, logs, db).await;
let anns =
windmill_common::worker::TypeScriptAnnotations::parse(
&content,
);
let nlang = if anns.native && language == ScriptLang::Bun {
let anns = get_annotation_ts(&content);
let nlang = if anns.native_mode && language == ScriptLang::Bun {
Some(ScriptLang::Bunnative)
} else if !anns.native && language == ScriptLang::Bunnative {
} else if !anns.native_mode && language == ScriptLang::Bunnative
{
Some(ScriptLang::Bun)
} else {
None
@@ -1439,7 +1437,7 @@ async fn capture_dependency_job(
}
ScriptLang::Bun | ScriptLang::Bunnative => {
let npm_mode = npm_mode.unwrap_or_else(|| {
windmill_common::worker::TypeScriptAnnotations::parse(job_raw_code).npm
windmill_common::worker::get_annotation_ts(job_raw_code).npm_mode
});
if !raw_deps {
let _ = write_file(job_dir, "main.ts", job_raw_code)?;

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.411.1";
export const VERSION = "v1.409.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -1,104 +0,0 @@
import { log } from "./deps.ts";
import crypto from "node:crypto";
// Helper function to convert strings to Uint8Array (binary)
function encode(input: string): Uint8Array {
return new TextEncoder().encode(input);
}
// Helper function to convert Uint8Array (binary) to base64
function toBase64(arr: Uint8Array): string {
return btoa(String.fromCharCode(...arr));
}
// Helper function to convert base64 to Uint8Array (binary)
function fromBase64(base64: string): Uint8Array {
return new Uint8Array(
atob(base64).split("").map((char) => char.charCodeAt(0))
);
}
// Function to derive a 256-bit key from any input string using SHA-256
async function deriveKey(keyString: string): Promise<CryptoKey> {
const keyMaterial = encode(keyString);
const keyHash = await crypto.subtle.digest("SHA-256", keyMaterial); // Generate SHA-256 hash
// Import the hash as a CryptoKey for AES-GCM
return crypto.subtle.importKey(
"raw",
keyHash,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"]
);
}
// Encrypt function
export async function encrypt(
plaintext: string,
keyString: string
): Promise<string> {
const key = await deriveKey(keyString); // Derive a 256-bit AES key from any input string
const iv = crypto.getRandomValues(new Uint8Array(12)); // AES-GCM needs a 12-byte IV
const encrypted = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
tagLength: 128
},
key,
encode(plaintext) // convert plaintext to binary
);
// Concatenate IV and encrypted data
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv, 0); // first part is the IV
combined.set(new Uint8Array(encrypted), iv.length); // second part is the ciphertext
// Convert to base64 for storage/transmission
return toBase64(combined);
}
// Decrypt function
export async function decrypt(
combinedCiphertext: string,
keyString: string
): Promise<string> {
const key = await deriveKey(keyString); // Derive the same 256-bit AES key from the input string
const combined = fromBase64(combinedCiphertext); // decode base64 to binary
// Split the IV and the ciphertext
const iv = combined.slice(0, 12); // First 12 bytes are the IV
const ciphertext = combined.slice(12); // The rest is the encrypted data
console.log()
// log.info({keyString, key, ciphertext})
// Perform decryption
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv,
tagLength: 128
},
key,
ciphertext
);
// Convert decrypted data from binary to string
return new TextDecoder().decode(decrypted);
}
// // Example usage:
// const key = "any-length-key-you-want"; // Now can be any length
// const message = "This is a secret message.";
// encrypt(message, key).then((combinedCiphertext) => {
// console.log("Encrypted message:", combinedCiphertext);
// // Now decrypt it
// decrypt(combinedCiphertext, key).then((decryptedMessage) => {
// console.log("Decrypted message:", decryptedMessage);
// });
// });

View File

@@ -60,7 +60,7 @@ export {
// }
// });
export const VERSION = "1.411.1";
export const VERSION = "1.409.2";
const command = new Command()
.name("wmill")
@@ -101,13 +101,9 @@ const command = new Command()
"wmill.yaml",
yamlStringify({
defaultTs: "bun",
includes: ["f/**"],
includes: ["**"],
excludes: [],
codebases: [],
skipVariables: true,
skipResources: true,
skipSecrets: true,
includeSchedules: false,
})
);
log.info(colors.green("wmill.yaml created"));
@@ -217,11 +213,9 @@ function isMain() {
const isMain = import.meta.main;
if (isMain) {
if (!Deno.args.includes("completions")) {
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
log.warn(
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true"
);
}
log.warn(
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli"
);
}
}
return isMain;

View File

@@ -9,7 +9,6 @@ import { deepEqual } from "./utils.ts";
import * as wmill from "./gen/services.gen.ts";
import { Config, GlobalSetting } from "./gen/types.gen.ts";
import { removeWorkerPrefix } from "./worker_groups.ts";
import process from "node:process";
export interface SimplifiedSettings {
// slack_team_id?: string;
@@ -113,10 +112,10 @@ export async function pushWorkspaceSettings(
workspace,
requestBody: localSettings.auto_invite_enabled
? {
operator: localSettings.auto_invite_as === "operator",
invite_all: true,
auto_add: localSettings.auto_invite_mode === "add",
}
operator: localSettings.auto_invite_as === "operator",
invite_all: true,
auto_add: localSettings.auto_invite_mode === "add",
}
: {},
});
} catch (_) {
@@ -128,10 +127,10 @@ export async function pushWorkspaceSettings(
workspace,
requestBody: localSettings.auto_invite_enabled
? {
operator: localSettings.auto_invite_as === "operator",
invite_all: false,
auto_add: localSettings.auto_invite_mode === "add",
}
operator: localSettings.auto_invite_as === "operator",
invite_all: false,
auto_add: localSettings.auto_invite_mode === "add",
}
: {},
});
}
@@ -156,7 +155,7 @@ export async function pushWorkspaceSettings(
settings.error_handler_extra_args
) ||
localSettings.error_handler_muted_on_cancel !==
settings.error_handler_muted_on_cancel
settings.error_handler_muted_on_cancel
) {
log.debug(`Updating error handler...`);
await wmill.editErrorHandler({
@@ -260,69 +259,27 @@ export async function pushWorkspaceKey(
}
}
const INSTANCE_SETTINGS_PATH = "instance_settings.yaml";
export async function readInstanceSettings() {
let localSettings: GlobalSetting[] = [];
try {
localSettings = (await yamlParseFile(INSTANCE_SETTINGS_PATH)) as GlobalSetting[];
localSettings = (await yamlParseFile(
"instance_settings.yaml"
)) as GlobalSetting[];
} catch {
log.warn(`No ${INSTANCE_SETTINGS_PATH} found`);
log.warn("No instance_settings.yaml found");
}
return localSettings;
}
import { decrypt, encrypt } from "./local_encryption.ts";
const SENSITIVE_FIELD: string[] = ["license_key", "jwt_secret"]
async function processInstanceSettings(settings: GlobalSetting[], mode: "encode" | "decode"): Promise<GlobalSetting[]> {
const encKey = process.env.WMILL_INSTANCE_LOCAL_ENCRYPTION_KEY;
if (encKey) {
const res: GlobalSetting[] = []
for (const s of settings) {
if (SENSITIVE_FIELD.includes(s.name) && typeof s.value === "string") {
res.push(await processField(s, "value", encKey, mode) as GlobalSetting);
} else if (s.name == "oauths") {
if (typeof s.value === "object") {
const oauths = s.value as { [key: string]: any };
for (const [k, v] of Object.entries(oauths)) {
oauths[k] = await processField(v, "secret", encKey, mode);
}
res.push(s);
} else {
log.warn(`Unexpected oauths value type: ${typeof s.value}`);
res.push(s);
}
} else {
res.push(s);
}
}
return res;
} else {
log.warn("No encryption key found, skipping encryption. Recommend setting WMILL_INSTANCE_LOCAL_ENCRYPTION_KEY");
}
return settings;
}
async function processField(obj: { [key: string]: any }, field: string, encKey: string, mode: "encode" | "decode"): Promise<{ [key: string]: any }> {
return {
...obj,
[field]: mode === "encode" ? await encrypt(obj[field], encKey) : await decrypt(obj[field], encKey) as any,
}
}
export async function pullInstanceSettings(preview = false) {
const remoteSettings = await wmill.listGlobalSettings();
if (preview) {
const localSettings: GlobalSetting[] = await readInstanceSettings();
const processedSettings = await processInstanceSettings(remoteSettings, "encode");
return compareInstanceObjects(
processedSettings,
remoteSettings,
localSettings,
"name",
"setting"
@@ -330,13 +287,12 @@ export async function pullInstanceSettings(preview = false) {
} else {
log.info("Pulling settings from instance");
const processedSettings = await processInstanceSettings(remoteSettings, "encode");
await Deno.writeTextFile(
INSTANCE_SETTINGS_PATH,
yamlStringify(processedSettings)
"instance_settings.yaml",
yamlStringify(remoteSettings as any)
);
log.info(colors.green(`Settings written to ${INSTANCE_SETTINGS_PATH}`));
log.info(colors.green("Settings written to instance_settings.yaml"));
}
}
@@ -346,7 +302,6 @@ export async function pushInstanceSettings(
) {
const remoteSettings = await wmill.listGlobalSettings();
let localSettings: GlobalSetting[] = await readInstanceSettings();
localSettings = await processInstanceSettings(localSettings, "decode");
if (baseUrl) {
localSettings = localSettings.filter((s) => s.name !== "base_url");

View File

@@ -618,8 +618,8 @@ export async function elementsToMap(
for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
if (entry.isDirectory || entry.ignored) continue;
const path = entry.path;
if (json && path.endsWith(".yaml") && !isFileResource(path)) continue;
if (!json && path.endsWith(".json") && !isFileResource(path)) continue;
if (json && path.endsWith(".yaml")) continue;
if (!json && path.endsWith(".json")) continue;
const ext = json ? ".json" : ".yaml";
if (!skips.includeSchedules && path.endsWith(".schedule" + ext)) continue;
if (!skips.includeUsers && path.endsWith(".user" + ext)) continue;

View File

@@ -20,7 +20,7 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
# Install UV
RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.4.18/uv-installer.sh | sh && mv /root/.cargo/bin/uv /usr/local/bin/uv
COPY --from=oven/bun:1.1.32 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.27 /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

@@ -19,7 +19,7 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
# Install UV
RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.4.18/uv-installer.sh | sh && mv /root/.cargo/bin/uv /usr/local/bin/uv
COPY --from=oven/bun:1.1.32 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.27 /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

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.411.1",
"version": "1.409.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.411.1",
"version": "1.409.2",
"license": "AGPL-3.0",
"dependencies": {
"@aws-crypto/sha256-js": "^4.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.411.1",
"version": "1.409.2",
"scripts": {
"dev": "vite dev",
"build": "vite build",

View File

@@ -120,16 +120,11 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => {
? f.raw
: '/' + config.scriptPath + (f.raw.startsWith('../') ? '/../' : '/.') + f.raw
let url = config.root + path
let localPath = f.raw
if (f.raw.startsWith('.') && !f.raw.endsWith('.ts')) {
url += '.ts'
localPath += '.ts'
}
console.log('fetching local file', url, f.raw, localPath)
// console.log('FOO', config.scriptPath, path, f.raw)
console.log('fetching local file', url, f.raw)
const res = await fetch(url)
if (res.ok) {
config.delegate.localFile?.(await res.text(), localPath)
config.delegate.localFile?.(await res.text(), f.raw)
}
})
}

View File

@@ -112,26 +112,26 @@
// If using the basic editor, set the cron string based on the selected options
if (executeEvery === 'second') {
if (seconds > 0) {
nschedule = `*/${seconds} * * * * *`
nschedule = `*/${seconds} * * * *`
} else {
nschedule = `* * * * * *`
nschedule = `* * * * *`
}
} else if (executeEvery === 'minute') {
if (minutes > 0) {
nschedule = `0 */${minutes} * * * *`
nschedule = `0 */${minutes} * * *`
} else {
nschedule = `* * * * * *`
nschedule = `* * * * *`
}
} else if (executeEvery === 'hour') {
if (hours > 0) {
nschedule = `0 0 */${hours} * * *`
nschedule = `0 0 */${hours} * *`
} else {
nschedule = `* * * * * *`
nschedule = `* * * * *`
}
} else if (executeEvery === 'day-month') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} * *`
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} *`
} else if (executeEvery === 'month') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} ${s_months} *`
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} ${s_months}`
} else if (executeEvery === 'day-week') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} * * ${s_daysOfWeek}`
}
@@ -214,7 +214,7 @@
type="text"
id="cron-schedule"
name="cron-schedule"
placeholder="0 0 */1 * * *"
placeholder="*/30 * * * *"
bind:value={schedule}
{disabled}
/>

View File

@@ -475,9 +475,9 @@
const testStepStore = writable<Record<string, any>>({})
const selectedIdStore = writable('settings-metadata')
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
>('webhooks')
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
const triggersCount = writable<TriggersCount | undefined>(undefined)

View File

@@ -25,12 +25,10 @@
encodeState,
formatCron,
orderedJsonStringify,
sleep,
type Value
sleep
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { Drawer } from '$lib/components/common'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import { setContext, tick, type ComponentType } from 'svelte'
import { writable, type Writable } from 'svelte/store'
@@ -104,30 +102,9 @@
export let disableAi: boolean = false
export let disabledFlowInputs = false
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
export let version: number | undefined = undefined
// Used by multiplayer deploy collision warning
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
$: setContext('customUi', customUi)
let onLatest = true
async function compareVersions() {
if (version === undefined) {
return
}
const flowVersion = await FlowService.getFlowLatestVersion({
workspace: $workspaceStore!,
path: $pathStore
})
onLatest = version === flowVersion?.id
}
const dispatch = createEventDispatcher()
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule)
@@ -276,46 +253,6 @@
)
}
async function handleSaveFlow(deploymentMsg?: string) {
await compareVersions();
if (onLatest) {
// Handle directly
await saveFlow(deploymentMsg)
} else {
// We need it for diff
await syncWithDeployed()
// Handle through confirmation modal
confirmCallback = async () => {
await saveFlow(deploymentMsg)
}
// Open confirmation modal
open = true
}
}
async function syncWithDeployed(){
const flow = await FlowService.getFlowByPath({
workspace: $workspaceStore!,
path: $pathStore,
withStarredInfo: true
})
deployedValue = {
...flow,
starred: undefined,
id: undefined,
edited_at: undefined,
edited_by: undefined,
workspace_id: undefined,
archived: undefined,
same_worker: undefined,
visible_to_runner_only: undefined,
ws_error_handler_muted: undefined,
}
deployedBy = flow.edited_by
}
async function saveFlow(deploymentMsg?: string): Promise<void> {
loadingSave = true
try {
@@ -393,7 +330,7 @@
requestBody: { enabled }
})
}
} else if (scheduleExists && !$triggersCount?.primary_schedule) {
} else if (scheduleExists) {
await ScheduleService.deleteSchedule({
workspace: $workspaceStore ?? '',
path: $pathStore
@@ -461,9 +398,9 @@
}
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
const selectedTriggerStore = writable<
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
>('webhooks')
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
export function getSelectedId() {
return $selectedIdStore
@@ -483,9 +420,7 @@
selectedIdStore.set(selectedId)
}
function selectTrigger(
selectedTrigger: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
) {
function selectTrigger(selectedTrigger: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes') {
selectedTriggerStore.set(selectedTrigger)
}
@@ -1187,15 +1122,6 @@
<slot />
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
currentValue={$flowStore}
/>
{#key renderCount}
{#if !$userStore?.operator}
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
@@ -1360,17 +1286,14 @@
color="light"
variant="border"
size="xs"
on:click={async () => {
on:click={() => {
if (!savedFlow) {
return
}
await syncWithDeployed()
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: deployedValue ?? savedFlow,
deployed: savedFlow,
draft: savedFlow['draft'],
current: { ...$flowStore, path: $pathStore }
})
@@ -1412,9 +1335,7 @@
loading={loadingSave}
size="xs"
startIcon={{ icon: Save }}
on:click={async () => {
await handleSaveFlow()
}}
on:click={() => saveFlow()}
dropdownItems={!newFlow ? dropdownItems : undefined}
>
Deploy
@@ -1425,16 +1346,16 @@
type="text"
placeholder="Deployment message"
bind:value={deploymentMsg}
on:keydown={async (e) => {
on:keydown={(e) => {
if (e.key === 'Enter') {
await handleSaveFlow(deploymentMsg)
saveFlow(deploymentMsg)
}
}}
bind:this={msgInput}
/>
<Button
size="xs"
on:click={async () => await handleSaveFlow(deploymentMsg)}
on:click={() => saveFlow(deploymentMsg)}
endIcon={{ icon: CornerDownLeft }}
loading={loadingSave}
>

View File

@@ -20,7 +20,7 @@
export let noSide = false
export let download = false
export let noGraph = false
export let triggerNode = false
export let stepDetail: FlowModule | string | undefined = undefined
const dispatch = createEventDispatcher()
@@ -33,7 +33,6 @@
class:overflow-auto={overflowAuto}
>
<FlowGraphV2
{triggerNode}
path={flow?.path}
{download}
minHeight={400}

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