Compare commits

..

9 Commits

Author SHA1 Message Date
Alexander Petric
f9d194894d updating hub script: adding error logging to git sync 2024-12-12 18:05:34 +01:00
Alexander Petric
503930ff56 Merge branch 'main' into alp/slack_interactive_approvals 2024-12-12 17:06:50 +01:00
Alexander Petric
3529625774 date time picker and default values 2024-12-12 17:00:46 +01:00
Alexander Petric
045307ab41 polish messages 2024-12-12 01:25:49 +01:00
Alexander Petric
e437764f5e adding python client 2024-12-12 00:11:43 +01:00
Alexander Petric
6b3881bd00 Merge branch 'main' into alp/slack_interactive_approvals 2024-12-11 10:35:01 +01:00
Alexander Petric
410fb25309 move form creation logic to backend 2024-12-11 10:34:33 +01:00
Alexander Petric
804fd3468c Merge branch 'main' into alp/slack_interactive_approvals 2024-12-05 14:56:30 -05:00
Alexander Petric
e27a97dfbb feat: interactive slack approvals 2024-12-03 18:04:01 -05:00
284 changed files with 5221 additions and 9517 deletions

View File

@@ -52,6 +52,4 @@ RUN unzip deno.zip && rm deno.zip && mv deno /usr/bin/deno
RUN apt-get update \
&& apt-get install -y postgresql-client --allow-unauthenticated
RUN rustup component add rustfmt
COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk
RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet
RUN rustup component add rustfmt

View File

@@ -1,134 +0,0 @@
name: Backend check
on:
push:
paths:
- "backend/**"
- ".github/workflows/backend-check.yml"
jobs:
check_oss:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
run: SQLX_OFFLINE=true cargo check
check_oss_full:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: install xmlsec1
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
run: |
mkdir -p fake_frontend_build
FRONTEND_BUILD_DIR=$(pwd)/fake_frontend_build SQLX_OFFLINE=true cargo check --all-features
check_ee:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Substitute EE code (EE logic is behind feature flag)
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
run: SQLX_OFFLINE=true cargo check
check_ee_full:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: install xmlsec1
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev
- name: Substitute EE code (EE logic is behind feature flag)
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
timeout-minutes: 16
working-directory: ./backend
run: |
mkdir -p fake_frontend_build
FRONTEND_BUILD_DIR=$(pwd)/fake_frontend_build SQLX_OFFLINE=true cargo check --all-features

View File

@@ -13,58 +13,40 @@ on:
- "backend/**"
- ".github/workflows/backend-test.yml"
defaults:
run:
working-directory: ./backend
jobs:
cargo_test:
runs-on: ubicloud-standard-8
container:
image: ghcr.io/windmill-labs/backend-tests
services:
postgres:
image: postgres
ports:
- 5432:5432
env:
POSTGRES_DB: windmill
POSTGRES_PASSWORD: changeme
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: actions/setup-go@v2
with:
go-version: 1.21.5
- uses: actions/setup-python@v5
with:
python-version: 3.11
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.1.40
- uses: astral-sh/setup-uv@v4
with:
version: "0.4.18"
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
toolchain: 1.80.0
# - uses: Swatinem/rust-cache@v2
# with:
# workspaces: |
# backend
# backend -> target
- name: cargo test
timeout-minutes: 16
timeout-minutes: 15
run:
deno --version && bun -v && go version && python3 --version &&
SQLX_OFFLINE=true
DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill
DISABLE_EMBEDDING=true RUST_LOG=info PYTHON_PATH=$(which python)
DENO_PATH=$(which deno) BUN_PATH=$(which bun) GO_PATH=$(which go)
UV_PATH=$(which uv) cargo test --features
enterprise,deno_core,license,python,rust,scoped_cache --all -- --nocapture
/usr/bin/deno --version &&
/usr/bin/bun -v &&
go version &&
/usr/local/bin/python3 --version &&
mkdir frontend/build && cd backend && touch
windmill-api/openapi-deref.yaml &&
DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill
DISABLE_EMBEDDING=true RUST_LOG=info cargo test --features
enterprise,deno_core --all -- --nocapture

View File

@@ -64,7 +64,7 @@ jobs:
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,http_trigger,zip,oauth2,kafka,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka,php,mysql
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -81,7 +81,7 @@ jobs:
platforms: linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,http_trigger,zip,oauth2,kafka,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka,php,mysql
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -111,16 +111,12 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: RHEL9-amd64 build
path:
${{ steps.extract-ee-amd64.outputs.destination
}}/windmill-ee-amd64-rhel9
path: ${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9
- uses: actions/upload-artifact@v4
with:
name: RHEL9-arm64 build
path:
${{ steps.extract-ee-arm64.outputs.destination
}}/windmill-ee-arm64-rhel9
path: ${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel9
# - name: Attach binary to release
# uses: softprops/action-gh-release@v2

View File

@@ -0,0 +1,70 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
name: Build windmill-staging
on:
workflow_dispatch:
permissions: write-all
jobs:
build_ee:
runs-on: ubicloud
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-staging-ee
flavor: |
latest=false
tags: |
type=sha
type=ref,event=branch
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Substitute EE code
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Build and push publicly ee
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,kafka,php,mysql
tags: |
${{ steps.meta-ee-public.outputs.tags }}
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License

View File

@@ -45,7 +45,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,php,mysql
- name: Rename binary with corresponding architecture
run: |

View File

@@ -19,10 +19,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# - uses: depot/setup-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
@@ -43,7 +40,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly
uses: docker/build-push-action@v6
uses: depot/build-push-action@v1
with:
context: .
file: ./docker/DockerfileMultiplayer

View File

@@ -67,7 +67,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=embedding,parquet,openidconnect,deno_core,license,http_trigger,zip,oauth2,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
features=embedding,parquet,openidconnect,deno_core,php,mysql
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ steps.meta-public.outputs.tags }}

View File

@@ -1,9 +1,10 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME:
${{ github.event_name != 'pull_request' && github.event_name != 'workflow_dispatch' && github.repository || 'windmill-labs/windmill-test' }}
DEV_SHA:
${{ github.event_name != 'pull_request' && github.event_name != 'workflow_dispatch' && 'dev' || github.event.inputs.tag || github.sha }}
IMAGE_NAME: ${{ github.event_name != 'pull_request' && github.repository ||
'windmill-labs/windmill-test' }}
DEV_SHA: ${{ github.event_name != 'pull_request' && 'dev' || format('pr-{0}',
github.event.number) }}
name: Build windmill:main
on:
push:
@@ -13,17 +14,6 @@ on:
types: [opened, synchronize, reopened]
paths:
- "Dockerfile"
workflow_dispatch:
inputs:
ee:
description: 'Build EE image (true, false)'
required: false
default: false
type: boolean
tag:
description: 'Tag the image'
required: true
default: 'test'
concurrency:
group: ${{ github.ref }}
@@ -34,7 +24,7 @@ permissions: write-all
jobs:
build:
runs-on: ubicloud
if: (github.event_name != 'workflow_dispatch') || (github.event.inputs && !github.event.inputs.ee)
if: (github.event_name != 'issue_comment') || (contains(github.event.comment.body, '/buildimage_all') || contains(github.event.comment.body, '/buildimage_base'))
steps:
- uses: actions/checkout@v4
with:
@@ -86,7 +76,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=embedding,parquet,openidconnect,jemalloc,deno_core,license,http_trigger,zip,oauth2,dind,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
features=embedding,parquet,openidconnect,jemalloc,deno_core,dind,php,mysql
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
${{ steps.meta-public.outputs.tags }}
@@ -96,8 +86,7 @@ jobs:
build_ee:
runs-on: ubicloud
if:
(github.event_name != 'workflow_dispatch') || (github.event.inputs.ee)
if: (github.event_name != 'issue_comment') || (contains(github.event.comment.body, '/buildimage_ee') || contains(github.event.comment.body, '/buildimage_nsjail')) || contains(github.event.comment.body, '/buildimage_all')
steps:
- uses: actions/checkout@v4
with:
@@ -149,7 +138,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,otel,dind,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,otel,dind,php,mysql
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}
${{ steps.meta-ee-public.outputs.tags }}
@@ -211,7 +200,7 @@ jobs:
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,otel,dind,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,otel,dind,php,mysql
PYTHON_IMAGE=python:3.12.2-slim-bookworm
tags: |
${{ steps.meta-ee-public-py312.outputs.tags }}
@@ -367,9 +356,7 @@ jobs:
tag_latest:
runs-on: ubicloud
needs: [run_integration_test, build]
if:
github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' ||
startsWith(github.ref, 'refs/tags/v'))
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
steps:
- uses: actions/checkout@v4
with:
@@ -388,9 +375,7 @@ jobs:
tag_latest_ee:
runs-on: ubicloud
needs: [run_integration_test, build_ee]
if:
github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' ||
startsWith(github.ref, 'refs/tags/v'))
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
steps:
- uses: actions/checkout@v4
with:
@@ -451,8 +436,7 @@ jobs:
build_ee_nsjail:
needs: [build_ee]
runs-on: ubicloud
if:
(github.event_name != 'pull_request') && (github.event_name != 'workflow_dispatch')
if: (github.event_name != 'issue_comment') || (github.event_name != 'pull_request') || (contains(github.event.comment.body, '/buildimage_nsjail') || contains(github.event.comment.body, '/buildimage_all'))
steps:
- uses: actions/checkout@v4
with:
@@ -545,7 +529,7 @@ jobs:
publish_ecr_s3:
needs: [build_ee_nsjail]
runs-on: ubicloud-standard-2-arm
if: (github.event_name != 'pull_request') && (github.event_name != 'workflow_dispatch')
if: github.event_name != 'pull_request'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View File

@@ -15,6 +15,7 @@ jobs:
with:
node-version: 18
- name: "npm check"
timeout-minutes: 5
run: cd frontend && npm ci && npm run generate-backend-client && npm run
timeout-minutes: 2
run:
cd frontend && npm ci && npm run generate-backend-client && npm run
check

View File

@@ -47,7 +47,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,php,mysql,mssql,bigquery,websocket,python,smtp,csharp,static_frontend,rust
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,kafka,php,mysql
- name: Rename binary with corresponding architecture
run: |

View File

@@ -1,78 +1,5 @@
# Changelog
## [1.441.0](https://github.com/windmill-labs/windmill/compare/v1.440.3...v1.441.0) (2024-12-20)
### Features
* interactive slack approvals ([#4942](https://github.com/windmill-labs/windmill/issues/4942)) ([6308bf0](https://github.com/windmill-labs/windmill/commit/6308bf0dcb1d6670e839a1a1e0b794bf3ce6520c))
## [1.440.3](https://github.com/windmill-labs/windmill/compare/v1.440.2...v1.440.3) (2024-12-19)
### Bug Fixes
* update bun from 1.1.38 to 1.1.40 ([c4fdd22](https://github.com/windmill-labs/windmill/commit/c4fdd2297efc43ce557cc9791151301377126c29))
## [1.440.2](https://github.com/windmill-labs/windmill/compare/v1.440.1...v1.440.2) (2024-12-18)
### Bug Fixes
* fix redeploying flows with attached schedules ([fb536df](https://github.com/windmill-labs/windmill/commit/fb536df0668d49d14f4aed98870caaad396d0389))
## [1.440.1](https://github.com/windmill-labs/windmill/compare/v1.440.0...v1.440.1) (2024-12-18)
### Bug Fixes
* **internal:** updating rust to 1.82 ([02a8f1f](https://github.com/windmill-labs/windmill/commit/02a8f1f86453a5f8769364ba3798998b8830d086))
## [1.440.0](https://github.com/windmill-labs/windmill/compare/v1.439.0...v1.440.0) (2024-12-18)
### Features
* **cache:** remove persistent raw values from queue ([#4866](https://github.com/windmill-labs/windmill/issues/4866)) ([977ac5c](https://github.com/windmill-labs/windmill/commit/977ac5c3f3c2e8224f4915e483ae60f28ce008fc))
### Bug Fixes
* add workspace selector and fix css for create webhook page ([#4939](https://github.com/windmill-labs/windmill/issues/4939)) ([d880655](https://github.com/windmill-labs/windmill/commit/d8806555f1d78a5fae7ae77acbcdad402e89951d))
* fix relative imports in cached flow scripts ([13be0cd](https://github.com/windmill-labs/windmill/commit/13be0cd1c822d7a809dc96914dd1286510b9f9eb))
## [1.439.0](https://github.com/windmill-labs/windmill/compare/v1.438.0...v1.439.0) (2024-12-15)
### Features
* add multipart/form-data support ([#4927](https://github.com/windmill-labs/windmill/issues/4927)) ([83a60cb](https://github.com/windmill-labs/windmill/commit/83a60cbc517d5ddab24b247fd5e452175d59ad07))
### Bug Fixes
* ECS terraform db url + ami issues ([#4924](https://github.com/windmill-labs/windmill/issues/4924)) ([5172c13](https://github.com/windmill-labs/windmill/commit/5172c13ab8e9aeb1a83c161e7e7f63ebfb40b008))
## [1.438.0](https://github.com/windmill-labs/windmill/compare/v1.437.1...v1.438.0) (2024-12-13)
### Features
* accept direct file upload for webhook/http (s3) ([#4903](https://github.com/windmill-labs/windmill/issues/4903)) ([563a492](https://github.com/windmill-labs/windmill/commit/563a49200898bc32ab114f63c7dd575000f49e60))
* add C# support ([#4908](https://github.com/windmill-labs/windmill/issues/4908)) ([c85d2a4](https://github.com/windmill-labs/windmill/commit/c85d2a495715e9506984b0d26bcf34a25d476c09))
* **backend:** handle xml payload as raw_string ([#4915](https://github.com/windmill-labs/windmill/issues/4915)) ([3864cfc](https://github.com/windmill-labs/windmill/commit/3864cfce246d2b0d1a61b46578ed39221ebbbd31))
* **cache:** re-work job results cache ([#4898](https://github.com/windmill-labs/windmill/issues/4898)) ([af5cca1](https://github.com/windmill-labs/windmill/commit/af5cca1b002ecb6eae14684620e0a695394b6df8))
### Bug Fixes
* add `DOTNET_ROOT` env variable ([#4921](https://github.com/windmill-labs/windmill/issues/4921)) ([eb3ed7a](https://github.com/windmill-labs/windmill/commit/eb3ed7a2c7623a5ee87e245dfda16266ed8e467c))
* app custom url diff ([#4914](https://github.com/windmill-labs/windmill/issues/4914)) ([952cbd1](https://github.com/windmill-labs/windmill/commit/952cbd182de5358cc326d7582f4494c860e3836e))
* c#: nsjail image, default langs and feature cage imports ([#4917](https://github.com/windmill-labs/windmill/issues/4917)) ([afac8a7](https://github.com/windmill-labs/windmill/commit/afac8a73f0a76850ff9eca0e368db0e3ec793328))
* flow node default tag ([3634ade](https://github.com/windmill-labs/windmill/commit/3634ade41b07cbae0fd1ba40b109815b7f57562d))
* **frontend:** form and content update when script is emptied ([#4887](https://github.com/windmill-labs/windmill/issues/4887)) ([4ce4fba](https://github.com/windmill-labs/windmill/commit/4ce4fba18f529f5f81ad6e242059f50254fc509b))
* **frontend:** schedule operator perms + add instance settings in operator menu ([#4912](https://github.com/windmill-labs/windmill/issues/4912)) ([762ac30](https://github.com/windmill-labs/windmill/commit/762ac30b59e1d23d2a043a86c2ce4b069030941d))
## [1.437.1](https://github.com/windmill-labs/windmill/compare/v1.437.0...v1.437.1) (2024-12-10)

View File

@@ -1,5 +1,5 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.82-slim-bookworm
ARG RUST_IMAGE=rust:1.80-slim-bookworm
ARG PYTHON_IMAGE=python:3.11.10-slim-bookworm
FROM ${RUST_IMAGE} AS rust_base
@@ -177,7 +177,7 @@ COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=denoland/deno:2.1.2 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.1.40 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.38 /usr/local/bin/bun /usr/bin/bun
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer

View File

@@ -1,67 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n job_kind AS \"job_kind: JobKind\",\n script_hash AS \"script_hash: ScriptHash\",\n flow_status AS \"flow_status!: Json<Box<RawValue>>\",\n raw_flow AS \"raw_flow: Json<Box<RawValue>>\"\n FROM queue WHERE id = $1 AND workspace_id = $2 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_kind: JobKind",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 1,
"name": "script_hash: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "flow_status!: Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "raw_flow: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
true,
true,
true
]
},
"hash": "0a6a89e6ab3037f02c3c4c84ee02138d5fded1e6360bb992046fe9711b5ea213"
}

View File

@@ -75,8 +75,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}

View File

@@ -1,72 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase AS \"codebase: String\" FROM script WHERE hash = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "content!: String",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "lock: String",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "language: Option<ScriptLang>",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp"
]
}
}
}
},
{
"ordinal": 3,
"name": "envs: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 4,
"name": "codebase: String",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
true,
false,
true,
true
]
},
"hash": "0bf123446bebbc357c58a53a9319f4954dbf3225e91cbe999e5b264c1a747664"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\"\n FROM flow_version WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "10cd655d3d2916721b530c7faa7652fb8fae25383b58f6b9e8dc431b76947315"
}

View File

@@ -52,8 +52,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow.versions[array_upper(flow.versions, 1)] AS \"version!: i64\"\n FROM flow WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version!: i64",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "2b2ee874dbd90beec26713d2effdc5d011d9f1091a13761642d064220add7b41"
}

View File

@@ -61,8 +61,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}

View File

@@ -1,73 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n script_path, script_hash AS \"script_hash: ScriptHash\",\n job_kind AS \"job_kind: JobKind\",\n flow_status AS \"flow_status: Json<Box<RawValue>>\",\n raw_flow AS \"raw_flow: Json<Box<RawValue>>\"\n FROM completed_job WHERE id = $1 and workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_hash: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "job_kind: JobKind",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 3,
"name": "flow_status: Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "raw_flow: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
true,
true,
false,
true,
true
]
},
"hash": "3df03ec2345c905f03450e1e3f0c3ce7f41a22e72c5180ef8cc910305e4d0fce"
}

View File

@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT code AS \"raw_code: String\", lock AS \"raw_lock: String\", flow AS \"raw_flow: Json<Box<RawValue>>\" FROM flow_node WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "raw_code: String",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "raw_lock: String",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "raw_flow: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "3ee2d60ca93eeaf02ab0ee96aca399ec055b044a06284c0ab19b67d97f803894"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n MAX (created_at) as last_deploy, \n COUNT (*) as deploys_count \n FROM metrics \n WHERE id = 'no_uv_usage_py'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "last_deploy",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "deploys_count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null
]
},
"hash": "497d93db931922b09f96cf73239513d7141f3d37f85ada46597079991b3bff30"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO metrics (id, value) \n VALUES ('no_uv_usage_ansible', $1)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "4a804ee30bfe86c4e2c15a9f6511be5adf0dd22cb942fac64b439fb4e20df447"
}

View File

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

View File

@@ -55,8 +55,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT hash 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": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "700f987ec9b2a17c9b8304d598269f8fb58a432934163588af5ea84481c1c087"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO metrics (id, value) \n VALUES ('no_uv_usage_py', $1)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "78cd3f9d43dcf292cfa97ed79f9b6ad60469d5a4949729676abdefb3ab2b1a7f"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\"\n FROM flow_version WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "7a9a711c7cb05ed8a61116586fb68ef270567ebd70266f07139f3e3e34940699"
}

View File

@@ -30,8 +30,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\"\n FROM flow_version_lite WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true
]
},
"hash": "85085cdc08c262c4750566a0a0b9754017b890ea8c1161a5892b6c29e663ee0e"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow.versions[array_upper(flow.versions, 1)] AS \"version!: i64\"\n FROM flow WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version!: i64",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "97048ce0bcabb9baecb80cde5ab3c989e1575fbd20ef22766d2887a86dce15e1"
}

View File

@@ -1,71 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock AS \"lock: String\", content AS \"code!: String\",\n language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase AS \"codebase: String\" FROM script WHERE hash = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock: String",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "code!: String",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "language: Option<ScriptLang>",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible"
]
}
}
}
},
{
"ordinal": 3,
"name": "envs: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 4,
"name": "codebase: String",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
false,
false,
true,
true
]
},
"hash": "9c1a1d0feb79f750c7143fabb0cfa7dab8dd683cd294c27d0549bd8d78ab60a0"
}

View File

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

View File

@@ -55,8 +55,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}

View File

@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock AS \"lock: String\", code AS \"code: String\", flow AS \"flow: Json<Box<RawValue>>\" FROM flow_node WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock: String",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "code: String",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "flow: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "9ee2f67042c1bed1e7d13eb7d07e78991e5d7cf01fc7993531dbedf33dac2e0b"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO metrics (id, value) \n VALUES ('no_uv_usage_py', ''::text::jsonb)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "a1f8667bfd5b000dd7ba384e78f2bd7fabb6b8055f4559f77e20eef7c2b1c902"
}

View File

@@ -38,8 +38,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}

View File

@@ -1,72 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase AS \"codebase: String\" FROM script WHERE hash = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "content!: String",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "lock: String",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "language: Option<ScriptLang>",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp"
]
}
}
}
},
{
"ordinal": 3,
"name": "envs: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 4,
"name": "codebase: String",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
true,
false,
true,
true
]
},
"hash": "b7f2ed32e933b65fa5455928c71f61068ad7dfee8352a76122f5af893ffe6517"
}

