Compare commits

...

45 Commits

Author SHA1 Message Date
hcourdent
9dcddf834e noopener noreferrer 2024-09-25 16:23:36 +02:00
hcourdent
e6ff31ca1d Small clarification message about all flow triggers 2024-09-25 16:18:31 +02:00
wendrul
398a09b741 feat: add static variable and resources support to ansible (#4435)
* Add static variable and resources to ansible

* Add wasm parser to cli

* Fix pulling of resources, rs and playbooks

* Remove unused import, add publish pkg-yaml

* Update package.json

* Update generate-metadata

* Fix null fields being serialized in script metadata

* npm install
2024-09-25 15:05:53 +02:00
Ruben Fiszel
fbd0071a54 chore(main): release 1.399.0 (#4436)
* chore(main): release 1.399.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-09-25 14:42:36 +02:00
Ruben Fiszel
81c2bb0691 fix(cli): version the whole client for deno compatibility 2024-09-25 14:38:35 +02:00
HugoCasa
aeb5b5bcd1 fix: correct AI generation for CRON and regex (#4437) 2024-09-25 12:53:32 +02:00
HugoCasa
590321fd3c feat: add tag filtering to external JWT authentication (#4425)
* feat: tag filtering jwt ext auth

* move tags to scopes

* fix symlink

* update ee ref
2024-09-25 11:21:19 +02:00
HugoCasa
8a277a0ad9 do not call workspace error handler if flow has error handler (#4434)
* do not call workspace error handler if flow has error handler

* optimize + UI improvements for error handler
2024-09-25 09:10:41 +02:00
Ruben Fiszel
8b7bbe80a0 nit + triggers sign 2024-09-24 20:04:15 +02:00
Ruben Fiszel
34a50ef8ca nit remove getLogs 2024-09-24 19:31:51 +02:00
Ruben Fiszel
93fa6a0e97 improve init script error handling 2024-09-24 19:27:00 +02:00
pyranota
74c2cabd9f Add Explicit Progress Hint (#4433)
* Add new component `ExecutionDuration`

Reusable component helps with tracking execution time of job
It is not using job.started_at, but instead uses it's own tracking mechanism

The key difference that it can give insights during execution of job and measures pure execution time
Accuracy is configurable with `updateResolution`

Can also detect if job is `longRunning` e.g. runs more than X-seconds

* Implement Hint for Explicit Progress
It uses ExecutionDuration component introduced in previous commit
and device's local storage to handle `Dont show again`

* Remove dublication of `FlowProgressBar` in `run` page

* Change Hint styling

* Change values in `ExecutionDuration` to match required

`longDefinition`: 3 -> 30
`updateResolution`: 2 -> 10

Meaning jobs running more than 30s counts as a `Long Running Job`

* Fix broken link

* Scope to langs: `python3`, `bun` and `deno` jobKinds: `script`

* Simplify ExecutionDuration for new scope

* Bring `preview` job kind into the scope

We need this to show this tip in preview pages.
e.g. By clicking on subjob details of flow (this subjob is preview)
2024-09-24 19:13:06 +02:00
HugoCasa
17a8700956 http routing docs link (#4431) 2024-09-24 17:23:14 +02:00
HugoCasa
8a469e7595 add bun/deno/python transform tests (#4432) 2024-09-24 17:14:00 +02:00
pyranota
ad69c08228 fix get_job_update returning error (#4427)
When get_progress specified to true, but there is no record of progress for given job
Endpoint will fail and return error.

Nothing cricial but should be fixed.
2024-09-24 10:24:26 +02:00
Henri Courdent
4d5415dbd2 Frontend update to docs (#4426) 2024-09-23 18:59:32 +02:00
Ruben Fiszel
99c24109de chore(main): release 1.398.1 (#4424)
* chore(main): release 1.398.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-09-23 18:04:41 +02:00
HugoCasa
61f27acbd5 fix: time handling in bun and deno (#4423)
* fix: time handling in bun and deno

* put dates conversion in try catch
2024-09-23 18:00:36 +02:00
Ruben Fiszel
5c6c0ff3cc chore(main): release 1.398.0 (#4412)
* chore(main): release 1.398.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-09-23 17:28:18 +02:00
wendrul
a500994cc2 fix: various improvements for ansible (#4419)
* Improve error messages, fix dependency job issue

* Add nsjail support for ansible

* Remove invalid comment syntax

* Change nsjail config and ansible controller config to be ephemeral

* Move ansible cfg to later to overwrite possible files named ansible.cfg

* Fix ansible.cfg env var

* Fix tmp dir issue
2024-09-23 16:44:37 +02:00
Ruben Fiszel
a17195d88f fix: update pip to 24.2 and python 3.11.8->3.11.10 2024-09-23 14:01:31 +02:00
HugoCasa
e99e7b2b0b feat(frontend): add http routing templates (#4421) 2024-09-23 12:47:51 +02:00
pyranota
01c16f29a8 floor progress in typescript client (#4420)
If user passes float to setProgress it will result BadRequest from server but without any explanation.

Sometimes it is non intuitive that endpoint accepts only integers, so to reduce confusion we convert progress to int.
2024-09-23 11:45:13 +02:00
Faton Ramadani
30017cc2b1 fix(frontend): Fix delete branch one index (#4418) 2024-09-23 11:32:47 +02:00
Ruben Fiszel
9c506a83cb critical channels build on non ee 2024-09-21 22:54:15 +02:00
Ruben Fiszel
50a6f789fa fix: migrate smtp instance settings to global settings (#4416)
* all

* all

* all

* fix

* update ee private

* all
2024-09-21 01:42:15 +02:00
HugoCasa
5be7be03a6 fix: no failed renewal alert if trial (#4414) 2024-09-20 18:47:17 +02:00
HugoCasa
70fa78c10d fix: allow no body in job requests (#4413) 2024-09-20 18:26:50 +02:00
HugoCasa
304dac3447 feat: http routing (#4339)
* feat: http routing

* all

* feat: improve UI

* final stuff

* fix: sqlx

* fix: nit

* fix: nits

* fix: error handler display

* fix: routes panel perms

* all

* fix: improve ability to paste from macos in vscode extension

* fix lock-write for deno

* all

* cleaning

* fix

* cli preprocessor

* nits

* nits

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2024-09-20 15:38:38 +02:00
Ruben Fiszel
815f34b2af chore(main): release 1.397.4 (#4410)
* chore(main): release 1.397.4

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-09-20 14:53:16 +02:00
Ruben Fiszel
794f87aa0d fix(cli): make CLI not require latest version of windmill 2024-09-20 14:49:06 +02:00
Ruben Fiszel
9e9562e1a4 chore(main): release 1.397.3 (#4409)
* chore(main): release 1.397.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-09-20 11:38:40 +02:00
Ruben Fiszel
8212532b29 fix(cli): make CLI compatible with Node 18 2024-09-20 11:10:19 +02:00
Ruben Fiszel
2a9d1b587b fix cli build 2024-09-20 10:38:38 +02:00
Ruben Fiszel
aec6972e29 chore(main): release 1.397.2 (#4408)
* chore(main): release 1.397.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-09-20 09:52:35 +02:00
Ruben Fiszel
69b8754aef fix(bun): never pre-bundle BASE_URL env variable 2024-09-20 09:48:00 +02:00
Ruben Fiszel
a20ee6096a chore(main): release 1.397.1 (#4406)
* chore(main): release 1.397.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-09-20 09:17:15 +02:00
Ruben Fiszel
30fe28ceec fix: update git sync script 2024-09-20 09:12:00 +02:00
Alexander Petric
3fd626c8d2 properly count steps of flows in benchmark (#4407)
* fix(benchmark): properly count steps of flows

* signing commit

---------

Co-authored-by: Alexander Petric <alex@inpher.io>
2024-09-19 17:46:21 +02:00
Ruben Fiszel
07372e7e65 fix: improve ability to paste from macos in vscode extension 2024-09-19 16:15:32 +02:00
Ruben Fiszel
623ece8c67 fix(go): put shared directory in job dir and not go parent 2024-09-19 10:05:08 +02:00
Ruben Fiszel
63f70f67d7 chore(main): release 1.397.0 (#4405)
* chore(main): release 1.397.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2024-09-19 01:40:02 +02:00
Ruben Fiszel
1b3e90ce7a add ansible to default tags 2024-09-19 00:47:30 +02:00
wendrul
6855b8da9a feat: ansible playbook support (#4399)
* Ansible execution and parsing

* Working collections and pip dependencies

* Remove unused vars

* File resources logic for ansible

* Make frontend for ansible and resource file

* Format file

* Change naming of file resource and inventory fields

* Add autocomplete for file extension resource creation

* Add endpoint to list file formats for resource types

* Add CLI functionality to pull/push file resources

* Add beta tag to ansible

* Add Full image dockerfiles containing ansible

* Prepare sqlx

* Update cargo.lock

* Update ansible path

* Remove unused imports

* Add back import removed by rust-analyzer

* Improve ansible init code

* Prepare sqlx

* Change dockerfile to make the full windmill image

* Remove old dockerfile

* Improve autocomplete file resource extension select

* Remove comment

* Validate file extension

* Add icons to text file resources

* Remove editability of file resource types

* Remove unused import

* Missing space

* Add yaml parser
2024-09-19 00:21:15 +02:00
Ruben Fiszel
69b53603a5 embed typescript client in cli (#4403)
* all

* all

* all

* all

* all
2024-09-18 16:45:39 +02:00
253 changed files with 22333 additions and 1551 deletions

View File

@@ -1,10 +1,8 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME:
${{ github.event_name != 'pull_request' && github.repository ||
IMAGE_NAME: ${{ github.event_name != 'pull_request' && github.repository ||
'windmill-labs/windmill-test' }}
DEV_SHA:
${{ github.event_name != 'pull_request' && 'dev' || format('pr-{0}',
DEV_SHA: ${{ github.event_name != 'pull_request' && 'dev' || format('pr-{0}',
github.event.number) }}
name: Build windmill:main
@@ -745,7 +743,7 @@ jobs:
context: .
platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileRust"
file: "./docker/DockerfileFull"
tags: |
${{ steps.meta-public.outputs.tags }}
labels: |
@@ -790,7 +788,7 @@ jobs:
context: .
platforms: linux/amd64,linux/arm64
push: true
file: "./docker/DockerfileRustEe"
file: "./docker/DockerfileFullEe"
tags: |
${{ steps.meta-ee-public.outputs.tags }}
labels: |

View File

@@ -1,5 +1,80 @@
# Changelog
## [1.399.0](https://github.com/windmill-labs/windmill/compare/v1.398.1...v1.399.0) (2024-09-25)
### Features
* add tag filtering to external JWT authentication ([#4425](https://github.com/windmill-labs/windmill/issues/4425)) ([590321f](https://github.com/windmill-labs/windmill/commit/590321fd3c88046a657e0a751b62a19424102060))
### Bug Fixes
* **cli:** version the whole client for deno compatibility ([81c2bb0](https://github.com/windmill-labs/windmill/commit/81c2bb069176a95b8fb9c52b31e5e03c1cc78afc))
* correct AI generation for CRON and regex ([#4437](https://github.com/windmill-labs/windmill/issues/4437)) ([aeb5b5b](https://github.com/windmill-labs/windmill/commit/aeb5b5bcd163e2e7d1413d7e4aed8bd769996f24))
## [1.398.1](https://github.com/windmill-labs/windmill/compare/v1.398.0...v1.398.1) (2024-09-23)
### Bug Fixes
* time handling in bun and deno ([#4423](https://github.com/windmill-labs/windmill/issues/4423)) ([61f27ac](https://github.com/windmill-labs/windmill/commit/61f27acbd5b477f0e2453d4b02d406a7aef76009))
## [1.398.0](https://github.com/windmill-labs/windmill/compare/v1.397.4...v1.398.0) (2024-09-23)
### Features
* **frontend:** add http routing templates ([#4421](https://github.com/windmill-labs/windmill/issues/4421)) ([e99e7b2](https://github.com/windmill-labs/windmill/commit/e99e7b2b0bb1bc63d1253d147b80bac06eaff103))
* http routing ([#4339](https://github.com/windmill-labs/windmill/issues/4339)) ([304dac3](https://github.com/windmill-labs/windmill/commit/304dac34475b8871621c54a768275fae1e9f845e))
### Bug Fixes
* allow no body in job requests ([#4413](https://github.com/windmill-labs/windmill/issues/4413)) ([70fa78c](https://github.com/windmill-labs/windmill/commit/70fa78c10d8afb3e06be84e3449fdfdeb2673765))
* **frontend:** Fix delete branch one index ([#4418](https://github.com/windmill-labs/windmill/issues/4418)) ([30017cc](https://github.com/windmill-labs/windmill/commit/30017cc2b1401ea439e3fbec5d7764b206100a38))
* migrate smtp instance settings to global settings ([#4416](https://github.com/windmill-labs/windmill/issues/4416)) ([50a6f78](https://github.com/windmill-labs/windmill/commit/50a6f789fa8a03d70407b0429da049cc0740b6fa))
* no failed renewal alert if trial ([#4414](https://github.com/windmill-labs/windmill/issues/4414)) ([5be7be0](https://github.com/windmill-labs/windmill/commit/5be7be03a6df34a9fae96ed7f844d693f7cfce3e))
* update pip to 24.2 and python 3.11.8-&gt;3.11.10 ([a17195d](https://github.com/windmill-labs/windmill/commit/a17195d88ff52b72f0c2cdd95c6218df8ef6d7cf))
* various improvements for ansible ([#4419](https://github.com/windmill-labs/windmill/issues/4419)) ([a500994](https://github.com/windmill-labs/windmill/commit/a500994cc2cd9cc6cbad722059cebd0bf74a8850))
## [1.397.4](https://github.com/windmill-labs/windmill/compare/v1.397.3...v1.397.4) (2024-09-20)
### Bug Fixes
* **cli:** make CLI not require latest version of windmill ([794f87a](https://github.com/windmill-labs/windmill/commit/794f87aa0d33a0b782fcb2726754fdc51bc1d73d))
## [1.397.3](https://github.com/windmill-labs/windmill/compare/v1.397.2...v1.397.3) (2024-09-20)
### Bug Fixes
* **cli:** make CLI compatible with Node 18 ([8212532](https://github.com/windmill-labs/windmill/commit/8212532b29fbcf971cb320b8cfc84f3a8d8ad795))
## [1.397.2](https://github.com/windmill-labs/windmill/compare/v1.397.1...v1.397.2) (2024-09-20)
### Bug Fixes
* **bun:** never pre-bundle BASE_URL env variable ([69b8754](https://github.com/windmill-labs/windmill/commit/69b8754aef32682bf812be783f79b81cd8526c7a))
## [1.397.1](https://github.com/windmill-labs/windmill/compare/v1.397.0...v1.397.1) (2024-09-20)
### Bug Fixes
* **go:** put shared directory in job dir and not go parent ([623ece8](https://github.com/windmill-labs/windmill/commit/623ece8c6791b2c18648a68c562fcd5c8291be9b))
* improve ability to paste from macos in vscode extension ([07372e7](https://github.com/windmill-labs/windmill/commit/07372e7e65d77df338718929f133e477a1daf993))
* update git sync script ([30fe28c](https://github.com/windmill-labs/windmill/commit/30fe28ceecb9043eee6811dce8c3e2cd9224927b))
## [1.397.0](https://github.com/windmill-labs/windmill/compare/v1.396.1...v1.397.0) (2024-09-18)
### Features
* ansible playbook support ([#4399](https://github.com/windmill-labs/windmill/issues/4399)) ([6855b8d](https://github.com/windmill-labs/windmill/commit/6855b8da9ad92aed514ee1ab214c9550d04e7a22))
## [1.396.1](https://github.com/windmill-labs/windmill/compare/v1.396.0...v1.396.1) (2024-09-18)

Binary file not shown.

View File

@@ -1,6 +1,6 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.80-slim-bookworm
ARG PYTHON_IMAGE=python:3.11.8-slim-bookworm
ARG PYTHON_IMAGE=python:3.11.10-slim-bookworm
FROM ${RUST_IMAGE} AS rust_base
@@ -95,6 +95,8 @@ ARG WITH_KUBECTL=true
ARG WITH_HELM=true
ARG WITH_GIT=true
RUN pip install --upgrade pip==24.2
RUN apt-get update \
&& apt-get install -y ca-certificates wget curl jq unzip build-essential unixodbc xmlsec1 software-properties-common \
&& apt-get clean \

View File

@@ -0,0 +1,89 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\" FROM http_trigger WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "requires_auth",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "02f1a6eeb27067dc438459238e7b016f5ccf9e3fe0ffbe88471f15aad8f74441"
}

View File

@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "format_extension",
"type_info": "Varchar"
}
],
"parameters": {
@@ -46,6 +51,7 @@
true,
true,
true,
true,
true
]
},

View File

@@ -57,7 +57,8 @@
"mssql",
"php",
"bunnative",
"rust"
"rust",
"ansible"
]
}
}

View File

@@ -71,7 +71,8 @@
"mssql",
"php",
"bunnative",
"rust"
"rust",
"ansible"
]
}
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue\n SET flow_status = JSONB_SET(\n JSONB_SET(flow_status, ARRAY['failure_module'], $1), ARRAY['step'], $2)\n WHERE id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Jsonb",
"Uuid"
]
},
"nullable": []
},
"hash": "0aff8b0f2cddc0dd696759f48368c7315ff39060be5430c59582e1e05a4b3d90"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE worker_ping SET \n ping_at = now(), \n jobs_executed = 1, \n current_job_id = $1, \n current_job_workspace_id = 'admins' \n WHERE worker = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "0e52a588f3edeb8fb58d6d62247b8590e51171e2811c62737bdb81fb0ac8f182"
}

View File

@@ -0,0 +1,36 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
]
},
"nullable": [
null
]
},
"hash": "16c4fb122ceb30722f54c92db97427b0ddd36f495d549f8cd23d659d2c866a63"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue\n SET flow_status = JSONB_SET(\n JSONB_SET(flow_status, ARRAY['preprocessor_module'], $1), ARRAY['step'], $2)\n WHERE id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Jsonb",
"Uuid"
]
},
"nullable": []
},
"hash": "17e3e2a3232865c33fa535b5d99455942e30f932c5b97f1e5b508128f39a288f"
}

View File

@@ -51,7 +51,8 @@
"mssql",
"php",
"bunnative",
"rust"
"rust",
"ansible"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return \n FROM flow \n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.path = $1 and flow.workspace_id = $2",
"query": "SELECT tag, dedicated_worker, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor \n FROM flow \n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.path = $1 and flow.workspace_id = $2",
"describe": {
"columns": [
{
@@ -15,8 +15,8 @@
},
{
"ordinal": 2,
"name": "early_return",
"type_info": "Text"
"name": "has_preprocessor",
"type_info": "Bool"
}
],
"parameters": {
@@ -31,5 +31,5 @@
null
]
},
"hash": "872dcaec230579e4480adf23075e323557efbe52c813e3a6a0da6b855291951e"
"hash": "3fedf12ee5d44e8af9c3864cc70a8f607dccc015ce2d5150c56c68074cd0f84d"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE completed_job SET args = '{\"reason\":\"PREPROCESSOR_ARGS_ARE_DISCARDED\"}'::jsonb WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "4c97fcc93b31c4b3262419d6ee183773a95de3b6694a398cb80d288bef4f130f"
}

View File

@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE http_trigger \n SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, edited_by = $7, email = $8, is_async = $9, requires_auth = $10, edited_at = now() \n WHERE workspace_id = $11 AND path = $12",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Varchar",
"Varchar",
"Bool",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "4d8640e84fccf1a0b799d8396a51e69345137e68d5096c70ba0a4332075d97ea"
}

View File

@@ -0,0 +1,108 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as \"http_method: _\", edited_by, email, edited_at, extra_perms, is_async, requires_auth\n FROM http_trigger\n WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "route_path_key",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 7,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 11,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 12,
"name": "requires_auth",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "4fb95eae1c871241efe2ef79615ce03cba0e4a12aad3274e4829d98e38ca1491"
}

View File

@@ -0,0 +1,36 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, edited_by = $5, email = $6, is_async = $7, requires_auth = $8, edited_at = now() \n WHERE workspace_id = $9 AND path = $10",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Varchar",
"Varchar",
"Bool",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "55d44f569f8ebfccddf975e1a330ef0dc286f4138efe923832371cdbac7157b0"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select path, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout from script where hash = $1 AND workspace_id = $2",
"query": "select path, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout, has_preprocessor from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -56,7 +56,8 @@
"mssql",
"php",
"bunnative",
"rust"
"rust",
"ansible"
]
}
}
@@ -81,6 +82,11 @@
"ordinal": 10,
"name": "timeout",
"type_info": "Int4"
},
{
"ordinal": 11,
"name": "has_preprocessor",
"type_info": "Bool"
}
],
"parameters": {
@@ -100,8 +106,9 @@
true,
true,
true,
true,
true
]
},
"hash": "8b10c9ade85c0307e300224e87962ceba8e7ccbe53b56c4670a1634bb9d0e89c"
"hash": "64ce3eabed6de7a4f99884640c21218ac19ab2a4bc5f8b42deb2476f85d0deac"
}

View File

@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "format_extension",
"type_info": "Varchar"
}
],
"parameters": {
@@ -45,6 +50,7 @@
true,
true,
true,
true,
true
]
},

View File

@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT config FROM config WHERE name = 'server'",
"query": "SELECT value FROM global_settings WHERE name = 'smtp_settings'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "config",
"name": "value",
"type_info": "Jsonb"
}
],
@@ -13,8 +13,8 @@
"Left": []
},
"nullable": [
true
false
]
},
"hash": "8ca76fcadc659d7069b97930cdde2d5200f3cef9dedb83b76c8a6a433d2a4de3"
"hash": "83ec97f6aad154e0e06ee05a3647dab8f89b1b2d7a569c7eac8c4169e37b9f8b"
}

View File

@@ -0,0 +1,37 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND workspace_id = $2 AND http_method = $3)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
]
},
"nullable": [
null
]
},
"hash": "867e0f8299e22a6b8e02d21adeb0281b1a10593715db823a5adeeac3ab1058cd"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue\n SET flow_status = JSONB_SET(\n JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2), ARRAY['step'], $3)\n WHERE id = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Jsonb",
"Jsonb",
"Uuid"
]
},
"nullable": []
},
"hash": "870d7feec169f70d140b7477562315ed2fd4662975bf0d01cfbffac86a959368"
}

View File

@@ -0,0 +1,41 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor\n FROM flow \n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.path = $1 and flow.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "dedicated_worker",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "early_return",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "has_preprocessor",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true,
true,
null,
null
]
},
"hash": "98081ee9a4a4a52ae70d74d26e8608a010722d7ea9dc343ae8f03e38978ffaba"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE path = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "9a050ab74cfc13a4b855408e61ebcb0a9d27e5563fa7af5e00a4916b03a62ddb"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET args = (select result FROM completed_job WHERE id = $1) WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "9c17ddca92e0a93051b36bb688ad6c1c24108e45c94be52595ca6cb82135f4eb"
}

View File

@@ -56,7 +56,8 @@
"mssql",
"php",
"bunnative",
"rust"
"rust",
"ansible"
]
}
}

View File

@@ -51,7 +51,8 @@
"mssql",
"php",
"bunnative",
"rust"
"rust",
"ansible"
]
}
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "9ebb9c16948a695a053068d9a1df0152691271ac33d8c82db47be11746ccbcef"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue\n SET flow_status = JSONB_SET(\n JSONB_SET(flow_status, ARRAY['failure_module'], $1), ARRAY['step'], $2)\n WHERE id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Jsonb",
"Uuid"
]
},
"nullable": []
},
"hash": "ad8f9e0b06f288051cbec3c91877430a1d61353b9122fd42777d92e4cbc9f4fa"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30)",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31)",
"describe": {
"columns": [],
"parameters": {
@@ -37,7 +37,8 @@
"mssql",
"php",
"bunnative",
"rust"
"rust",
"ansible"
]
}
}
@@ -71,10 +72,11 @@
"Varchar",
"Bool",
"Bool",
"Varchar"
"Varchar",
"Bool"
]
},
"nullable": []
},
"hash": "8e7ff45c5378c3a3406ba94dc0653afa5d28c072203617c598caefaaf1bafcfb"
"hash": "b49b1eaf58e62063c7a03039d0c36e65991b1d21a3306390fb1c7ca38babafe3"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1 AND path = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "b57a188ca137162a2848ebf81fa3aeca9a14ac628c61a358e2c6612c57249b0f"
}

View File

@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue\n SET flow_status = JSONB_SET(\n JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2), ARRAY['step'], $3)\n WHERE id = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Jsonb",
"Jsonb",
"Uuid"
]
},
"nullable": []
},
"hash": "b64c7f3b483c74c51aecda65664c36d9686112182dbf35651e494da739b922f9"
}

View File

@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "format_extension",
"type_info": "Varchar"
}
],
"parameters": {
@@ -45,6 +50,7 @@
true,
true,
true,
true,
true
]
},

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['preprocessor_module'], $1)\n WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Uuid"
]
},
"nullable": []
},
"hash": "c14009d133956710f4435a5984c18b9517e256978512f014e1bf5c270f499772"
}

