Compare commits
1 Commits
v1.411.1
...
rf/oidcIns
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
649872ab0d |
4
.github/DockerfileBackendTests
vendored
4
.github/DockerfileBackendTests
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
@@ -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:
|
||||
|
||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
316
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"] }
|
||||
@@ -1 +1 @@
|
||||
d61c163e0a311ecd86d1398aff883eaea8d0b09a
|
||||
0428068e4fbbd1380a4d8bbaab5c8e7955decdb8
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE websocket_trigger;
|
||||
@@ -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));
|
||||
@@ -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
1
backend/src/ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/src/ee.rs
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
1
backend/windmill-api/src/ee.rs
Symbolic link
1
backend/windmill-api/src/ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-api/src/ee.rs
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
use axum::Router;
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
1
backend/windmill-api/src/indexer_ee.rs
Symbolic link
1
backend/windmill-api/src/indexer_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-api/src/indexer_ee.rs
|
||||
@@ -1,5 +0,0 @@
|
||||
use axum::Router;
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
1
backend/windmill-api/src/job_helpers_ee.rs
Symbolic link
1
backend/windmill-api/src/job_helpers_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-api/src/job_helpers_ee.rs
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
1
backend/windmill-api/src/oauth2_ee.rs
Symbolic link
1
backend/windmill-api/src/oauth2_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-api/src/oauth2_ee.rs
|
||||
@@ -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()
|
||||
}
|
||||
1
backend/windmill-api/src/oidc_ee.rs
Symbolic link
1
backend/windmill-api/src/oidc_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-api/src/oidc_ee.rs
|
||||
@@ -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()
|
||||
}
|
||||
1
backend/windmill-api/src/saml_ee.rs
Symbolic link
1
backend/windmill-api/src/saml_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-api/src/saml_ee.rs
|
||||
@@ -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!()
|
||||
}
|
||||
1
backend/windmill-api/src/scim_ee.rs
Symbolic link
1
backend/windmill-api/src/scim_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-api/src/scim_ee.rs
|
||||
@@ -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>,
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
1
backend/windmill-api/src/smtp_server_ee.rs
Symbolic link
1
backend/windmill-api/src/smtp_server_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-api/src/smtp_server_ee.rs
|
||||
@@ -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");
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#[cfg(feature = "stripe")]
|
||||
use axum::Router;
|
||||
|
||||
#[cfg(feature = "stripe")]
|
||||
pub fn add_stripe_routes(router: Router) -> Router {
|
||||
return router;
|
||||
}
|
||||
1
backend/windmill-api/src/stripe_ee.rs
Symbolic link
1
backend/windmill-api/src/stripe_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-api/src/stripe_ee.rs
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
}
|
||||
1
backend/windmill-audit/src/audit_ee.rs
Symbolic link
1
backend/windmill-audit/src/audit_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-audit/src/audit_ee.rs
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) {}
|
||||
1
backend/windmill-common/src/ee.rs
Symbolic link
1
backend/windmill-common/src/ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-common/src/ee.rs
|
||||
@@ -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) {}
|
||||
@@ -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) {}
|
||||
@@ -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!()
|
||||
}
|
||||
1
backend/windmill-common/src/job_s3_helpers_ee.rs
Symbolic link
1
backend/windmill-common/src/job_s3_helpers_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-common/src/job_s3_helpers_ee.rs
|
||||
@@ -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! {
|
||||
|
||||
@@ -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;
|
||||
|
||||
170
backend/windmill-common/src/oidc_ee.rs
Normal file
170
backend/windmill-common/src/oidc_ee.rs
Normal 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,
|
||||
}
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
1
backend/windmill-common/src/stats_ee.rs
Symbolic link
1
backend/windmill-common/src/stats_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-common/src/stats_ee.rs
|
||||
@@ -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))
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
1
backend/windmill-git-sync/src/git_sync_ee.rs
Symbolic link
1
backend/windmill-git-sync/src/git_sync_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-git-sync/src/git_sync_ee.rs
|
||||
@@ -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");
|
||||
}
|
||||
1
backend/windmill-indexer/src/indexer_ee.rs
Symbolic link
1
backend/windmill-indexer/src/indexer_ee.rs
Symbolic link
@@ -0,0 +1 @@
|
||||
/git/windmill/../windmill-ee-private/windmill-indexer/src/indexer_ee.rs
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()))?;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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!(
|
||||
"{}.{}",
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
|
||||
16
cli/main.ts
16
cli/main.ts
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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/
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.411.1",
|
||||
"version": "1.409.2",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user