View File

@@ -1,67 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n queue.job_kind AS \"job_kind: JobKind\",\n queue.script_hash AS \"script_hash: ScriptHash\",\n queue.raw_flow AS \"raw_flow: sqlx::types::Json<Box<RawValue>>\",\n completed_job.parent_job AS \"parent_job: Uuid\"\n FROM queue\n JOIN completed_job ON completed_job.parent_job = queue.id\n WHERE completed_job.id = $1 AND completed_job.workspace_id = $2\n LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_kind: JobKind",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 1,
"name": "script_hash: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "raw_flow: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "parent_job: Uuid",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
true,
true,
true
]
},
"hash": "bb6141ad0e93986b38ccdf4d027c486137d4fe79906e22842b711f4a9379b8c8"
}

View File

@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_code, raw_lock, raw_flow AS \"raw_flow: Json<Box<RawValue>>\" FROM job WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "raw_code",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "raw_lock",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "raw_flow: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "bc8ac03254669951654cda4bcfa12491341e745aef5e0e5090c2f4e4a4dc54fb"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT true FROM token WHERE token = $1 and expiration IS NOT NULL and expiration > now() + $2::int * '1 sec'::interval",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Int4"
]
},
"nullable": [
null
]
},
"hash": "bfff3d8df18db198d6ebba8a049b00147fc8bcd42f3df37ef81b9ded80974bd0"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource\n (workspace_id, path, value, resource_type, created_by, edited_at)\n VALUES ($1, $2, $3, $4, $5, now()) ON CONFLICT (workspace_id, path)\n DO UPDATE SET value = $3, edited_at = now()",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "c4ee16065fa021cf6443c2f29f1ca986b9730eae9df26dd33c3ea21e35df62c4"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\"\n FROM flow_version_lite WHERE id = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true
]
},
"hash": "c7de95ef4934752b62d6d01bf663ab6a104bbcfb827a4a9e4e03e38732375d55"
}

View File

@@ -1,72 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock AS \"lock: String\", content AS \"code!: String\",\n language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase AS \"codebase: String\" FROM script WHERE hash = $1 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock: String",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "code!: String",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "language: Option<ScriptLang>",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp"
]
}
}
}
},
{
"ordinal": 3,
"name": "envs: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 4,
"name": "codebase: String",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
false,
false,
true,
true
]
},
"hash": "d061e7ca73987036928e17245360fd7f2969aab0c422a00bbdade6a7236aa75d"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO metrics (id, value) \n VALUES ('no_uv_usage_ansible', $1)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "d4878143442a303e624ab78300464a77334a0aad0cbf219250e4e811b1d16052"
}

View File

@@ -37,8 +37,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n MAX (created_at) as last_deploy, \n COUNT (*) as deploys_count \n FROM metrics \n WHERE id = 'no_uv_usage_ansible'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "last_deploy",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "deploys_count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null
]
},
"hash": "eb125df0f64c0caa07f50dc82e8ae9c6cd2872c7afe96d41bed731c5041ab671"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO metrics (id, value) \n VALUES ('no_uv_usage_py', $1)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb"
]
},
"nullable": []
},
"hash": "ed318070b26861fda2d591a4356fdbeb6c7fdc965be43bddb010fd8299af1286"
}

View File

@@ -81,8 +81,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}

View File

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

678
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.441.0"
version = "1.437.1"
authors.workspace = true
edition.workspace = true
@@ -21,7 +21,6 @@ members = [
"./parsers/windmill-parser-wasm",
"./parsers/windmill-parser-go",
"./parsers/windmill-parser-rust",
"./parsers/windmill-parser-csharp",
"./parsers/windmill-parser-bash",
"./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-imports",
@@ -30,7 +29,7 @@ members = [
]
[workspace.package]
version = "1.441.0"
version = "1.437.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -47,40 +46,27 @@ lto = "thin"
[features]
default = []
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
enterprise_saml = ["windmill-api/enterprise_saml", "oauth2"]
stripe = ["windmill-api/stripe", "enterprise"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise", "windmill-indexer/enterprise"]
enterprise_saml = ["windmill-api/enterprise_saml"]
stripe = ["windmill-api/stripe"]
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark", "windmill-common/benchmark"]
flamegraph = ["windmill-common/flamegraph", "windmill-worker/flamegraph"]
loki = ["windmill-common/loki"]
embedding = ["windmill-api/embedding"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet", "dep:object_store"]
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus", "dep:prometheus"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet", "windmill-indexer/parquet", "dep:object_store"]
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus"]
flow_testing = ["windmill-worker/flow_testing"]
openidconnect = ["windmill-api/openidconnect"]
cloud = ["windmill-queue/cloud", "windmill-worker/cloud"]
jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-ctl"]
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy", "windmill-indexer/enterprise", "windmill-indexer/parquet", "enterprise", "parquet"]
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy"]
sqlx = ["windmill-worker/sqlx"]
deno_core = ["windmill-worker/deno_core", "dep:deno_core"]
kafka = ["windmill-api/kafka"]
otel = ["windmill-common/otel", "windmill-worker/otel"]
dind = ["windmill-worker/dind"]
php = ["windmill-worker/php"]
rust = ["windmill-worker/rust"]
mysql = ["windmill-worker/mysql"]
mssql = ["windmill-worker/mssql"]
bigquery = ["windmill-worker/bigquery"]
websocket = ["windmill-api/websocket"]
python = ["windmill-worker/python"]
smtp = ["windmill-api/smtp", "windmill-common/smtp"]
csharp = ["windmill-worker/csharp"]
license = ["windmill-api/license"]
oauth2 = ["windmill-api/oauth2"]
http_trigger = ["windmill-api/http_trigger"]
zip = ["windmill-api/zip"]
static_frontend = ["windmill-api/static_frontend"]
scoped_cache = ["windmill-common/scoped_cache"]
[dependencies]
anyhow.workspace = true
@@ -104,7 +90,7 @@ sha2.workspace = true
url.workspace = true
lazy_static.workspace = true
once_cell.workspace = true
prometheus = { workspace = true, optional = true }
prometheus.workspace = true
uuid.workspace = true
gethostname.workspace = true
serde_json.workspace = true
@@ -145,7 +131,6 @@ 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-csharp = { path = "./parsers/windmill-parser-csharp" }
windmill-parser-bash = { path = "./parsers/windmill-parser-bash" }
windmill-parser-sql = { path = "./parsers/windmill-parser-sql" }
windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
@@ -182,7 +167,7 @@ rand = "^0"
rand_core = { version = "^0", features = ["std"] }
magic-crypt = "^3"
git-version = "^0"
rustpython-parser = "^0"
rustpython-parser = { git = "https://github.com/RustPython/Parser", rev = "9ce55aefdeb35e2f706ce0b02d5a2dfe6295fc57" }
php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb411dec09450946ef57920b7ffced7f6495d" }
cron = "^0"
mail-send = { version = "0.4.0", features = ["builder"], default-features=false }
@@ -241,13 +226,13 @@ lazy_static = "1.4.0"
serde_derive = "1.0.147"
const_format = { version = "0.2", features = ["rust_1_64", "rust_1_51"] }
dyn-iter = "0.2.0"
rsa = "^0"
rsa = "0.7.2"
async-stripe = { version = "0.39.1", features = [
"runtime-tokio-hyper",
"checkout",
"billing",
] }
async_zip = { version = "0.0.17", features = ["tokio", "tokio-fs", "deflate", "chrono"] }
async_zip = { version = "0.0.11", features = ["full"] }
once_cell = "1.17.1"
gosyn = "0.2.6"
bytes = "1.4.0"
@@ -277,7 +262,7 @@ tokenizers = "0.14.1"
candle-core = "0.3.0"
candle-transformers = "0.3.0"
candle-nn = "0.3.0"
tiberius = { version = "0.12.3", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"]}
tiberius = { git = "https://github.com/prisma/tiberius", rev = "8f66a699dfa041e7b5f736c7e94f92c945453c9e", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"]}
pin-project = "1"
indexmap = { version = "2.2.5", features = ["serde"]}
tokio-native-tls = "^0"
@@ -288,7 +273,8 @@ rdkafka = { version = "0.36.2", features = ["cmake-build", "ssl-vendored"] }
datafusion = "39.0.0"
object_store = { version = "0.10.0", features = ["aws", "azure"] }
openidconnect = { version = "4.0.0-rc.1" }
openidconnect = { version = "3.4.0" }
zstd = "=0.12.4"
aws-config = "^1"
aws-sdk-sts = "^1"
@@ -328,5 +314,3 @@ quote = "1.0.36"
regex-lite = "0.1.6"
yaml-rust = "0.4.5"
tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] }
tree-sitter = {version = "0.23.0", features = []}
tree-sitter-c-sharp = "0.23.0"

View File

@@ -1 +1 @@
586b02014d57f862a5c4313dd1e529d50c315c30
271c210ddbbdcad0f4d6c007e650eceda5ddfa64

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
[package]
name = "windmill-parser-csharp"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
name = "windmill_parser_csharp"
path = "./src/lib.rs"
[dependencies]
windmill-parser.workspace = true
tree-sitter.workspace = true
tree-sitter-c-sharp.workspace = true
anyhow.workspace = true
wasm-bindgen.workspace = true
serde_json.workspace = true
# convert_case.workspace = true
# lazy_static.workspace = true
# regex.workspace = true

View File

@@ -1,314 +0,0 @@
#![cfg_attr(target_arch = "wasm32", feature(c_variadic))]
#[cfg(target_arch = "wasm32")]
pub mod wasm_libc;
use anyhow::anyhow;
use tree_sitter::Node;
use windmill_parser::Arg;
use windmill_parser::MainArgSignature;
use windmill_parser::Typ;
#[derive(Debug)]
pub struct CsharpMainSigMeta {
pub is_async: bool,
pub is_public: bool,
pub returns_void: bool,
pub class_name: Option<String>,
pub main_sig: MainArgSignature,
}
fn csharp_param_default_value<'a>(def: Node<'a>, code: &str) -> Option<serde_json::Value> {
def.utf8_text(code.as_bytes())
.ok()
.and_then(|content| serde_json::from_str(content).ok())
}
pub fn parse_csharp_sig_meta(code: &str) -> anyhow::Result<CsharpMainSigMeta> {
let mut parser = tree_sitter::Parser::new();
let language = tree_sitter_c_sharp::LANGUAGE;
parser
.set_language(&language.into())
.map_err(|e| anyhow!("Error setting c# as language: {e}"))?;
// Parse code
let tree = parser.parse(code, None).expect("Failed to parse code");
let root_node = tree.root_node();
// Traverse the AST to find the Main method signature
let main_sig = find_main_signature(root_node, code);
let no_main_func = Some(main_sig.is_none());
let mut is_async = false;
let mut is_public = false;
let mut returns_void = false;
let mut class_name = None;
let mut args = vec![];
if let Some((sig, name)) = main_sig {
class_name = name;
for sig_node in sig.children(&mut sig.walk()) {
if sig_node.kind() == "modifier" && sig_node.utf8_text(code.as_bytes())? == "async" {
is_async = true;
}
if sig_node.kind() == "modifier" && sig_node.utf8_text(code.as_bytes())? == "public" {
is_public = true;
}
}
if let Some(return_type) = sig.child_by_field_name("returns") {
let return_type = return_type.utf8_text(code.as_bytes())?;
if return_type == "void" || (is_async && return_type == "Task") {
returns_void = true;
}
}
if let Some(param_list) = sig.child_by_field_name("parameters") {
for p_list_node in param_list.children(&mut param_list.walk()) {
if p_list_node.kind() == "parameter" {
let mut default = None;
for a in p_list_node.children(&mut p_list_node.walk()) {
if a.kind() == "=" {
if let Some(node) = a.next_sibling() {
default = csharp_param_default_value(node, code);
}
}
}
let (otyp, typ, name) = parse_csharp_typ(p_list_node, code)?;
args.push(Arg { name, otyp, typ, default, has_default: false, oidx: None });
}
}
}
}
let main_sig = MainArgSignature {
star_args: false,
star_kwargs: false,
args,
has_preprocessor: None,
no_main_func,
};
Ok(CsharpMainSigMeta { is_async, returns_void, class_name, main_sig, is_public })
}
pub fn parse_csharp_signature(code: &str) -> anyhow::Result<MainArgSignature> {
Ok(parse_csharp_sig_meta(code)?.main_sig)
}
fn find_typ<'a>(typ_node: Node<'a>, code: &str) -> anyhow::Result<Typ> {
match typ_node.kind() {
"predefined_type" => {
match typ_node.utf8_text(code.as_bytes()) {
Ok("string") => Ok(Typ::Str(None)),
Ok("sbyte") | Ok("System.SByte") => Ok(Typ::Bytes),
Ok("byte") | Ok("System.Byte") => Ok(Typ::Bytes),
Ok("short") | Ok("System.Int16") => Ok(Typ::Int),
Ok("ushort") | Ok("System.UInt16") => Ok(Typ::Int),
Ok("int") | Ok("System.Int32") => Ok(Typ::Int),
Ok("uint") | Ok("System.UInt32") => Ok(Typ::Int),
Ok("long") | Ok("System.Int64") => Ok(Typ::Int),
Ok("ulong") | Ok("System.UInt64") => Ok(Typ::Int),
Ok("char") | Ok("System.Char") => Ok(Typ::Str(None)),
Ok("float") | Ok("System.Single") => Ok(Typ::Float),
Ok("double") | Ok("System.Double") => Ok(Typ::Float),
Ok("bool") | Ok("System.Boolean") => Ok(Typ::Bool),
Ok("decimal") | Ok("System.Decimal") => Ok(Typ::Float),
Ok("object") => Ok(Typ::Object(vec![])), // TODO: Complete the object type
Ok(s) => Err(anyhow!("Unknown type `{s}`")),
Err(e) => Err(anyhow!("Error getting type name: {}", e)),
}
}
"array_type" => {
let new_typ_node = typ_node
.child_by_field_name("type")
.ok_or(anyhow!("Failed to find inner type of array type"))?;
Ok(Typ::List(Box::new(find_typ(new_typ_node, code)?)))
}
"identifier" => Ok(Typ::Unknown),
"generic_name" => Ok(Typ::Unknown),
"pointer_type" => Ok(Typ::Int),
"nullable_type" => {
let new_typ_node = typ_node
.child_by_field_name("type")
.ok_or(anyhow!("Failed to find inner type of nullable_type"))?;
Ok(find_typ(new_typ_node, code)?)
}
wc => Err(anyhow!(
"Unexpected C# type node kind: {} for '{}'. This type is not handeled by Windmill, please open an issue if this seems to be an error",
wc,
typ_node.utf8_text(code.as_bytes())?
)),
}
}
fn parse_csharp_typ<'a>(
param_node: Node<'a>,
code: &str,
) -> anyhow::Result<(Option<String>, Typ, String)> {
let name = param_node
.child_by_field_name("name")
.and_then(|n| n.utf8_text(code.as_bytes()).ok())
.unwrap_or("");
let otyp_node = param_node.child_by_field_name("type");
let otyp = otyp_node
.and_then(|n| n.utf8_text(code.as_bytes()).ok())
.map(|s| s.to_string());
let typ = find_typ(otyp_node.unwrap(), code)?;
Ok((otyp, typ, name.to_string()))
}
// Function to find the Main method's signature
fn find_main_signature<'a>(root_node: Node<'a>, code: &str) -> Option<(Node<'a>, Option<String>)> {
let mut cursor = root_node.walk();
for x in root_node.children(&mut cursor) {
if x.kind() == "class_declaration" {
let class_name = x
.child_by_field_name("name")
.and_then(|n| n.utf8_text(code.as_bytes()).ok().map(|s| s.to_string()));
for c in x.children(&mut x.walk()) {
if c.kind() == "declaration_list" {
for w in c.children(&mut c.walk()) {
if w.kind() == "method_declaration" {
for child in w.children(&mut w.walk()) {
if child
.utf8_text(code.as_bytes())
.map(|name| name == "Main")
.unwrap_or(false)
{
return Some((w, class_name));
}
}
}
}
}
}
}
}
return None;
}
pub fn parse_csharp_reqs(code: &str) -> (Vec<(String, Option<String>)>, Vec<usize>) {
let mut nuget_reqs = Vec::new();
let mut pkg_lines = Vec::new();
for (i, line) in code.split("\n").enumerate() {
if line.starts_with('#') {
if let Some(req) = parse_nuget_req(&line) {
pkg_lines.push(i);
nuget_reqs.push(req);
}
} else {
break; // Stop processing after the first non-comment line
}
}
(nuget_reqs, pkg_lines)
}
fn parse_nuget_req(line: &str) -> Option<(String, Option<String>)> {
// Check if the line starts with `#r "nuget:`
if let Some(start) = line.find("#r \"nuget:") {
// Extract the content after `#r "nuget:`
let start_idx = start + 10;
let end_idx = line[start_idx..].find('"')?;
let line = &line[start_idx..start_idx + end_idx];
let mut splitted = line.split(",");
if let Some(pkg) = splitted.next() {
return Some((
pkg.trim().to_string(),
splitted.next().map(|s| s.trim().to_string()),
));
}
}
None
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_csharp_sig_and_meta() {
let code = r#"
using System;
class LilProgram
{
public async static string Main(string myString = "World", int myInt = 2, string[] jj = ["asd", "ss"])
{
Console.Writeline("Hello!!");
return "yeah";
}
}"#;
let sig_meta = parse_csharp_sig_meta(code).unwrap();
assert_eq!(sig_meta.class_name, Some("LilProgram".to_string()));
assert_eq!(sig_meta.is_async, true);
let ret = sig_meta.main_sig;
assert_eq!(ret.args.len(), 3);
}
#[test]
fn test_parse_csharp_sig() {
let code = r#"
using System;
class LilProgram
{
public static string Main(string myString = "World", int myInt, string[] jj)
{
Console.Writeline("Hello!!");
return "yeah";
}
}"#;
let ret = parse_csharp_signature(code).unwrap();
assert_eq!(ret.args.len(), 3);
assert_eq!(ret.args[0].name, "myString");
assert_eq!(ret.args[0].otyp, Some("string".to_string()));
assert_eq!(ret.args[0].typ, Typ::Str(None));
assert_eq!(ret.args[1].name, "myInt");
assert_eq!(ret.args[1].otyp, Some("int".to_string()));
assert_eq!(ret.args[1].typ, Typ::Int);
assert_eq!(ret.args[2].name, "jj");
assert_eq!(ret.args[2].otyp, Some("string[]".to_string()));
assert_eq!(ret.args[2].typ, Typ::List(Box::new(Typ::Str(None))));
}
#[test]
fn test_parse_csharp_reqs() {
let file_content = r#"#r "nuget: AutoMapper, 6.1.0"
#r "nuget: Newtonsoft.Json, 13.0.1"
#r "nuget: Serilog, 2.10.0"
#r "nuget: Serilog, 2.10.0"
using System;
"#;
let requirements = parse_csharp_reqs(file_content).0;
assert_eq!(requirements.len(), 3);
assert_eq!(
requirements[0],
("AutoMapper".to_string(), Some("6.1.0".to_string()))
);
assert_eq!(
requirements[1],
("Newtonsoft.Json".to_string(), Some("13.0.1".to_string()))
);
assert_eq!(
requirements[2],
("Serilog".to_string(), Some("2.10.0".to_string()))
);
}
}