View File

@@ -0,0 +1,37 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now())",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Bool",
"Bool",
{
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
},
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "c229744534f17f7622c3dee21bb1e7292ff17e6dffe58e80e53bff8baade07c8"
}

View File

@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT name, format_extension FROM resource_type WHERE format_extension IS NOT NULL AND (workspace_id = $1 OR workspace_id = 'admins')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "format_extension",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "cf1cef7e0fe2e7e3db96b0ec005360361b9eec023a6fc2a4a7a917f59d86af4d"
}

View File

@@ -77,7 +77,8 @@
"mssql",
"php",
"bunnative",
"rust"
"rust",
"ansible"
]
}
}

View File

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

View File

@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "format_extension",
"type_info": "Varchar"
}
],
"parameters": {
@@ -43,6 +48,7 @@
true,
true,
true,
true,
true
]
},

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (flow_status->'step')::integer as step, jsonb_array_length(flow_status->'modules') as len FROM queue WHERE id = $1",
"query": "SELECT (flow_status->'step')::integer as step, jsonb_array_length(flow_status->'modules') as len FROM queue WHERE id = $1",
"describe": {
"columns": [
{
@@ -24,5 +24,5 @@
null
]
},
"hash": "f80e94176224ef23416da99b26ad5c330d65016fb93345b054cbb63a4bf37fc8"
"hash": "f9e0e35b4789a4da89f7bb21fa6b250fd64ee0e0b299471979db5ff8ee906929"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
"query": "select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout, has_preprocessor from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
"describe": {
"columns": [
{
@@ -56,7 +56,8 @@
"mssql",
"php",
"bunnative",
"rust"
"rust",
"ansible"
]
}
}
@@ -81,6 +82,11 @@
"ordinal": 10,
"name": "timeout",
"type_info": "Int4"
},
{
"ordinal": 11,
"name": "has_preprocessor",
"type_info": "Bool"
}
],
"parameters": {
@@ -100,8 +106,9 @@
true,
true,
true,
true,
true
]
},
"hash": "e1c715020f1efb00123171edfb6173bfeaaa95d4f870240e42b76e5df304ce99"
"hash": "fc7ac95d8d5f4532a2b0e50f84c1e273d25543401639a68f868e1e4fd8b0640a"
}

View File

@@ -0,0 +1,87 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\" FROM http_trigger",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "requires_auth",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "fe4f91ca7e179e58041a5c0b1a36015175e9301c01a99e8f2b82cb853349a183"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource_type\n (workspace_id, name, schema, description, created_by, edited_at)\n VALUES ($1, $2, $3, $4, $5, now())",
"query": "INSERT INTO resource_type\n (workspace_id, name, schema, description, created_by, format_extension, edited_at)\n VALUES ($1, $2, $3, $4, $5, $6, now())",
"describe": {
"columns": [],
"parameters": {
@@ -9,10 +9,11 @@
"Varchar",
"Jsonb",
"Text",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "d249343a1d9fce2f7ca9e6e447eb32d7d11b61acdab80dda24db4a9ba3e1f7e6"
"hash": "ffedbb3a2676a6d7b71f81f89109a02a8dba90d40144e942527f8a3fc36dfbc1"
}

186
backend/Cargo.lock generated
View File

@@ -696,9 +696,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.82"
version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
@@ -1056,9 +1056,9 @@ dependencies = [
[[package]]
name = "axum"
version = "0.7.5"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec"
dependencies = [
"async-trait",
"axum-core",
@@ -1083,7 +1083,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper 1.0.1",
"tokio",
"tower 0.4.13",
"tower 0.5.1",
"tower-layer",
"tower-service",
"tracing",
@@ -1091,9 +1091,9 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00"
dependencies = [
"async-trait",
"bytes",
@@ -1104,7 +1104,7 @@ dependencies = [
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 0.1.2",
"sync_wrapper 1.0.1",
"tower-layer",
"tower-service",
"tracing",
@@ -1735,9 +1735,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.17"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1745,9 +1745,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.17"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
dependencies = [
"anstream",
"anstyle",
@@ -1757,9 +1757,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.13"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -2795,7 +2795,7 @@ dependencies = [
"rustls-webpki 0.102.8",
"serde",
"tokio",
"webpki-roots 0.26.5",
"webpki-roots 0.26.6",
]
[[package]]
@@ -4670,9 +4670,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.158"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libgit2-sys"
@@ -4710,7 +4710,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"libc",
"redox_syscall 0.5.4",
"redox_syscall 0.5.5",
]
[[package]]
@@ -4892,7 +4892,7 @@ dependencies = [
"smtp-proto",
"tokio",
"tokio-rustls 0.26.0",
"webpki-roots 0.26.5",
"webpki-roots 0.26.6",
]
[[package]]
@@ -5442,7 +5442,7 @@ dependencies = [
"md-5 0.10.6",
"parking_lot",
"percent-encoding",
"quick-xml 0.36.1",
"quick-xml 0.36.2",
"rand 0.8.5",
"reqwest 0.12.7",
"ring 0.17.8",
@@ -5672,7 +5672,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.4",
"redox_syscall 0.5.5",
"smallvec",
"windows-targets 0.52.6",
]
@@ -5991,9 +5991,9 @@ dependencies = [
[[package]]
name = "pkg-config"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "polling"
@@ -6012,9 +6012,9 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265"
checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce"
[[package]]
name = "postgres-native-tls"
@@ -6120,7 +6120,7 @@ version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"toml_edit 0.22.21",
"toml_edit 0.22.22",
]
[[package]]
@@ -6374,9 +6374,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.36.1"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [
"memchr",
"serde",
@@ -6623,9 +6623,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.4"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0"
dependencies = [
"bitflags 2.6.0",
]
@@ -7415,9 +7415,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.11.1"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [
"core-foundation-sys",
"libc",
@@ -7755,9 +7755,9 @@ dependencies = [
[[package]]
name = "simdutf8"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "simple_asn1"
@@ -8051,7 +8051,7 @@ dependencies = [
"tracing",
"url",
"uuid 1.10.0",
"webpki-roots 0.26.5",
"webpki-roots 0.26.6",
]
[[package]]
@@ -9019,9 +9019,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.41"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909"
checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020"
dependencies = [
"filetime",
"libc",
@@ -9061,18 +9061,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.63"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
@@ -9433,7 +9433,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.21",
"toml_edit 0.22.22",
]
[[package]]
@@ -9460,15 +9460,15 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.21"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap 2.5.0",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.18",
"winnow 0.6.19",
]
[[package]]
@@ -9497,7 +9497,6 @@ dependencies = [
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@@ -9506,8 +9505,14 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper 0.1.2",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@@ -9924,9 +9929,9 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-id"
version = "0.3.4"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f"
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
[[package]]
name = "unicode-id-start"
@@ -9972,15 +9977,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.13"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-xid"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unicode_categories"
@@ -10044,7 +10049,7 @@ dependencies = [
"serde",
"serde_json",
"url",
"webpki-roots 0.26.5",
"webpki-roots 0.26.6",
]
[[package]]
@@ -10340,9 +10345,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webpki-roots"
version = "0.26.5"
version = "0.26.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a"
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
dependencies = [
"rustls-pki-types",
]
@@ -10377,7 +10382,7 @@ version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
dependencies = [
"redox_syscall 0.5.4",
"redox_syscall 0.5.5",
"wasite",
"web-sys",
]
@@ -10421,7 +10426,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"axum",
@@ -10463,7 +10468,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"argon2",
@@ -10497,6 +10502,7 @@ dependencies = [
"lazy_static",
"magic-crypt",
"mail-parser",
"matchit",
"mime_guess",
"native-tls",
"object_store",
@@ -10547,7 +10553,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"base64 0.21.7",
"chrono",
@@ -10565,7 +10571,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"chrono",
"serde",
@@ -10578,7 +10584,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"async-stream",
@@ -10622,7 +10628,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"regex",
"rsmq_async",
@@ -10637,7 +10643,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"bytes",
@@ -10658,7 +10664,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -10667,7 +10673,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -10679,7 +10685,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"gosyn",
@@ -10691,7 +10697,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -10703,7 +10709,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"itertools 0.13.0",
@@ -10714,7 +10720,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"itertools 0.13.0",
@@ -10725,7 +10731,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -10743,7 +10749,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -10760,7 +10766,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -10772,7 +10778,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -10790,7 +10796,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"getrandom 0.2.15",
@@ -10806,11 +10812,22 @@ dependencies = [
"windmill-parser-rust",
"windmill-parser-sql",
"windmill-parser-ts",
"windmill-parser-yaml",
]
[[package]]
name = "windmill-parser-yaml"
version = "1.399.0"
dependencies = [
"anyhow",
"serde_json",
"windmill-parser",
"yaml-rust",
]
[[package]]
name = "windmill-queue"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -10843,7 +10860,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -10853,7 +10870,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.396.1"
version = "1.399.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -10921,7 +10938,9 @@ dependencies = [
"windmill-parser-rust",
"windmill-parser-sql",
"windmill-parser-ts",
"windmill-parser-yaml",
"windmill-queue",
"yaml-rust",
"zstd 0.12.4",
]
@@ -11123,9 +11142,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.6.18"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587"
dependencies = [
"memchr",
]
@@ -11181,6 +11200,15 @@ dependencies = [
"lzma-sys",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "0.5.1"

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.396.1"
version = "1.399.0"
authors.workspace = true
edition.workspace = true
@@ -23,10 +23,11 @@ members = [
"./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-imports",
"./parsers/windmill-sql-datatype-parser-wasm",
"./parsers/windmill-parser-yaml",
]
[workspace.package]
version = "1.396.1"
version = "1.399.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -118,6 +119,7 @@ windmill-parser-py = { path = "./parsers/windmill-parser-py" }
windmill-parser-py-imports = { path = "./parsers/windmill-parser-py-imports" }
windmill-parser-go = { path = "./parsers/windmill-parser-go" }
windmill-parser-rust = { path = "./parsers/windmill-parser-rust" }
windmill-parser-yaml = { path = "./parsers/windmill-parser-yaml" }
windmill-parser-bash = { path = "./parsers/windmill-parser-bash" }
windmill-parser-sql = { path = "./parsers/windmill-parser-sql" }
windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
@@ -250,6 +252,7 @@ indexmap = { version = "2.2.5", features = ["serde"]}
tokio-native-tls = "^0"
openssl = "=0.10"
mail-parser = "^0"
matchit = "=0.7.3"
datafusion = "39.0.0"
object_store = { version = "0.10.0", features = ["aws", "azure"] }
@@ -277,3 +280,4 @@ toml = "0.7"
syn = { version = "2.0.74", features = ["full"] }
quote = "1.0.36"
regex-lite = "0.1.6"
yaml-rust = "0.4.5"

View File

@@ -1 +1 @@
534f50aae2f89281304b3c3ba943d18afc07fad4
3d37b6c31155265d8d026ae9d6ced0b433078f87

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'ansible';
UPDATE config set config = jsonb_set(config, '{worker_tags}', config->'worker_tags' || '["ansible"]'::jsonb) where name = 'worker__default' and config @> '{"worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun", "php", "rust"]}'::jsonb AND NOT config->'worker_tags' @> '"ansible"'::jsonb;

View File

@@ -0,0 +1,3 @@
-- Add down migration script here
ALTER TABLE resource_type
DROP COLUMN format_extension;

View File

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

View File

@@ -0,0 +1,7 @@
-- Add down migration script here
DROP TABLE http_trigger;
DROP TYPE http_method;
ALTER TABLE script DROP COLUMN has_preprocessor;
DROP FUNCTION prevent_route_path_change();

View File

@@ -0,0 +1,61 @@
-- Add up migration script here
CREATE TYPE http_method AS ENUM ('get', 'post', 'put', 'delete', 'patch');
CREATE TABLE http_trigger (
path VARCHAR(255) NOT NULL,
route_path VARCHAR(255) NOT NULL,
route_path_key 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 '{}',
is_async BOOLEAN NOT NULL DEFAULT FALSE,
requires_auth BOOLEAN NOT NULL DEFAULT FALSE,
http_method http_method NOT NULL,
PRIMARY KEY (path, workspace_id)
);
GRANT SELECT, UPDATE ON http_trigger TO windmill_user;
GRANT ALL ON http_trigger TO windmill_admin;
ALTER TABLE http_trigger ENABLE ROW LEVEL SECURITY;
CREATE POLICY see_folder_extra_perms_user_select ON http_trigger FOR SELECT TO windmill_user
USING (SPLIT_PART(http_trigger.path, '/', 1) = 'f' AND SPLIT_PART(http_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_update ON http_trigger FOR UPDATE TO windmill_user
USING (SPLIT_PART(http_trigger.path, '/', 1) = 'f' AND SPLIT_PART(http_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_extra_perms_user_select ON http_trigger FOR SELECT TO windmill_user
USING (extra_perms ? CONCAT('u/', current_setting('session.user')));
CREATE POLICY see_extra_perms_user_update ON http_trigger FOR UPDATE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_groups_select ON http_trigger FOR SELECT TO windmill_user
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]);
CREATE POLICY see_extra_perms_groups_update ON http_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 OR REPLACE FUNCTION prevent_route_path_change()
RETURNS TRIGGER AS $$
BEGIN
IF CURRENT_USER <> 'windmill_admin' AND NEW.route_path <> OLD.route_path THEN
RAISE EXCEPTION 'Modification of route_path is only allowed by admins';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER check_route_path_change
BEFORE UPDATE ON http_trigger
FOR EACH ROW
EXECUTE FUNCTION prevent_route_path_change();
ALTER TABLE script ADD COLUMN has_preprocessor BOOLEAN;

View File

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

View File

@@ -0,0 +1,8 @@
-- Add up migration script here
INSERT INTO global_settings (name, value)
SELECT 'smtp_settings', config
FROM config
WHERE name = 'server'
ON CONFLICT (name) DO UPDATE SET value = excluded.value;
DELETE FROM config WHERE name = 'server';

View File

@@ -16,7 +16,13 @@ pub fn parse_bash_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_bash_file(&code)?;
if let Some(x) = parsed {
let args = x;
Ok(MainArgSignature { star_args: false, star_kwargs: false, args, no_main_func: None })
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing bash script".to_string()))
}
@@ -26,7 +32,13 @@ pub fn parse_powershell_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_powershell_file(&code)?;
if let Some(x) = parsed {
let args = x;
Ok(MainArgSignature { star_args: false, star_kwargs: false, args, no_main_func: None })
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing powershell script".to_string()))
}
@@ -173,7 +185,8 @@ non_required="${5:-}"
oidx: None
}
],
no_main_func: None
no_main_func: None,
has_preprocessor: None
}
);

View File

@@ -42,6 +42,7 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result<MainArgSignature> {
star_kwargs: false,
args,
no_main_func: Some(false),
has_preprocessor: None,
})
} else {
Ok(MainArgSignature {
@@ -49,6 +50,7 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result<MainArgSignature> {
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
has_preprocessor: None,
})
}
}
@@ -241,7 +243,8 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
oidx: None
},
],
no_main_func: Some(false)
no_main_func: Some(false),
has_preprocessor: None
}
);

View File

@@ -15,7 +15,13 @@ pub fn parse_graphql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_graphql_file(&code)?;
if let Some(x) = parsed {
let args = x;
Ok(MainArgSignature { star_args: false, star_kwargs: false, args, no_main_func: None })
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
}
@@ -119,7 +125,8 @@ query($i: Int, $arr: [String]!, $wahoo: String = "wahoo") {
oidx: None
}
],
no_main_func: None
no_main_func: None,
has_preprocessor: None
}
);

View File

@@ -82,6 +82,7 @@ pub fn parse_php_signature(
star_kwargs: false,
args,
no_main_func: Some(false),
has_preprocessor: None,
})
} else {
Ok(MainArgSignature {
@@ -89,6 +90,7 @@ pub fn parse_php_signature(
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
has_preprocessor: None,
})
}
}
@@ -159,7 +161,8 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f
oidx: None
}
],
no_main_func: Some(false)
no_main_func: Some(false),
has_preprocessor: None
}
);

View File

@@ -63,9 +63,17 @@ pub fn parse_python_signature(
) -> anyhow::Result<MainArgSignature> {
let main_name = override_main.unwrap_or("main".to_string());
let has_preprocessor = !filter_non_main(code, "preprocessor").is_empty();
let filtered_code = filter_non_main(code, &main_name);
if filtered_code.is_empty() {
return Err(anyhow::anyhow!("No main function found".to_string(),));
return Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
has_preprocessor: Some(has_preprocessor),
});
}
let ast = Suite::parse(&filtered_code, "main.py")
.map_err(|e| anyhow::anyhow!("Error parsing code: {}", e.to_string()))?;
@@ -119,6 +127,7 @@ pub fn parse_python_signature(
})
.collect(),
no_main_func: Some(false),
has_preprocessor: Some(has_preprocessor),
})
} else {
Ok(MainArgSignature {
@@ -126,6 +135,7 @@ pub fn parse_python_signature(
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
has_preprocessor: Some(has_preprocessor),
})
}
}
@@ -325,6 +335,7 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
},
],
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
@@ -389,6 +400,7 @@ def main(test1: str,
}
],
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
@@ -448,6 +460,7 @@ def main(test1: str,
}
],
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
@@ -491,6 +504,7 @@ def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): retu
}
],
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
@@ -521,6 +535,59 @@ def main(test1: DynSelect_foo): return
oidx: None
}],
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
Ok(())
}
#[test]
fn test_parse_python_sig_6() -> anyhow::Result<()> {
let code = r#"
import os
def hello(): return
"#;
//println!("{}", serde_json::to_string()?);
assert_eq!(
parse_python_signature(code, None)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
has_preprocessor: Some(false)
}
);
Ok(())
}
#[test]
fn test_parse_python_sig_7() -> anyhow::Result<()> {
let code = r#"
import os
def preprocessor(): return
def main(): return
"#;
//println!("{}", serde_json::to_string()?);
assert_eq!(
parse_python_signature(code, None)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![],
no_main_func: Some(false),
has_preprocessor: Some(true)
}
);

View File

@@ -29,6 +29,7 @@ pub fn parse_rust_signature(code: &str) -> anyhow::Result<MainArgSignature> {
star_kwargs: false,
args,
no_main_func: Some(false),
has_preprocessor: None,
})
} else {
Ok(MainArgSignature {
@@ -36,6 +37,7 @@ pub fn parse_rust_signature(code: &str) -> anyhow::Result<MainArgSignature> {
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
has_preprocessor: None,
})
}
}
@@ -455,10 +457,7 @@ fn main(
assert_eq!(ret.args[2].typ, Typ::Str(None));
assert_eq!(ret.args[3].name, "my_string_vec");
assert_eq!(
ret.args[3].otyp,
Some("Vec < String >".to_string())
);
assert_eq!(ret.args[3].otyp, Some("Vec < String >".to_string()));
assert_eq!(ret.args[3].typ, Typ::List(Box::new(Typ::Str(None))));
}

View File

@@ -20,7 +20,13 @@ pub fn parse_mysql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_mysql_file(&code)?;
if let Some(x) = parsed {
let args = x;
Ok(MainArgSignature { star_args: false, star_kwargs: false, args, no_main_func: None })
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
}
@@ -30,7 +36,13 @@ pub fn parse_pgsql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_pg_file(&code)?;
if let Some(x) = parsed {
let args = x;
Ok(MainArgSignature { star_args: false, star_kwargs: false, args, no_main_func: None })
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
}
@@ -40,7 +52,13 @@ pub fn parse_bigquery_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_bigquery_file(&code)?;
if let Some(x) = parsed {
let args = x;
Ok(MainArgSignature { star_args: false, star_kwargs: false, args, no_main_func: None })
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
}
@@ -50,7 +68,13 @@ pub fn parse_snowflake_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_snowflake_file(&code)?;
if let Some(x) = parsed {
let args = x;
Ok(MainArgSignature { star_args: false, star_kwargs: false, args, no_main_func: None })
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
}
@@ -60,7 +84,13 @@ pub fn parse_mssql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_mssql_file(&code)?;
if let Some(x) = parsed {
let args = x;
Ok(MainArgSignature { star_args: false, star_kwargs: false, args, no_main_func: None })
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
}
@@ -570,7 +600,8 @@ SELECT * FROM table WHERE token=$1::TEXT AND image=$2::BIGINT
oidx: Some(2),
},
],
no_main_func: None
no_main_func: None,
has_preprocessor: None
}
);
@@ -618,7 +649,8 @@ SELECT $2::TEXT;
oidx: Some(3),
},
],
no_main_func: None
no_main_func: None,
has_preprocessor: None
}
);
@@ -744,7 +776,8 @@ SELECT ?, ?;
oidx: None,
},
],
no_main_func: None
no_main_func: None,
has_preprocessor: None
}
);
@@ -791,7 +824,8 @@ SELECT :param2;
oidx: None,
},
],
no_main_func: None
no_main_func: None,
has_preprocessor: None
}
);
@@ -830,7 +864,8 @@ SELECT @token;
oidx: None,
},
],
no_main_func: None
no_main_func: None,
has_preprocessor: None
}
);
@@ -877,7 +912,8 @@ SELECT ?;
oidx: None,
}
],
no_main_func: None
no_main_func: None,
has_preprocessor: None
}
);
@@ -924,7 +960,8 @@ SELECT @p2;
oidx: None,
},
],
no_main_func: None
no_main_func: None,
has_preprocessor: None
}
);

View File

@@ -159,6 +159,14 @@ pub fn parse_deno_signature(
})?
.body;
let has_preprocessor = ast.iter().any(|x| match x {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Fn(FnDecl { ident: Ident { sym, .. }, .. }),
..
})) => &sym.to_string() == "preprocessor",
_ => false,
});
let main_name = main_override.unwrap_or("main".to_string());
let params = ast.into_iter().find_map(|x| match x {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
@@ -178,6 +186,7 @@ pub fn parse_deno_signature(
.map(|x| parse_param(x, &cm, skip_dflt, &mut c))
.collect::<anyhow::Result<Vec<Arg>>>()?,
no_main_func: Some(false),
has_preprocessor: Some(has_preprocessor),
};
Ok(r)
} else {
@@ -186,6 +195,7 @@ pub fn parse_deno_signature(
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
has_preprocessor: Some(has_preprocessor),
})
}
}

View File

@@ -25,6 +25,7 @@ ts-parser = [ "dep:windmill-parser-ts"]
php-parser = [ "dep:windmill-parser-php"]
rust-parser = [ "dep:windmill-parser-rust"]
graphql-parser = [ "dep:windmill-parser-graphql"]
ansible-parser = [ "dep:windmill-parser-yaml"]
[dependencies]
anyhow.workspace = true
@@ -37,6 +38,7 @@ windmill-parser-ts = { workspace = true, optional = true }
windmill-parser-php = { workspace = true, optional = true }
windmill-parser-graphql = { workspace = true, optional = true }
windmill-parser-rust = { workspace = true, optional = true }
windmill-parser-yaml = { workspace = true, optional = true }
wasm-bindgen.workspace = true
serde_json.workspace = true
getrandom = { workspace = true, features = ["js"] }