View File

@@ -1,208 +0,0 @@
use std::{
alloc::{self, Layout},
ffi::{c_char, c_int, c_void},
mem::align_of,
ptr,
};
use std::collections::BTreeMap;
use std::sync::{Mutex, OnceLock};
use wasm_bindgen::prelude::*;
/* -------------------------------- stdlib.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn abort() {
panic!("Aborted from C");
}
macro_rules! console_log {
($($t:tt)*) => (unsafe { log(&format_args!($($t)*).to_string()) })
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(a: &str);
}
#[no_mangle]
pub unsafe extern "C" fn malloc(size: usize) -> *mut c_void {
if size == 0 {
return ptr::null_mut();
}
let (layout, offset_to_data) = layout_for_size_prepended(size);
let buf = alloc::alloc(layout);
store_layout(buf, layout, offset_to_data)
}
#[no_mangle]
pub unsafe extern "C" fn calloc(count: usize, size: usize) -> *mut c_void {
if count == 0 || size == 0 {
return ptr::null_mut();
}
let (layout, offset_to_data) = layout_for_size_prepended(size * count);
let buf = alloc::alloc_zeroed(layout);
store_layout(buf, layout, offset_to_data)
}
#[no_mangle]
pub unsafe extern "C" fn realloc(buf: *mut c_void, new_size: usize) -> *mut c_void {
if buf.is_null() {
malloc(new_size)
} else if new_size == 0 {
free(buf);
ptr::null_mut()
} else {
let (old_buf, old_layout) = retrieve_layout(buf);
let (new_layout, offset_to_data) = layout_for_size_prepended(new_size);
let new_buf = alloc::realloc(old_buf, old_layout, new_layout.size());
store_layout(new_buf, new_layout, offset_to_data)
}
}
#[no_mangle]
pub unsafe extern "C" fn free(buf: *mut c_void) {
if buf.is_null() {
return;
}
let (buf, layout) = retrieve_layout(buf);
alloc::dealloc(buf, layout);
}
// In all these allocations, we store the layout before the data for later retrieval.
// This is because we need to know the layout when deallocating the memory.
// Here are some helper methods for that:
/// Given a pointer to the data, retrieve the layout and the pointer to the layout.
unsafe fn retrieve_layout(buf: *mut c_void) -> (*mut u8, Layout) {
let (_, layout_offset) = Layout::new::<Layout>()
.extend(Layout::from_size_align(0, align_of::<*const u8>() * 2).unwrap())
.unwrap();
let buf = (buf as *mut u8).offset(-(layout_offset as isize));
let layout = *(buf as *mut Layout);
(buf, layout)
}
/// Calculate a layout for a given size with space for storing a layout at the start.
/// Returns the layout and the offset to the data.
fn layout_for_size_prepended(size: usize) -> (Layout, usize) {
Layout::new::<Layout>()
.extend(Layout::from_size_align(size, align_of::<*const u8>() * 2).unwrap())
.unwrap()
}
/// Store a layout in the pointer, returning a pointer to where the data should be stored.
unsafe fn store_layout(buf: *mut u8, layout: Layout, offset_to_data: usize) -> *mut c_void {
*(buf as *mut Layout) = layout;
(buf as *mut u8).offset(offset_to_data as isize) as *mut c_void
}
/* -------------------------------- string.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn strncmp(ptr1: *const c_void, ptr2: *const c_void, n: usize) -> c_int {
let s1 = std::slice::from_raw_parts(ptr1 as *const u8, n);
let s2 = std::slice::from_raw_parts(ptr2 as *const u8, n);
for (a, b) in s1.iter().zip(s2.iter()) {
if *a != *b || *a == 0 {
return (*a as i32) - (*b as i32);
}
}
0
}
/* -------------------------------- wctype.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn iswspace(c: c_int) -> bool {
char::from_u32(c as u32).map_or(false, |c| c.is_whitespace())
}
#[no_mangle]
pub unsafe extern "C" fn iswalnum(c: c_int) -> bool {
char::from_u32(c as u32).map_or(false, |c| c.is_alphanumeric())
}
/* --------------------------------- time.h --------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn clock() -> u64 {
panic!("clock is not supported");
}
/* --------------------------------- ctype.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn isprint(c: c_int) -> bool {
c >= 32 && c <= 126
}
/* --------------------------------- stdio.h -------------------------------- */
#[no_mangle]
pub unsafe extern "C" fn fprintf(_file: *mut c_void, _format: *const c_void, _args: ...) -> c_int {
panic!("fprintf is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fputs(_s: *const c_void, _file: *mut c_void) -> c_int {
panic!("fputs is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fputc(_c: c_int, _file: *mut c_void) -> c_int {
panic!("fputc is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fdopen(_fd: c_int, _mode: *const c_void) -> *mut c_void {
panic!("fdopen is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fclose(_file: *mut c_void) -> c_int {
panic!("fclose is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn fwrite(
_ptr: *const c_void,
_size: usize,
_nmemb: usize,
_stream: *mut c_void,
) -> usize {
panic!("fwrite is not supported");
}
#[no_mangle]
pub unsafe extern "C" fn vsnprintf(
_buf: *mut c_char,
_size: usize,
_format: *const c_char,
_args: ...
) -> c_int {
panic!("vsnprintf is not supported");
}
#[no_mangle]
pub extern "C" fn clock_gettime(ptr: usize, new_size: usize) {
panic!("asdasd");
}
// int snprintf( char* restrict buffer, size_t bufsz, const char* restrict format, ... );
#[no_mangle]
pub extern "C" fn snprintf() {
panic!("snprintf is not supported");
}
#[no_mangle]
pub extern "C" fn __assert_fail(_: *const i32, _: *const i32, _: *const i32, _: *const i32) {
panic!("oh no");
}

View File

@@ -26,7 +26,6 @@ php-parser = [ "dep:windmill-parser-php"]
rust-parser = [ "dep:windmill-parser-rust"]
graphql-parser = [ "dep:windmill-parser-graphql"]
ansible-parser = [ "dep:windmill-parser-yaml"]
csharp-parser = [ "dep:windmill-parser-csharp"]
[dependencies]
anyhow.workspace = true
@@ -40,7 +39,6 @@ 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 }
windmill-parser-csharp = { workspace = true, optional = true }
wasm-bindgen.workspace = true
serde_json.workspace = true
getrandom = { workspace = true, features = ["js"] }

View File

@@ -48,9 +48,3 @@ 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
# C# (needs some more stuff to compile C tree sitter into wasm)
# TODO: hasn't been tested on mac, might need fixing
OUT_DIR="pkg-csharp"
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser"
sed -i '' 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json

View File

@@ -48,8 +48,3 @@ 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
# C# (needs some more stuff to compile C tree sitter into wasm)
OUT_DIR="pkg-csharp"
CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin" RUSTFLAGS="-Zwasm-c-abi=spec" wasm-pack build --release --target web --out-dir $OUT_DIR --features "csharp-parser"
sed -i 's/"windmill-parser-wasm"/"windmill-parser-wasm-csharp"/' $OUT_DIR/package.json

View File

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

View File

@@ -135,9 +135,3 @@ pub fn parse_rust(code: &str) -> String {
pub fn parse_ansible(code: &str) -> String {
wrap_sig(windmill_parser_yaml::parse_ansible_sig(code))
}
#[cfg(feature = "csharp-parser")]
#[wasm_bindgen]
pub fn parse_csharp(code: &str) -> String {
wrap_sig(windmill_parser_csharp::parse_csharp_signature(code))
}

View File

@@ -1,4 +0,0 @@
#pragma once
#define assert(ignore) ((void)0)
#define static_assert(cnd, msg) assert(cnd && msg)

View File

@@ -1,3 +0,0 @@
#pragma once
int isprint(int c);

View File

@@ -1,3 +0,0 @@
#pragma once
#define PRId32 "d"

View File

@@ -1,5 +0,0 @@
#pragma once
#define bool _Bool
#define true 1
#define false 0

View File

@@ -1,19 +0,0 @@
#pragma once
typedef signed char int8_t;
typedef short int16_t;
typedef long int32_t;
typedef long long int64_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned long uint32_t;
typedef unsigned long long uint64_t;
typedef unsigned long size_t;
typedef unsigned int uintptr_t;
#define UINT8_MAX 0xff
#define UINT16_MAX 0xffff
#define UINT32_MAX 0xffffffff

View File

@@ -1,19 +0,0 @@
#pragma once
// just some filler type
#define FILE void
#define stdin NULL
#define stdout NULL
#define stderr NULL
int fprintf(FILE *__restrict__, const char *__restrict__, ...);
int fputs(const char *__restrict, FILE *__restrict);
int fputc(int, FILE *);
FILE *fdopen(int, const char *);
int fclose(FILE *);
int vsnprintf(char *s, unsigned long n, const char *format, ...);
#define sprintf(str, ...) 0
#define snprintf(str, len, ...) 0

View File

@@ -1,12 +0,0 @@
#pragma once
#include <stdint.h>
#define NULL ((void*)0)
void* malloc(size_t size);
void* calloc(size_t nmemb, size_t size);
void free(void* ptr);
void* realloc(void* ptr, size_t size);
void abort(void);

View File

@@ -1,7 +0,0 @@
#pragma once
void *memcpy(void *dest, const void *src, unsigned long n);
void *memmove(void *dest, const void *src, unsigned long n);
void *memset(void *s, int c, unsigned long n);
int memcmp(const void *ptr1, const void *ptr2, unsigned long n);
int strncmp(const char *s1, const char *s2, unsigned long n);

View File

@@ -1,5 +0,0 @@
#pragma once
typedef unsigned long clock_t;
#define CLOCKS_PER_SEC ((clock_t)1000000)
clock_t clock(void);

View File

@@ -1,3 +0,0 @@
#pragma once
int dup(int);

View File

@@ -1,7 +0,0 @@
#pragma once
typedef __WCHAR_TYPE__ wchar_t;
typedef __WINT_TYPE__ wint_t;
int iswspace(wchar_t ch);
int iswalnum(wint_t _wc);

View File

@@ -9,8 +9,8 @@
use anyhow::Context;
use monitor::{
load_base_url, load_otel, reload_delete_logs_periodically_setting, reload_indexer_config,
reload_nuget_config_setting, reload_timeout_wait_result_setting,
send_current_log_file_to_object_store, send_logs_to_object_store,
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};
@@ -37,10 +37,9 @@ use windmill_common::{
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING,
NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_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, SMTP_SETTING,
TIMEOUT_WAIT_RESULT_SETTING,
OAUTH_SETTING, OTEL_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, SMTP_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
},
scripts::ScriptLang,
stats_ee::schedule_stats,
@@ -67,15 +66,15 @@ use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING;
use windmill_worker::{
get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR,
BUN_DEPSTAR_CACHE_DIR, CSHARP_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS,
DENO_CACHE_DIR_NPM, GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR,
POWERSHELL_CACHE_DIR, PY311_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TAR_PY311_CACHE_DIR,
TMP_LOGS_DIR, UV_CACHE_DIR,
BUN_DEPSTAR_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM,
GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR, POWERSHELL_CACHE_DIR,
PY311_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TAR_PY311_CACHE_DIR, TMP_LOGS_DIR,
UV_CACHE_DIR,
};
use crate::monitor::{
initial_load, load_keep_job_dir, load_metrics_debug_enabled, load_require_preexisting_user,
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db,
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db, monitor_pool,
reload_base_url_setting, reload_bunfig_install_scopes_setting,
reload_critical_alert_mute_ui_setting, reload_critical_error_channels_setting,
reload_extra_pip_index_url_setting, reload_hub_base_url_setting,
@@ -142,7 +141,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
for path in paths.values() {
tracing::info!("Caching hub script at {path}");
let res = get_hub_script_content_and_requirements(Some(path), None).await?;
let res = get_hub_script_content_and_requirements(Some(path.to_string()), None).await?;
if res
.language
.as_ref()
@@ -189,7 +188,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
if let Err(e) = windmill_worker::prebundle_bun_script(
&res.content,
Some(&lockfile),
Some(lockfile),
&path,
&job_id,
"admins",
@@ -283,6 +282,9 @@ async fn windmill_main() -> anyhow::Result<()> {
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]
println!("jemalloc enabled");
#[cfg(feature = "flamegraph")]
let _guard = windmill_common::tracing_init::setup_flamegraph();
let cli_arg = std::env::args().nth(1).unwrap_or_default();
match cli_arg.as_str() {
@@ -359,6 +361,7 @@ async fn windmill_main() -> anyhow::Result<()> {
.unwrap_or_else(|| "local")
.to_string();
#[cfg(not(feature = "flamegraph"))]
let _guard = windmill_common::tracing_init::initialize_tracing(&hostname, &mode, &environment);
let num_version = sqlx::query_scalar!("SELECT version()").fetch_one(&db).await;
@@ -485,8 +488,7 @@ Windmill Community Edition {GIT_VERSION}
)
.await;
#[cfg(feature = "prometheus")]
crate::monitor::monitor_pool(&db).await;
monitor_pool(&db).await;
send_logs_to_object_store(&db, &hostname, &mode);
@@ -609,7 +611,6 @@ Windmill Community Edition {GIT_VERSION}
server_killpill_rx,
base_internal_tx,
server_mode,
#[cfg(feature = "smtp")]
base_internal_url.clone(),
)
.await?;
@@ -768,9 +769,6 @@ Windmill Community Edition {GIT_VERSION}
BUNFIG_INSTALL_SCOPES_SETTING => {
reload_bunfig_install_scopes_setting(&db).await
},
NUGET_CONFIG_SETTING => {
reload_nuget_config_setting(&db).await
},
KEEP_JOB_DIR_SETTING => {
load_keep_job_dir(&db).await;
},
@@ -1025,7 +1023,6 @@ pub async fn run_workers(
GO_CACHE_DIR,
GO_BIN_CACHE_DIR,
RUST_CACHE_DIR,
CSHARP_CACHE_DIR,
HUB_CACHE_DIR,
POWERSHELL_CACHE_DIR,
] {

View File

@@ -1,6 +1,5 @@
#[cfg(feature = "oauth2")]
use std::collections::HashMap;
use std::{
collections::HashMap,
fmt::Display,
ops::Mul,
str::FromStr,
@@ -23,15 +22,12 @@ use tokio::{
#[cfg(feature = "embedding")]
use windmill_api::embeddings::update_embeddings_db;
use windmill_api::{
jobs::TIMEOUT_WAIT_RESULT, DEFAULT_BODY_LIMIT, IS_SECURE, REQUEST_SIZE_LIMIT, SAML_METADATA,
SCIM_TOKEN,
jobs::TIMEOUT_WAIT_RESULT,
oauth2_ee::{build_oauth_clients, OAuthClient},
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN,
};
#[cfg(feature = "enterprise")]
use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts};
#[cfg(feature = "oauth2")]
use windmill_common::global_settings::OAUTH_SETTING;
use windmill_common::{
auth::JWT_SECRET,
ee::CriticalErrorChannel,
@@ -43,7 +39,7 @@ use windmill_common::{
DEFAULT_TAGS_WORKSPACES_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING,
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
OTEL_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, TIMEOUT_WAIT_RESULT_SETTING,
@@ -69,7 +65,7 @@ use windmill_queue::cancel_job;
use windmill_worker::{
create_token_for_owner, handle_job_error, AuthedClient, SameWorkerPayload, SameWorkerSender,
SendResult, BUNFIG_INSTALL_SCOPES, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, NPM_CONFIG_REGISTRY,
NUGET_CONFIG, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
};
#[cfg(feature = "parquet")]
@@ -86,27 +82,6 @@ use crate::ee::verify_license_key;
use crate::ee::set_license_key;
#[cfg(feature = "prometheus")]
lazy_static::lazy_static! {
static ref QUEUE_ZOMBIE_RESTART_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_zombie_restart_count",
"Total number of jobs restarted due to ping timeout."
)
.unwrap();
static ref QUEUE_ZOMBIE_DELETE_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_zombie_delete_count",
"Total number of jobs deleted due to their ping timing out in an unrecoverable state."
)
.unwrap();
static ref QUEUE_COUNT: prometheus::IntGaugeVec = prometheus::register_int_gauge_vec!(
"queue_count",
"Number of jobs in the queue",
&["tag"]
).unwrap();
}
lazy_static::lazy_static! {
static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT")
.ok()
@@ -124,7 +99,22 @@ lazy_static::lazy_static! {
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(true);
static ref QUEUE_ZOMBIE_RESTART_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_zombie_restart_count",
"Total number of jobs restarted due to ping timeout."
)
.unwrap();
static ref QUEUE_ZOMBIE_DELETE_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_zombie_delete_count",
"Total number of jobs deleted due to their ping timing out in an unrecoverable state."
)
.unwrap();
static ref QUEUE_COUNT: prometheus::IntGaugeVec = prometheus::register_int_gauge_vec!(
"queue_count",
"Number of jobs in the queue",
&["tag"]
).unwrap();
static ref QUEUE_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
@@ -198,7 +188,6 @@ pub async fn initial_load(
reload_pip_index_url_setting(&db).await;
reload_npm_config_registry_setting(&db).await;
reload_bunfig_install_scopes_setting(&db).await;
reload_nuget_config_setting(&db).await;
}
}
@@ -344,7 +333,6 @@ pub async fn load_metrics_debug_enabled(db: &DB) -> error::Result<()> {
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]
#[derive(Debug, Clone)]
pub struct MallctlError {
#[allow(unused)]
pub code: i32,
}
@@ -928,16 +916,6 @@ pub async fn reload_bunfig_install_scopes_setting(db: &DB) {
.await;
}
pub async fn reload_nuget_config_setting(db: &DB) {
reload_option_setting_with_tracing(
db,
NUGET_CONFIG_SETTING,
"NUGET_CONFIG",
NUGET_CONFIG.clone(),
)
.await;
}
pub async fn reload_retention_period_setting(db: &DB) {
if let Err(e) = reload_setting(
db,
@@ -1170,7 +1148,6 @@ pub async fn reload_setting<T: FromStr + DeserializeOwned + Display>(
Ok(())
}
#[cfg(feature = "prometheus")]
pub async fn monitor_pool(db: &DB) {
if METRICS_ENABLED.load(Ordering::Relaxed) {
let db = db.clone();
@@ -1296,7 +1273,6 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
if metrics_enabled || save_metrics {
let queue_counts = windmill_common::queue::get_queue_counts(db).await;
#[cfg(feature = "prometheus")]
if metrics_enabled {
for q in QUEUE_COUNT_TAGS.read().await.iter() {
if queue_counts.get(q).is_none() {
@@ -1305,13 +1281,11 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
}
}
#[allow(unused_mut)]
let mut tags_to_watch = vec![];
for q in queue_counts {
let count = q.1;
let tag = q.0;
#[cfg(feature = "prometheus")]
if metrics_enabled {
let metric = (*QUEUE_COUNT).with_label_values(&[&tag]);
metric.set(count as i64);
@@ -1455,15 +1429,10 @@ pub async fn load_base_url(db: &DB) -> error::Result<String> {
}
pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
#[cfg(feature = "oauth2")]
let q_oauth = load_value_from_global_settings(db, OAUTH_SETTING).await?;
#[cfg(feature = "oauth2")]
let oauths = if let Some(q) = q_oauth {
if let Ok(v) = serde_json::from_value::<
Option<HashMap<String, windmill_api::oauth2_ee::OAuthClient>>,
>(q.clone())
{
if let Ok(v) = serde_json::from_value::<Option<HashMap<String, OAuthClient>>>(q.clone()) {
v
} else {
tracing::error!("Could not parse oauth setting as a json, found: {:#?}", &q);
@@ -1476,10 +1445,9 @@ pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
let base_url = load_base_url(db).await?;
let is_secure = base_url.starts_with("https://");
#[cfg(feature = "oauth2")]
{
let mut l = windmill_api::OAUTH_CLIENTS.write().await;
*l = windmill_api::oauth2_ee::build_oauth_clients(&base_url, oauths)
let mut l = OAUTH_CLIENTS.write().await;
*l = build_oauth_clients(&base_url, oauths)
.map_err(|e| tracing::error!("Error building oauth clients (is the oauth.json mounted and in correct format? Use '{}' as minimal oauth.json): {}", "{}", e))
.unwrap();
}
@@ -1505,11 +1473,9 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
.ok()
.unwrap_or_else(|| vec![]);
#[cfg(feature = "prometheus")]
if METRICS_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
QUEUE_ZOMBIE_RESTART_COUNT.inc_by(restarted.len() as _);
}
let base_url = BASE_URL.read().await.clone();
for r in restarted {
let last_ping = if let Some(x) = r.last_ping {
@@ -1546,7 +1512,6 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
.ok()
.unwrap_or_else(|| vec![]);
#[cfg(feature = "prometheus")]
if METRICS_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
QUEUE_ZOMBIE_DELETE_COUNT.inc_by(timeouts.len() as _);
}

View File

@@ -1,54 +0,0 @@
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'system',
'
export function main(world: string) {
const greet = `Hello ${world}!`;
console.log(greet)
return greet
}
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
'',
'',
'f/system/hello', 123412, 'deno', '');
INSERT INTO public.flow(workspace_id, summary, description, path, versions, schema, value, edited_by) VALUES (
'test-workspace',
'',
'',
'f/system/hello_flow',
'{1443253234253453}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
'{"modules": [{"id": "a", "value": {"path": "f/system/hello", "type": "script", "input_transforms": {"world": {"expr": "flow_input.world", "type": "javascript"}}}}]}',
'system'
);
INSERT INTO public.flow_version(id, workspace_id, path, schema, value, created_by) VALUES (
1443253234253453,
'test-workspace',
'f/system/hello_flow',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
'{"modules": [{"id": "a", "value": {"path": "f/system/failing_script", "type": "script", "input_transforms": {"fail": {"expr": "flow_input.fail", "type": "javascript"}}}}]}',
'system'
);
INSERT INTO public.flow(workspace_id, summary, description, path, versions, schema, value, edited_by) VALUES (
'test-workspace',
'',
'',
'f/system/hello_with_nodes_flow',
'{1443253234253454}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
E'{"modules":[{"id":"a","value":{"type":"forloopflow","modules":[{"id":"b","value":{"type":"rawscript","content":"export function main(world: string) {\\n const greet = `Hello ${world}!`;\\n console.log(greet)\\n return greet\\n}\\n","language":"deno","input_transforms":{"world":{"type":"javascript","expr":"flow_input.iter.value"}},"is_trigger":false}},{"id":"c","value":{"type":"rawscript","content":"export function main(hello: string) {\\n const dareyou = `Did you just say \\"${hello}\\"??!`;\\n console.log(dareyou)\\n return dareyou\\n}","language":"deno","input_transforms":{"hello":{"type":"javascript","value":"${results.b}","expr":"`${results.b}`"}},"is_trigger":false}}],"iterator":{"type":"javascript","expr":"[\'foo\', \'bar\', \'baz\']"},"skip_failures":true,"parallel":false}}],"same_worker":false}',
'system'
);
INSERT INTO public.flow_version(id, workspace_id, path, schema, value, created_by) VALUES (
1443253234253454,
'test-workspace',
'f/system/hello_with_nodes_flow',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"world":{"default":"world","description":"","type":"string"}},"required":[],"type":"object"}',
E'{"modules":[{"id":"a","value":{"type":"forloopflow","modules":[{"id":"b","value":{"type":"rawscript","content":"export function main(world: string) {\\n const greet = `Hello ${world}!`;\\n console.log(greet)\\n return greet\\n}\\n","language":"deno","input_transforms":{"world":{"type":"javascript","expr":"flow_input.iter.value"}},"is_trigger":false}},{"id":"c","value":{"type":"rawscript","content":"export function main(hello: string) {\\n const dareyou = `Did you just say \\"${hello}\\"??!`;\\n console.log(dareyou)\\n return dareyou\\n}","language":"deno","input_transforms":{"hello":{"type":"javascript","value":"${results.b}","expr":"`${results.b}`"}},"is_trigger":false}}],"iterator":{"type":"javascript","expr":"[\'foo\', \'bar\', \'baz\']"},"skip_failures":true,"parallel":false}}],"same_worker":false}',
'system'
);

View File

@@ -9,7 +9,7 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/same_folder_script', 12340, 'bun', '');
'f/system/same_folder_script', -28028598712388162, 'bun', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -22,7 +22,7 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/different_folder_script', 12341, 'bun', '');
'f/system_relative/different_folder_script', -28028598712388161, 'bun', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
@@ -41,4 +41,4 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/nested_script', 12342, 'bun', '');
'f/system_relative/nested_script', -28028598712388160, 'bun', '');

View File

@@ -9,7 +9,7 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/same_folder_script', 12343, 'deno', '');
'f/system/same_folder_script', -28028598712388162, 'deno', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -22,7 +22,7 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/different_folder_script', 12344, 'deno', '');
'f/system_relative/different_folder_script', -28028598712388161, 'deno', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
@@ -41,4 +41,4 @@ export function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/nested_script', 12345, 'deno', '');
'f/system_relative/nested_script', -28028598712388160, 'deno', '');

View File

@@ -8,7 +8,7 @@ def main():
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/same_folder_script', 12346, 'python3', '');
'f/system/same_folder_script', -28028598712388162, 'python3', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -20,7 +20,7 @@ def main():
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/different_folder_script', 12347, 'python3', '');
'f/system_relative/different_folder_script', -28028598712388161, 'python3', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
@@ -38,4 +38,4 @@ def main():
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system_relative/nested_script', 12348, 'python3', '');
'f/system_relative/nested_script', -28028598712388160, 'python3', '');

View File

@@ -13,7 +13,7 @@ export async function main(fail: boolean = true) {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"fail":{"default":true,"description":"","type":"boolean"}},"required":[],"type":"object"}',
'',
'',
'f/system/failing_script', 12349, 'deno', '');
'f/system/failing_script', -28028598712388162, 'deno', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -26,7 +26,7 @@ export async function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"path":{"default":null,"description":"","type":"string"},"schedule_path":{"default":null,"description":"","type":"string"},"error":{"default":null,"description":"","properties":{},"type":"object"}},"required":["path","schedule_path","error"],"type":"object"}',
'',
'',
'f/system/schedule_error_handler', 123410, 'deno', '');
'f/system/schedule_error_handler', -28028598712388161, 'deno', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -39,21 +39,21 @@ export async function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"path":{"default":null,"description":"","type":"string"},"schedule_path":{"default":null,"description":"","type":"string"},"previous_job_error":{"default":null,"description":"","type":"string"},"result":{"default":null,"description":"","type":"string"}},"required":["path","schedule_path","previous_job_error","result"],"type":"object"}',
'',
'',
'f/system/schedule_recovery_handler', 123411, 'deno', '');
'f/system/schedule_recovery_handler', -28028598712388160, 'deno', '');
INSERT INTO public.flow(workspace_id, summary, description, path, versions, schema, value, edited_by) VALUES (
'test-workspace',
'',
'',
'f/system/failing_flow',
'{1443253234253452}',
'{1}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"fail":{"default":true,"description":"","type":"boolean","format":""}},"required":[],"type":"object"}',
'{"modules": [{"id": "a", "value": {"path": "f/system/failing_script", "type": "script", "input_transforms": {"fail": {"expr": "flow_input.fail", "type": "javascript"}}}}]}',
'system'
);
INSERT INTO public.flow_version(id, workspace_id, path, schema, value, created_by) VALUES (
1443253234253452,
1,
'test-workspace',
'f/system/failing_flow',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"fail":{"default":true,"description":"","type":"boolean","format":""}},"required":[],"type":"object"}',

View File

@@ -1,6 +1,5 @@
use serde::de::DeserializeOwned;
use std::future::Future;
use std::{str::FromStr, sync::Arc};
use std::str::FromStr;
use windmill_api_client::types::{NewScript, NewScriptLanguage};
#[cfg(feature = "enterprise")]
@@ -12,7 +11,6 @@ use serde::Deserialize;
use serde_json::json;
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres};
use tokio::sync::RwLock;
#[cfg(feature = "enterprise")]
use tokio::time::{timeout, Duration};
@@ -31,9 +29,6 @@ use windmill_common::{
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
jobs::{JobKind, JobPayload, RawCode},
scripts::{ScriptHash, ScriptLang},
worker::{
MIN_VERSION_IS_AT_LEAST_1_427, MIN_VERSION_IS_AT_LEAST_1_432, MIN_VERSION_IS_AT_LEAST_1_440,
},
};
use windmill_queue::PushIsolationLevel;
@@ -139,16 +134,12 @@ impl ApiServer {
rx,
port_tx,
false,
#[cfg(feature = "smtp")]
format!("http://localhost:{}", addr.port()),
));
_port_rx.await.unwrap();
// clear the cache between tests
windmill_common::cache::clear();
Self { addr, tx, task }
return Self { addr, tx, task };
}
async fn close(self) -> anyhow::Result<()> {
@@ -1807,48 +1798,6 @@ fn main(world: String) -> Result<String, String> {
assert_eq!(result, serde_json::json!("Hello Hyrule!"));
}
// #[sqlx::test(fixtures("base"))]
// async fn test_csharp_job(db: Pool<Postgres>) {
// initialize_tracing().await;
// let server = ApiServer::start(db.clone()).await;
// let port = server.addr.port();
//
// let content = r#"
// using System;
//
// class Script
// {
// public static string Main(string world, int b = 2)
// {
// Console.WriteLine($"Hello {world} - {b}. This is a log line");
// return $"Hello {world} - {b}";
// }
// }
// "#
// .to_owned();
//
// let result = RunJob::from(JobPayload::Code(RawCode {
// hash: None,
// content,
// path: None,
// lock: None,
// language: ScriptLang::CSharp,
// custom_concurrency_key: None,
// concurrent_limit: None,
// concurrency_time_window_s: None,
// cache_ttl: None,
// dedicated_worker: None,
// }))
// .arg("world", json!("Arakis"))
// .arg("b", json!(3))
// .run_until_complete(&db, port)
// .await
// .json_result()
// .unwrap();
//
// assert_eq!(result, serde_json::json!("Hello Arakis - 3"));
// }
#[sqlx::test(fixtures("base"))]
async fn test_bash_job(db: Pool<Postgres>) {
initialize_tracing().await;
@@ -3740,472 +3689,3 @@ async fn test_result_format(db: Pool<Postgres>) {
.unwrap();
assert_eq!(result.get(), correct_result);
}
async fn test_for_versions<F: Future<Output = ()>>(
version_flags: impl Iterator<Item = Arc<RwLock<bool>>>,
test: impl Fn() -> F,
) {
for version_flag in version_flags {
*version_flag.write().await = true;
test().await;
}
}
mod job_payload {
use super::*;
use lazy_static::lazy_static;
use windmill_common::cache;
use windmill_common::flows::FlowNodeId;
lazy_static! {
static ref VERSION_FLAGS: [Arc<RwLock<bool>>; 3] = [
MIN_VERSION_IS_AT_LEAST_1_427.clone(),
MIN_VERSION_IS_AT_LEAST_1_432.clone(),
MIN_VERSION_IS_AT_LEAST_1_440.clone(),
];
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_script_hash_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(123412),
path: "f/system/hello".to_string(),
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
language: ScriptLang::Deno,
priority: None,
apply_preprocessor: false,
})
.arg("world", json!("foo"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Hello foo!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_flow_script_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
// Deploy the flow to produce the "lite" version.
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
let flow_data = cache::flow::fetch_version_lite(&db, 1443253234253454)
.await
.unwrap();
let flow_value = flow_data.value();
let flow_scripts = {
async fn load(db: &Pool<Postgres>, modules: &[FlowModule]) -> Vec<FlowNodeId> {
let mut res = vec![];
for module in modules {
let value =
serde_json::from_str::<FlowModuleValue>(module.value.get()).unwrap();
match value {
FlowModuleValue::FlowScript { id, .. } => res.push(id),
FlowModuleValue::ForloopFlow { modules_node: Some(flow_node), .. } => {
let flow_data = cache::flow::fetch_flow(db, flow_node).await.unwrap();
res.extend(Box::pin(load(db, &flow_data.value().modules)).await);
}
_ => {}
}
}
res
}
load(&db, &flow_value.modules).await
};
assert_eq!(flow_scripts.len(), 2);
let test = || async {
let result = RunJob::from(JobPayload::FlowScript {
id: flow_scripts[0],
language: ScriptLang::Deno,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
path: "f/system/hello/test-0".into(),
})
.arg("world", json!("foo"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Hello foo!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
let test = || async {
let result = RunJob::from(JobPayload::FlowScript {
id: flow_scripts[1],
language: ScriptLang::Deno,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
path: "f/system/hello/test-0".into(),
})
.arg("hello", json!("You know nothing Jean Neige"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result,
json!("Did you just say \"You know nothing Jean Neige\"??!")
);
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_flow_node_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
// Deploy the flow to produce the "lite" version.
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
let flow_data = cache::flow::fetch_version_lite(&db, 1443253234253454)
.await
.unwrap();
let flow_value = flow_data.value();
let forloop_module =
serde_json::from_str::<FlowModuleValue>(flow_value.modules[0].value.get()).unwrap();
let FlowModuleValue::ForloopFlow { modules_node: Some(id), .. } = forloop_module else {
panic!("Expected a forloop module with a flow node");
};
let test = || async {
let result = RunJob::from(JobPayload::FlowNode {
id,
path: "f/system/hello_with_nodes_flow/forloop-0".into(),
})
.arg("iter", json!({ "value": "tests", "index": 0 }))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Did you just say \"Hello tests!\"??!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dependencies_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::Dependencies {
path: "f/system/hello".to_string(),
hash: ScriptHash(123412),
language: ScriptLang::Deno,
dedicated_worker: None,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result.get("status").unwrap(),
&json!("Successful lock file generation")
);
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
// Just test that deploying a flow work as expected.
#[sqlx::test(fixtures("base", "hello"))]
async fn test_flow_dependencies_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result.get("status").unwrap(),
&json!("Successful lock file generation")
);
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_raw_flow_dependencies_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::RawFlowDependencies {
path: "none".to_string(),
flow_value: serde_json::from_value(json!({
"modules": [{
"id": "a",
"value": {
"type": "rawscript",
"content": r#"export function main(world: string) {
const greet = `Hello ${world}!`;
console.log(greet)
return greet
}"#,
"language": "deno",
"input_transforms": {
"world": { "type": "javascript", "expr": "flow_input.world" }
}
}
}],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": { "world": { "type": "string" } },
"type": "object",
"order": [ "world" ]
}
}))
.unwrap(),
})
.arg("skip_flow_update", json!(true))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
let result = RunJob::from(JobPayload::RawFlow {
value: serde_json::from_value::<FlowValue>(
result.get("updated_flow_value").unwrap().clone(),
)
.unwrap(),
path: None,
restarted_from: None,
})
.arg("world", json!("Jean Neige"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Hello Jean Neige!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_raw_script_dependencies_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::RawScriptDependencies {
script_path: "none".into(),
content: r#"export function main(world: string) {
const greet = `Hello ${world}!`;
console.log(greet)
return greet
}"#
.into(),
language: ScriptLang::Deno,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result,
json!({ "lock": "", "status": "Successful lock file generation" })
);
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_flow_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::Flow {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result,
json!([
"Did you just say \"Hello foo!\"??!",
"Did you just say \"Hello bar!\"??!",
"Did you just say \"Hello baz!\"??!",
])
);
};
// Test the not "lite" flow.
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
// Deploy the flow to produce the "lite" version.
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
// Test the "lite" flow.
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_restarted_flow_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let completed_job_id = RunJob::from(JobPayload::Flow {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
})
.run_until_complete(&db, port)
.await
.id;
let result = RunJob::from(JobPayload::RestartedFlow {
completed_job_id,
step_id: "a".into(),
branch_or_iteration_n: None,
})
.arg("iter", json!({ "value": "tests", "index": 0 }))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result,
json!([
"Did you just say \"Hello foo!\"??!",
"Did you just say \"Hello bar!\"??!",
"Did you just say \"Hello baz!\"??!",
])
);
};
// Test the not "lite" flow.
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
// Deploy the flow to produce the "lite" version.
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
// Test the "lite" flow.
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_raw_flow_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::RawFlow {
value: serde_json::from_value(json!({
"modules": [{
"id": "a",
"value": {
"type": "rawscript",
"content": r#"export function main(world: string) {
const greet = `Hello ${world}!`;
console.log(greet)
return greet
}"#,
"language": "deno",
"input_transforms": {
"world": { "type": "javascript", "expr": "flow_input.world" }
}
}
}],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": { "world": { "type": "string" } },
"type": "object",
"order": [ "world" ]
}
}))
.unwrap(),
path: None,
restarted_from: None,
})
.arg("world", json!("Jean Neige"))
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(result, json!("Hello Jean Neige!"));
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
}