View File

@@ -10,35 +10,41 @@ wasm-pack build --release --target web --out-dir $OUT_DIR --all-features \
OUT_DIR="pkg-ts"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ts-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-ts"/' $OUT_DIR/package.json
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-ts"/' $OUT_DIR/package.json
# sql languages, graphql and bash/powershell, since they all use regex
OUT_DIR="pkg-regex"
wasm-pack build --release --target web --out-dir $OUT_DIR \
--features "sql-parser,graphql-parser,bash-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-regex"/' $OUT_DIR/package.json
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-regex"/' $OUT_DIR/package.json
# python
OUT_DIR="pkg-py"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "py-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-py"/' $OUT_DIR/package.json
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-py"/' $OUT_DIR/package.json
# go
OUT_DIR="pkg-go"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "go-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-go"/' $OUT_DIR/package.json
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-go"/' $OUT_DIR/package.json
# php
OUT_DIR="pkg-php"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "php-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-php"/' $OUT_DIR/package.json
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-php"/' $OUT_DIR/package.json
# rust
OUT_DIR="pkg-rust"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "rust-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-rust"/' $OUT_DIR/package.json
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-rust"/' $OUT_DIR/package.json
# ansible
OUT_DIR="pkg-yaml"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json

View File

@@ -42,3 +42,9 @@ OUT_DIR="pkg-rust"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "rust-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-rust"/' $OUT_DIR/package.json
# ansible
OUT_DIR="pkg-yaml"
wasm-pack build --release --target web --out-dir $OUT_DIR --features "ansible-parser" \
-Z build-std=panic_abort,std -Z build-std-features=panic_immediate_abort
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-yaml"/' $OUT_DIR/package.json

View File

@@ -1,4 +1,4 @@
#!/bin/bash
set -eou pipefail
deno task wasmbuild --out ../../../cli/wasm/ -p windmill-parser-wasm
deno task wasmbuild --out ../../../cli/wasm/ -p windmill-parser-wasm --all-features

View File

@@ -21,3 +21,6 @@ popd
pushd "pkg-rust" && npm publish ${args}
popd
pushd "pkg-yaml" && npm publish ${args}
popd

View File

@@ -17,8 +17,12 @@ fn wrap_sig(r: anyhow::Result<MainArgSignature>) -> String {
#[cfg(feature = "ts-parser")]
#[wasm_bindgen]
pub fn parse_deno(code: &str) -> String {
wrap_sig(windmill_parser_ts::parse_deno_signature(code, false, None))
pub fn parse_deno(code: &str, main_override: Option<String>) -> String {
wrap_sig(windmill_parser_ts::parse_deno_signature(
code,
false,
main_override,
))
}
#[cfg(feature = "ts-parser")]
@@ -65,8 +69,11 @@ pub fn parse_go(code: &str) -> String {
#[cfg(feature = "py-parser")]
#[wasm_bindgen]
pub fn parse_python(code: &str) -> String {
wrap_sig(windmill_parser_py::parse_python_signature(code, None))
pub fn parse_python(code: &str, main_override: Option<String>) -> String {
wrap_sig(windmill_parser_py::parse_python_signature(
code,
main_override,
))
}
#[cfg(feature = "sql-parser")]
@@ -122,3 +129,9 @@ pub fn parse_php(code: &str) -> String {
pub fn parse_rust(code: &str) -> String {
wrap_sig(windmill_parser_rust::parse_rust_signature(code))
}
#[cfg(feature = "ansible-parser")]
#[wasm_bindgen]
pub fn parse_ansible(code: &str) -> String {
wrap_sig(windmill_parser_yaml::parse_ansible_sig(code))
}

View File

@@ -139,7 +139,8 @@ export function main(test1?: string, test2: string = \"burkina\",
oidx: None
}
],
no_main_func: Some(false)
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
@@ -215,7 +216,8 @@ export function main(test2 = \"burkina\",
oidx: None
}
],
no_main_func: Some(false)
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
@@ -264,7 +266,8 @@ export function main(foo: FooBar, {a, b}: FooBar, {c, d}: FooBar = {a: \"foo\",
oidx: None
}
],
no_main_func: Some(false)
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
@@ -294,7 +297,8 @@ export function main(foo: (\"foo\" | \"bar\")[]) {
has_default: false,
oidx: None
}],
no_main_func: Some(false)
no_main_func: Some(false),
has_preprocessor: Some(false)
}
);
@@ -434,6 +438,7 @@ Write-Output 'Testing...'
}
],
no_main_func: None,
has_preprocessor: None
}
);

View File

@@ -0,0 +1,15 @@
[package]
name = "windmill-parser-yaml"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_parser_yaml"
path = "./src/lib.rs"
[dependencies]
yaml-rust.workspace = true
windmill-parser.workspace = true
anyhow.workspace = true
serde_json.workspace = true

View File

@@ -0,0 +1,482 @@
use anyhow::anyhow;
use serde_json::json;
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
use yaml_rust::{Yaml, YamlEmitter, YamlLoader};
pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature> {
let docs = YamlLoader::load_from_str(inner_content)
.map_err(|e| anyhow!("Failed to parse yaml: {}", e))?;
if docs.len() < 2 {
return Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![],
no_main_func: None,
has_preprocessor: None,
});
}
let mut args = vec![];
if let Yaml::Hash(doc) = &docs[0] {
for (key, value) in doc {
match key {
Yaml::String(key) if key == "extra_vars" => {
if let Yaml::Hash(v) = &value {
for (key, arg) in v {
if let Yaml::String(arg_name) = key {
// if not then it is a static resource or var, so skip
// it will be fetched by the worker
if let ArgTyp::Typ(typ) = parse_ansible_arg_typ(arg) {
let default = get_default_for_typ(&arg);
args.push(Arg {
name: arg_name.to_string(),
otyp: None,
typ,
has_default: default.is_some(),
default,
oidx: None,
})
}
}
}
}
}
Yaml::String(key) if key == "inventory" => {
for inv in parse_inventories(value)? {
if inv.pinned_resource.is_some() {
continue;
}
args.push(Arg {
name: inv.name,
otyp: None,
typ: Typ::Resource(inv.resource_type.unwrap_or("Unknown".to_string())),
has_default: inv.default.is_some(),
default: inv.default.map(|v| json!(format!("$res:{}", v))),
oidx: None,
})
}
}
_ => (),
}
}
}
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
}
fn get_default_for_typ(arg: &Yaml) -> Option<serde_json::Value> {
if let Yaml::Hash(arg) = arg {
if let Some(def) = arg.get(&Yaml::String("default".to_string())) {
if let Some(Yaml::String(typ)) = arg.get(&Yaml::String("type".to_string())) {
if let Yaml::String(path) = def {
if typ.as_str() == "windmill_resource" {
return Some(serde_json::Value::String(format!("$res:{}", path)));
}
}
}
return Some(yaml_to_json(def));
}
}
None
}
enum ArgTyp {
Typ(Typ),
StaticVar(String),
StaticResource(String),
}
fn parse_ansible_arg_typ(arg: &Yaml) -> ArgTyp {
if let Yaml::Hash(a) = arg {
if let Some(Yaml::String(typ)) = a.get(&Yaml::String("type".to_string())) {
if typ.as_str() == "windmill_variable" {
if let Some(Yaml::String(variable_path)) =
a.get(&Yaml::String("variable".to_string()))
{
return ArgTyp::StaticVar(variable_path.to_string());
}
}
if typ.as_str() == "windmill_resource" {
if let Some(Yaml::String(resource_path)) =
a.get(&Yaml::String("resource".to_string()))
{
return ArgTyp::StaticResource(resource_path.to_string());
}
}
}
}
ArgTyp::Typ(parse_ansible_typ(arg))
}
fn parse_ansible_typ(arg: &Yaml) -> Typ {
if let Yaml::Hash(arg) = arg {
if let Some(Yaml::String(typ)) = arg.get(&Yaml::String("type".to_string())) {
match typ.as_ref() {
"boolean" => Typ::Bool,
"integer" => Typ::Int,
"number" => Typ::Float,
"string" => {
if let Some(Yaml::String(fmt)) = arg.get(&Yaml::String("format".to_string())) {
match fmt.as_ref() {
"date-time" | "datetime" => Typ::Datetime,
"email" => Typ::Email,
_ => Typ::Str(None),
}
} else {
Typ::Str(None)
}
}
"object" => {
if let Some(Yaml::Hash(props)) =
arg.get(&Yaml::String("properties".to_string()))
{
let mut prop_vec = vec![];
for (key, value) in props {
if let Yaml::String(key) = key {
prop_vec.push(ObjectProperty {
key: key.clone(),
typ: Box::new(parse_ansible_typ(value)),
})
}
}
Typ::Object(prop_vec)
} else {
Typ::Object(vec![])
}
}
"array" => {
if let Some(items) = arg.get(&Yaml::String("items".to_string())) {
Typ::List(Box::new(parse_ansible_typ(items)))
} else {
Typ::List(Box::new(Typ::Unknown))
}
}
"windmill_resource" => {
if let Some(Yaml::String(res_name)) =
arg.get(&Yaml::String("resource_type".to_string()))
{
Typ::Resource(res_name.clone())
} else {
Typ::Resource("".to_string())
}
}
_ => Typ::Unknown,
}
} else {
Typ::Unknown
}
} else {
Typ::Unknown
}
}
#[derive(Debug, Clone)]
pub struct AnsiblePlaybookOptions {
// There are a lot more options
// TODO: Add the options as customers require them
// Add it here and then on the executors cmd_options
pub verbosity: Option<String>,
pub forks: Option<i64>,
pub timeout: Option<i64>,
pub flush_cache: Option<()>,
pub force_handlers: Option<()>,
}
#[derive(Debug, Clone)]
pub struct FileResource {
pub resource_path: String,
pub target_path: String,
}
#[derive(Debug, Clone)]
pub struct AnsibleInventory {
pub default: Option<String>,
pub name: String,
resource_type: Option<String>,
pub pinned_resource: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AnsibleRequirements {
pub python_reqs: Vec<String>,
pub collections: Option<String>,
pub file_resources: Vec<FileResource>,
pub inventories: Vec<AnsibleInventory>,
pub vars: Vec<(String, String)>,
pub resources: Vec<(String, String)>,
pub options: AnsiblePlaybookOptions,
}
fn parse_inventories(inventory_yaml: &Yaml) -> anyhow::Result<Vec<AnsibleInventory>> {
if let Yaml::Array(arr) = inventory_yaml {
let mut ret = vec![];
for (i, inv) in arr.iter().enumerate() {
if let Yaml::Hash(inv) = inv {
let resource_type = inv
.get(&Yaml::String("resource_type".to_string()))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let windmill_path = inv
.get(&Yaml::String("default".to_string()))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let name = if i == 0 {
"inventory.ini".to_string()
} else {
format!("inventory-{}.ini", i)
};
let name = inv
.get(&Yaml::String("name".to_string()))
.and_then(|v| v.as_str())
.unwrap_or(name.as_str())
.to_string();
let pin = inv
.get(&Yaml::String("resource".to_string()))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
ret.push(AnsibleInventory {
default: windmill_path,
name,
resource_type,
pinned_resource: pin,
});
}
}
return Ok(ret);
}
return Err(anyhow!("Invalid inventory definition"));
}
pub fn parse_ansible_reqs(
inner_content: &str,
) -> anyhow::Result<(String, Option<AnsibleRequirements>, String)> {
let mut logs = String::new();
let docs = YamlLoader::load_from_str(inner_content)
.map_err(|e| anyhow!("Failed to parse yaml: {}", e))?;
if docs.len() < 2 {
return Ok((logs, None, inner_content.to_string()));
}
let opts = AnsiblePlaybookOptions {
verbosity: None,
forks: None,
timeout: None,
flush_cache: None,
force_handlers: None,
};
let mut ret = AnsibleRequirements {
python_reqs: vec![],
collections: None,
file_resources: vec![],
inventories: vec![],
vars: vec![],
resources: vec![],
options: opts,
};
if let Yaml::Hash(doc) = &docs[0] {
for (key, value) in doc {
match key {
Yaml::String(key) if key == "dependencies" => {
if let Yaml::Hash(deps) = value {
if let Some(galaxy_requirements) =
deps.get(&Yaml::String("galaxy".to_string()))
{
let mut out_str = String::new();
let mut emitter = YamlEmitter::new(&mut out_str);
emitter.dump(galaxy_requirements)?;
ret.collections = Some(out_str);
}
if let Some(Yaml::Array(py_reqs)) =
deps.get(&Yaml::String("python".to_string()))
{
ret.python_reqs = py_reqs
.iter()
.map(|d| d.as_str().map(|s| s.to_string()))
.filter_map(|x| x)
.collect();
}
}
}
Yaml::String(key) if key == "file_resources" => {
if let Yaml::Array(file_resources) = value {
let resources: anyhow::Result<Vec<FileResource>> =
file_resources.iter().map(parse_file_resource).collect();
ret.file_resources.append(&mut resources?);
}
}
Yaml::String(key) if key == "extra_vars" => {
if let Yaml::Hash(v) = &value {
for (key, arg) in v {
if let Yaml::String(arg_name) = key {
match parse_ansible_arg_typ(arg) {
ArgTyp::StaticVar(p) => {
ret.vars
.push((arg_name.to_string(), format!("$var:{}", p)));
}
ArgTyp::StaticResource(p) => {
ret.resources
.push((arg_name.to_string(), format!("$res:{}", p)));
}
ArgTyp::Typ(_) => (),
}
}
}
}
}
Yaml::String(key) if key == "inventory" => {
ret.inventories = parse_inventories(value)?;
}
Yaml::String(key) if key == "options" => {
if let Yaml::Array(opts) = &value {
ret.options = parse_ansible_options(opts);
}
}
Yaml::String(key) => logs.push_str(&format!("\nUnknown field `{}`. Ignoring", key)),
_ => (),
}
}
}
let mut out_str = String::new();
let mut emitter = YamlEmitter::new(&mut out_str);
for i in 1..docs.len() {
emitter.dump(&docs[i])?;
}
Ok((logs, Some(ret), out_str))
}
fn parse_ansible_options(opts: &Vec<Yaml>) -> AnsiblePlaybookOptions {
let mut ret = AnsiblePlaybookOptions {
verbosity: None,
forks: None,
timeout: None,
flush_cache: None,
force_handlers: None,
};
for opt in opts {
if let Yaml::String(o) = opt {
match o.as_str() {
"flush_cache" => {
ret.flush_cache = Some(());
}
"force_handlers" => {
ret.force_handlers = Some(());
}
s if count_consecutive_vs(s) > 0 => {
ret.verbosity = Some("v".repeat(count_consecutive_vs(s).min(6)));
}
_ => (),
}
}
if let Yaml::Hash(m) = opt {
if let Some((Yaml::String(name), value)) = m.iter().last() {
match name.as_str() {
"forks" => {
if let Yaml::Integer(count) = value {
ret.forks = Some(*count);
}
}
"timeout" => {
if let Yaml::Integer(timeout) = value {
ret.timeout = Some(*timeout);
}
}
"verbosity" => {
if let Yaml::String(verbosity) = value {
let c = count_consecutive_vs(verbosity);
if c > 0 && c <= 6 {
ret.verbosity = Some("v".repeat(c.min(6)));
}
}
}
_ => ()
}
}
}
}
ret
}
fn count_consecutive_vs(s: &str) -> usize {
let mut max_count = 0;
let mut current_count = 0;
for c in s.chars() {
if c == 'v' {
current_count += 1;
if current_count == 6 {
return 6; // Stop early if we reach 6
}
} else {
current_count = 0; // Reset count if the character is not 'v'
}
max_count = max_count.max(current_count);
}
max_count
}
fn parse_file_resource(yaml: &Yaml) -> anyhow::Result<FileResource> {
if let Yaml::Hash(f) = yaml {
if let Some(Yaml::String(resource_path)) = f.get(&Yaml::String("resource".to_string())) {
let target_path = f
.get(&Yaml::String("target".to_string()))
.and_then(|x| x.as_str())
.map(|x| x.to_string())
.ok_or(anyhow!(
"No `target` provided for file resource {}. Please input a target relative path for the ansible playbook to see this file.",
resource_path
))?;
return Ok(FileResource { resource_path: resource_path.clone(), target_path });
}
return Err(anyhow!(
"File resource should have a `resource` field, linking to a text file resource"
));
}
return Err(anyhow!("Invalid file resource: Should be a dictionary."));
}
fn yaml_to_json(yaml: &Yaml) -> serde_json::Value {
match yaml {
Yaml::Array(arr) => {
let json_array: Vec<serde_json::Value> = arr.into_iter().map(yaml_to_json).collect();
serde_json::Value::Array(json_array)
}
Yaml::Hash(hash) => {
let json_object = hash
.into_iter()
.map(|(k, v)| {
let key = match k {
Yaml::String(s) => s.clone(),
_ => k.as_str().unwrap_or("").to_string(),
};
(key, yaml_to_json(v))
})
.collect();
serde_json::Value::Object(json_object)
}
Yaml::String(s) => serde_json::Value::String(s.to_string()),
Yaml::Integer(i) => serde_json::Value::Number(i.clone().into()),
Yaml::Real(r) => serde_json::Value::Number(r.parse().unwrap_or(0.into())),
Yaml::Boolean(b) => serde_json::Value::Bool(*b),
Yaml::Null => serde_json::Value::Null,
_ => serde_json::Value::Null,
}
}

View File

@@ -16,6 +16,7 @@ pub struct MainArgSignature {
pub star_kwargs: bool,
pub args: Vec<Arg>,
pub no_main_func: Option<bool>,
pub has_preprocessor: Option<bool>,
}
#[derive(Serialize, Clone, Debug, PartialEq)]
@@ -153,7 +154,10 @@ mod test {
#[test]
fn test_mixed_delimiters() {
assert_eq!(to_snake_case("test-Case_with Spaces"), "test_case_with_spaces");
assert_eq!(
to_snake_case("test-Case_with Spaces"),
"test_case_with_spaces"
);
}
#[test]

View File

@@ -8,7 +8,10 @@
use anyhow::Context;
use git_version::git_version;
use monitor::{send_current_log_file_to_object_store, send_logs_to_object_store};
use monitor::{
reload_timeout_wait_result_setting, send_current_log_file_to_object_store,
send_logs_to_object_store,
};
use rand::Rng;
use sqlx::{postgres::PgListener, Pool, Postgres};
use std::{
@@ -35,7 +38,7 @@ use windmill_common::{
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING,
OAUTH_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
},
scripts::ScriptLang,
stats_ee::schedule_stats,
@@ -74,7 +77,7 @@ use crate::monitor::{
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
reload_hub_base_url_setting, reload_job_default_timeout_setting, reload_jwt_secret_setting,
reload_license_key, reload_npm_config_registry_setting, reload_pip_index_url_setting,
reload_retention_period_setting, reload_scim_token_setting, reload_server_config,
reload_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
reload_worker_config,
};
@@ -173,7 +176,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
None,
&job_dir,
"cache_init",
windmill_worker::get_common_bun_proc_envs("").await,
windmill_worker::get_common_bun_proc_envs(None).await,
false,
)
.await?;
@@ -563,9 +566,7 @@ Windmill Community Edition {GIT_VERSION}
"notify_config_change" => {
match n.payload() {
"server" if server_mode => {
tracing::info!("Server config change detected: {}", n.payload());
reload_server_config(&db).await;
tracing::error!("Server config change detected but server config is obsolete: {}", n.payload());
},
a@ _ if worker_mode && a == format!("worker__{}", *WORKER_GROUP) => {
tracing::info!("Worker config change detected: {}", n.payload());
@@ -609,6 +610,12 @@ Windmill Community Edition {GIT_VERSION}
tracing::error!("Error loading default tag per workspace workspaces: {e:#}");
}
}
SMTP_SETTING => {
reload_smtp_config(&db).await;
},
TIMEOUT_WAIT_RESULT_SETTING => {
reload_timeout_wait_result_setting(&db).await
},
RETENTION_PERIOD_SECS_SETTING => {
reload_retention_period_setting(&db).await
},

View File

@@ -22,6 +22,7 @@ use tokio::{
#[cfg(feature = "embedding")]
use windmill_api::embeddings::update_embeddings_db;
use windmill_api::{
jobs::TIMEOUT_WAIT_RESULT,
oauth2_ee::{build_oauth_clients, OAuthClient},
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN,
};
@@ -40,17 +41,17 @@ use windmill_common::{
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
},
jobs::QueuedJob,
oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH,
server::load_server_config,
server::load_smtp_config,
tracing_init::JSON_FMT,
users::truncate_token,
utils::{now_from_db, rd_string, report_critical_error, Mode},
worker::{
load_worker_config, make_pull_query, make_suspended_pull_query, reload_custom_tags_setting,
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SERVER_CONFIG, WORKER_CONFIG,
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SMTP_CONFIG, WORKER_CONFIG,
WORKER_GROUP,
},
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS,
@@ -172,8 +173,9 @@ pub async fn initial_load(
reload_s3_cache_setting(&db).await;
}
reload_smtp_config(&db).await;
if server_mode {
reload_server_config(&db).await;
reload_retention_period_setting(&db).await;
reload_request_size(&db).await;
reload_saml_metadata_setting(&db).await;
@@ -698,6 +700,15 @@ pub async fn reload_scim_token_setting(db: &DB) {
.await;
}
pub async fn reload_timeout_wait_result_setting(db: &DB) {
reload_option_setting_with_tracing(
db,
TIMEOUT_WAIT_RESULT_SETTING,
"TIMEOUT_WAIT_RESULT",
TIMEOUT_WAIT_RESULT.clone(),
)
.await;
}
pub async fn reload_saml_metadata_setting(db: &DB) {
reload_option_setting_with_tracing(
db,
@@ -1129,14 +1140,14 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
}
}
pub async fn reload_server_config(db: &Pool<Postgres>) {
let config = load_server_config(&db).await;
if let Err(e) = config {
tracing::error!("Error reloading server config: {:?}", e)
pub async fn reload_smtp_config(db: &Pool<Postgres>) {
let smtp_config = load_smtp_config(&db).await;
if let Err(e) = smtp_config {
tracing::error!("Error reloading smtp config: {:?}", e)
} else {
let mut wc = SERVER_CONFIG.write().await;
tracing::info!("Reloading server config...");
*wc = config.unwrap()
let mut wc = SMTP_CONFIG.write().await;
tracing::info!("Reloading smtp config...");
*wc = smtp_config.unwrap()
}
}

View File

@@ -1912,6 +1912,109 @@ def main():
assert_eq!(result, serde_json::json!("test-workspace"));
}
#[sqlx::test(fixtures("base"))]
async fn test_bun_job_datetime(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let content = r#"
export async function main(a: Date) {
return typeof a;
}
"#
.to_owned();
let result = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
language: ScriptLang::Bun,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
}))
.arg("a", json!("2024-09-24T10:00:00.000Z"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("object"));
}
#[sqlx::test(fixtures("base"))]
async fn test_deno_job_datetime(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let content = r#"
export async function main(a: Date) {
return typeof a;
}
"#
.to_owned();
let result = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
language: ScriptLang::Deno,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
}))
.arg("a", json!("2024-09-24T10:00:00.000Z"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("object"));
}
#[sqlx::test(fixtures("base"))]
async fn test_python_job_datetime_and_bytes(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let content = r#"
from datetime import datetime
def main(a: datetime, b: bytes):
return (isinstance(a, datetime), isinstance(b, bytes))
"#
.to_owned();
let result = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
language: ScriptLang::Python3,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
}))
.arg("a", json!("2024-09-24T10:00:00.000Z"))
.arg("b", json!("dGVzdA=="))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!([true, true]));
}
#[sqlx::test(fixtures("base"))]
async fn test_empty_loop_1(db: Pool<Postgres>) {
initialize_tracing().await;
@@ -3294,6 +3397,7 @@ async fn run_deployed_relative_imports(
visible_to_runner_only: None,
no_main_func: None,
codebase: None,
has_preprocessor: None,
},
)
.await
@@ -3324,6 +3428,7 @@ async fn run_deployed_relative_imports(
dedicated_worker: None,
language,
priority: None,
apply_preprocessor: false,
})
.push(&db2)
.await;