View File

@@ -10,7 +10,7 @@ path = "src/lib.rs"
[features]
default = []
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise"]
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "dep:openidconnect"]
stripe = ["dep:async-stripe"]
enterprise_saml = ["dep:samael"]
benchmark = []
@@ -20,19 +20,13 @@ prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:pr
openidconnect = ["dep:openidconnect"]
tantivy = ["dep:windmill-indexer"]
kafka = ["dep:rdkafka"]
websocket = ["dep:tokio-tungstenite"]
smtp = ["dep:mail-parser", "dep:openssl", "windmill-common/smtp"]
license = ["dep:rsa"]
zip = ["dep:async_zip"]
oauth2 = ["dep:async-oauth2"]
http_trigger = ["dep:matchit"]
static_frontend = ["dep:rust-embed"]
[dependencies]
windmill-queue.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
windmill-parser.workspace = true
windmill-parser-py-imports.workspace = true
windmill-parser-ts.workspace = true
windmill-git-sync.workspace = true
windmill-indexer = { workspace = true, optional = true }
@@ -50,7 +44,7 @@ itertools.workspace = true
reqwest.workspace = true
serde.workspace = true
sqlx.workspace = true
async-oauth2 = { workspace = true, optional = true }
async-oauth2.workspace = true
tracing.workspace = true
sql-builder.workspace = true
serde_json.workspace = true
@@ -62,15 +56,15 @@ base32.workspace = true
serde_urlencoded.workspace = true
cron.workspace = true
mime_guess.workspace = true
rust-embed = { workspace = true, optional = true }
rust-embed.workspace = true
tracing-subscriber.workspace = true
quick_cache.workspace = true
rand.workspace = true
time.workspace = true
native-tls.workspace = true
tokio-native-tls.workspace = true
openssl = { workspace = true, optional = true }
mail-parser = { workspace = true, features = ["serde_support"], optional = true }
openssl.workspace = true
mail-parser = { workspace = true, features = ["serde_support"] }
magic-crypt.workspace = true
tempfile.workspace = true
tokio-util.workspace = true
@@ -82,12 +76,12 @@ urlencoding.workspace = true
async-stripe = { workspace = true, optional = true }
lazy_static.workspace = true
prometheus = { workspace = true, optional = true }
async_zip = { workspace = true, optional = true }
async_zip.workspace = true
regex.workspace = true
bytes.workspace = true
samael = { workspace = true, optional = true }
async-recursion.workspace = true
rsa = { workspace = true, optional = true}
rsa.workspace = true
uuid.workspace = true
tinyvector = { workspace = true, optional = true}
hf-hub = { workspace = true, optional = true}
@@ -100,8 +94,8 @@ object_store = { workspace = true, optional = true}
openidconnect = { workspace = true, optional = true}
url = { workspace = true, optional = true}
jsonwebtoken = { workspace = true }
matchit = { workspace = true, optional = true }
tokio-tungstenite = { workspace = true, optional = true}
matchit.workspace = true
tokio-tungstenite.workspace = true
rdkafka = { workspace = true, optional = true }
const_format.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.441.0
version: 1.437.1
title: Windmill API
contact:
@@ -1121,15 +1121,9 @@ paths:
operationId: refreshUserToken
tags:
- user
parameters:
- name: if_expiring_in_less_than_s
in: query
required: false
schema:
type: integer
responses:
"200":
description: new token
description: free usage
content:
text/plain:
schema:
@@ -6943,15 +6937,20 @@ paths:
- resume
- cancel
/w/{workspace}/jobs/slack_approval/{id}:
/w/{workspace}/jobs/slack_approval/{id}/{resume_id}:
get:
summary: generate interactive slack approval for suspended job
summary: get interactive slack approval payload given the job_id, resume_id and a nonce to resume a flow
operationId: getSlackApprovalPayload
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
- name: resume_id
in: path
required: true
schema:
type: integer
- name: approver
in: query
schema:
@@ -6960,24 +6959,20 @@ paths:
in: query
schema:
type: string
- name: slack_resource_path
in: query
required: true
schema:
type: string
- name: channel_id
in: query
required: true
schema:
type: string
- name: flow_step_id
in: query
required: true
schema:
type: string
responses:
"200":
description: Interactive slack approval message sent successfully
description: Blocks array for posting interactive slack approval
content:
application/json:
schema:
type: object
properties:
blocks:
type: array
items:
type: object
required:
- blocks
/w/{workspace}/jobs_u/resume/{id}/{resume_id}/{signature}:
get:
@@ -10734,7 +10729,6 @@ components:
php,
rust,
ansible,
csharp,
]
kind:
type: string
@@ -10838,7 +10832,6 @@ components:
php,
rust,
ansible,
csharp,
]
kind:
type: string
@@ -11063,7 +11056,6 @@ components:
php,
rust,
ansible,
csharp,
]
email:
type: string
@@ -11186,7 +11178,6 @@ components:
php,
rust,
ansible,
csharp,
]
is_skipped:
type: boolean
@@ -11742,7 +11733,6 @@ components:
php,
rust,
ansible,
csharp,
]
tag:
type: string
@@ -12983,8 +12973,6 @@ components:
type: object
additionalProperties:
type: boolean
custom_path:
type: string
required:
- id
- workspace_id
@@ -13006,6 +12994,8 @@ components:
draft_only:
type: boolean
draft: {}
custom_path:
type: string
AppHistory:
type: object
@@ -13359,7 +13349,6 @@ components:
php,
rust,
ansible,
csharp,
]
required:
- raw_code