View File

@@ -94,6 +94,7 @@ object_store = { workspace = true, optional = true}
openidconnect = { workspace = true, optional = true}
url = { workspace = true, optional = true}
jsonwebtoken = { workspace = true }
matchit.workspace = true
pin-project.workspace = true
crc.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.396.1
version: 1.399.0
title: Windmill API
contact:
@@ -742,6 +742,35 @@ paths:
schema:
type: string
/settings/test_critical_channels:
post:
summary: test critical channels
operationId: testCriticalChannels
tags:
- setting
requestBody:
description: test critical channel payload
required: true
content:
application/json:
schema:
type: array
items:
type: object
properties:
email:
type: string
slack_channel:
type: string
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/settings/test_license_key:
post:
summary: test license key
@@ -3088,6 +3117,22 @@ paths:
schema:
type: string
/w/{workspace}/resources/file_resource_type_to_file_ext_map:
get:
summary: get map from resource type to format extension
operationId: fileResourceTypeToFileExtMap
tags:
- resource
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: map from resource type to file ext
content:
application/json:
schema: {}
/w/{workspace}/resources/type/delete/{path}:
delete:
summary: delete resource_type
@@ -4166,6 +4211,11 @@ paths:
in: query
schema:
type: integer
- name: skip_preprocessor
description: skip the preprocessor
in: query
schema:
type: boolean
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
@@ -5312,6 +5362,11 @@ paths:
in: query
schema:
type: integer
- name: skip_preprocessor
description: skip the preprocessor
in: query
schema:
type: boolean
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
@@ -5420,6 +5475,11 @@ paths:
in: query
schema:
type: integer
- name: skip_preprocessor
description: skip the preprocessor
in: query
schema:
type: boolean
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
@@ -6806,6 +6866,190 @@ paths:
"201":
description: default error handler set
/w/{workspace}/http_triggers/create:
post:
summary: create http trigger
operationId: createHttpTrigger
tags:
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: new http trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewHttpTrigger"
responses:
"201":
description: http trigger created
content:
text/plain:
schema:
type: string
/w/{workspace}/http_triggers/update/{path}:
post:
summary: update http trigger
operationId: updateHttpTrigger
tags:
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: updated trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EditHttpTrigger"
responses:
"200":
description: http trigger updated
content:
text/plain:
schema:
type: string
/w/{workspace}/http_triggers/delete/{path}:
delete:
summary: delete http trigger
operationId: deleteHttpTrigger
tags:
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: http trigger deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/http_triggers/get/{path}:
get:
summary: get http trigger
operationId: getHttpTrigger
tags:
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: http trigger deleted
content:
application/json:
schema:
$ref: "#/components/schemas/HttpTrigger"
/w/{workspace}/http_triggers/list:
get:
summary: list http triggers
operationId: listHttpTriggers
tags:
- http_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: http trigger list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/HttpTrigger"
/w/{workspace}/http_triggers/exists/{path}:
get:
summary: does http trigger exists
operationId: existsHttpTrigger
tags:
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: http trigger exists
content:
application/json:
schema:
type: boolean
/w/{workspace}/http_triggers/route_exists:
post:
summary: does route exists
operationId: existsRoute
tags:
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: route exists request
required: true
content:
application/json:
schema:
type: object
properties:
route_path:
type: string
http_method:
type: string
enum: ["get", "post", "put", "delete", "patch"]
required:
- kind
- route_path
- http_method
responses:
"200":
description: route exists
content:
application/json:
schema:
type: boolean
/w/{workspace}/http_triggers/used:
get:
summary: whether http triggers are used
operationId: used
tags:
- http_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: whether http triggers are used
content:
application/json:
schema:
type: boolean
/groups/list:
get:
summary: list instance groups
@@ -7632,6 +7876,7 @@ paths:
folder,
app,
raw_app,
http_trigger,
]
responses:
"200":
@@ -7668,6 +7913,7 @@ paths:
folder,
app,
raw_app,
http_trigger,
]
requestBody:
description: acl to add
@@ -7715,6 +7961,7 @@ paths:
folder,
app,
raw_app,
http_trigger,
]
requestBody:
description: acl to add
@@ -9362,6 +9609,7 @@ components:
bun,
php,
rust,
ansible,
]
kind:
type: string
@@ -9404,6 +9652,8 @@ components:
type: boolean
codebase:
type: string
has_preprocessor:
type: boolean
required:
- hash
@@ -9421,6 +9671,7 @@ components:
- kind
- starred
- no_main_func
- has_preprocessor
NewScript:
@@ -9461,6 +9712,7 @@ components:
bun,
php,
rust,
ansible,
]
kind:
type: string
@@ -9501,6 +9753,8 @@ components:
type: boolean
codebase:
type: string
has_preprocessor:
type: boolean
required:
- path
- summary
@@ -9679,6 +9933,7 @@ components:
bun,
php,
rust,
ansible,
]
email:
type: string
@@ -9795,6 +10050,7 @@ components:
bun,
php,
rust,
ansible,
]
is_skipped:
type: boolean
@@ -10299,6 +10555,9 @@ components:
no_main_func:
type: boolean
nullable: true
has_preprocessor:
type: boolean
nullable: true
required:
- star_args
- start_kwargs
@@ -10306,6 +10565,7 @@ components:
- type
- error
- no_main_func
- has_preprocessor
Preview:
type: object
@@ -10335,6 +10595,7 @@ components:
bun,
php,
rust,
ansible,
]
tag:
type: string
@@ -10482,6 +10743,8 @@ components:
edited_at:
type: string
format: date-time
format_extension:
type: string
required:
- name
@@ -10692,6 +10955,122 @@ components:
- is_flow
- args
HttpTrigger:
type: object
properties:
path:
type: string
edited_by:
type: string
edited_at:
type: string
format: date-time
script_path:
type: string
route_path:
type: string
is_flow:
type: boolean
extra_perms:
type: object
additionalProperties:
type: boolean
email:
type: string
workspace_id:
type: string
http_method:
type: string
enum:
- get
- post
- put
- delete
- patch
is_async:
type: boolean
requires_auth:
type: boolean
required:
- path
- edited_by
- edited_at
- script_path
- route_path
- extra_perms
- is_flow
- email
- workspace_id
- is_async
- requires_auth
- http_method
NewHttpTrigger:
type: object
properties:
path:
type: string
script_path:
type: string
route_path:
type: string
is_flow:
type: boolean
http_method:
type: string
enum:
- get
- post
- put
- delete
- patch
is_async:
type: boolean
requires_auth:
type: boolean
required:
- path
- script_path
- route_path
- is_flow
- is_async
- requires_auth
- http_method
EditHttpTrigger:
type: object
properties:
path:
type: string
script_path:
type: string
route_path:
type: string
is_flow:
type: boolean
http_method:
type: string
enum:
- get
- post
- put
- delete
- patch
is_async:
type: boolean
requires_auth:
type: boolean
required:
- path
- script_path
- is_flow
- kind
- is_async
- requires_auth
- http_method
Group:
type: object
properties:
@@ -11478,6 +11857,7 @@ components:
bun,
php,
rust,
ansible,
]
required:
- raw_code

View File

@@ -144,7 +144,7 @@ async fn get_concurrent_intervals(
Query(iq): Query<ExtendedJobsParams>,
Query(lq): Query<ListCompletedQuery>,
) -> JsonResult<ExtendedJobs> {
check_scopes(&authed, || format!("listjobs"))?;
check_scopes(&authed, || format!("jobs:listjobs"))?;
if lq.success.is_some() && lq.running.is_some_and(|x| x) {
return Err(error::Error::BadRequest(

View File

@@ -1258,6 +1258,7 @@ mod tests {
delete_after_use: None,
continue_on_error: None,
})),
preprocessor_module: None,
same_worker: false,
concurrent_limit: None,
concurrency_time_window_s: None,
@@ -1330,7 +1331,8 @@ mod tests {
"expr": "previous.isEmpty()",
"skip_if_stopped": false
}
}
},
"preprocessor_module": Option::<String>::None
});
assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect));
}