View File

@@ -20,7 +20,7 @@ use crate::{
#[cfg(feature = "parquet")]
use crate::{
job_helpers_ee::{
get_random_file_name, get_s3_resource, get_workspace_s3_resource, upload_file_from_req,
get_random_file_name, get_s3_resource, get_workspace_s3_resource, upload_file_internal,
UploadFileResponse,
},
users::fetch_api_authed_from_permissioned_as,
@@ -101,6 +101,11 @@ pub fn global_service() -> Router {
.route("/hub/get/:id", get(get_hub_app_by_id))
}
#[cfg(not(feature = "enterprise"))]
pub fn global_unauthed_service() -> Router {
Router::new()
}
#[derive(FromRow, Deserialize, Serialize)]
pub struct ListableApp {
pub id: i64,
@@ -140,8 +145,6 @@ pub struct AppWithLastVersion {
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub extra_perms: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_path: Option<String>,
}
#[derive(Serialize, FromRow)]
@@ -171,6 +174,8 @@ pub struct AppWithLastVersionAndDraft {
pub draft: Option<sqlx::types::Json<Box<RawValue>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_path: Option<String>,
}
#[derive(Serialize)]
@@ -373,7 +378,7 @@ async fn get_app(
let app_o = if query.with_starred_info.unwrap_or(false) {
sqlx::query_as::<_, AppWithLastVersionAndStarred>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, favorite.path IS NOT NULL as starred
FROM app
@@ -393,7 +398,7 @@ async fn get_app(
.await?
} else {
sqlx::query_as::<_, AppWithLastVersionAndStarred>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by, NULL as starred
FROM app, app_version
@@ -419,7 +424,7 @@ async fn get_app_lite(
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, coalesce(app_version_lite.value::json, app_version.value) as value,
app_version.created_at, app_version.created_by, NULL as starred
FROM app, app_version
@@ -446,8 +451,8 @@ async fn get_app_w_draft(
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersionAndDraft>(
r#"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
app.extra_perms, app_version.value,
r#"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value, app.custom_path,
app_version.created_at, app_version.created_by,
app.draft_only, draft.value as "draft"
from app
@@ -577,7 +582,7 @@ async fn get_app_by_id(
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by from app, app_version
WHERE app_version.id = $1 AND app.id = app_version.app_id AND app.workspace_id = $2",
@@ -608,7 +613,7 @@ async fn get_public_app_by_secret(
let id: i64 = bytes.parse().map_err(to_anyhow)?;
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
null as extra_perms, coalesce(app_version_lite.value::json, app_version.value::json) as value,
app_version.created_at, app_version.created_by from app, app_version
LEFT JOIN app_version_lite ON app_version_lite.id = app_version.id
@@ -1322,7 +1327,10 @@ async fn execute_component(
// 2. Otherwise, always fetch the policy from the database.
let policy = if let Some(id) = payload.version {
let cache = cache::anon!({ u64 => Arc<Policy> } in "policy" <= 1000);
arc_policy = policy_fut.map_ok(Arc::new).cached(cache, id as u64).await?;
arc_policy = policy_fut
.map_ok(Arc::new)
.cached(cache, &(id as u64))
.await?;
&*arc_policy
} else {
policy = policy_fut.await?;
@@ -1350,7 +1358,7 @@ async fn execute_component(
.fetch_one(&db)
.map_err(Into::<Error>::into)
.map_ok(Arc::new)
.cached(cache, *id as u64)
.cached(cache, &(*id as u64))
.await?
}
_ => unreachable!(),
@@ -1673,7 +1681,7 @@ async fn upload_s3_file_from_app(
])
.into();
upload_file_from_req(s3_client, &file_key, request, options).await?;
upload_file_internal(s3_client, &file_key, request, options).await?;
return Ok(Json(UploadFileResponse { file_key }));
}

View File

@@ -3,12 +3,12 @@ use std::collections::HashMap;
#[cfg(feature = "parquet")]
use crate::job_helpers_ee::get_workspace_s3_resource;
use axum::{
extract::{FromRequest, FromRequestParts, Multipart, Query, Request},
extract::{FromRequest, FromRequestParts, Query, Request},
http::{HeaderMap, Uri},
response::{IntoResponse, Response},
};
use bytes::Bytes;
use http::{header::CONTENT_TYPE, request::Parts, StatusCode};
use http::{header::CONTENT_TYPE, request::Parts};
#[cfg(feature = "parquet")]
use object_store::{Attribute, Attributes};
use serde::Deserialize;
@@ -23,11 +23,9 @@ use crate::db::ApiAuthed;
#[cfg(feature = "parquet")]
use crate::job_helpers_ee::{get_random_file_name, upload_file_internal};
#[derive(Default)]
pub struct WebhookArgs {
pub args: PushArgsOwned,
pub multipart: Option<Multipart>,
pub wrap_body: Option<bool>,
pub file_req: Option<Request>,
}
impl WebhookArgs {
@@ -38,13 +36,9 @@ impl WebhookArgs {
_db: &DB,
_w_id: &str,
) -> Result<PushArgsOwned, Error> {
if self.multipart.is_some() {
return Err(Error::BadRequest(format!(
"multipart/form-data requires the parquet feature"
)));
}
Ok(self.args)
return Err(Error::BadRequest(format!(
"Uploading files requires the parquet feature"
)));
}
#[cfg(feature = "parquet")]
@@ -54,9 +48,7 @@ impl WebhookArgs {
db: &DB,
w_id: &str,
) -> Result<PushArgsOwned, Error> {
use futures::TryStreamExt;
if let Some(mut multipart) = self.multipart {
if let Some(req) = self.file_req {
{
let (_, s3_resource) =
get_workspace_s3_resource(authed, db, None, "", w_id, None).await?;
@@ -64,78 +56,47 @@ impl WebhookArgs {
if let Some(s3_resource) = s3_resource {
let s3_client = build_object_store_client(&s3_resource).await?;
let mut body = HashMap::new();
let content_type = req
.headers()
.get(CONTENT_TYPE)
.map(|x| x.to_str().ok().map(|x| x.to_string()))
.flatten();
while let Some(field) = multipart.next_field().await.map_err(|e| {
Error::BadRequest(format!(
"Error reading multipart field: {}",
e.body_text()
))
})? {
if let Some(name) = field.name().map(|x| x.to_string()) {
if let Some(content_type) = field.content_type() {
let ext = field
.file_name()
.map(|x| x.split('.').last())
.flatten()
.map(|x| x.to_string());
let file_extension = content_type
.as_ref()
.map(|mime_str| {
mime_guess::get_mime_extensions_str(mime_str)
.map(|x| x.first().map(|x| x.to_string()))
})
.flatten()
.flatten();
let file_key = get_random_file_name(ext);
let file_key = get_random_file_name(file_extension);
let options = Attributes::from_iter(vec![
(Attribute::ContentType, content_type.to_string()),
(
Attribute::ContentDisposition,
if let Some(filename) = field.file_name() {
format!("inline; filename=\"{}\"", filename)
} else {
"inline".to_string()
},
),
])
.into();
let options = Attributes::from_iter(vec![
(
Attribute::ContentType,
content_type.unwrap_or("application/octet-stream".to_string()),
),
(Attribute::ContentDisposition, "inline".to_string()),
])
.into();
let bytes_stream = field.into_stream().map_err(|err| {
std::io::Error::new(std::io::ErrorKind::Other, err)
});
upload_file_internal(s3_client, &file_key, req, options).await?;
upload_file_internal(
s3_client.clone(),
&file_key,
bytes_stream,
options,
)
.await?;
body.insert(
name,
to_raw_value(&serde_json::json!({
"s3": &file_key
})),
);
} else {
body.insert(
name,
to_raw_value(&field.text().await.unwrap_or_default()),
);
}
}
}
if self.wrap_body.unwrap_or(false) {
self.args
.args
.insert("body".to_string(), to_raw_value(&body));
} else {
self.args.args.extend(body);
}
self.args.args.insert(
"body".to_string(),
to_raw_value(&serde_json::json!({
"s3": &file_key
})),
);
return Ok(self.args);
}
}
return Err(Error::BadRequest(format!(
"You need to connect your workspace to an S3 bucket to use multipart/form-data"
"You need to connect your workspace to an S3 bucket to upload files"
)));
}
@@ -150,17 +111,6 @@ pub struct RequestQuery {
pub include_header: Option<String>,
}
async fn req_to_string<S: Send + Sync>(
req: Request<axum::body::Body>,
_state: &S,
) -> Result<String, Response> {
let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
String::from_utf8(bytes.to_vec())
.map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())
}
#[axum::async_trait]
impl<S> FromRequest<S, axum::body::Body> for WebhookArgs
where
@@ -203,7 +153,7 @@ where
}
return Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: args },
..Default::default()
file_req: None,
});
}
let str = String::from_utf8(bytes.to_vec())
@@ -211,16 +161,20 @@ where
PushArgsOwned::from_json(extra, use_raw, wrap_body, str)
.await
.map(|args| Self { args, ..Default::default() })
.map(|args| Self { args, file_req: None })
} else if content_type
.unwrap()
.starts_with("application/cloudevents+json")
{
let str = req_to_string(req, _state).await?;
let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
let str = String::from_utf8(bytes.to_vec())
.map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())?;
PushArgsOwned::from_ce_json(extra, use_raw, str)
.await
.map(|args| Self { args, ..Default::default() })
.map(|args| Self { args, file_req: None })
} else if content_type
.unwrap()
.starts_with("application/cloudevents-batch+json")
@@ -230,11 +184,15 @@ where
.into_response(),
)
} else if content_type.unwrap().starts_with("text/plain") {
let str = req_to_string(req, _state).await?;
let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
let str = String::from_utf8(bytes.to_vec())
.map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())?;
extra.insert("raw_string".to_string(), to_raw_value(&str));
Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: HashMap::new() },
..Default::default()
file_req: None,
})
} else if content_type
.unwrap()
@@ -262,29 +220,15 @@ where
return Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: payload },
..Default::default()
file_req: None,
});
} else if content_type.unwrap().starts_with("application/xml")
|| content_type.unwrap().starts_with("text/xml")
{
let str = req_to_string(req, _state).await?;
extra.insert("raw_string".to_string(), to_raw_value(&str));
Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: HashMap::new() },
..Default::default()
})
} else if content_type.unwrap().starts_with("multipart/form-data") {
let multipart = Multipart::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
Ok(Self {
args: PushArgsOwned { extra: Some(extra), args: HashMap::new() },
multipart: Some(multipart),
wrap_body: Some(wrap_body),
})
} else {
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
return Ok(Self {
args: PushArgsOwned { extra: None, args: HashMap::new() },
file_req: Some(req),
});
// Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
}
}
}

View File