View File

@@ -23,8 +23,17 @@ use windmill_common::{
utils::{not_found_if_none, StripPath},
};
const KINDS: [&str; 9] = [
"script", "group_", "resource", "schedule", "variable", "flow", "folder", "app", "raw_app",
const KINDS: [&str; 10] = [
"script",
"group_",
"resource",
"schedule",
"variable",
"flow",
"folder",
"app",
"raw_app",
"http_trigger",
];
pub fn workspaced_service() -> Router {

View File

@@ -0,0 +1,651 @@
use axum::{
extract::{Path, Query},
response::IntoResponse,
routing::{delete, get, post},
Extension, Json, Router,
};
use http::{HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::prelude::FromRow;
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},
};
use windmill_queue::PushArgsOwned;
use crate::{
db::{ApiAuthed, DB},
jobs::{
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::OptAuthed,
};
lazy_static::lazy_static! {
static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/:\w+").unwrap();
}
pub fn routes_global_service() -> Router {
let cors = CorsLayer::new()
.allow_methods([
http::Method::GET,
http::Method::POST,
http::Method::DELETE,
http::Method::PUT,
http::Method::PATCH,
])
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
.allow_origin(tower_http::cors::Any);
Router::new()
.route(
"/*path",
get(route_job)
.post(route_job)
.delete(route_job)
.put(route_job)
.patch(route_job)
.head(|| async { "" }),
)
.layer(cors)
}
pub fn workspaced_service() -> Router {
Router::new()
.route("/create", post(create_trigger))
.route("/list", get(list_triggers))
.route("/get/*path", get(get_trigger))
.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))
}
#[derive(Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "HTTP_METHOD", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
}
impl From<HttpMethod> for http::Method {
fn from(method: HttpMethod) -> Self {
match method {
HttpMethod::Get => http::Method::GET,
HttpMethod::Post => http::Method::POST,
HttpMethod::Put => http::Method::PUT,
HttpMethod::Delete => http::Method::DELETE,
HttpMethod::Patch => http::Method::PATCH,
}
}
}
#[derive(Deserialize)]
struct NewTrigger {
path: String,
route_path: String,
script_path: String,
is_flow: bool,
is_async: bool,
requires_auth: bool,
http_method: HttpMethod,
}
#[derive(FromRow, Serialize)]
struct Trigger {
workspace_id: String,
path: String,
route_path: String,
route_path_key: String,
script_path: String,
is_flow: bool,
edited_by: String,
email: String,
edited_at: chrono::DateTime<chrono::Utc>,
extra_perms: serde_json::Value,
is_async: bool,
requires_auth: bool,
http_method: HttpMethod,
}
#[derive(Deserialize)]
struct EditTrigger {
path: String,
route_path: Option<String>,
script_path: String,
is_flow: bool,
is_async: bool,
requires_auth: bool,
http_method: HttpMethod,
}
#[derive(Deserialize)]
pub struct ListTriggerQuery {
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_triggers(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(lst): Query<ListTriggerQuery>,
) -> error::JsonResult<Vec<Trigger>> {
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("http_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::<_, Trigger>(&sql)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(rows))
}
async fn get_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> error::JsonResult<Trigger> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
let trigger = sqlx::query_as!(
Trigger,
r#"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as "http_method: _", edited_by, email, edited_at, extra_perms, is_async, requires_auth
FROM http_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_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(ct): Json<NewTrigger>,
) -> error::Result<(StatusCode, String)> {
require_admin(authed.is_admin, &authed.username)?;
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(ct.route_path.as_str(), ":key");
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now())",
w_id,
ct.path,
ct.route_path,
&route_path_key,
ct.script_path,
ct.is_flow,
ct.is_async,
ct.requires_auth,
ct.http_method as HttpMethod,
&authed.username,
&authed.email
)
.execute(&mut *tx).await?;
audit_log(
&mut *tx,
&authed,
"http_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_trigger(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ct): Json<EditTrigger>,
) -> error::Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
if authed.is_admin {
if ct.route_path.is_none() {
return Err(error::Error::BadRequest(
"route_path is required".to_string(),
));
}
let route_path_key =
ROUTE_PATH_KEY_RE.replace_all(ct.route_path.as_ref().unwrap().as_str(), ":key");
sqlx::query!(
"UPDATE http_trigger
SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, edited_by = $7, email = $8, is_async = $9, requires_auth = $10, edited_at = now()
WHERE workspace_id = $11 AND path = $12",
ct.route_path,
&route_path_key,
ct.script_path,
ct.path,
ct.is_flow,
ct.http_method as HttpMethod,
&authed.username,
&authed.email,
ct.is_async,
ct.requires_auth,
w_id,
path,
)
.execute(&mut *tx).await?;
} else {
sqlx::query!(
"UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, edited_by = $5, email = $6, is_async = $7, requires_auth = $8, edited_at = now()
WHERE workspace_id = $9 AND path = $10",
ct.script_path,
ct.path,
ct.is_flow,
ct.http_method as HttpMethod,
&authed.username,
&authed.email,
ct.is_async,
ct.requires_auth,
w_id,
path,
)
.execute(&mut *tx).await?;
}
audit_log(
&mut *tx,
&authed,
"http_triggers.update",
ActionKind::Create,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
Ok(path.to_string())
}
async fn delete_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 http_trigger WHERE workspace_id = $1 AND path = $2",
w_id,
path,
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"http_triggers.delete",
ActionKind::Delete,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
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)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE path = $1 AND workspace_id = $2)",
path,
w_id,
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
#[derive(Deserialize)]
struct RouteExists {
route_path: String,
http_method: HttpMethod,
}
async fn exists_route(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(RouteExists { route_path, http_method }): Json<RouteExists>,
) -> JsonResult<bool> {
let route_path_key = ROUTE_PATH_KEY_RE.replace_all(route_path.as_str(), ":key");
let exists = if *CLOUD_HOSTED {
sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND workspace_id = $2 AND http_method = $3)",
&route_path_key,
w_id,
http_method as HttpMethod
)
.fetch_one(&db)
.await?
.unwrap_or(false)
} else {
sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE route_path_key = $1 AND http_method = $2)",
&route_path_key,
http_method as HttpMethod
)
.fetch_one(&db)
.await?
.unwrap_or(false)
};
Ok(Json(exists))
}
struct TriggerRoute {
path: String,
script_path: String,
is_flow: bool,
route_path: String,
workspace_id: String,
is_async: bool,
requires_auth: bool,
edited_by: String,
email: String,
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>,
db: &DB,
user_db: UserDB,
) -> error::Result<(TriggerRoute, String, HashMap<String, String>, ApiAuthed)> {
let (mut triggers, route_path) = if *CLOUD_HOSTED {
let mut splitted = route_path.split("/");
let w_id = splitted.next().ok_or_else(|| {
error::Error::BadRequest("Missing workspace id in route path".to_string())
})?;
let route_path = StripPath(splitted.collect::<Vec<_>>().join("/"));
let triggers = sqlx::query_as!(
TriggerRoute,
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _" FROM http_trigger WHERE workspace_id = $1"#,
w_id
)
.fetch_all(db)
.await?;
(triggers, route_path)
} else {
let triggers = sqlx::query_as!(
TriggerRoute,
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _" FROM http_trigger"#,
)
.fetch_all(db)
.await?;
(triggers, StripPath(route_path.to_string()))
};
let mut router = matchit::Router::new();
for (idx, trigger) in triggers.iter().enumerate() {
let route_path = trigger.route_path.clone();
router.insert(route_path.as_str(), idx).unwrap_or_else(|e| {
tracing::warn!(
"Failed to consider http trigger route {}: {:?}",
route_path,
e,
);
});
}
let trigger_idx = router.at(route_path.0.as_str()).ok();
let matchit::Match { value: trigger_idx, params } =
not_found_if_none(trigger_idx, "Trigger", route_path.0.as_str())?;
let trigger = triggers.remove(trigger_idx.to_owned());
let params: HashMap<String, String> = params
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let username_override = if trigger.requires_auth {
if let Some(authed) = opt_authed {
// check that the user has access to the trigger
let mut tx = user_db.begin(&authed).await?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1 AND path = $2)",
trigger.workspace_id,
trigger.path
)
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
tx.commit().await?;
if exists {
Some(authed.display_username().to_owned())
} else {
return Err(error::Error::NotAuthorized("Unauthorized".to_string()));
}
} else {
return Err(error::Error::NotAuthorized(
"Requires authentication".to_string(),
));
}
} else {
None
};
let authed = fetch_api_authed(
trigger.edited_by.clone(),
trigger.email.clone(),
&trigger.workspace_id,
&db,
username_override.unwrap_or("anonymous".to_string()),
)
.await?;
Ok((trigger, route_path.0, params, authed))
}
async fn route_job(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(route_path): Path<StripPath>,
OptAuthed(opt_authed): OptAuthed,
Query(query): Query<HashMap<String, String>>,
method: http::Method,
headers: HeaderMap,
mut args: PushArgsOwned,
) -> impl IntoResponse {
let route_path = route_path.to_path();
let (trigger, called_path, params, authed) =
match get_http_route_trigger(route_path, opt_authed, &db, user_db.clone()).await {
Ok(trigger) => trigger,
Err(e) => return e.into_response(),
};
let headers = headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect::<HashMap<String, String>>();
let extra = args.extra.get_or_insert_with(HashMap::new);
extra.insert(
"wm_trigger".to_string(),
to_raw_value(&serde_json::json!({
"kind": "http",
"http": {
"route": trigger.route_path,
"path": called_path,
"method": method.to_string().to_lowercase(),
"params": params,
"query": query,
"headers": headers
},
})),
);
let http_method = http::Method::from(trigger.http_method);
if http_method != method {
return error::Error::BadRequest("Invalid HTTP method".to_string()).into_response();
}
let label_prefix = Some(format!(
"http-{}-{}-",
http_method.as_str().to_lowercase(),
trigger.route_path
));
let run_query = RunJobQuery::default();
if trigger.is_flow {
if trigger.is_async {
run_flow_by_path_inner(
authed,
db,
user_db,
rsmq,
trigger.workspace_id.clone(),
StripPath(trigger.script_path.to_owned()),
run_query,
args,
label_prefix,
)
.await
.into_response()
} else {
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
.into_response()
}
} else {
if trigger.is_async {
run_script_by_path_inner(
authed,
db,
user_db,
rsmq,
trigger.workspace_id.clone(),
StripPath(trigger.script_path.to_owned()),
run_query,
args,
label_prefix,
)
.await
.into_response()
} 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
.into_response()
}
}
}

View File

@@ -33,6 +33,7 @@ use crate::add_webhook_allowed_origin;
use crate::concurrency_groups::join_concurrency_key;
use crate::db::ApiAuthed;
use crate::users::get_scope_tags;
use crate::utils::content_plain;
use crate::{
db::DB,
@@ -58,7 +59,7 @@ use tower_http::cors::{Any, CorsLayer};
use urlencoding::encode;
use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE, SERVER_CONFIG};
use windmill_common::worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE};
use windmill_common::{
db::UserDB,
error::{self, to_anyhow, Error},
@@ -556,9 +557,10 @@ pub async fn get_path_tag_limits_cache_for_hash<'c, R: rsmq_async::RsmqConnectio
Option<i16>,
Option<bool>,
Option<i32>,
Option<bool>,
)> {
let script = sqlx::query!(
"select path, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout from script where hash = $1 AND workspace_id = $2",
"select path, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout, has_preprocessor from script where hash = $1 AND workspace_id = $2",
hash,
w_id
)
@@ -583,6 +585,7 @@ pub async fn get_path_tag_limits_cache_for_hash<'c, R: rsmq_async::RsmqConnectio
script.priority,
script.delete_after_use,
script.timeout,
script.has_preprocessor,
))
}
@@ -1058,6 +1061,7 @@ pub struct RunJobQuery {
pub tag: Option<String>,
pub timeout: Option<i32>,
pub cache_ttl: Option<i32>,
pub skip_preprocessor: Option<bool>,
}
impl RunJobQuery {
@@ -1245,13 +1249,18 @@ pub fn list_queue_jobs_query(
lq: &ListQueueQuery,
fields: &[&str],
join_outstanding_wait_times: bool,
tags: Option<Vec<&str>>,
) -> SqlBuilder {
let sqlb = SqlBuilder::select_from("queue")
let mut sqlb = SqlBuilder::select_from("queue")
.fields(fields)
.order_by("created_at", lq.order_desc.unwrap_or(true))
.limit(1000)
.clone();
if let Some(tags) = tags {
sqlb.and_where_in("tag", &tags.iter().map(|x| quote(x)).collect::<Vec<_>>());
}
filter_list_queue_query(sqlb, lq, w_id, join_outstanding_wait_times)
}
@@ -1307,6 +1316,7 @@ async fn list_queue_jobs(
"workspace_id",
],
false,
get_scope_tags(&authed),
)
.sql()?;
let mut tx = user_db.begin(&authed).await?;
@@ -1495,6 +1505,10 @@ async fn list_filtered_uuids(
sqlb.and_where_is_null("schedule_path");
if let Some(tags) = get_scope_tags(&authed) {
sqlb.and_where_in("tag", &tags.iter().map(|x| quote(x)).collect::<Vec<_>>());
}
sqlb = filter_list_queue_query(sqlb, &lq, w_id.as_str(), false);
let sql = sqlb.query()?;
@@ -1554,7 +1568,7 @@ async fn list_jobs(
Query(lq): Query<ListCompletedQuery>,
Extension(_api_list_jobs_query_duration): Extension<Option<Histo>>,
) -> error::JsonResult<Vec<Job>> {
check_scopes(&authed, || format!("listjobs"))?;
check_scopes(&authed, || format!("jobs:listjobs"))?;
let (per_page, offset) = paginate(pagination);
let lqc = lq.clone();
@@ -1572,6 +1586,7 @@ async fn list_jobs(
&ListCompletedQuery { order_desc: Some(true), ..lqc },
UnifiedJob::completed_job_fields(),
true,
get_scope_tags(&authed),
))
} else {
None
@@ -1587,6 +1602,7 @@ async fn list_jobs(
&ListQueueQuery { order_desc: Some(true), ..lq.into() },
UnifiedJob::queued_job_fields(),
true,
get_scope_tags(&authed),
);
if let Some(sqlc) = sqlc {
@@ -1637,7 +1653,7 @@ pub async fn resume_suspended_flow_as_owner(
Path((_w_id, flow_id)): Path<(String, Uuid)>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
check_scopes(&authed, || format!("resumeflow"))?;
check_scopes(&authed, || format!("jobs:resumeflow"))?;
let value = value.unwrap_or(serde_json::Value::Null);
let mut tx = db.begin().await?;
@@ -2784,14 +2800,18 @@ pub async fn run_flow_by_path_inner(
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let (tag, dedicated_worker) = sqlx::query!(
"SELECT tag, dedicated_worker from flow WHERE path = $1 and workspace_id = $2",
let (tag, dedicated_worker, has_preprocessor) = sqlx::query!(
"SELECT tag, dedicated_worker, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor
FROM flow
LEFT JOIN flow_version
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
WHERE flow.path = $1 and flow.workspace_id = $2",
flow_path,
w_id
)
.fetch_optional(&mut tx)
.await?
.map(|x| (x.tag, x.dedicated_worker))
.map(|x| (x.tag, x.dedicated_worker, x.has_preprocessor))
.ok_or_else(|| {
Error::NotFound(format!(
"flow not found at path {flow_path} in workspace {w_id}"
@@ -2807,7 +2827,12 @@ pub async fn run_flow_by_path_inner(
&db,
tx,
&w_id,
JobPayload::Flow { path: flow_path.to_string(), dedicated_worker },
JobPayload::Flow {
path: flow_path.to_string(),
dedicated_worker,
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
},
PushArgs { args: &args.args, extra: args.extra },
&label_prefix
.map(|x| x + authed.display_username())
@@ -2973,7 +2998,7 @@ pub async fn run_script_by_path_inner(
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let (job_payload, tag, _delete_after_use, timeout) =
script_path_to_payload(script_path, &mut tx, &w_id).await?;
script_path_to_payload(script_path, &mut tx, &w_id, run_query.skip_preprocessor).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(tag);
@@ -3049,7 +3074,15 @@ pub async fn run_workflow_as_code(
None,
run_query.timeout,
),
JobKind::Script => script_path_to_payload(job.script_path(), &mut tx, &w_id).await?,
JobKind::Script => {
script_path_to_payload(
job.script_path(),
&mut tx,
&w_id,
run_query.skip_preprocessor,
)
.await?
}
_ => return Err(anyhow::anyhow!("Not supported").into()),
};
@@ -3145,6 +3178,17 @@ impl Drop for Guard {
}
}
use std::sync::Arc;
use tokio::sync::RwLock;
lazy_static::lazy_static! {
pub static ref TIMEOUT_WAIT_RESULT: Arc<RwLock<Option<u64>>> = Arc::new(RwLock::new(
std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse::<u64>().ok())
));
}
#[derive(Deserialize)]
pub struct WindmillCompositeResult {
windmill_status_code: Option<u16>,
@@ -3159,7 +3203,7 @@ async fn run_wait_result(
username: &str,
) -> error::Result<Response> {
let mut result = None;
let timeout = SERVER_CONFIG.read().await.timeout_wait_result.clone();
let timeout = TIMEOUT_WAIT_RESULT.read().await.clone().unwrap_or(600);
let timeout_ms = if timeout <= 0 {
2000
} else {
@@ -3451,7 +3495,7 @@ pub async fn run_wait_result_job_by_path_get(
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let (job_payload, tag, delete_after_use, timeout) =
script_path_to_payload(script_path, &mut tx, &w_id).await?;
script_path_to_payload(script_path, &mut tx, &w_id, run_query.skip_preprocessor).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
@@ -3527,7 +3571,7 @@ pub async fn run_wait_result_flow_by_path_get(
let args = PushArgsOwned { extra: Some(payload_args), args: HashMap::new() };
run_wait_result_flow_by_path_internal(
db, run_query, flow_path, authed, rsmq, user_db, args, w_id,
db, run_query, flow_path, authed, rsmq, user_db, args, w_id, None,
)
.await
}
@@ -3553,11 +3597,12 @@ pub async fn run_wait_result_script_by_path(
user_db,
w_id,
args,
None,
)
.await
}
async fn run_wait_result_script_by_path_internal(
pub async fn run_wait_result_script_by_path_internal(
db: sqlx::Pool<Postgres>,
run_query: RunJobQuery,
script_path: StripPath,
@@ -3566,6 +3611,7 @@ async fn run_wait_result_script_by_path_internal(
user_db: UserDB,
w_id: String,
args: PushArgsOwned,
label_prefix: Option<String>,
) -> error::Result<Response> {
check_queue_too_long(&db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
let script_path = script_path.to_path();
@@ -3574,7 +3620,7 @@ async fn run_wait_result_script_by_path_internal(
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let (job_payload, tag, delete_after_use, timeout) =
script_path_to_payload(script_path, &mut tx, &w_id).await?;
script_path_to_payload(script_path, &mut tx, &w_id, run_query.skip_preprocessor).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
@@ -3587,7 +3633,9 @@ async fn run_wait_result_script_by_path_internal(
&w_id,
job_payload,
PushArgs { args: &args.args, extra: args.extra },
authed.display_username(),
&label_prefix
.map(|x| x + authed.display_username())
.unwrap_or_else(|| authed.display_username().to_string()),
&authed.email,
username_to_permissioned_as(&authed.username),
None,
@@ -3644,6 +3692,7 @@ pub async fn run_wait_result_script_by_hash(
priority,
delete_after_use,
timeout,
has_preprocessor,
) = get_path_tag_limits_cache_for_hash(&mut tx, &w_id, hash).await?;
if let Some(run_query_cache_ttl) = run_query.cache_ttl {
cache_ttl = Some(run_query_cache_ttl);
@@ -3669,6 +3718,8 @@ pub async fn run_wait_result_script_by_hash(
language,
dedicated_worker,
priority,
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
},
PushArgs { args: &args.args, extra: args.extra },
authed.display_username(),
@@ -3712,12 +3763,12 @@ pub async fn run_wait_result_flow_by_path(
check_license_key_valid().await?;
run_wait_result_flow_by_path_internal(
db, run_query, flow_path, authed, rsmq, user_db, args, w_id,
db, run_query, flow_path, authed, rsmq, user_db, args, w_id, None,
)
.await
}
async fn run_wait_result_flow_by_path_internal(
pub async fn run_wait_result_flow_by_path_internal(
db: sqlx::Pool<Postgres>,
run_query: RunJobQuery,
flow_path: StripPath,
@@ -3726,6 +3777,7 @@ async fn run_wait_result_flow_by_path_internal(
user_db: UserDB,
args: PushArgsOwned,
w_id: String,
label_prefix: Option<String>,
) -> error::Result<Response> {
check_queue_too_long(&db, run_query.queue_limit).await?;
@@ -3736,8 +3788,8 @@ async fn run_wait_result_flow_by_path_internal(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let (tag, dedicated_worker, early_return) = sqlx::query!(
"SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return
let (tag, dedicated_worker, early_return, has_preprocessor) = sqlx::query!(
"SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor
FROM flow
LEFT JOIN flow_version
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
@@ -3747,7 +3799,7 @@ async fn run_wait_result_flow_by_path_internal(
)
.fetch_optional(&mut tx)
.await?
.map(|x| (x.tag, x.dedicated_worker, x.early_return))
.map(|x| (x.tag, x.dedicated_worker, x.early_return, x.has_preprocessor))
.ok_or_else(|| {
Error::NotFound(format!(
"flow not found at path {flow_path} in workspace {w_id}"
@@ -3763,9 +3815,16 @@ async fn run_wait_result_flow_by_path_internal(
&db,
tx,
&w_id,
JobPayload::Flow { path: flow_path.to_string(), dedicated_worker },
JobPayload::Flow {
path: flow_path.to_string(),
dedicated_worker,
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
},
PushArgs { args: &args.args, extra: args.extra },
authed.display_username(),
&label_prefix
.map(|x| x + authed.display_username())
.unwrap_or_else(|| authed.display_username().to_string()),
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
@@ -3801,7 +3860,7 @@ async fn run_preview_script(
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
check_scopes(&authed, || format!("runscript"))?;
check_scopes(&authed, || format!("jobs:runscript"))?;
if authed.is_operator {
return Err(error::Error::NotAuthorized(
"Operators cannot run preview jobs for security reasons".to_string(),
@@ -3871,7 +3930,7 @@ async fn run_bundle_preview_script(
check_license_key_valid().await?;
check_scopes(&authed, || format!("runscript"))?;
check_scopes(&authed, || format!("jobs:runscript"))?;
if authed.is_operator {
return Err(error::Error::NotAuthorized(
"Operators cannot run preview jobs for security reasons".to_string(),
@@ -4202,6 +4261,7 @@ async fn add_batch_jobs(
_priority,
_delete_after_use,
timeout,
_,
) = get_latest_deployed_hash_for_path(&db, &w_id, &path).await?;
(
Some(script_hash),
@@ -4228,7 +4288,11 @@ async fn add_batch_jobs(
JobPayload::RawFlow { value: fv.clone(), path: None, restarted_from: None }
} else {
if let Some(path) = batch_info.path.as_ref() {
JobPayload::Flow { path: path.to_string(), dedicated_worker: None }
JobPayload::Flow {
path: path.to_string(),
dedicated_worker: None,
apply_preprocessor: false,
}
} else {
Err(anyhow::anyhow!(
"Path is required if no value is not provided"
@@ -4352,7 +4416,7 @@ async fn run_preview_flow_job(
Query(run_query): Query<RunJobQuery>,
Json(raw_flow): Json<PreviewFlow>,
) -> error::Result<(StatusCode, String)> {
check_scopes(&authed, || format!("runflow"))?;
check_scopes(&authed, || format!("jobs:runflow"))?;
if authed.is_operator {
return Err(error::Error::NotAuthorized(
"Operators cannot run preview jobs for security reasons".to_string(),
@@ -4450,6 +4514,7 @@ pub async fn run_job_by_hash_inner(
priority,
_delete_after_use, // not taken into account in async endpoints
timeout,
has_preprocessor,
) = get_path_tag_limits_cache_for_hash(&mut tx, &w_id, hash).await?;
check_scopes(&authed, || format!("run:script/{path}"))?;
if let Some(run_query_cache_ttl) = run_query.cache_ttl {
@@ -4475,6 +4540,8 @@ pub async fn run_job_by_hash_inner(
language,
dedicated_worker,
priority,
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
},
PushArgs { args: &args.args, extra: args.extra },
&label_prefix
@@ -4609,8 +4676,8 @@ async fn get_job_update(
"progress_perc"
)
.fetch_one(&db)
.await?
.fetch_optional(&db)
.await?.and_then(|inner| inner)
} else {
None
};
@@ -4789,14 +4856,19 @@ pub fn list_completed_jobs_query(
lq: &ListCompletedQuery,
fields: &[&str],
join_outstanding_wait_times: bool,
tags: Option<Vec<&str>>,
) -> SqlBuilder {
let sqlb = SqlBuilder::select_from("completed_job")
let mut sqlb = SqlBuilder::select_from("completed_job")
.fields(fields)
.order_by("created_at", lq.order_desc.unwrap_or(true))
.offset(offset)
.limit(per_page)
.clone();
if let Some(tags) = tags {
sqlb.and_where_in("tag", &tags.iter().map(|x| quote(x)).collect::<Vec<_>>());
}
filter_list_completed_query(sqlb, lq, w_id, join_outstanding_wait_times)
}
#[derive(Deserialize, Clone)]
@@ -4841,7 +4913,7 @@ async fn list_completed_jobs(
Query(pagination): Query<Pagination>,
Query(lq): Query<ListCompletedQuery>,
) -> error::JsonResult<Vec<ListableCompletedJob>> {
check_scopes(&authed, || format!("listjobs"))?;
check_scopes(&authed, || format!("jobs:listjobs"))?;
let (per_page, offset) = paginate(pagination);
@@ -4883,6 +4955,7 @@ async fn list_completed_jobs(
"'CompletedJob' as type",
],
false,
get_scope_tags(&authed),
)
.sql()?;
let mut tx = user_db.begin(&authed).await?;
@@ -5107,7 +5180,7 @@ async fn delete_completed_job<'a>(
Extension(user_db): Extension<UserDB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
check_scopes(&authed, || format!("deletejob"))?;
check_scopes(&authed, || format!("jobs:deletejob"))?;
let mut tx = user_db.begin(&authed).await?;

View File

@@ -60,6 +60,7 @@ mod flows;
mod folders;
mod granular_acls;
mod groups;
mod http_triggers;
mod indexer_ee;
mod inputs;
mod integration;
@@ -283,7 +284,8 @@ pub async fn run_server(
)
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc_ee::workspaced_service()),
.nest("/oidc", oidc_ee::workspaced_service())
.nest("/http_triggers", http_triggers::workspaced_service()),
)
.nest("/workspaces", workspaces::global_service())
.nest(
@@ -336,7 +338,7 @@ pub async fn run_server(
)
.nest(
"/w/:workspace_id/capture_u",
capture::global_service().layer(cors),
capture::global_service().layer(cors.clone()),
)
.nest(
"/auth",
@@ -346,6 +348,10 @@ pub async fn run_server(
"/oauth",
oauth2_ee::global_service().layer(Extension(Arc::clone(&sp_extension))),
)
.nest(
"/r",
http_triggers::routes_global_service().layer(from_extractor::<OptAuthed>()),
)
.route("/version", get(git_v))
.route("/uptodate", get(is_up_to_date))
.route("/ee_license", get(ee_license))

View File

@@ -6,6 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::collections::HashMap;
use crate::{
db::{ApiAuthed, DB},
users::{maybe_refresh_folders, require_owner_of_path, Tokened},
@@ -56,6 +58,7 @@ pub fn workspaced_service() -> Router {
.route("/type/exists/:name", get(exists_resource_type))
.route("/type/update/:name", post(update_resource_type))
.route("/type/delete/:name", delete(delete_resource_type))
.route("/file_resource_type_to_file_ext_map", get(file_resource_ext_to_resource_type))
.route("/type/create", post(create_resource_type))
}
@@ -71,6 +74,7 @@ pub struct ResourceType {
pub description: Option<String>,
pub created_by: Option<String>,
pub edited_at: Option<chrono::DateTime<chrono::Utc>>,
pub format_extension: Option<String>,
}
#[derive(Deserialize)]
@@ -78,6 +82,7 @@ pub struct CreateResourceType {
pub name: String,
pub schema: Option<serde_json::Value>,
pub description: Option<String>,
pub format_extension: Option<String>,
}
#[derive(Deserialize)]
@@ -913,6 +918,35 @@ async fn update_resource_value(
Ok(format!("value of resource {} updated", path))
}
async fn file_resource_ext_to_resource_type(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<HashMap<String, String>> {
#[derive(Serialize, sqlx::FromRow)]
struct LocalFileResourceExtension {
name: String,
format_extension: Option<String>,
}
let r = sqlx::query_as!(LocalFileResourceExtension, "
SELECT name, format_extension FROM resource_type WHERE format_extension IS NOT NULL AND (workspace_id = $1 OR workspace_id = 'admins')", w_id)
.fetch_all(&db)
.await?;
let hashmap: HashMap<String, String> = r
.into_iter()
.filter_map(|entry| {
if let Some(format_extension) = entry.format_extension {
Some((entry.name, format_extension))
} else {
None
}
})
.collect();
Ok(Json(hashmap))
}
async fn list_resource_types(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
@@ -996,13 +1030,14 @@ async fn create_resource_type(
sqlx::query!(
"INSERT INTO resource_type
(workspace_id, name, schema, description, created_by, edited_at)
VALUES ($1, $2, $3, $4, $5, now())",
(workspace_id, name, schema, description, created_by, format_extension, edited_at)
VALUES ($1, $2, $3, $4, $5, $6, now())",
w_id,
resource_type.name,
resource_type.schema,
resource_type.description,
authed.username
authed.username,
resource_type.format_extension,
)
.execute(&mut *tx)
.await?;

View File

@@ -103,6 +103,8 @@ pub struct ScriptWDraft {
pub visible_to_runner_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_main_func: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_preprocessor: Option<bool>,
}
pub fn global_service() -> Router {
@@ -581,6 +583,7 @@ async fn create_script_internal<'c>(
|| ns.language == ScriptLang::Bunnative
|| ns.language == ScriptLang::Deno
|| ns.language == ScriptLang::Rust
|| ns.language == ScriptLang::Ansible
|| ns.language == ScriptLang::Php)
{
Some(String::new())
@@ -612,8 +615,8 @@ async fn create_script_internal<'c>(
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, \
dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \
delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30)",
delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31)",
&w_id,
&hash.0,
ns.path,
@@ -643,7 +646,8 @@ async fn create_script_internal<'c>(
ns.concurrency_key,
ns.visible_to_runner_only,
ns.no_main_func,
codebase
codebase,
ns.has_preprocessor,
)
.execute(&mut tx)
.await?;
@@ -738,6 +742,7 @@ async fn create_script_internal<'c>(
},
);
}
let permissioned_as = username_to_permissioned_as(&authed.username);
if needs_lock_gen {
let tag = if ns.dedicated_worker.is_some_and(|x| x) {
@@ -878,7 +883,7 @@ async fn get_script_by_path_w_draft(
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func FROM script LEFT JOIN draft ON
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, has_preprocessor FROM script LEFT JOIN draft ON
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
WHERE script.path = $1 AND script.workspace_id = $2 \
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \

View File

@@ -24,6 +24,8 @@ use axum::{
#[cfg(feature = "enterprise")]
use axum::extract::Query;
#[cfg(feature = "enterprise")]
use windmill_common::ee::{send_critical_alert, CriticalAlertKind, CriticalErrorChannel};
use serde::Deserialize;
use windmill_common::{
error::{self, JsonResult, Result},
@@ -55,7 +57,8 @@ pub fn global_service() -> Router {
get(get_latest_key_renewal_attempt),
)
.route("/renew_license_key", post(renew_license_key))
.route("/customer_portal", post(create_customer_portal_session));
.route("/customer_portal", post(create_customer_portal_session))
.route("/test_critical_channels", post(test_critical_channels));
#[cfg(feature = "parquet")]
{
@@ -387,3 +390,27 @@ pub async fn create_customer_portal_session(
return Ok(url);
}
#[cfg(feature = "enterprise")]
pub async fn test_critical_channels(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Json(test_critical_channels): Json<Vec<CriticalErrorChannel>>,
) -> Result<String> {
require_super_admin(&db, &authed.email).await?;
#[cfg(feature = "enterprise")]
send_critical_alert(
"Test critical error".to_string(),
&db,
CriticalAlertKind::CriticalError,
Some(test_critical_channels),
)
.await;
Ok("Sent test critical error".to_string())
}
#[cfg(not(feature = "enterprise"))]
pub async fn test_critical_channels() -> Result<String> {
Ok("Critical channels require EE".to_string())
}

View File

@@ -51,7 +51,7 @@ use windmill_audit::ActionKind;
use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING;
use windmill_common::users::truncate_token;
use windmill_common::utils::{paginate, send_email};
use windmill_common::worker::{CLOUD_HOSTED, SERVER_CONFIG};
use windmill_common::worker::{CLOUD_HOSTED, SMTP_CONFIG};
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, JWT_SECRET},
db::UserDB,
@@ -651,9 +651,12 @@ where
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
parts.extensions.insert(authed.clone());
if authed.scopes.is_some()
&& (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
@@ -681,15 +684,30 @@ pub fn check_scopes<F>(authed: &ApiAuthed, required: F) -> error::Result<()>
where
F: FnOnce() -> String,
{
if let Some(scopes) = &authed.scopes {
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) {
let req = &required();
if !scopes.contains(req) {
if !authed.scopes.as_ref().unwrap().contains(req) {
return Err(Error::BadRequest(format!("missing required scope: {req}")));
}
}
Ok(())
}
pub fn get_scope_tags(authed: &ApiAuthed) -> Option<Vec<&str>> {
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)]
pub struct OptAuthed(pub Option<ApiAuthed>);
@@ -1966,7 +1984,7 @@ pub fn send_email_if_possible(subject: &str, content: &str, to: &str) {
}
pub async fn send_email_if_possible_intern(subject: &str, content: &str, to: String) -> Result<()> {
if let Some(smtp) = SERVER_CONFIG.read().await.smtp.clone() {
if let Some(smtp) = SMTP_CONFIG.read().await.clone() {
send_email(subject, content, vec![to], smtp, None).await?;
}
return Ok(());

View File

@@ -2317,6 +2317,8 @@ struct ScriptMetadata {
pub codebase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_preprocessor: Option<bool>,
}
pub fn is_none_or_false(val: &Option<bool>) -> bool {
@@ -2575,6 +2577,7 @@ async fn tarball_workspace(
}
ScriptLang::Php => "php",
ScriptLang::Rust => "rs",
ScriptLang::Ansible => "playbook.yml",
};
archive
.write_to_archive(&script.content, &format!("{}.{}", script.path, ext))
@@ -2601,6 +2604,7 @@ async fn tarball_workspace(
no_main_func: script.no_main_func,
codebase: script.codebase,
concurrency_key: script.concurrency_key,
has_preprocessor: script.has_preprocessor,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
archive

View File

@@ -25,6 +25,7 @@ pub struct JWTAuthClaims {
pub workspace_id: String,
pub exp: usize,
pub job_id: Option<String>,
pub scopes: Option<Vec<String>>,
}
#[derive(Deserialize)]

View File

@@ -26,7 +26,10 @@ pub async fn get_license_plan() -> LicensePlan {
#[derive(Deserialize)]
#[serde(untagged)]
pub enum CriticalErrorChannel {}
pub enum CriticalErrorChannel {
Email { email: String },
Slack { slack_channel: String },
}
pub enum CriticalAlertKind {
#[cfg(feature = "enterprise")]
@@ -36,7 +39,13 @@ pub enum CriticalAlertKind {
}
#[cfg(feature = "enterprise")]
pub async fn send_critical_alert(_error_message: String, _db: &DB, _kind: CriticalAlertKind) {}
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) -> () {

View File

@@ -29,6 +29,7 @@ pub struct FlowStatus {
pub step: i32,
pub modules: Vec<FlowStatusModule>,
pub failure_module: Box<FlowStatusModuleWParent>,
pub preprocessor_module: Option<FlowStatusModule>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
@@ -355,7 +356,11 @@ impl FlowStatusModule {
impl FlowStatus {
pub fn new(f: &FlowValue) -> Self {
Self {
step: 0,
step: if f.preprocessor_module.is_some() {
-1
} else {
0
},
approval_conditions: None,
modules: f
.modules
@@ -372,6 +377,13 @@ impl FlowStatus {
.unwrap_or_else(|| "failure".to_string()),
},
}),
preprocessor_module: if f.preprocessor_module.is_some() {
Some(FlowStatusModule::WaitingForPriorSteps {
id: f.preprocessor_module.as_ref().unwrap().id.clone(),
})
} else {
None
},
cleanup_module: FlowCleanupModule { flow_jobs_to_clean: vec![] },
retry: RetryStatus { fail_count: 0, failed_jobs: vec![] },
restarted_from: None,

View File

@@ -102,6 +102,8 @@ pub struct FlowValue {
#[serde(default)]
pub failure_module: Option<Box<FlowModule>>,
#[serde(default)]
pub preprocessor_module: Option<Box<FlowModule>>,
#[serde(default)]
#[serde(skip_serializing_if = "is_default")]
pub same_worker: bool,

View File

@@ -14,6 +14,8 @@ pub const EXTRA_PIP_INDEX_URL_SETTING: &str = "pip_extra_index_url";
pub const PIP_INDEX_URL_SETTING: &str = "pip_index_url";
pub const SCIM_TOKEN_SETTING: &str = "scim_token";
pub const SAML_METADATA_SETTING: &str = "saml_metadata";
pub const SMTP_SETTING: &str = "smtp_settings";
pub const TIMEOUT_WAIT_RESULT_SETTING: &str = "timeout_wait_result";
pub const UNIQUE_ID_SETTING: &str = "uid";
pub const DISABLE_STATS_SETTING: &str = "disable_stats";

View File

@@ -11,6 +11,8 @@ use uuid::Uuid;
pub const ENTRYPOINT_OVERRIDE: &str = "_ENTRYPOINT_OVERRIDE";
pub const PREPROCESSOR_FAKE_ENTRYPOINT: &str = "__WM_PREPROCESSOR";
use crate::{
error::{self, to_anyhow, Error},
flow_status::{FlowStatus, RestartedFrom},
@@ -302,6 +304,7 @@ pub enum JobPayload {
dedicated_worker: Option<bool>,
language: ScriptLang,
priority: Option<i16>,
apply_preprocessor: bool,
},
Code(RawCode),
Dependencies {
@@ -331,6 +334,7 @@ pub enum JobPayload {
Flow {
path: String,
dedicated_worker: Option<bool>,
apply_preprocessor: bool,
},
RestartedFlow {
completed_job_id: Uuid,
@@ -383,6 +387,7 @@ pub async fn script_path_to_payload<'e, E: sqlx::Executor<'e, Database = Postgre
script_path: &str,
db: E,
w_id: &str,
skip_preprocessor: Option<bool>,
) -> error::Result<(JobPayload, Option<Tag>, Option<bool>, Option<i32>)> {
let (job_payload, tag, delete_after_use, script_timeout) = if script_path.starts_with("hub/") {
(
@@ -404,6 +409,7 @@ pub async fn script_path_to_payload<'e, E: sqlx::Executor<'e, Database = Postgre
priority,
delete_after_use,
script_timeout,
has_preprocessor,
) = get_latest_deployed_hash_for_path(db, w_id, script_path).await?;
(
JobPayload::ScriptHash {
@@ -416,6 +422,8 @@ pub async fn script_path_to_payload<'e, E: sqlx::Executor<'e, Database = Postgre
language,
dedicated_worker,
priority,
apply_preprocessor: !skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
},
tag,
delete_after_use,
@@ -473,7 +481,7 @@ pub async fn get_payload_tag_from_prefixed_path<'e, E: sqlx::Executor<'e, Databa
w_id: &str,
) -> Result<(JobPayload, Option<String>), Error> {
let (payload, tag, _, _) = if path.starts_with("script/") {
script_path_to_payload(path.strip_prefix("script/").unwrap(), db, w_id).await?
script_path_to_payload(path.strip_prefix("script/").unwrap(), db, w_id, Some(true)).await?
} else if path.starts_with("flow/") {
let path = path.strip_prefix("flow/").unwrap().to_string();
let r = sqlx::query!(
@@ -486,7 +494,12 @@ pub async fn get_payload_tag_from_prefixed_path<'e, E: sqlx::Executor<'e, Databa
let (tag, dedicated_worker) = r
.map(|x| (x.tag, x.dedicated_worker))
.unwrap_or_else(|| (None, None));
(JobPayload::Flow { path, dedicated_worker }, tag, None, None)
(
JobPayload::Flow { path, dedicated_worker, apply_preprocessor: false },
tag,
None,
None,
)
} else {
return Err(Error::BadRequest(format!(
"path must start with script/ or flow/ (got {})",

View File

@@ -271,9 +271,10 @@ pub async fn get_latest_deployed_hash_for_path<'e, E: sqlx::Executor<'e, Databas
Option<i16>,
Option<bool>,
Option<i32>,
Option<bool>,
)> {
let r_o = sqlx::query!(
"select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout from script where path = $1 AND workspace_id = $2 AND
"select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker, priority, delete_after_use, timeout, has_preprocessor from script where path = $1 AND workspace_id = $2 AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
script_path,
@@ -296,6 +297,7 @@ pub async fn get_latest_deployed_hash_for_path<'e, E: sqlx::Executor<'e, Databas
script.priority,
script.delete_after_use,
script.timeout,
script.has_preprocessor,
))
}

View File

@@ -44,6 +44,7 @@ pub enum ScriptLang {
Mssql,
Php,
Rust,
Ansible,
}
impl ScriptLang {
@@ -64,7 +65,8 @@ impl ScriptLang {
ScriptLang::Mssql => "mssql",
ScriptLang::Graphql => "graphql",
ScriptLang::Php => "php",
ScriptLang::Rust => "rust"
ScriptLang::Rust => "rust",
ScriptLang::Ansible => "ansible",
}
}
}
@@ -191,6 +193,8 @@ pub struct Script {
pub no_main_func: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub codebase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_preprocessor: Option<bool>,
}
#[derive(Serialize, sqlx::FromRow)]
@@ -287,6 +291,7 @@ pub struct NewScript {
pub visible_to_runner_only: Option<bool>,
pub no_main_func: Option<bool>,
pub codebase: Option<String>,
pub has_preprocessor: Option<bool>,
}
fn lock_deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>

View File

@@ -13,22 +13,20 @@ pub struct Smtp {
}
#[derive(Serialize, Deserialize, PartialEq)]
pub struct ServerConfigOpt {
pub struct SmtpConfigOpt {
pub smtp_host: Option<String>,
pub smtp_username: Option<String>,
pub smtp_password: Option<String>,
pub smtp_port: Option<u16>,
pub smtp_from: Option<String>,
pub smtp_tls_implicit: Option<bool>,
pub timeout_wait_result: Option<u64>,
}
pub async fn load_server_config(db: &DB) -> error::Result<ServerConfig> {
let config: ServerConfigOpt =
sqlx::query_scalar!("SELECT config FROM config WHERE name = 'server'",)
pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
let config: SmtpConfigOpt =
sqlx::query_scalar!("SELECT value FROM global_settings WHERE name = 'smtp_settings'",)
.fetch_optional(db)
.await?
.flatten()
.map(|x| serde_json::from_value(x).ok())
.flatten()
.unwrap_or_default();
@@ -77,20 +75,10 @@ pub async fn load_server_config(db: &DB) -> error::Result<ServerConfig> {
tracing::warn!("SMTP not configured");
}
Ok(ServerConfig {
smtp,
timeout_wait_result: config
.timeout_wait_result
.ok_or(
std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse::<u64>().ok()),
)
.unwrap_or(600),
})
Ok(smtp)
}
impl Default for ServerConfigOpt {
impl Default for SmtpConfigOpt {
fn default() -> Self {
Self {
smtp_from: None,
@@ -99,13 +87,6 @@ impl Default for ServerConfigOpt {
smtp_port: None,
smtp_tls_implicit: None,
smtp_username: None,
timeout_wait_result: Default::default(),
}
}
}
#[derive(PartialEq, Clone, Debug)]
pub struct ServerConfig {
pub smtp: Option<Smtp>,
pub timeout_wait_result: u64,
}

View File

@@ -288,7 +288,7 @@ pub async fn report_critical_error(error_message: String, _db: DB) -> () {
}
#[cfg(feature = "enterprise")]
send_critical_alert(error_message, &_db, CriticalAlertKind::CriticalError).await;
send_critical_alert(error_message, &_db, CriticalAlertKind::CriticalError, None).await;
}
pub async fn report_recovered_critical_error(message: String, _db: DB) -> () {
@@ -304,5 +304,11 @@ pub async fn report_recovered_critical_error(message: String, _db: DB) -> () {
tracing::error!("Failed to save critical error to database: {}", err);
}
#[cfg(feature = "enterprise")]
send_critical_alert(message, &_db, CriticalAlertKind::RecoveredCriticalError).await;
send_critical_alert(
message,
&_db,
CriticalAlertKind::RecoveredCriticalError,
None,
)
.await;
}

View File

@@ -8,13 +8,13 @@ use std::{
collections::{HashMap, HashSet},
fs::File,
io::Write,
path::Path,
path::{Component, Path, PathBuf},
str::FromStr,
sync::{atomic::AtomicBool, Arc},
};
use tokio::sync::RwLock;
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, server::ServerConfig, DB};
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, server::Smtp, DB};
lazy_static::lazy_static! {
pub static ref WORKER_GROUP: String = std::env::var("WORKER_GROUP").unwrap_or_else(|_| "default".to_string());
@@ -41,6 +41,7 @@ lazy_static::lazy_static! {
"graphql".to_string(),
"php".to_string(),
"rust".to_string(),
"ansible".to_string(),
"dependency".to_string(),
"flow".to_string(),
"other".to_string()
@@ -65,8 +66,7 @@ lazy_static::lazy_static! {
pub static ref WORKER_SUSPENDED_PULL_QUERY: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref SERVER_CONFIG: Arc<RwLock<ServerConfig>> = Arc::new(RwLock::new(ServerConfig { smtp: Default::default(), timeout_wait_result: 20 }));
pub static ref SMTP_CONFIG: Arc<RwLock<Option<Smtp>>> = Arc::new(RwLock::new(None));
pub static ref CLOUD_HOSTED: bool = std::env::var("CLOUD_HOSTED").is_ok();
@@ -167,6 +167,67 @@ pub fn write_file(dir: &str, path: &str, content: &str) -> error::Result<File> {
Ok(file)
}
/// from : https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
pub fn write_file_at_user_defined_location(
job_dir: &str,
user_defined_path: &str,
content: &str,
) -> error::Result<PathBuf> {
let job_dir = Path::new(job_dir);
let user_path = PathBuf::from(user_defined_path);
let full_path = job_dir.join(&user_path);
// let normalized_job_dir = std::fs::canonicalize(job_dir)?;
// let normalized_full_path = std::fs::canonicalize(&full_path)?;
let normalized_job_dir = normalize_path(job_dir);
let normalized_full_path = normalize_path(&full_path);
if !normalized_full_path.starts_with(&normalized_job_dir) {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Path is outside the allowed job directory.",
)
.into());
}
let full_path = normalized_full_path.as_path();
if let Some(parent_dir) = full_path.parent() {
std::fs::create_dir_all(parent_dir)?;
}
let mut file = File::create(full_path)?;
file.write_all(content.as_bytes())?;
file.flush()?;
Ok(normalized_full_path)
}
pub async fn reload_custom_tags_setting(db: &DB) -> error::Result<()> {
let q = sqlx::query!(
"SELECT value FROM global_settings WHERE name = $1",

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