@@ -1,587 +0,0 @@
#[cfg(feature = "enterprise")]
use crate::ee::ExternalJwks;
use axum::{
async_trait,
extract::{FromRequestParts, OriginalUri, Query},
Extension,
};
use chrono::TimeZone;
use http::{request::Parts, StatusCode};
use quick_cache::sync::Cache;
use serde::Deserialize;
use tower_cookies::Cookies;
use tracing::Span;
use crate::db::{ApiAuthed, DB};
use std::sync::{
atomic::{AtomicI64, AtomicU64, Ordering},
Arc,
};
#[cfg(feature = "enterprise")]
use tokio::sync::RwLock;
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, JWT_SECRET},
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
};
#[derive(Clone)]
pub struct ExpiringAuthCache {
pub authed: ApiAuthed,
pub expiry: chrono::DateTime<chrono::Utc>,
}
pub struct AuthCache {
cache: Cache<(String, String), ExpiringAuthCache>,
db: DB,
superadmin_secret: Option<String>,
#[cfg(feature = "enterprise")]
ext_jwks: Option<Arc<RwLock<ExternalJwks>>>,
}
impl AuthCache {
pub fn new(
db: DB,
superadmin_secret: Option<String>,
#[cfg(feature = "enterprise")] ext_jwks: Option<Arc<RwLock<ExternalJwks>>>,
) -> Self {
AuthCache {
cache: Cache::new(300),
db,
superadmin_secret,
#[cfg(feature = "enterprise")]
ext_jwks,
}
}
pub async fn invalidate(&self, w_id: &str, token: String) {
self.cache.remove(&(w_id.to_string(), token));
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<ApiAuthed> {
let key = (
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
token.to_string(),
);
let s = self.cache.get(&key).map(|c| c.to_owned());
match s {
Some(ExpiringAuthCache { authed, expiry }) if expiry > chrono::Utc::now() => {
Some(authed)
}
#[cfg(feature = "enterprise")]
_ if token.starts_with("jwt_ext_") => {
let authed_and_exp = match crate::ee::jwt_ext_auth(
w_id.as_ref(),
token.trim_start_matches("jwt_ext_"),
self.ext_jwks.clone(),
)
.await
{
Ok(r) => Some(r),
Err(e) => {
tracing::error!("JWT_EXT auth error: {:?}", e);
None
}
};
if let Some((authed, exp)) = authed_and_exp.clone() {
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc.timestamp_nanos(exp as i64 * 1_000_000_000),
},
);
Some(authed)
} else {
None
}
}
_ if token.starts_with("jwt_") => {
let jwt_secret = JWT_SECRET.read().await;
if !jwt_secret.is_empty() {
let jwt_token = token.trim_start_matches("jwt_");
let jwt_result = jsonwebtoken::decode::<JWTAuthClaims>(
jwt_token,
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
&jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
);
match jwt_result {
Ok(payload) => {
if w_id.is_some_and(|w_id| w_id != payload.claims.workspace_id) {
tracing::error!("JWT auth error: workspace_id mismatch");
return None;
}
let username_override =
username_override_from_label(payload.claims.label);
let authed = crate::db::ApiAuthed {
email: payload.claims.email,
username: payload.claims.username,
is_admin: payload.claims.is_admin,
is_operator: payload.claims.is_operator,
groups: payload.claims.groups,
folders: payload.claims.folders,
scopes: None,
username_override,
};
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc
.timestamp_nanos(payload.claims.exp as i64 * 1_000_000_000),
},
);
Some(authed)
}
Err(err) => {
tracing::error!("JWT auth error: {:?}", err);
None
}
}
} else {
tracing::error!("JWT auth error: no jwt secret set");
None
}
}
_ => {
let user_o = sqlx::query_as::<_, (Option<String>, Option<String>, bool, Option<Vec<String>>, Option<String>)>(
"UPDATE token SET last_used_at = now() WHERE token = $1 AND (expiration > NOW() \
OR expiration IS NULL) AND (workspace_id IS NULL OR workspace_id = $2) RETURNING owner, email, super_admin, scopes, label",
)
.bind(token)
.bind(w_id.as_ref())
.fetch_optional(&self.db)
.await
.ok()
.flatten();
if let Some(user) = user_o {
let authed_o = {
match user {
(Some(owner), Some(email), super_admin, _, label) if w_id.is_some() => {
let username_override = username_override_from_label(label);
if let Some((prefix, name)) = owner.split_once('/') {
if prefix == "u" {
let (is_admin, is_operator) = if super_admin {
(true, false)
} else {
let r = sqlx::query!(
"SELECT is_admin, operator FROM usr where username = $1 AND \
workspace_id = $2 AND disabled = false",
name,
&w_id.as_ref().unwrap()
)
.fetch_one(&self.db)
.await
.ok();
if let Some(r) = r {
(r.is_admin, r.operator)
} else {
(false, true)
}
};
let w_id = &w_id.unwrap();
let groups =
get_groups_for_user(w_id, &name, &email, &self.db)
.await
.ok()
.unwrap_or_default();
let folders =
get_folders_for_user(w_id, &name, &groups, &self.db)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: name.to_string(),
is_admin,
is_operator,
groups,
folders,
scopes: None,
username_override,
})
} else {
let groups = vec![name.to_string()];
let folders = get_folders_for_user(
&w_id.unwrap(),
"",
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: format!("group-{name}"),
is_admin: false,
groups,
is_operator: false,
folders,
scopes: None,
username_override,
})
}
} else {
let groups = vec![];
let folders = vec![];
Some(ApiAuthed {
email: email,
username: owner,
is_admin: super_admin,
is_operator: true,
groups,
folders,
scopes: None,
username_override,
})
}
}
(_, Some(email), super_admin, scopes, label) => {
let username_override = username_override_from_label(label);
if w_id.is_some() {
let row_o = sqlx::query_as::<_, (String, bool, bool)>(
"SELECT username, is_admin, operator FROM usr where email = $1 AND \
workspace_id = $2 AND disabled = false",
)
.bind(&email)
.bind(&w_id.as_ref().unwrap())
.fetch_optional(&self.db)
.await
.unwrap_or(Some(("error".to_string(), false, false)));
match row_o {
Some((username, is_admin, is_operator)) => {
let groups = get_groups_for_user(
&w_id.as_ref().unwrap(),
&username,
&email,
&self.db,
)
.await
.ok()
.unwrap_or_default();
let folders = get_folders_for_user(
&w_id.unwrap(),
&username,
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email,
username,
is_admin: is_admin || super_admin,
is_operator,
groups,
folders,
scopes,
username_override,
})
}
None if super_admin => Some(ApiAuthed {
email: email.clone(),
username: email,
is_admin: super_admin,
is_operator: false,
groups: vec![],
folders: vec![],
scopes,
username_override,
}),
None => None,
}
} else {
Some(ApiAuthed {
email: email.to_string(),
username: email,
is_admin: super_admin,
is_operator: true,
groups: Vec::new(),
folders: Vec::new(),
scopes,
username_override,
})
}
}
_ => None,
}
};
if let Some(authed) = authed_o.as_ref() {
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc::now()
+ chrono::Duration::try_seconds(120).unwrap(),
},
);
}
authed_o
} else if self
.superadmin_secret
.as_ref()
.map(|x| x == token)
.unwrap_or(false)
{
Some(ApiAuthed {
email: SUPERADMIN_SECRET_EMAIL.to_string(),
username: "superadmin_secret".to_string(),
is_admin: true,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
})
} else {
None
}
}
}
}
}
async fn extract_token<S: Send + Sync>(parts: &mut Parts, state: &S) -> Option<String> {
let auth_header = parts
.headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "));
let from_cookie = match auth_header {
Some(x) => Some(x.to_owned()),
None => Extension::<Cookies>::from_request_parts(parts, state)
.await
.ok()
.and_then(|cookies| cookies.get(COOKIE_NAME).map(|c| c.value().to_owned())),
};
#[derive(Deserialize)]
struct Token {
token: Option<String>,
}
match from_cookie {
Some(token) => Some(token),
None => Query::<Token>::from_request_parts(parts, state)
.await
.ok()
.and_then(|token| token.token.clone()),
}
}
#[derive(Clone, Debug)]
pub struct Tokened {
pub token: String,
}
pub struct OptTokened {
#[allow(dead_code)]
pub token: Option<String>,
}
struct BruteForceCounter {
counter: AtomicU64,
last_reset: AtomicI64,
}
lazy_static::lazy_static! {
static ref BRUTE_FORCE_COUNTER: BruteForceCounter =
BruteForceCounter { last_reset: AtomicI64::new(0), counter: AtomicU64::new(0) };
}
impl BruteForceCounter {
async fn increment(&self) {
let now = time::OffsetDateTime::now_utc().unix_timestamp();
if self.counter.fetch_add(1, Ordering::Relaxed) > 10000 {
tracing::error!(
"Brute force attack to find valid token detected, sleeping unauthorized response for 2 seconds"
);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
if now - self.last_reset.load(Ordering::Relaxed) > 60 {
self.counter.store(0, Ordering::Relaxed);
self.last_reset.store(now, Ordering::Relaxed);
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Tokened
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(Tokened { token: "".to_string() });
};
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(tokened.clone())
} else {
let token_o = extract_token(parts, state).await;
if let Some(token) = token_o {
let tokened = Self { token };
parts.extensions.insert(tokened.clone());
Ok(tokened)
} else {
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for OptTokened
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(OptTokened { token: None });
};
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(OptTokened { token: Some(tokened.token.clone()) })
} else {
let token_o = extract_token(parts, state).await;
Ok(OptTokened { token: token_o })
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for ApiAuthed
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(ApiAuthed {
email: "".to_owned(),
username: "".to_owned(),
is_admin: false,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
});
};
let already_authed = parts.extensions.get::<ApiAuthed>();
if let Some(authed) = already_authed {
Ok(authed.clone())
} else {
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(parts, state).await
};
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else {
if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[2] == "srch"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_string())
} else {
None
}
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
parts.extensions.insert(authed.clone());
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((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
}
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
}
}
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}
}
fn username_override_from_label(label: Option<String>) -> Option<String> {
match label {
Some(label)
if label.starts_with("webhook-")
|| label.starts_with("http-")
|| label.starts_with("email-")
|| label.starts_with("ws-") =>
{
Some(label)
}
Some(label) if label.starts_with("ephemeral-script-end-user-") => Some(
label
.trim_start_matches("ephemeral-script-end-user-")
.to_string(),
),
Some(label) if label == "Ephemeral lsp token" => Some("lsp".to_string()),
Some(label) if label != "ephemeral-script" && label != "session" && !label.is_empty() => {
Some(format!("label-{label}"))
}
_ => None,
}
}

View File

@@ -11,9 +11,8 @@ use std::sync::Arc;
use crate::db::ApiAuthed;
use crate::{
auth::AuthCache,
db::DB,
users::Tokened,
users::{AuthCache, Tokened},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{

View File

@@ -2,13 +2,12 @@
use crate::job_helpers_ee::get_workspace_s3_resource;
use crate::{
args::WebhookArgs,
auth::{AuthCache, OptTokened},
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::fetch_api_authed,
users::{fetch_api_authed, AuthCache, OptTokened},
};
use axum::{
extract::{Path, Query},

View File

@@ -12,12 +12,6 @@ use std::sync::Arc;
use windmill_common::error;
#[cfg(feature = "parquet")]
use windmill_common::{db::UserDB, s3_helpers::ObjectStoreResource};
#[cfg(feature = "parquet")]
use bytes::Bytes;
#[cfg(feature = "parquet")]
use futures::Stream;
#[derive(Serialize)]
pub struct UploadFileResponse {
pub file_key: String,
@@ -41,7 +35,7 @@ pub async fn get_workspace_s3_resource<'c>(
}
pub fn get_random_file_name(_file_extension: Option<String>) -> String {
unimplemented!("Not implemented in Windmill's Open Source repository")
todo!()
}
pub async fn get_s3_resource<'c>(
@@ -54,31 +48,14 @@ pub async fn get_s3_resource<'c>(
_resource_type: Option<StorageResourceType>,
_job_id: Option<Uuid>,
) -> error::Result<ObjectStoreResource> {
Err(error::Error::InternalErr(
"Not implemented in Windmill's Open Source repository".to_string(),
))
todo!()
}
#[cfg(feature = "parquet")]
pub async fn upload_file_from_req(
_s3_client: Arc<dyn ObjectStore>,
_file_key: &str,
_req: axum::extract::Request,
_options: PutMultipartOpts,
) -> error::Result<()> {
Err(error::Error::InternalErr(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
#[cfg(feature = "parquet")]
pub async fn upload_file_internal(
_s3_client: Arc<dyn ObjectStore>,
_file_key: &str,
_stream: impl Stream<Item = Result<Bytes, std::io::Error>> + Unpin,
_request: axum::extract::Request,
_options: PutMultipartOpts,
) -> error::Result<()> {
Err(error::Error::InternalErr(
"Not implemented in Windmill's Open Source repository".to_string(),
))
todo!()
}

View File

@@ -8,13 +8,13 @@
use axum::body::Body;
use axum::http::HeaderValue;
use futures::TryFutureExt;
use itertools::Itertools;
use quick_cache::sync::Cache;
use serde_json::value::RawValue;
use sqlx::Pool;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
#[cfg(feature = "prometheus")]
use std::sync::atomic::Ordering;
use tokio::io::AsyncReadExt;
@@ -63,7 +63,6 @@ use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE};
use windmill_common::{
cache,
db::UserDB,
error::{self, to_anyhow, Error},
flow_status::{Approval, FlowStatus, FlowStatusModule},
@@ -778,7 +777,7 @@ impl<'a> GetQuery<'a> {
Self { with_in_tags: in_tags, ..self }
}
fn check_auth(&self, email: Option<&str>) -> error::Result<()> {
fn check_auth(self, email: Option<&str>) -> error::Result<()> {
if let Some(email) = email {
if self.with_auth.is_some_and(|x| x.is_none()) && email != "anonymous" {
return Err(Error::BadRequest(
@@ -790,53 +789,6 @@ impl<'a> GetQuery<'a> {
Ok(())
}
/// Resolve job raw values.
/// This fetch the raw values from the cache and update the job accordingly.
///
/// # Details
/// Most of the raw values (code, lock and flow) had been removed from the `job`, `queue` and
/// `completed_job` tables. Only remains ones for "preview" jobs (i.e. [`JobKind::Preview`],
/// [`JobKind::FlowPreview`] and [`JobKind::Dependencies`]). [`JobKind::Flow`] as well but only
/// when pushed from an un-updated workers.
/// This function is used to make the above change transparent for the API, as the returned jobs
/// will have the raw values as if they were still in the tables.
async fn resolve_raw_values<T>(
&self,
db: &DB,
id: Uuid,
kind: JobKind,
hash: Option<ScriptHash>,
job: &mut JobExtended<T>,
) {
let (raw_code, raw_lock, raw_flow) = (
job.raw_code.take(),
job.raw_lock.take(),
job.raw_flow.take(),
);
if self.with_flow {
// Try to fetch the flow from the cache, fallback to the preview flow.
// NOTE: This could check for the job kinds instead of the `or_else` but it's not
// necessary as `fetch_flow` return early if the job kind is not a preview one.
cache::job::fetch_flow(db, kind, hash)
.or_else(|_| cache::job::fetch_preview_flow(db, &id, raw_flow))
.await
.ok()
.inspect(|data| job.raw_flow = Some(sqlx::types::Json(data.raw_flow.clone())));
}
if self.with_code {
// Try to fetch the code from the cache, fallback to the preview code.
// NOTE: This could check for the job kinds instead of the `or_else` but it's not
// necessary as `fetch_script` return early if the job kind is not a preview one.
cache::job::fetch_script(db, kind, hash)
.or_else(|_| cache::job::fetch_preview_script(db, &id, raw_lock, raw_code))
.await
.ok()
.inspect(|data| {
(job.raw_lock, job.raw_code) = (data.lock.clone(), Some(data.code.clone()))
});
}
}
async fn fetch_queued(
self,
db: &DB,
@@ -856,10 +808,6 @@ impl<'a> GetQuery<'a> {
let mut job = query.fetch_optional(db).await?;
self.check_auth(job.as_ref().map(|job| job.created_by.as_str()))?;
if let Some(job) = job.as_mut() {
self.resolve_raw_values(db, job.id, job.job_kind, job.script_hash, job)
.await;
}
if self.with_flow {
job = resolve_maybe_value(db, workspace_id, self.with_code, job, |job| {
job.raw_flow.as_mut()
@@ -888,10 +836,6 @@ impl<'a> GetQuery<'a> {
let mut cjob = query.fetch_optional(db).await?;
self.check_auth(cjob.as_ref().map(|job| job.created_by.as_str()))?;
if let Some(job) = cjob.as_mut() {
self.resolve_raw_values(db, job.id, job.job_kind, job.script_hash, job)
.await;
}
if self.with_flow {
cjob = resolve_maybe_value(db, workspace_id, self.with_code, cjob, |job| {
job.raw_flow.as_mut()
@@ -2160,7 +2104,7 @@ pub struct SuspendedJobFlow {
pub approvers: Vec<Approval>,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize)]
pub struct QueryApprover {
pub approver: Option<String>,
}
@@ -2375,9 +2319,9 @@ fn create_signature(
#[allow(non_snake_case)]
#[derive(Serialize, Debug)]
pub struct ResumeUrls {
pub approvalPage: String,
pub cancel: String,
pub resume: String,
approvalPage: String,
cancel: String,
resume: String,
}
fn build_resume_url(
@@ -2397,14 +2341,6 @@ pub async fn get_resume_urls(
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id)): Path<(String, Uuid, u32)>,
Query(approver): Query<QueryApprover>,
) -> error::JsonResult<ResumeUrls> {
get_resume_urls_internal(Extension(db), Path((w_id, job_id, resume_id)), Query(approver)).await
}
pub async fn get_resume_urls_internal(
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id)): Path<(String, Uuid, u32)>,
Query(approver): Query<QueryApprover>,
) -> error::JsonResult<ResumeUrls> {
let key = get_workspace_key(&w_id, &db).await?;
let signature = create_signature(key, job_id, resume_id, approver.approver.clone())?;
@@ -2552,7 +2488,10 @@ impl Job {
}
pub fn is_flow(&self) -> bool {
self.job_kind().is_flow()
matches!(
self.job_kind(),
JobKind::Flow | JobKind::FlowPreview | JobKind::SingleScriptFlow | JobKind::FlowNode
)
}
pub fn job_kind(&self) -> &JobKind {
@@ -5570,3 +5509,552 @@ async fn delete_completed_job<'a>(
Ok(response)
}
use axum::extract::Form;
use regex::Regex;
use reqwest::Client;
use serde_json::Value;
#[derive(Deserialize, Debug)]
pub struct SlackFormData {
payload: String,
}
#[derive(Deserialize, Debug)]
struct Payload {
actions: Vec<Action>,
state: State,
response_url: Option<String>,
message: Message,
}
#[derive(Deserialize, Debug)]
struct Message {
blocks: Option<Vec<Value>>,
}
#[derive(Deserialize, Debug)]
struct Action {
value: String,
}
#[derive(Deserialize, Debug)]
struct State {
values: HashMap<String, HashMap<String, ValueInput>>,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
enum ValueInput {
PlainTextInput { value: Option<Value> },
Datepicker { selected_date: Option<String> },
Timepicker { selected_time: Option<String> },
StaticSelect { selected_option: Option<SelectedOption> },
RadioButtons { selected_option: Option<SelectedOption> },
Checkboxes { selected_options: Option<Vec<SelectedOption>> },
}
#[derive(Deserialize, Debug)]
struct SelectedOption {
value: String,
}
pub async fn slack_app_callback_handler(
authed: Option<ApiAuthed>,
Extension(db): Extension<DB>,
Form(form_data): Form<SlackFormData>,
) -> error::Result<StatusCode> {
tracing::debug!("Form data: {:?}", form_data);
let payload: Payload = serde_json::from_str(&form_data.payload)?;
let action_value = payload.actions[0].value.clone();
let response_url = payload.response_url;
let re = Regex::new(r"/api/w/(?P<w_id>[^/]+)/jobs_u/(?P<action>resume|cancel)/(?P<job_id>[^/]+)/(?P<resume_id>[^/]+)/(?P<secret>[a-fA-F0-9]+)(?:\?approver=(?P<approver>[^&]+))?").unwrap();
if let Some(captures) = re.captures(&action_value) {
let w_id = captures.name("w_id").map_or("", |m| m.as_str());
let action = captures.name("action").map_or("", |m| m.as_str());
let job_id = captures.name("job_id").map_or("", |m| m.as_str());
let resume_id = captures.name("resume_id").map_or("", |m| m.as_str());
let secret = captures.name("secret").map_or("", |m| m.as_str());
let approver =
QueryApprover { approver: captures.name("approver").map(|m| m.as_str().to_string()) };
tracing::debug!("Request: {:?}", &form_data.payload.clone());
let state_values: HashMap<String, serde_json::Value> = payload
.state
.values
.iter()
.flat_map(|(_, inputs)| {
inputs.iter().filter_map(|(action_id, input)| {
if action_id.ends_with("_date") {
let base_key = action_id.strip_suffix("_date").unwrap();
let time_key = format!("{}_time", base_key);
// Check for Datepicker and Timepicker inputs specifically
if let ValueInput::Datepicker { selected_date: Some(date) } = input {
let matching_time = payload.state.values.values().flat_map(|inputs| {
inputs.get(&time_key).and_then(|time_input| {
if let ValueInput::Timepicker { selected_time: Some(time) } = time_input {
Some(time)
} else {
None
}
})
}).next();
if let Some(time) = matching_time {
return Some((
base_key.to_string(),
serde_json::json!(format!("{}T{}:00.000Z", date, time)),
));
}
}
}
// Process non-datetime inputs, including plain text or other types with `_date`
match input {
ValueInput::PlainTextInput { value } => {
value.as_ref().map(|v| (action_id.clone(), v.clone().into()))
}
ValueInput::StaticSelect { selected_option } => selected_option
.as_ref()
.map(|so| (action_id.clone(), serde_json::json!(so.value))),
ValueInput::RadioButtons { selected_option } => selected_option
.as_ref()
.map(|so| (action_id.clone(), serde_json::json!(so.value))),
ValueInput::Checkboxes { selected_options } => {
selected_options.as_ref().map(|so| {
(
action_id.clone(),
serde_json::json!(so
.iter()
.map(|option| option.value.clone())
.collect::<Vec<_>>()),
)
})
}
_ => None,
}
})
})
.collect();
let state_json = serde_json::to_value(state_values)
.unwrap_or_else(|_| serde_json::json!({}));
tracing::debug!("W ID: {}", w_id);
tracing::debug!("Action: {}", action);
tracing::debug!("Job ID: {}", job_id);
tracing::debug!("Resume ID: {}", resume_id);
tracing::debug!("Secret: {}", secret);
tracing::debug!("Approver: {:?}", approver.approver);
tracing::debug!("State JSON: {:?}", state_json);
let res = resume_suspended_job_internal(
Some(state_json),
db,
w_id.to_string(),
Uuid::from_str(job_id).unwrap_or_default(),
resume_id.parse::<u32>().unwrap_or_default(),
approver,
secret.to_string(),
authed,
action == "resume",
)
.await;
tracing::debug!("Res: {:?}", res);
if let Some(url) = response_url {
let message = if action == "resume" {
"\n\n*Workflow has been resumed!*"
} else {
"\n\n*Workflow has been canceled!*"
};
let _ = post_slack_response(&url, message).await;
}
} else {
tracing::error!("Resume URL does not match the pattern.");
}
Ok(StatusCode::OK)
}
#[derive(Deserialize)]
pub struct QueryMessage {
pub message: Option<String>,
}
pub async fn request_slack_approval(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id)): Path<(String, Uuid, u32)>,
Query(approver): Query<QueryApprover>,
Query(message): Query<QueryMessage>,
) -> windmill_common::error::JsonResult<serde_json::Value> {
let res = get_resume_urls(
authed,
axum::Extension(db.clone()),
Path((w_id, job_id, resume_id)),
axum::extract::Query(approver),
)
.await;
let schema: Option<ResumeFormRow> = sqlx::query_as!(
ResumeFormRow,
"SELECT
module.value->'suspend'->'resume_form' AS resume_form,
(module.value->'suspend'->>'hide_cancel')::boolean AS hide_cancel
FROM
job
LEFT JOIN
queue ON job.id = queue.parent_job
LEFT JOIN
jsonb_array_elements(job.raw_flow->'modules') AS module
ON module->>'id' = queue.flow_step_id
WHERE
queue.id = $1",
job_id
)
.fetch_optional(&db)
.await?;
tracing::debug!("schema: {:?}", schema);
tracing::debug!("job_id: {:?}", job_id);
let message_str = message.message.as_deref().unwrap_or("*A workflow has been suspended and is waiting for approval:*\n");
if let Some(resume_schema) = schema {
let hide_cancel = resume_schema.hide_cancel.unwrap_or(false);
let schema_obj = match resume_schema.resume_form {
Some(schema) => schema,
None => {
tracing::debug!("No suspend form found!");
return transform_schemas(message_str, None, &res.unwrap().0, None, hide_cancel)
.await
.map(Json);
}
};
let inner_schema = schema_obj
.get("schema")
.ok_or_else(|| Error::BadRequest("Schema object is missing the 'schema' field!".to_string()))?;
let order_value = inner_schema
.get("order")
.ok_or_else(|| Error::BadRequest("Schema does not contain order field!".to_string()))?;
let order: Vec<String> = serde_json::from_value(order_value.clone())
.map_err(|e| {
tracing::error!("Failed to deserialize order: {:?}", e);
Error::BadRequest("Failed to deserialize order!".to_string())
})?;
let properties_value = inner_schema
.get("properties")
.ok_or_else(|| Error::BadRequest("Schema does not contain properties field!".to_string()))?;
let properties: HashMap<String, ResumeFormField> = serde_json::from_value(properties_value.clone())
.map_err(|e| {
tracing::error!("Deserialization failed: {:?}", e);
Error::BadRequest("Failed to deserialize properties!".to_string())
})?;
let blocks = transform_schemas(message_str, Some(&properties), &res.unwrap().0, Some(&order), hide_cancel)
.await?;
Ok(Json(blocks))
} else {
Err(Error::BadRequest(
"Could not generate interactive Slack message!".to_string(),
))
}
}
async fn post_slack_response(
response_url: &str,
message: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut final_blocks = vec![serde_json::json!({
"type": "section",
"text": {
"type": "mrkdwn",
"text": message
}
})];
let payload = serde_json::json!({
"replace_original": "true",
"text": message,
"blocks": final_blocks
});
let client = Client::new();
let response = client
.post(response_url)
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await?;
if response.status().is_success() {
tracing::debug!("Slack response to approval sent successfully!");
} else {
tracing::error!(
"Slack response to approval failed. Status: {}",
response.status()
);
}
Ok(())
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ResumeSchema {
pub schema: Schema,
}
#[derive(Debug, Deserialize)]
pub struct ResumeFormRow {
pub resume_form: Option<serde_json::Value>,
pub hide_cancel: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Schema {
pub order: Vec<String>,
pub required: Vec<String>,
pub properties: HashMap<String, ResumeFormField>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ResumeFormField {
#[serde(rename = "type")]
pub r#type: String,
pub format: Option<String>,
pub default: Option<String>,
pub description: Option<String>,
pub title: Option<String>,
#[serde(rename = "enum")]
pub r#enum: Option<Vec<String>>,
#[serde(rename = "enumLabels")]
pub enum_labels: Option<HashMap<String, String>>,
}
async fn transform_schemas(
text: &str,
properties: Option<&HashMap<String, ResumeFormField>>,
urls: &ResumeUrls,
order: Option<&Vec<String>>,
hide_cancel: bool,
) -> Result<serde_json::Value, Error> {
tracing::debug!("{:?}", urls);
let mut blocks = vec![serde_json::json!({
"type": "section",
"text": {
"type": "mrkdwn",
"text": format!("{}\n<{}|Flow suspension details>", text, urls.approvalPage),
}
})];
if let Some(properties) = properties {
if let Some(order) = order {
for key in order {
if let Some(schema) = properties.get(key) {
let input_block = create_input_block(key, schema);
match input_block {
serde_json::Value::Array(arr) => blocks.extend(arr),
_ => blocks.push(input_block),
}
}
}
} else {
for (key, schema) in properties {
let input_block = create_input_block(key, schema);
match input_block {
serde_json::Value::Array(arr) => blocks.extend(arr),
_ => blocks.push(input_block),
}
}
}
}
blocks.push(create_action_buttons(urls, hide_cancel));
Ok(serde_json::Value::Array(blocks))
}
fn create_input_block(key: &str, schema: &ResumeFormField) -> serde_json::Value {
let placeholder = schema
.description
.as_deref()
.filter(|desc| !desc.is_empty())
.unwrap_or("Select an option");
// Handle date-time format
if schema.r#type == "string" && schema.format.as_deref() == Some("date-time") {
let now = chrono::Local::now();
let current_date = now.format("%Y-%m-%d").to_string();
let current_time = now.format("%H:%M").to_string();
let (default_date, default_time) = if let Some(default) = &schema.default {
if let Ok(parsed_date) = chrono::DateTime::parse_from_rfc3339(default) {
(
parsed_date.format("%Y-%m-%d").to_string(),
parsed_date.format("%H:%M").to_string(),
)
} else {
(current_date.clone(), current_time.clone())
}
} else {
(current_date.clone(), current_time.clone())
};
return serde_json::json!([
{
"type": "input",
"element": {
"type": "datepicker",
"initial_date": &default_date,
"placeholder": {
"type": "plain_text",
"text": "Select a date",
"emoji": true
},
"action_id": format!("{}_date", key)
},
"label": {
"type": "plain_text",
"text": schema.title.as_deref().unwrap_or(key),
"emoji": true
}
},
{
"type": "input",
"element": {
"type": "timepicker",
"initial_time": &default_time,
"placeholder": {
"type": "plain_text",
"text": "Select time",
"emoji": true
},
"action_id": format!("{}_time", key)
},
"label": {
"type": "plain_text",
"text": " ",
"emoji": true
}
}
]);
}
// Handle enum type
if let Some(enums) = &schema.r#enum {
let initial_option = schema.default.as_ref().and_then(|default_value| {
enums.iter().find(|enum_value| enum_value == &default_value).map(|enum_value| {
serde_json::json!({
"text": {
"type": "plain_text",
"text": schema.enum_labels.as_ref()
.and_then(|labels| labels.get(enum_value))
.unwrap_or(enum_value),
"emoji": true
},
"value": enum_value
})
})
});
let mut element = serde_json::json!({
"type": "static_select",
"placeholder": {
"type": "plain_text",
"text": placeholder,
"emoji": true,
},
"options": enums.iter().map(|enum_value| {
serde_json::json!({
"text": {
"type": "plain_text",
"text": schema.enum_labels.as_ref()
.and_then(|labels| labels.get(enum_value))
.unwrap_or(enum_value),
"emoji": true
},
"value": enum_value
})
}).collect::<Vec<_>>(),
"action_id": key
});
if let Some(option) = initial_option {
element["initial_option"] = option;
}
serde_json::json!({
"type": "input",
"element": element,
"label": {
"type": "plain_text",
"text": schema.title.as_deref().unwrap_or(key),
"emoji": true
}
})
} else {
// Handle other types
serde_json::json!({
"type": "input",
"element": {
"type": "plain_text_input",
"action_id": key,
"initial_value": schema.default.as_deref().unwrap_or("")
},
"label": {
"type": "plain_text",
"text": schema.title.as_deref().unwrap_or(key),
"emoji": true
}
})
}
}
fn create_action_buttons(urls: &ResumeUrls, hide_cancel: bool) -> serde_json::Value {
let mut elements = vec![
serde_json::json!({
"type": "button",
"text": {
"type": "plain_text",
"text": "Continue"
},
"style": "primary",
"action_id": "resume_action",
"value": urls.resume
})
];
if !hide_cancel {
elements.push(serde_json::json!({
"type": "button",
"text": {
"type": "plain_text",
"text": "Abort"
},
"style": "danger",
"action_id": "cancel_action",
"value": urls.cancel
}));
}
serde_json::json!({
"type": "actions",
"elements": elements
})
}

View File

@@ -11,20 +11,15 @@ use crate::db::ApiAuthed;
use crate::ee::ExternalJwks;
#[cfg(feature = "embedding")]
use crate::embeddings::load_embeddings_db;
#[cfg(feature = "oauth2")]
use crate::oauth2_ee::AllClients;
#[cfg(feature = "oauth2")]
use crate::oauth2_ee::SlackVerifier;
#[cfg(feature = "smtp")]
use crate::smtp_server_ee::SmtpServer;
use crate::tracing_init::MyOnFailure;
use crate::{
oauth2_ee::SlackVerifier,
tracing_init::{MyMakeSpan, MyOnResponse},
users::OptAuthed,
webhook_util::WebhookShared,
};
use anyhow::Context;
use argon2::Argon2;
use axum::extract::DefaultBodyLimit;
@@ -32,9 +27,7 @@ use axum::{middleware::from_extractor, routing::get, routing::post, Extension, R
use db::DB;
use http::HeaderValue;
use reqwest::Client;
#[cfg(feature = "oauth2")]
use std::collections::HashMap;
use std::time::Duration;
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::RwLock;
@@ -55,7 +48,6 @@ mod ai;
mod apps;
mod args;
mod audit;
mod auth;
mod capture;
mod concurrency_groups;
mod configs;
@@ -68,7 +60,6 @@ mod flows;
mod folders;
mod granular_acls;
mod groups;
#[cfg(feature = "http_trigger")]
mod http_triggers;
mod indexer_ee;
mod inputs;
@@ -82,7 +73,6 @@ pub mod job_metrics;
pub mod jobs;
#[cfg(all(feature = "enterprise", feature = "kafka"))]
mod kafka_triggers_ee;
#[cfg(feature = "oauth2")]
pub mod oauth2_ee;
mod oidc_ee;
mod raw_apps;
@@ -93,8 +83,7 @@ mod scim_ee;
mod scripts;
mod service_logs;
mod settings;
#[cfg(feature = "smtp")]
mod smtp_server_ee;
pub mod smtp_server_ee;
mod static_assets;
mod stripe_ee;
mod tracing_init;
@@ -104,14 +93,10 @@ mod users_ee;
mod utils;
mod variables;
mod webhook_util;
#[cfg(feature = "websocket")]
mod websocket_triggers;
mod workers;
mod workspaces;
mod workspaces_ee;
mod slack_approvals;
mod workspaces_export;
mod workspaces_extra;
pub const DEFAULT_BODY_LIMIT: usize = 2097152 * 100; // 200MB
@@ -125,6 +110,10 @@ lazy_static::lazy_static! {
pub static ref COOKIE_DOMAIN: Option<String> = std::env::var("COOKIE_DOMAIN").ok();
pub static ref SLACK_SIGNING_SECRET: Option<SlackVerifier> = std::env::var("SLACK_SIGNING_SECRET")
.ok()
.map(|x| SlackVerifier::new(x).unwrap());
pub static ref IS_SECURE: Arc<RwLock<bool>> = Arc::new(RwLock::new(false));
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
@@ -134,22 +123,11 @@ lazy_static::lazy_static! {
.danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())
.build().unwrap();
}
#[cfg(feature = "oauth2")]
lazy_static::lazy_static! {
pub static ref OAUTH_CLIENTS: Arc<RwLock<AllClients>> = Arc::new(RwLock::new(AllClients {
logins: HashMap::new(),
connects: HashMap::new(),
slack: None
}));
pub static ref SLACK_SIGNING_SECRET: Option<SlackVerifier> = std::env::var("SLACK_SIGNING_SECRET")
.ok()
.map(|x| SlackVerifier::new(x).unwrap());
}
// Compliance with cloud events spec.
@@ -193,13 +171,13 @@ pub async fn run_server(
mut rx: tokio::sync::broadcast::Receiver<()>,
port_tx: tokio::sync::oneshot::Sender<String>,
server_mode: bool,
#[cfg(feature = "smtp")] base_internal_url: String,
base_internal_url: String,
) -> anyhow::Result<()> {
let user_db = UserDB::new(db.clone());
#[cfg(feature = "enterprise")]
let ext_jwks = ExternalJwks::load().await;
let auth_cache = Arc::new(crate::auth::AuthCache::new(
let auth_cache = Arc::new(users::AuthCache::new(
db.clone(),
std::env::var("SUPERADMIN_SECRET").ok(),
#[cfg(feature = "enterprise")]
@@ -236,17 +214,14 @@ pub async fn run_server(
#[cfg(feature = "embedding")]
load_embeddings_db(&db);
#[cfg(feature = "smtp")]
{
let smtp_server = Arc::new(SmtpServer {
db: db.clone(),
user_db: user_db,
auth_cache: auth_cache.clone(),
base_internal_url: base_internal_url.clone(),
});
if let Err(err) = smtp_server.start_listener_thread(addr).await {
tracing::error!("Error starting SMTP server: {err:#}");
}
let smtp_server = Arc::new(SmtpServer {
db: db.clone(),
user_db: user_db,
auth_cache: auth_cache.clone(),
base_internal_url: base_internal_url.clone(),
});
if let Err(err) = smtp_server.start_listener_thread(addr).await {
tracing::error!("Error starting SMTP server: {err:#}");
}
}
@@ -278,11 +253,8 @@ pub async fn run_server(
};
if !*CLOUD_HOSTED {
#[cfg(feature = "websocket")]
{
let ws_killpill_rx = rx.resubscribe();
websocket_triggers::start_websockets(db.clone(), ws_killpill_rx).await;
}
let ws_killpill_rx = rx.resubscribe();
websocket_triggers::start_websockets(db.clone(), ws_killpill_rx).await;
#[cfg(all(feature = "enterprise", feature = "kafka"))]
{
@@ -318,15 +290,7 @@ pub async fn run_server(
.nest("/job_metrics", job_metrics::workspaced_service())
.nest("/job_helpers", job_helpers_service)
.nest("/jobs", jobs::workspaced_service())
.nest("/oauth", {
#[cfg(feature = "oauth2")]
{
oauth2_ee::workspaced_service()
}
#[cfg(not(feature = "oauth2"))]
Router::new()
})
.nest("/oauth", oauth2_ee::workspaced_service())
.nest("/ai", ai::workspaced_service())
.nest("/raw_apps", raw_apps::workspaced_service())
.nest("/resources", resources::workspaced_service())
@@ -339,24 +303,11 @@ pub async fn run_server(
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc_ee::workspaced_service())
.nest("/http_triggers", {
#[cfg(feature = "http_trigger")]
{
http_triggers::workspaced_service()
}
#[cfg(not(feature = "http_trigger"))]
Router::new()
})
.nest("/websocket_triggers", {
#[cfg(feature = "websocket")]
{
websocket_triggers::workspaced_service()
}
#[cfg(not(feature = "websocket"))]
Router::new()
})
.nest("/http_triggers", http_triggers::workspaced_service())
.nest(
"/websocket_triggers",
websocket_triggers::workspaced_service(),
)
.nest("/kafka_triggers", kafka_triggers_service),
)
.nest("/workspaces", workspaces::global_service())
@@ -416,8 +367,8 @@ pub async fn run_server(
"/w/:workspace_id/jobs_u",
jobs::workspace_unauthed_service().layer(cors.clone()),
)
.route("/slack", post(slack_approvals::slack_app_callback_handler))
.route("/w/:workspace_id/jobs/slack_approval/:job_id", get(slack_approvals::request_slack_approval))
.route("/slack", post(jobs::slack_app_callback_handler))
.route("/w/:workspace_id/jobs/slack_approval/:job_id/:resume_id", get(jobs::request_slack_approval))
.nest(
"/w/:workspace_id/resources_u",
resources::public_service().layer(cors.clone()),
@@ -430,29 +381,13 @@ pub async fn run_server(
"/auth",
users::make_unauthed_service().layer(Extension(argon2)),
)
.nest("/oauth", {
#[cfg(feature = "oauth2")]
{
oauth2_ee::global_service().layer(Extension(Arc::clone(&sp_extension)))
}
#[cfg(not(feature = "oauth2"))]
Router::new()
})
.nest(
"/oauth",
oauth2_ee::global_service().layer(Extension(Arc::clone(&sp_extension))),
)
.nest(
"/r",
{
#[cfg(feature = "http_trigger")]
{
http_triggers::routes_global_service()
}
#[cfg(not(feature = "http_trigger"))]
{
Router::new()
}
}
.layer(from_extractor::<OptAuthed>()),
http_triggers::routes_global_service().layer(from_extractor::<OptAuthed>()),
)
.route("/version", get(git_v))
.route("/uptodate", get(is_up_to_date))

View File

@@ -10,17 +10,14 @@ use std::{collections::HashMap, fmt::Debug};
use axum::{routing::get, Json, Router};
use hmac::Mac;
use hyper::HeaderMap;
#[cfg(feature = "oauth2")]
use itertools::Itertools;
#[cfg(feature = "oauth2")]
use oauth2::{Client as OClient, *};
use serde::{Deserialize, Serialize};
use sqlx::{Postgres, Transaction};
#[cfg(feature = "oauth2")]
use windmill_common::more_serde::maybe_number_opt;
#[cfg(feature = "oauth2")]
use crate::OAUTH_CLIENTS;
use windmill_common::error;
use windmill_common::oauth2::*;
@@ -30,6 +27,7 @@ use std::str;
pub fn global_service() -> Router {
Router::new()
.route("/list_supabase", get(list_supabase))
.route("/list_logins", get(list_logins))
.route("/list_connects", get(list_connects))
}
@@ -38,7 +36,17 @@ pub fn workspaced_service() -> Router {
Router::new()
}
#[cfg(feature = "oauth2")]
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum InstanceEvent {
UserAdded { email: String },
// UserDeleted { email: String },
// UserDeletedWorkspace { workspace: String, email: String },
UserAddedWorkspace { workspace: String, email: String },
UserInvitedWorkspace { workspace: String, email: String },
UserJoinedWorkspace { workspace: String, email: String, username: String },
}
#[derive(Debug, Clone)]
pub struct ClientWithScopes {
_client: OClient,
@@ -48,7 +56,7 @@ pub struct ClientWithScopes {
_allowed_domains: Option<Vec<String>>,
_userinfo_url: Option<String>,
}
#[cfg(feature = "oauth2")]
pub type BasicClientsMap = HashMap<String, ClientWithScopes>;
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -71,7 +79,6 @@ pub struct OAuthClient {
login_config: Option<OAuthConfig>,
}
#[cfg(feature = "oauth2")]
#[derive(Debug)]
pub struct AllClients {
pub logins: BasicClientsMap,
@@ -79,7 +86,6 @@ pub struct AllClients {
pub slack: Option<OClient>,
}
#[cfg(feature = "oauth2")]
pub fn build_oauth_clients(
_base_url: &str,
_oauths_from_config: Option<HashMap<String, OAuthClient>>,
@@ -92,7 +98,6 @@ pub fn build_oauth_clients(
});
}
#[cfg(feature = "oauth2")]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TokenResponse {
access_token: AccessToken,
@@ -116,7 +121,6 @@ async fn list_logins() -> error::JsonResult<Logins> {
return Ok(Json(Logins { oauth: vec![], saml: None }));
}
#[cfg(feature = "oauth2")]
async fn list_connects() -> error::JsonResult<Vec<String>> {
Ok(Json(
(&OAUTH_CLIENTS.read().await.connects)
@@ -126,12 +130,6 @@ async fn list_connects() -> error::JsonResult<Vec<String>> {
))
}
#[cfg(not(feature = "oauth2"))]
async fn list_connects() -> error::JsonResult<Vec<String>> {
// Implementation is not open source
return Ok(Json(vec![]));
}
pub async fn _refresh_token<'c>(
_tx: Transaction<'c, Postgres>,
_path: &str,
@@ -145,6 +143,13 @@ pub async fn _refresh_token<'c>(
))
}
async fn list_supabase(_headers: HeaderMap) -> error::Result<String> {
// Implementation is not open source
Err(error::Error::BadRequest(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
pub async fn check_nb_of_user(db: &DB) -> error::Result<()> {
let nb_users_sso =
sqlx::query_scalar!("SELECT COUNT(*) FROM password WHERE login_type != 'password'",)

View File

@@ -12,8 +12,7 @@ use crate::{
triggers::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
},
users::{maybe_refresh_folders, require_owner_of_path},
auth::AuthCache,
users::{maybe_refresh_folders, require_owner_of_path, AuthCache},
utils::WithStarredInfoQuery,
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
@@ -584,7 +583,6 @@ async fn create_script_internal<'c>(
|| ns.language == ScriptLang::Deno
|| ns.language == ScriptLang::Rust
|| ns.language == ScriptLang::Ansible
|| ns.language == ScriptLang::CSharp
|| ns.language == ScriptLang::Php)
{
Some(String::new())

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
use crate::{auth::AuthCache, db::DB};
use crate::{db::DB, users::AuthCache};
use std::{net::SocketAddr, sync::Arc};
use windmill_common::db::UserDB;
@@ -11,10 +11,6 @@ pub struct SmtpServer {
impl SmtpServer {
pub async fn start_listener_thread(self: Arc<Self>, _addr: SocketAddr) -> anyhow::Result<()> {
let _ = self.auth_cache;
let _ = self.db;
let _ = self.user_db;
let _ = self.base_internal_url;
Err(anyhow::anyhow!("Implementation not open source"))
}
}

View File

@@ -6,15 +6,15 @@
* LICENSE-AGPL for a copy of the license.
*/
use axum::{body::Body, extract::OriginalUri, http::Response, response::IntoResponse};
#[cfg(feature = "static_frontend")]
use axum::http::header;
use axum::{
body::Body,
extract::OriginalUri,
http::{header, Response},
response::IntoResponse,
};
use hyper::Uri;
#[cfg(feature = "static_frontend")]
use mime_guess::mime;
#[cfg(feature = "static_frontend")]
use rust_embed::RustEmbed;
// static_handler is a handler that serves static files from the
@@ -22,7 +22,6 @@ pub async fn static_handler(OriginalUri(original_uri): OriginalUri) -> StaticFil
StaticFile(original_uri)
}
#[cfg(feature = "static_frontend")]
#[derive(RustEmbed)]
#[folder = "${FRONTEND_BUILD_DIR:-../../frontend/build/}"]
struct Asset;
@@ -35,7 +34,6 @@ impl IntoResponse for StaticFile {
}
}
#[cfg(feature = "static_frontend")]
const TWO_HUNDRED: &str = "200.html";
fn serve_path(path: &str) -> Response<Body> {
@@ -43,7 +41,6 @@ fn serve_path(path: &str) -> Response<Body> {
return Response::builder().status(404).body(Body::empty()).unwrap();
}
#[cfg(feature = "static_frontend")]
match Asset::get(path) {
Some(content) => {
let body = Body::from(content.data);
@@ -71,9 +68,4 @@ fn serve_path(path: &str) -> Response<Body> {
}
None => serve_path(TWO_HUNDRED),
}
#[cfg(not(feature = "static_frontend"))]
{
Response::builder().status(404).body(Body::empty()).unwrap()
}
}

View File

@@ -8,13 +8,14 @@
#![allow(non_snake_case)]
use std::sync::atomic::AtomicBool;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::Arc;
use crate::db::ApiAuthed;
pub use crate::auth::Tokened;
#[cfg(feature = "enterprise")]
use crate::ee::ExternalJwks;
use crate::oauth2_ee::InstanceEvent;
use crate::utils::{
generate_instance_wide_unique_username, get_instance_username_or_create_pending,
};
@@ -24,38 +25,43 @@ use crate::{
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use axum::{
async_trait,
extract::{Extension, FromRequestParts, Path, Query},
extract::{Extension, FromRequestParts, OriginalUri, Path, Query},
http::request::Parts,
response::{IntoResponse, Response},
routing::{delete, get, post},
Json, Router,
};
use chrono::TimeZone;
use hyper::{header::LOCATION, StatusCode};
use lazy_static::lazy_static;
use quick_cache::sync::Cache;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use time::OffsetDateTime;
#[cfg(feature = "enterprise")]
use tokio::sync::RwLock;
use tower_cookies::{Cookie, Cookies};
use tracing::Instrument;
use tracing::{Instrument, Span};
use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::auth::fetch_authed_from_permissioned_as;
use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING;
use windmill_common::oauth2::InstanceEvent;
use windmill_common::users::COOKIE_NAME;
use windmill_common::users::{truncate_token, username_to_permissioned_as};
use windmill_common::utils::paginate;
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user},
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, JWT_SECRET},
db::UserDB,
error::{self, Error, JsonResult, Result},
users::SUPERADMIN_SECRET_EMAIL,
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
};
use windmill_git_sync::handle_deployment_metadata;
pub const TTL_TOKEN_DB_H: u32 = 72;
const COOKIE_NAME: &str = "token";
const COOKIE_PATH: &str = "/";
pub fn workspaced_service() -> Router {
@@ -120,6 +126,478 @@ pub fn make_unauthed_service() -> Router {
.route("/is_first_time_setup", get(is_first_time_setup))
}
fn username_override_from_label(label: Option<String>) -> Option<String> {
match label {
Some(label)
if label.starts_with("webhook-")
|| label.starts_with("http-")
|| label.starts_with("email-")
|| label.starts_with("ws-") =>
{
Some(label)
}
Some(label) if label.starts_with("ephemeral-script-end-user-") => Some(
label
.trim_start_matches("ephemeral-script-end-user-")
.to_string(),
),
Some(label) if label == "Ephemeral lsp token" => Some("lsp".to_string()),
Some(label) if label != "ephemeral-script" && label != "session" && !label.is_empty() => {
Some(format!("label-{label}"))
}
_ => None,
}
}
#[derive(Clone)]
pub struct ExpiringAuthCache {
pub authed: ApiAuthed,
pub expiry: chrono::DateTime<chrono::Utc>,
}
pub struct AuthCache {
cache: Cache<(String, String), ExpiringAuthCache>,
db: DB,
superadmin_secret: Option<String>,
#[cfg(feature = "enterprise")]
ext_jwks: Option<Arc<RwLock<ExternalJwks>>>,
}
impl AuthCache {
pub fn new(
db: DB,
superadmin_secret: Option<String>,
#[cfg(feature = "enterprise")] ext_jwks: Option<Arc<RwLock<ExternalJwks>>>,
) -> Self {
AuthCache {
cache: Cache::new(300),
db,
superadmin_secret,
#[cfg(feature = "enterprise")]
ext_jwks,
}
}
pub async fn invalidate(&self, w_id: &str, token: String) {
self.cache.remove(&(w_id.to_string(), token));
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<ApiAuthed> {
let key = (
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
token.to_string(),
);
let s = self.cache.get(&key).map(|c| c.to_owned());
match s {
Some(ExpiringAuthCache { authed, expiry }) if expiry > chrono::Utc::now() => {
Some(authed)
}
#[cfg(feature = "enterprise")]
_ if token.starts_with("jwt_ext_") => {
let authed_and_exp = match crate::ee::jwt_ext_auth(
w_id.as_ref(),
token.trim_start_matches("jwt_ext_"),
self.ext_jwks.clone(),
)
.await
{
Ok(r) => Some(r),
Err(e) => {
tracing::error!("JWT_EXT auth error: {:?}", e);
None
}
};
if let Some((authed, exp)) = authed_and_exp.clone() {
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc.timestamp_nanos(exp as i64 * 1_000_000_000),
},
);
Some(authed)
} else {
None
}
}
_ if token.starts_with("jwt_") => {
let jwt_secret = JWT_SECRET.read().await;
if !jwt_secret.is_empty() {
let jwt_token = token.trim_start_matches("jwt_");
let jwt_result = jsonwebtoken::decode::<JWTAuthClaims>(
jwt_token,
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
&jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
);
match jwt_result {
Ok(payload) => {
if w_id.is_some_and(|w_id| w_id != payload.claims.workspace_id) {
tracing::error!("JWT auth error: workspace_id mismatch");
return None;
}
let username_override =
username_override_from_label(payload.claims.label);
let authed = crate::db::ApiAuthed {
email: payload.claims.email,
username: payload.claims.username,
is_admin: payload.claims.is_admin,
is_operator: payload.claims.is_operator,
groups: payload.claims.groups,
folders: payload.claims.folders,
scopes: None,
username_override,
};
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc
.timestamp_nanos(payload.claims.exp as i64 * 1_000_000_000),
},
);
Some(authed)
}
Err(err) => {
tracing::error!("JWT auth error: {:?}", err);
None
}
}
} else {
tracing::error!("JWT auth error: no jwt secret set");
None
}
}
_ => {
let user_o = sqlx::query_as::<_, (Option<String>, Option<String>, bool, Option<Vec<String>>, Option<String>)>(
"UPDATE token SET last_used_at = now() WHERE token = $1 AND (expiration > NOW() \
OR expiration IS NULL) AND (workspace_id IS NULL OR workspace_id = $2) RETURNING owner, email, super_admin, scopes, label",
)
.bind(token)
.bind(w_id.as_ref())
.fetch_optional(&self.db)
.await
.ok()
.flatten();
if let Some(user) = user_o {
let authed_o = {
match user {
(Some(owner), Some(email), super_admin, _, label) if w_id.is_some() => {
let username_override = username_override_from_label(label);
if let Some((prefix, name)) = owner.split_once('/') {
if prefix == "u" {
let (is_admin, is_operator) = if super_admin {
(true, false)
} else {
let r = sqlx::query!(
"SELECT is_admin, operator FROM usr where username = $1 AND \
workspace_id = $2 AND disabled = false",
name,
&w_id.as_ref().unwrap()
)
.fetch_one(&self.db)
.await
.ok();
if let Some(r) = r {
(r.is_admin, r.operator)
} else {
(false, true)
}
};
let w_id = &w_id.unwrap();
let groups =
get_groups_for_user(w_id, &name, &email, &self.db)
.await
.ok()
.unwrap_or_default();
let folders =
get_folders_for_user(w_id, &name, &groups, &self.db)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: name.to_string(),
is_admin,
is_operator,
groups,
folders,
scopes: None,
username_override,
})
} else {
let groups = vec![name.to_string()];
let folders = get_folders_for_user(
&w_id.unwrap(),
"",
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: format!("group-{name}"),
is_admin: false,
groups,
is_operator: false,
folders,
scopes: None,
username_override,
})
}
} else {
let groups = vec![];
let folders = vec![];
Some(ApiAuthed {
email: email,
username: owner,
is_admin: super_admin,
is_operator: true,
groups,
folders,
scopes: None,
username_override,
})
}
}
(_, Some(email), super_admin, scopes, label) => {
let username_override = username_override_from_label(label);
if w_id.is_some() {
let row_o = sqlx::query_as::<_, (String, bool, bool)>(
"SELECT username, is_admin, operator FROM usr where email = $1 AND \
workspace_id = $2 AND disabled = false",
)
.bind(&email)
.bind(&w_id.as_ref().unwrap())
.fetch_optional(&self.db)
.await
.unwrap_or(Some(("error".to_string(), false, false)));
match row_o {
Some((username, is_admin, is_operator)) => {
let groups = get_groups_for_user(
&w_id.as_ref().unwrap(),
&username,
&email,
&self.db,
)
.await
.ok()
.unwrap_or_default();
let folders = get_folders_for_user(
&w_id.unwrap(),
&username,
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(ApiAuthed {
email,
username,
is_admin: is_admin || super_admin,
is_operator,
groups,
folders,
scopes,
username_override,
})
}
None if super_admin => Some(ApiAuthed {
email: email.clone(),
username: email,
is_admin: super_admin,
is_operator: false,
groups: vec![],
folders: vec![],
scopes,
username_override,
}),
None => None,
}
} else {
Some(ApiAuthed {
email: email.to_string(),
username: email,
is_admin: super_admin,
is_operator: true,
groups: Vec::new(),
folders: Vec::new(),
scopes,
username_override,
})
}
}
_ => None,
}
};
if let Some(authed) = authed_o.as_ref() {
self.cache.insert(
key,
ExpiringAuthCache {
authed: authed.clone(),
expiry: chrono::Utc::now()
+ chrono::Duration::try_seconds(120).unwrap(),
},
);
}
authed_o
} else if self
.superadmin_secret
.as_ref()
.map(|x| x == token)
.unwrap_or(false)
{
Some(ApiAuthed {
email: SUPERADMIN_SECRET_EMAIL.to_string(),
username: "superadmin_secret".to_string(),
is_admin: true,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
})
} else {
None
}
}
}
}
}
async fn extract_token<S: Send + Sync>(parts: &mut Parts, state: &S) -> Option<String> {
let auth_header = parts
.headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "));
let from_cookie = match auth_header {
Some(x) => Some(x.to_owned()),
None => Extension::<Cookies>::from_request_parts(parts, state)
.await
.ok()
.and_then(|cookies| cookies.get(COOKIE_NAME).map(|c| c.value().to_owned())),
};
#[derive(Deserialize)]
struct Token {
token: Option<String>,
}
match from_cookie {
Some(token) => Some(token),
None => Query::<Token>::from_request_parts(parts, state)
.await
.ok()
.and_then(|token| token.token.clone()),
}
}
#[derive(Clone, Debug)]
pub struct Tokened {
pub token: String,
}
pub struct OptTokened {
pub token: Option<String>,
}
struct BruteForceCounter {
counter: AtomicU64,
last_reset: AtomicI64,
}
lazy_static! {
static ref BRUTE_FORCE_COUNTER: BruteForceCounter =
BruteForceCounter { last_reset: AtomicI64::new(0), counter: AtomicU64::new(0) };
}
impl BruteForceCounter {
async fn increment(&self) {
let now = time::OffsetDateTime::now_utc().unix_timestamp();
if self.counter.fetch_add(1, Ordering::Relaxed) > 10000 {
tracing::error!(
"Brute force attack to find valid token detected, sleeping unauthorized response for 2 seconds"
);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
if now - self.last_reset.load(Ordering::Relaxed) > 60 {
self.counter.store(0, Ordering::Relaxed);
self.last_reset.store(now, Ordering::Relaxed);
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Tokened
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(Tokened { token: "".to_string() });
};
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(tokened.clone())
} else {
let token_o = extract_token(parts, state).await;
if let Some(token) = token_o {
let tokened = Self { token };
parts.extensions.insert(tokened.clone());
Ok(tokened)
} else {
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}
}
}
#[async_trait]
impl<S> FromRequestParts<S> for OptTokened
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(OptTokened { token: None });
};
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(OptTokened { token: Some(tokened.token.clone()) })
} else {
let token_o = extract_token(parts, state).await;
Ok(OptTokened { token: token_o })
}
}
}
pub async fn maybe_refresh_folders(
path: &str,
w_id: &str,
@@ -150,6 +628,94 @@ pub async fn maybe_refresh_folders(
}
}
#[async_trait]
impl<S> FromRequestParts<S> for ApiAuthed
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(ApiAuthed {
email: "".to_owned(),
username: "".to_owned(),
is_admin: false,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
});
};
let already_authed = parts.extensions.get::<ApiAuthed>();
if let Some(authed) = already_authed {
Ok(authed.clone())
} else {
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(parts, state).await
};
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else {
if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[2] == "srch"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_string())
} else {
None
}
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
parts.extensions.insert(authed.clone());
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((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
}
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
}
}
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}
}
pub fn check_scopes<F>(authed: &ApiAuthed, required: F) -> error::Result<()>
where
F: FnOnce() -> String,
@@ -202,7 +768,6 @@ where
}
}
#[allow(unused)]
pub async fn fetch_api_authed(
username: String,
email: String,
@@ -214,7 +779,6 @@ pub async fn fetch_api_authed(
fetch_api_authed_from_permissioned_as(permissioned_as, email, w_id, db, username_override).await
}
#[allow(unused)]
pub async fn fetch_api_authed_from_permissioned_as(
permissioned_as: String,
email: String,
@@ -1653,30 +2217,13 @@ async fn login(
}
}
#[derive(Deserialize)]
struct RefreshTokenQuery {
if_expiring_in_less_than_s: Option<i32>,
}
async fn refresh_token(
Extension(db): Extension<DB>,
Query(query): Query<RefreshTokenQuery>,
Tokened { token }: Tokened,
authed: ApiAuthed,
cookies: Cookies,
) -> Result<String> {
let mut tx = db.begin().await?;
if let Some(thresh_s) = query.if_expiring_in_less_than_s {
let not_expired = sqlx::query_scalar!("SELECT true FROM token WHERE token = $1 and expiration IS NOT NULL and expiration > now() + $2::int * '1 sec'::interval", &token, thresh_s)
.fetch_optional(&db)
.await?
.flatten()
.unwrap_or(false);
if not_expired {
return Ok("token expiry is far enough".to_string());
}
}
let super_admin = sqlx::query_scalar!(
"SELECT super_admin FROM password WHERE email = $1",
&authed.email
@@ -1947,7 +2494,7 @@ async fn get_all_runnables(
Extension(db): Extension<UserDB>,
authed: ApiAuthed,
Tokened { token }: Tokened,
Extension(cache): Extension<Arc<crate::auth::AuthCache>>,
Extension(cache): Extension<Arc<AuthCache>>,
) -> JsonResult<Vec<Runnable>> {
let mut tx = db.clone().begin(&authed).await?;
let mut runnables = Vec::new();

View File

@@ -10,7 +10,6 @@ use axum::{body::Body, response::Response};
use regex::Regex;
use serde::Deserialize;
use sqlx::{Postgres, Transaction};
#[cfg(feature = "enterprise")]
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
auth::{is_devops_email, is_super_admin_email},

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