Merge branch 'main' into bump-latest-stable-to-py313

This commit is contained in:
pyranota
2025-01-31 14:00:10 +03:00
committed by GitHub
211 changed files with 5558 additions and 6474 deletions
+10 -15
View File
@@ -2,8 +2,8 @@ name: Backend check
on:
push:
paths:
- "backend/**"
- ".github/workflows/backend-check.yml"
- "backend/**"
- ".github/workflows/backend-check.yml"
jobs:
check_oss:
@@ -16,7 +16,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
toolchain: 1.83.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -32,16 +32,15 @@ jobs:
with:
fetch-depth: 0
- name: install xmlsec1
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev
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
toolchain: 1.83.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -71,16 +70,14 @@ jobs:
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
toolchain: 1.83.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -110,8 +107,8 @@ jobs:
- name: install xmlsec1
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev
- name: Substitute EE code (EE logic is behind feature flag)
run: |
@@ -120,7 +117,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
toolchain: 1.83.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -130,5 +127,3 @@ jobs:
run: |
mkdir -p fake_frontend_build
FRONTEND_BUILD_DIR=$(pwd)/fake_frontend_build SQLX_OFFLINE=true cargo check --all-features
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.82.0
toolchain: 1.83.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
with:
deno-version: v1.x
- name: benchmark
timeout-minutes: 20
timeout-minutes: 30
run: deno run --unstable -A -r
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
-c
+2 -2
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,nats,php,mysql,mssql,bigquery,oracledb,websocket,python,smtp,csharp,static_frontend,rust
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,http_trigger,zip,oauth2,kafka,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust
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,nats,php,mysql,mssql,bigquery,oracledb,websocket,python,smtp,csharp,static_frontend,rust
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core,license,http_trigger,zip,oauth2,kafka,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
+1 -1
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,nats,php,mysql,mssql,bigquery,oracledb,websocket,python,smtp,csharp,static_frontend,rust
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust
- name: Rename binary with corresponding architecture
run: |
-48
View File
@@ -1,48 +0,0 @@
name: Publish deno-client
on:
push:
tags:
- "v*"
env:
repo: windmill-deno-client
jobs:
build_deno_and_push_to_repo:
runs-on: ubicloud
steps:
- uses: actions/checkout@v4
- name: generate_deno
run: |
cd deno-client
rm .gitignore
./build.sh
- name: Pushes to another repository
id: push_directory
uses: cpina/github-action-push-to-another-repository@devel
env:
API_TOKEN_GITHUB: ${{ secrets.DENO_PAT }}
with:
source-directory: deno-client/
destination-github-username: ${{ github.repository_owner }}
destination-repository-name: ${{ env.repo }}
user-email: ruben@windmill.dev
commit-message: See ORIGIN_COMMIT from $GITHUB_REF
target-branch: main
tag_repo:
needs: [build_deno_and_push_to_repo]
runs-on: ubicloud
steps:
- uses: actions/checkout@v4
with:
repository: ${{ github.repository_owner }}/${{ env.repo }}
token: ${{ secrets.DENO_PAT }}
path: ./client
- name: Push client
run: |
cd ./client
git config --global user.email "ruben@windmill.dev"
git config --global user.name "rubenfiszel[bot]"
git tag -a ${{ github.ref_name }} -m "${{ github.ref_name }}"
git push --tags
+1 -1
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,oracledb,websocket,python,smtp,csharp,static_frontend,rust
features=embedding,parquet,openidconnect,deno_core,license,http_trigger,zip,oauth2,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ steps.meta-public.outputs.tags }}
+24 -73
View File
@@ -26,7 +26,11 @@ on:
description: "Tag the image"
required: true
default: "test"
nsjail:
description: "Build nsjail image (true, false)"
required: false
default: false
type: boolean
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
@@ -42,6 +46,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Read EE repo commit hash
@@ -90,7 +95,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,oracledb,websocket,python,smtp,csharp,static_frontend,rust
features=embedding,parquet,openidconnect,jemalloc,deno_core,license,http_trigger,zip,oauth2,dind,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
${{ steps.meta-public.outputs.tags }}
@@ -100,10 +105,11 @@ jobs:
build_ee:
runs-on: ubicloud
if: (github.event_name != 'workflow_dispatch') || (github.event.inputs.ee)
if: (github.event_name != 'workflow_dispatch') || (github.event.inputs.ee || github.event.inputs.nsjail)
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Read EE repo commit hash
@@ -152,7 +158,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,nats,otel,dind,php,mysql,mssql,bigquery,oracledb,websocket,python,smtp,csharp,static_frontend,rust
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,nats,otel,dind,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}
${{ steps.meta-ee-public.outputs.tags }}
@@ -160,68 +166,6 @@ jobs:
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
build_ee_312:
runs-on: ubicloud
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
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-py312
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-py312
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- 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
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,nats,otel,dind,php,mysql,mssql,bigquery,oracledb,websocket,python,smtp,csharp,static_frontend,rust
PYTHON_IMAGE=python:3.12.2-slim-bookworm
tags: |
${{ steps.meta-ee-public-py312.outputs.tags }}
labels: |
${{ steps.meta-ee-public-py312.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
# disabled until we make it 100% reliable and add more meaningful tests
# playwright:
# runs-on: [self-hosted, new]
@@ -263,7 +207,6 @@ jobs:
ARCH: amd64
steps:
- uses: actions/checkout@v4
- run: |
# pulling docker image with desired arch so that actions-docker-extract doesn't do it
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
@@ -349,6 +292,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Prepare test run
if: ${{ ! startsWith(github.ref, 'refs/tags/v') }}
@@ -372,7 +316,7 @@ jobs:
needs: [run_integration_test, build]
if:
github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' ||
startsWith(github.ref, 'refs/tags/v'))
startsWith(github.ref, 'refs/tags/v')) && (github.event_name != 'workflow_dispatch')
steps:
- uses: actions/checkout@v4
with:
@@ -392,7 +336,7 @@ jobs:
runs-on: ubicloud
needs: [run_integration_test, build_ee]
if:
github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' ||
github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch') && (github.ref == 'refs/heads/main' ||
startsWith(github.ref, 'refs/tags/v'))
steps:
- uses: actions/checkout@v4
@@ -412,7 +356,7 @@ jobs:
verify_ee_image_vulnerabilities:
runs-on: ubicloud
needs: [tag_latest_ee]
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
if: startsWith(github.ref, 'refs/tags/v') && (github.event_name != 'workflow_dispatch')
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -455,12 +399,13 @@ jobs:
needs: [build_ee]
runs-on: ubicloud
if:
(github.event_name != 'pull_request') && (github.event_name !=
'workflow_dispatch')
(github.event_name != 'pull_request') && ((github.event_name != 'workflow_dispatch') || (github.event.inputs.ee || github.event.inputs.nsjail))
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
@@ -489,6 +434,11 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Update Dockerfile image reference
run: |
sed -i 's|FROM ghcr.io/windmill-labs/windmill-ee:dev|FROM ghcr.io/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}|' ./docker/DockerfileNsjail
cat ./docker/DockerfileNsjail | grep "FROM"
- name: Build and push publicly ee
uses: depot/build-push-action@v1
with:
@@ -505,7 +455,7 @@ jobs:
build_ee_reports_privately:
needs: [build_ee_nsjail]
runs-on: ubicloud
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch')
steps:
- uses: actions/checkout@v4
with:
@@ -600,6 +550,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
+1 -1
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,nats,php,mysql,mssql,bigquery,oracledb,websocket,python,smtp,csharp,static_frontend,rust
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core,license,http_trigger,zip,oauth2,kafka,nats,php,mysql,mssql,bigquery,oracledb,postgres_trigger,websocket,python,smtp,csharp,static_frontend,rust
- name: Rename binary with corresponding architecture
run: |
+123
View File
@@ -1,5 +1,128 @@
# Changelog
## [1.454.1](https://github.com/windmill-labs/windmill/compare/v1.454.0...v1.454.1) (2025-01-30)
### Bug Fixes
* fix codebase script execution ([b1155a9](https://github.com/windmill-labs/windmill/commit/b1155a9012c726c1b1c3f1a62f3aa96b8504681c))
## [1.454.0](https://github.com/windmill-labs/windmill/compare/v1.453.1...v1.454.0) (2025-01-30)
### Features
* history for flow inputs ([#5117](https://github.com/windmill-labs/windmill/issues/5117)) ([cd44056](https://github.com/windmill-labs/windmill/commit/cd440564d29aff39f6dc30ecbf0055696b07cf12))
### Bug Fixes
* fix app form password handling ([4812c0c](https://github.com/windmill-labs/windmill/commit/4812c0cc9e46fdb5944f7010efdf617f2fd23cff))
* improve codebase support ([c8cc9d2](https://github.com/windmill-labs/windmill/commit/c8cc9d2941dda2fedb959e1156c4f9b4f7dfab43))
## [1.453.1](https://github.com/windmill-labs/windmill/compare/v1.453.0...v1.453.1) (2025-01-30)
### Bug Fixes
* **frontend:** fetching job result (db schema) separatly if too big ([#5171](https://github.com/windmill-labs/windmill/issues/5171)) ([f29ee67](https://github.com/windmill-labs/windmill/commit/f29ee67d9d8547d7c4a79e14698b7d88a722e730))
* **typescript-client:** fix typescript client mocked api behavior ([7f516f0](https://github.com/windmill-labs/windmill/commit/7f516f08682876f5a7ca409ec002cba6ec0c5c5b))
## [1.453.0](https://github.com/windmill-labs/windmill/compare/v1.452.1...v1.453.0) (2025-01-29)
### Features
* custom response headers ([#5156](https://github.com/windmill-labs/windmill/issues/5156)) ([5544d42](https://github.com/windmill-labs/windmill/commit/5544d42a2db652660051b5f2e05f8db0102400fa))
* **python:** add `custom_wheels` directory to `PYTHONPATH` ([#5169](https://github.com/windmill-labs/windmill/issues/5169)) ([c60f8da](https://github.com/windmill-labs/windmill/commit/c60f8dacf191c5255ef8fca462d6d3d571b86b07))
* websocket trigger allow returning messages ([#5168](https://github.com/windmill-labs/windmill/issues/5168)) ([487c273](https://github.com/windmill-labs/windmill/commit/487c273bfbc5bf1731947c922f32cd026cca47f9))
### Bug Fixes
* oracledb tag for native workers + client not working on arm systems ([#5162](https://github.com/windmill-labs/windmill/issues/5162)) ([a1579c1](https://github.com/windmill-labs/windmill/commit/a1579c1654a2ddf9f6fc8f05f62b37bb09303752))
* **python:** fix uv can't find ssl certificates ([#5157](https://github.com/windmill-labs/windmill/issues/5157)) ([680fb18](https://github.com/windmill-labs/windmill/commit/680fb1852a192e384e23e2f5ceea473383f9f5aa))
## [1.452.1](https://github.com/windmill-labs/windmill/compare/v1.452.0...v1.452.1) (2025-01-27)
### Bug Fixes
* fix broken tabs for apps ([eaf633c](https://github.com/windmill-labs/windmill/commit/eaf633ce7675bf86108ebfc81ed735c26031ad71))
## [1.452.0](https://github.com/windmill-labs/windmill/compare/v1.451.0...v1.452.0) (2025-01-27)
### Features
* mssql executor supports azure db redirect ([#5150](https://github.com/windmill-labs/windmill/issues/5150)) ([fdb7122](https://github.com/windmill-labs/windmill/commit/fdb712234eb7f1a9374012d51e1cb25ee5aaa410))
* **postgres, trigger:** support ssl ([#5149](https://github.com/windmill-labs/windmill/issues/5149)) ([e0b6aa4](https://github.com/windmill-labs/windmill/commit/e0b6aa42a28a4273f3a7bb7465396f2756e60a92))
* test trigger connection ([#5145](https://github.com/windmill-labs/windmill/issues/5145)) ([d17397b](https://github.com/windmill-labs/windmill/commit/d17397b0703fb6a7dabf9db75d3c30ec26cdbe1e))
## [1.451.0](https://github.com/windmill-labs/windmill/compare/v1.450.1...v1.451.0) (2025-01-27)
### Features
* app builder accordion list and accordion tab component ([#5132](https://github.com/windmill-labs/windmill/issues/5132)) ([0e41023](https://github.com/windmill-labs/windmill/commit/0e41023889fb2f26ef5dc2a736dd59b819d92bf1))
* **typescript-client:** add ability to mock typescript api for testing ([f9ce01c](https://github.com/windmill-labs/windmill/commit/f9ce01c1dcda581d943e3cf58008cc6c89d39c5f))
### Bug Fixes
* **frontend:** make argInput indent optional ([#5144](https://github.com/windmill-labs/windmill/issues/5144)) ([b8152a3](https://github.com/windmill-labs/windmill/commit/b8152a3259fa0f791990e41d825f196d82c4084d))
## [1.450.1](https://github.com/windmill-labs/windmill/compare/v1.450.0...v1.450.1) (2025-01-26)
### Bug Fixes
* fix SCIM/SAML settings persistence ([697a060](https://github.com/windmill-labs/windmill/commit/697a060e3e01a183b6ac2371eee319b01ee891cf))
## [1.450.0](https://github.com/windmill-labs/windmill/compare/v1.449.3...v1.450.0) (2025-01-26)
### Features
* **cli:** improve codebase support + remove --stateful + warn when pushing stale metadata ([#5139](https://github.com/windmill-labs/windmill/issues/5139)) ([b47e9c1](https://github.com/windmill-labs/windmill/commit/b47e9c14f277339ae51fd3a40e00e8ba4e8a1390))
## [1.449.3](https://github.com/windmill-labs/windmill/compare/v1.449.2...v1.449.3) (2025-01-25)
### Bug Fixes
* include postgres triggers in build [#5137](https://github.com/windmill-labs/windmill/issues/5137) ([27a25d4](https://github.com/windmill-labs/windmill/commit/27a25d4af9ca0f0e0ed1d06a462fdd7cf966ce2b))
* update tests to rust 1.83.0 ([f29c702](https://github.com/windmill-labs/windmill/commit/f29c70279f14c2cdbf6e330ae266060f81f32bb6))
## [1.449.2](https://github.com/windmill-labs/windmill/compare/v1.449.1...v1.449.2) (2025-01-25)
### Bug Fixes
* update rust to 1.83.0 ([#5135](https://github.com/windmill-labs/windmill/issues/5135)) ([5381c76](https://github.com/windmill-labs/windmill/commit/5381c76b7d194d75f58d70b29aa3f5f8589fa899))
## [1.449.1](https://github.com/windmill-labs/windmill/compare/v1.449.0...v1.449.1) (2025-01-25)
### Bug Fixes
* Fix python refetching wheels from S3 ([#5133](https://github.com/windmill-labs/windmill/issues/5133)) ([2f1804b](https://github.com/windmill-labs/windmill/commit/2f1804b6d0e6f3492608967f8937d5e34481661b))
## [1.449.0](https://github.com/windmill-labs/windmill/compare/v1.448.1...v1.449.0) (2025-01-24)
### Features
* **frontend:** operator visibility setting per workspace ([#5124](https://github.com/windmill-labs/windmill/issues/5124)) ([1a1ea68](https://github.com/windmill-labs/windmill/commit/1a1ea682166af62133d3df2816490e3ebc92462f))
* msft teams support for critical alerts ([#5113](https://github.com/windmill-labs/windmill/issues/5113)) ([a88fbb2](https://github.com/windmill-labs/windmill/commit/a88fbb238ae21ca53bccd53ccfd3c77e2946fda2))
* postgres triggers ([#4860](https://github.com/windmill-labs/windmill/issues/4860)) ([316a216](https://github.com/windmill-labs/windmill/commit/316a2167e7648052afb3d7456c5ab9324348e56c))
* **python:** Multiple runtime versions ([#4579](https://github.com/windmill-labs/windmill/issues/4579)) ([e47dd69](https://github.com/windmill-labs/windmill/commit/e47dd697f96883f8f97bf469e69d3344f12d9633))
### Bug Fixes
* **bun, windows:** Fix "A required privilege is not held by the client." ([#5126](https://github.com/windmill-labs/windmill/issues/5126)) ([74bb660](https://github.com/windmill-labs/windmill/commit/74bb660b9799e79e31c33d96a4f06d77094287d5))
## [1.448.1](https://github.com/windmill-labs/windmill/compare/v1.448.0...v1.448.1) (2025-01-22)
+1 -1
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.83-slim-bookworm
ARG PYTHON_IMAGE=python:3.11.10-slim-bookworm
FROM ${RUST_IMAGE} AS rust_base
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false",
"query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color,\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false",
"describe": {
"columns": [
{
@@ -22,6 +22,11 @@
"ordinal": 3,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "operator_settings",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -33,8 +38,9 @@
false,
false,
false,
true
true,
null
]
},
"hash": "6c845f168b6265e6cf92e4d33c5409edfdc1847d0348df2ea55504ddaaa67736"
"hash": "1452033a8e2b160883a649c986d6c7ba2f60f41e1abb6ff8332bd2dfa7379d14"
}
@@ -127,6 +127,11 @@
"ordinal": 24,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 25,
"name": "operator_settings",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -159,6 +164,7 @@
true,
true,
true,
true,
true
]
},
@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as \"websocket_used!\", \n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\"",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "websocket_used!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "http_routes_used!",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "kafka_used!",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "nats_used!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null,
null,
null
]
},
"hash": "1ef48cc430870ab6c062b046bdbf0a2db057141619e9818ddb904d7c242efec3"
}
@@ -127,6 +127,11 @@
"ordinal": 24,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 25,
"name": "operator_settings",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -159,6 +164,7 @@
true,
true,
true,
true,
true
]
},
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET operator_settings = $1 WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Text"
]
},
"nullable": []
},
"hash": "597b89889c3820fe7986b834c0e5a0652d6a72024a0c17f5271add38168d1ab3"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", payload as \"payload: _\", trigger_extra as \"trigger_extra: _\"\n FROM capture\n WHERE workspace_id = $1\n AND path = $2 AND is_flow = $3\n AND ($4::trigger_kind IS NULL OR trigger_kind = $4)\n ORDER BY created_at DESC\n OFFSET $5\n LIMIT $6",
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", CASE WHEN pg_column_size(payload) < 40000 THEN payload ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as \"payload!: _\", trigger_extra as \"trigger_extra: _\"\n FROM capture\n WHERE workspace_id = $1\n AND path = $2 AND is_flow = $3\n AND ($4::trigger_kind IS NULL OR trigger_kind = $4)\n ORDER BY created_at DESC\n OFFSET $5\n LIMIT $6",
"describe": {
"columns": [
{
@@ -34,7 +34,7 @@
},
{
"ordinal": 3,
"name": "payload: _",
"name": "payload!: _",
"type_info": "Jsonb"
},
{
@@ -71,9 +71,9 @@
false,
false,
false,
false,
null,
true
]
},
"hash": "e08dddf6af2656b561c453460ff928e7a158bd29d9035bee136f9952fb96bad4"
"hash": "5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, edited_at = now(), server_id = NULL, error = NULL\n WHERE workspace_id = $10 AND path = $11",
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, can_return_message = $10, edited_at = now(), server_id = NULL, error = NULL\n WHERE workspace_id = $11 AND path = $12",
"describe": {
"columns": [],
"parameters": {
@@ -14,11 +14,12 @@
"Jsonb",
"Varchar",
"Varchar",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "561b7935d687f6b9f3d6488f8489f55e6737ffa2d3716f803d32f8b68cc1e915"
"hash": "63b42286804a3f3977235935d7ac200a07c5a4ad3a50051b73f382385b89466e"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "select path from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "64920b845c0ce81fb99497c03b249bb6cb06581079b5fc5bea5ddd8e7a895b79"
}
@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET running = false, started_at = null\n WHERE last_ping < now() - ($1 || ' seconds')::interval\n AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false RETURNING id, workspace_id, last_ping",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "last_ping",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "84576f6add15108ade2ed3a88185d67db17904859e5f01f1d578af4e1d062a85"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "WITH zombie_jobs AS (\n UPDATE queue SET running = false, started_at = null\n WHERE last_ping < now() - ($1 || ' seconds')::interval\n AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false \n RETURNING id, workspace_id, last_ping\n ),\n update_concurrency AS (\n UPDATE concurrency_counter cc\n SET job_uuids = job_uuids - zj.id::text\n FROM zombie_jobs zj\n INNER JOIN concurrency_key ck ON ck.job_id = zj.id\n WHERE cc.concurrency_id = ck.key\n )\n SELECT id, workspace_id, last_ping FROM zombie_jobs",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "last_ping",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "9a9b639611459659ae355a43f219c6da2c3d1e04d49306adaaaaf06e54ee8357"
}
@@ -1,6 +1,6 @@
{
"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",
"query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase LIKE '%.tar' as use_tar FROM script WHERE hash = $1 LIMIT 1",
"describe": {
"columns": [
{
@@ -52,8 +52,8 @@
},
{
"ordinal": 4,
"name": "codebase: String",
"type_info": "Varchar"
"name": "use_tar",
"type_info": "Bool"
}
],
"parameters": {
@@ -66,8 +66,8 @@
true,
false,
true,
true
null
]
},
"hash": "0bf123446bebbc357c58a53a9319f4954dbf3225e91cbe999e5b264c1a747664"
"hash": "d55eabd42559893bdf91c5fdab1ae58006013be26fcb9367b3c6830f38749799"
}
@@ -0,0 +1,61 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", payload as \"payload!: _\", trigger_extra as \"trigger_extra: _\" FROM capture WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "trigger_kind: _",
"type_info": {
"Custom": {
"name": "trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats"
]
}
}
}
},
{
"ordinal": 3,
"name": "payload!: _",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "trigger_extra: _",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true
]
},
"hash": "e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7"
}
@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value, is_secret\n FROM variable\n WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "is_secret",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "f3d3d105b76ec2eded521c3bb3735a3765c9aa2d103e3df85c245d85ba32b3b9"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow_version.value->>'early_return' as early_return\n FROM flow \n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.path = $1 and flow.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "early_return",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "fc243af1bc70f04e28c006364d6dfdfc49981568e8c496fb6a163c499c3e4ad1"
}
+209 -103
View File
@@ -67,7 +67,7 @@ dependencies = [
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -651,9 +651,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "aws-config"
version = "1.5.14"
version = "1.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f40e82e858e02445402906e454a73e244c7f501fcae198977585946c48e8697"
checksum = "dc47e70fc35d054c8fcd296d47a61711f043ac80534a10b4f741904f81e73a90"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -718,9 +718,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.55.0"
version = "1.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33993c0b054f4251ff2946941b56c26b582677303eeca34087594eb901ece022"
checksum = "c54bab121fe1881a74c338c5f723d1592bf3b53167f80268a1274f404e1acc38"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -740,9 +740,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.56.0"
version = "1.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bd3ceba74a584337a8f3839c818f14f1a2288bfd24235120ff22d7e17a0dd54"
checksum = "8c8234fd024f7ac61c4e44ea008029bde934250f371efe7d4a39708397b1080c"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -762,9 +762,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.56.0"
version = "1.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07835598e52dd354368429cb2abf447ce523ea446d0a533a63cb42cd0d2d9280"
checksum = "ba60e1d519d6f23a9df712c04fdeadd7872ac911c84b2f62a8bda92e129b7962"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -962,7 +962,7 @@ dependencies = [
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"itoa",
"matchit",
@@ -1282,7 +1282,7 @@ dependencies = [
"hex",
"http 1.2.0",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-named-pipe",
"hyper-util",
"hyperlocal",
@@ -1388,9 +1388,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.16.0"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
dependencies = [
"allocator-api2",
]
@@ -1708,9 +1708,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "cmake"
version = "0.1.52"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e"
checksum = "e24a03c8b52922d68a1589ad61032f2c1aa5a8158d2aa0d93c6e9534944bbad6"
dependencies = [
"cc",
]
@@ -1893,9 +1893,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
@@ -1941,7 +1941,7 @@ checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740"
dependencies = [
"chrono",
"once_cell",
"winnow 0.6.24",
"winnow 0.6.26",
]
[[package]]
@@ -2641,7 +2641,7 @@ dependencies = [
"hickory-resolver",
"http 1.2.0",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-rustls 0.27.5",
"hyper-util",
"ipnet",
@@ -2661,9 +2661,9 @@ dependencies = [
[[package]]
name = "deno_media_type"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "577fe2bbe04f3e9b1b7c6fac6a75101a9fbd611c50a6b68789e69f4d63dcb2b4"
checksum = "600222d059ab31ff31182b3e12615df2134a9e01605836b78ad8df91ba39eab3"
dependencies = [
"data-url",
"serde",
@@ -3764,6 +3764,18 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets 0.52.6",
]
[[package]]
name = "gimli"
version = "0.31.1"
@@ -3944,6 +3956,17 @@ dependencies = [
"foldhash",
]
[[package]]
name = "hashify"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f208758247e68e239acaa059e72e4ce1f30f2a4b6523f19c1b923d25b7e9cceb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "hashlink"
version = "0.10.0"
@@ -4081,9 +4104,9 @@ dependencies = [
[[package]]
name = "hstr"
version = "0.2.15"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63d6824358c0fd9a68bb23999ed2ef76c84f79408a26ef7ae53d5f370c94ad36"
checksum = "a1a26def229ea95a8709dad32868d975d0dd40235bd2ce82920e4a8fe692b5e0"
dependencies = [
"hashbrown 0.14.5",
"new_debug_unreachable",
@@ -4178,9 +4201,9 @@ dependencies = [
[[package]]
name = "httparse"
version = "1.9.5"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
[[package]]
name = "httpdate"
@@ -4220,9 +4243,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.5.2"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [
"bytes",
"futures-channel",
@@ -4246,7 +4269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
dependencies = [
"hex",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -4278,7 +4301,7 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http 1.2.0",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"rustls 0.23.21",
"rustls-native-certs 0.8.1",
@@ -4295,7 +4318,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793"
dependencies = [
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -4323,7 +4346,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"native-tls",
"tokio",
@@ -4342,7 +4365,7 @@ dependencies = [
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"hyper 1.5.2",
"hyper 1.6.0",
"pin-project-lite",
"socket2",
"tokio",
@@ -4359,7 +4382,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
dependencies = [
"hex",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -4564,9 +4587,9 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.17.9"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console",
"number_prefix",
@@ -5037,11 +5060,12 @@ dependencies = [
[[package]]
name = "mail-parser"
version = "0.9.4"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93c3b9e5d8b17faf573330bbc43b37d6e918c0a3bf8a88e7d0a220ebc84af9fc"
checksum = "187a2b93c4c8c32f552ee06c2d99915e575de2fc7e04b07891c9edfee5b8edd6"
dependencies = [
"encoding_rs",
"hashify",
"serde",
]
@@ -5064,9 +5088,9 @@ dependencies = [
[[package]]
name = "malachite"
version = "0.4.16"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5616515d632967cd329b6f6db96be9a03ea0b3a49cdbc45b0016803dad8a77b7"
checksum = "4a6ecab92657eb234bfe98abd0b17920772c6b14ce69256950142e2eb36d000b"
dependencies = [
"malachite-base",
"malachite-nz",
@@ -5075,9 +5099,9 @@ dependencies = [
[[package]]
name = "malachite-base"
version = "0.4.16"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46059721011b0458b7bd6d9179be5d0b60294281c23320c207adceaecc54d13b"
checksum = "06f6d078bb4dad5b76f6a85737e6c9113667d199612a73eef375c56e75d5d4d5"
dependencies = [
"hashbrown 0.14.5",
"itertools 0.11.0",
@@ -5100,9 +5124,9 @@ dependencies = [
[[package]]
name = "malachite-nz"
version = "0.4.16"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1503b27e825cabd1c3d0ff1e95a39fb2ec9eab6fd3da6cfa41aec7091d273e78"
checksum = "ad61a09cde72dcdb8b2baaf3e71430c5bd94fb6753da89f17301a459f3e466db"
dependencies = [
"itertools 0.11.0",
"libm",
@@ -5111,9 +5135,9 @@ dependencies = [
[[package]]
name = "malachite-q"
version = "0.4.16"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a475503a70a3679dbe3b9b230a23622516742528ba614a7b2490f180ea9cb514"
checksum = "be4a9dfffb87667ae94e8320213d3f5419e3e37311bc6bf8f4d2ab9b44f3a535"
dependencies = [
"itertools 0.11.0",
"malachite-base",
@@ -5388,9 +5412,9 @@ dependencies = [
[[package]]
name = "native-tls"
version = "0.2.12"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c"
dependencies = [
"libc",
"log",
@@ -5642,7 +5666,7 @@ dependencies = [
"chrono",
"futures",
"humantime",
"hyper 1.5.2",
"hyper 1.6.0",
"itertools 0.13.0",
"md-5 0.10.6",
"parking_lot",
@@ -5753,9 +5777,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.68"
version = "0.10.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
@@ -5779,9 +5803,9 @@ dependencies = [
[[package]]
name = "openssl-probe"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-src"
@@ -6274,6 +6298,17 @@ dependencies = [
"tokio-postgres 0.7.12",
]
[[package]]
name = "postgres-native-tls"
version = "0.5.0"
source = "git+https://github.com/imor/rust-postgres?rev=20265ef38e32a06f76b6f9b678e2077fc2211f6b#20265ef38e32a06f76b6f9b678e2077fc2211f6b"
dependencies = [
"native-tls",
"tokio",
"tokio-native-tls",
"tokio-postgres 0.7.11",
]
[[package]]
name = "postgres-protocol"
version = "0.6.7"
@@ -6348,7 +6383,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -6755,6 +6790,17 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.0",
"zerocopy 0.8.14",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
@@ -6775,6 +6821,16 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.0",
]
[[package]]
name = "rand_core"
version = "0.5.1"
@@ -6793,6 +6849,16 @@ dependencies = [
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
dependencies = [
"getrandom 0.3.1",
"zerocopy 0.8.14",
]
[[package]]
name = "rand_distr"
version = "0.4.3"
@@ -7052,7 +7118,7 @@ dependencies = [
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-rustls 0.27.5",
"hyper-tls 0.6.0",
"hyper-util",
@@ -7401,9 +7467,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.10.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
dependencies = [
"web-time",
]
@@ -7506,9 +7572,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "ryu-js"
@@ -7776,9 +7842,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.137"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"indexmap 2.7.1",
"itoa",
@@ -9233,13 +9299,13 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.15.0"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
dependencies = [
"cfg-if",
"fastrand 2.3.0",
"getrandom 0.2.15",
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
@@ -9762,7 +9828,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.24",
"winnow 0.6.26",
]
[[package]]
@@ -9780,7 +9846,7 @@ dependencies = [
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-timeout",
"hyper-util",
"percent-encoding",
@@ -10280,9 +10346,9 @@ checksum = "2f322b60f6b9736017344fa0635d64be2f458fbc04eef65f6be22976dd1ffd5b"
[[package]]
name = "unicode-ident"
version = "1.0.14"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]]
name = "unicode-normalization"
@@ -10554,6 +10620,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasite"
version = "0.1.0"
@@ -10785,7 +10860,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"axum",
@@ -10802,7 +10877,7 @@ dependencies = [
"once_cell",
"prometheus",
"quote",
"rand 0.8.5",
"rand 0.9.0",
"reqwest 0.12.9",
"serde",
"serde_json",
@@ -10815,6 +10890,7 @@ dependencies = [
"tracing",
"url",
"uuid 1.12.1",
"v8",
"windmill-api",
"windmill-api-client",
"windmill-autoscaling",
@@ -10827,7 +10903,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"argon2",
@@ -10857,7 +10933,7 @@ dependencies = [
"hf-hub",
"hmac",
"http 1.2.0",
"hyper 1.5.2",
"hyper 1.6.0",
"itertools 0.14.0",
"jsonwebtoken",
"lazy_static",
@@ -10872,9 +10948,10 @@ dependencies = [
"openssl",
"pg_escape",
"pin-project",
"postgres-native-tls 0.5.0 (git+https://github.com/imor/rust-postgres?rev=20265ef38e32a06f76b6f9b678e2077fc2211f6b)",
"prometheus",
"quick_cache",
"rand 0.8.5",
"rand 0.9.0",
"rdkafka",
"regex",
"reqwest 0.12.9",
@@ -10919,7 +10996,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -10927,7 +11004,7 @@ dependencies = [
"prettyplease 0.1.25",
"progenitor",
"progenitor-client",
"rand 0.8.5",
"rand 0.9.0",
"reqwest 0.11.27",
"serde",
"serde_json",
@@ -10937,7 +11014,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"chrono",
"serde",
@@ -10950,7 +11027,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"serde",
@@ -10964,7 +11041,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"async-stream",
@@ -10984,7 +11061,7 @@ dependencies = [
"git-version",
"hex",
"hmac",
"hyper 1.5.2",
"hyper 1.6.0",
"indexmap 2.7.1",
"itertools 0.14.0",
"lazy_static",
@@ -10999,7 +11076,7 @@ dependencies = [
"pin-project-lite",
"prometheus",
"quick_cache",
"rand 0.8.5",
"rand 0.9.0",
"regex",
"reqwest 0.12.9",
"semver 1.0.25",
@@ -11023,7 +11100,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"regex",
"serde",
@@ -11037,7 +11114,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"bytes",
@@ -11060,7 +11137,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -11072,7 +11149,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -11081,7 +11158,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -11093,7 +11170,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"serde_json",
@@ -11105,7 +11182,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"gosyn",
@@ -11117,7 +11194,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -11129,7 +11206,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -11140,7 +11217,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -11151,7 +11228,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -11169,7 +11246,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -11186,7 +11263,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -11198,7 +11275,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -11216,7 +11293,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"getrandom 0.2.15",
@@ -11238,7 +11315,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"serde_json",
@@ -11248,7 +11325,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -11281,7 +11358,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -11291,7 +11368,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.448.1"
version = "1.454.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -11331,9 +11408,9 @@ dependencies = [
"opentelemetry",
"oracle",
"pem 3.0.4",
"postgres-native-tls",
"postgres-native-tls 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"prometheus",
"rand 0.8.5",
"rand 0.9.0",
"regex",
"reqwest 0.12.9",
"rust_decimal",
@@ -11566,9 +11643,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.6.24"
version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
dependencies = [
"memchr",
]
@@ -11589,6 +11666,15 @@ version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "write16"
version = "1.0.0"
@@ -11682,7 +11768,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468"
dependencies = [
"zerocopy-derive 0.8.14",
]
[[package]]
@@ -11696,6 +11791,17 @@ dependencies = [
"syn 2.0.96",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
+7 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.448.1"
version = "1.454.1"
authors.workspace = true
edition.workspace = true
@@ -30,7 +30,7 @@ members = [
]
[workspace.package]
version = "1.448.1"
version = "1.454.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -61,7 +61,7 @@ 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"]
sqlx = ["windmill-worker/sqlx"]
deno_core = ["windmill-worker/deno_core", "dep:deno_core"]
deno_core = ["windmill-worker/deno_core", "dep:deno_core", "dep:v8"]
kafka = ["windmill-api/kafka"]
nats = ["windmill-api/nats"]
otel = ["windmill-common/otel", "windmill-worker/otel"]
@@ -116,6 +116,7 @@ deno_core = { workspace = true, optional = true }
object_store = { workspace = true, optional = true }
quote.workspace = true
memchr.workspace = true
v8 = { workspace = true, optional = true }
[target.'cfg(not(target_env = "msvc"))'.dependencies]
@@ -133,6 +134,7 @@ windmill-api-client.workspace = true
deno_core = { workspace = true, features = ["include_js_files_for_snapshotting", "unsafe_use_unprotected_platform"] }
[workspace.dependencies]
windmill-api = { path = "./windmill-api", default-features = false }
windmill-queue = { path = "./windmill-queue" }
@@ -157,6 +159,7 @@ windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
windmill-parser-php = { path = "./parsers/windmill-parser-php" }
windmill-api-client = { path = "./windmill-api-client" }
v8 = "=130.0.7" # Exact version
memchr = "2.7.4"
axum = { version = "^0.7", features = ["multipart"] }
headers = "^0"
@@ -265,6 +268,7 @@ convert_case = "0.6.0"
getrandom = "0.2"
tokio-postgres = {version = "^0.7", features = ["array-impls", "with-serde_json-1", "with-chrono-0_4", "with-uuid-1", "with-bit-vec-0_6"]}
rust-postgres = { package = "tokio-postgres", git = "https://github.com/imor/rust-postgres", rev = "20265ef38e32a06f76b6f9b678e2077fc2211f6b"}
rust-postgres-native-tls = { package = "postgres-native-tls", git = "https://github.com/imor/rust-postgres", features = ["runtime"], rev = "20265ef38e32a06f76b6f9b678e2077fc2211f6b" }
bit-vec = "=0.6.3"
mappable-rc = "^0"
mysql_async = { version = "*", default-features = false, features = ["minimal", "default", "native-tls-tls", "rust_decimal"]}
+1 -1
View File
@@ -1 +1 @@
0c89b8974ff6e1c9eda2134f09d4a03f18b57c15
7cc103e5827b1ec91180a5939a9c022a2a1856c6
@@ -0,0 +1 @@
ALTER TABLE workspace_settings DROP COLUMN operator_settings;
@@ -0,0 +1,11 @@
ALTER TABLE workspace_settings ADD COLUMN operator_settings JSONB DEFAULT '{
"runs": true,
"groups": true,
"folders": true,
"workers": true,
"triggers": true,
"resources": true,
"schedules": true,
"variables": true,
"audit_logs": true
}';
@@ -0,0 +1 @@
ALTER TABLE cloud_workspace_settings DROP COLUMN last_warning_sent;
@@ -0,0 +1 @@
ALTER TABLE cloud_workspace_settings ADD COLUMN last_warning_sent TIMESTAMP NULL;
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
UPDATE config set config = jsonb_set(config, '{worker_tags}', config->'worker_tags' || '["oracledb"]'::jsonb) where name = 'worker__native' and config @> '{"worker_tags": ["nativets", "postgresql", "mysql", "graphql", "snowflake", "bigquery", "mssql"]}'::jsonb AND NOT config->'worker_tags' @> '"oracledb"'::jsonb;
@@ -0,0 +1 @@
ALTER TABLE websocket_trigger DROP COLUMN can_return_message;
@@ -0,0 +1 @@
ALTER TABLE websocket_trigger ADD COLUMN can_return_message BOOLEAN NOT NULL DEFAULT FALSE;
@@ -37,6 +37,7 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
"smb" => "pysmb",
"PIL" => "Pillow",
"googleapiclient" => "google-api-python-client",
"googlecloudbigquery" => "google-cloud-bigquery",
"dateutil" => "python-dateutil",
"mailparser" => "mail-parser",
"mailparser-reply" => "mail-parser-reply",
+95
View File
@@ -0,0 +1,95 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1737885589,
"narHash": "sha256-Zf0hSrtzaM1DEz8//+Xs51k/wdSajticVrATqDrfQjg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "852ff1d9e153d8875a83602e03fdef8a63f0ecf8",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1736320768,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1738117527,
"narHash": "sha256-GFviGfaezjGLFUlxdv3zyC7rSZvTXqwcG/YsF6MDkOw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "6a3dc6ce4132bd57359214d986db376f2333c14d",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
@@ -0,0 +1,37 @@
{
inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = {
nixpkgs,
flake-utils,
rust-overlay,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
rust = pkgs.rust-bin.nightly.latest.default.override {
extensions = [
"rust-src"
];
targets = ["wasm32-unknown-unknown"];
};
in {
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
rust
nodejs
wasm-pack
sccache
];
RUSTC_WRAPPER = "${pkgs.sccache}/bin/sccache";
CARGO_PATH = "${rust}/bin/cargo";
};
});
}
+10 -9
View File
@@ -36,12 +36,13 @@ use windmill_common::{
CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
DEFAULT_TAGS_WORKSPACES_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING,
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
INSTANCE_PYTHON_VERSION_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, TEAMS_SETTING
INSTANCE_PYTHON_VERSION_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, TEAMS_SETTING,
TIMEOUT_WAIT_RESULT_SETTING,
},
scripts::ScriptLang,
stats_ee::schedule_stats,
@@ -788,7 +789,7 @@ Windmill Community Edition {GIT_VERSION}
EXPOSE_METRICS_SETTING => {
tracing::info!("Metrics setting changed, restarting");
// we wait a bit randomly to avoid having all servers and workers shutdown at same time
let rd_delay = rand::thread_rng().gen_range(0..40);
let rd_delay = rand::rng().random_range(0..40);
tokio::time::sleep(Duration::from_secs(rd_delay)).await;
if let Err(e) = tx.send(()) {
tracing::error!(error = %e, "Could not send killpill to server");
@@ -802,7 +803,7 @@ Windmill Community Edition {GIT_VERSION}
OTEL_SETTING => {
tracing::info!("OTEL setting changed, restarting");
// we wait a bit randomly to avoid having all servers and workers shutdown at same time
let rd_delay = rand::thread_rng().gen_range(0..4);
let rd_delay = rand::rng().random_range(0..4);
tokio::time::sleep(Duration::from_secs(rd_delay)).await;
if let Err(e) = tx.send(()) {
tracing::error!(error = %e, "Could not send killpill");
@@ -812,7 +813,7 @@ Windmill Community Edition {GIT_VERSION}
if server_mode {
tracing::info!("Request limit size change detected, killing server expecting to be restarted");
// we wait a bit randomly to avoid having all servers shutdown at same time
let rd_delay = rand::thread_rng().gen_range(0..4);
let rd_delay = rand::rng().random_range(0..4);
tokio::time::sleep(Duration::from_secs(rd_delay)).await;
if let Err(e) = tx.send(()) {
tracing::error!(error = %e, "Could not send killpill to server");
+34 -4
View File
@@ -1506,9 +1506,20 @@ pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker_name: &str) {
if *RESTART_ZOMBIE_JOBS {
let restarted = sqlx::query!(
"UPDATE queue SET running = false, started_at = null
WHERE last_ping < now() - ($1 || ' seconds')::interval
AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false RETURNING id, workspace_id, last_ping",
"WITH zombie_jobs AS (
UPDATE queue SET running = false, started_at = null
WHERE last_ping < now() - ($1 || ' seconds')::interval
AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false
RETURNING id, workspace_id, last_ping
),
update_concurrency AS (
UPDATE concurrency_counter cc
SET job_uuids = job_uuids - zj.id::text
FROM zombie_jobs zj
INNER JOIN concurrency_key ck ON ck.job_id = zj.id
WHERE cc.concurrency_id = ck.key
)
SELECT id, workspace_id, last_ping FROM zombie_jobs",
*ZOMBIE_JOB_TIMEOUT,
)
.fetch_all(db)
@@ -1652,12 +1663,31 @@ async fn handle_zombie_flows(db: &DB) -> error::Result<()> {
tracing::error!(error_message);
report_critical_error(error_message, db.clone(), Some(&flow.workspace_id), None).await;
// if the flow hasn't started and is a zombie, we can simply restart it
let mut tx = db.begin().await?;
let concurrency_key =
sqlx::query_scalar!("SELECT key FROM concurrency_key WHERE job_id = $1", flow.id)
.fetch_optional(&mut *tx)
.await?;
if let Some(key) = concurrency_key {
sqlx::query!(
"UPDATE concurrency_counter SET job_uuids = job_uuids - $2 WHERE concurrency_id = $1",
key,
flow.id.hyphenated().to_string()
)
.execute(&mut *tx)
.await?;
}
sqlx::query!(
"UPDATE queue SET running = false, started_at = null WHERE id = $1 AND canceled = false",
flow.id
)
.execute(db)
.execute(&mut *tx)
.await?;
tx.commit().await?;
} else {
let id = flow.id.clone();
let last_ping = flow.last_ping.clone();
+3 -2
View File
@@ -28,7 +28,7 @@ zip = ["dep:async_zip"]
oauth2 = ["dep:async-oauth2"]
http_trigger = ["dep:matchit"]
static_frontend = ["dep:rust-embed"]
postgres_trigger = ["dep:rust-postgres", "dep:pg_escape", "dep:byteorder", "dep:thiserror", "dep:rust_decimal"]
postgres_trigger = ["dep:rust-postgres", "dep:pg_escape", "dep:byteorder", "dep:thiserror", "dep:rust_decimal", "dep:rust-postgres-native-tls"]
[dependencies]
windmill-queue.workspace = true
@@ -116,4 +116,5 @@ rust-postgres = { workspace = true, optional = true }
pg_escape = { workspace = true, optional = true }
byteorder = { workspace = true, optional = true }
thiserror = { workspace = true, optional = true }
rust_decimal = { workspace = true, optional = true }
rust_decimal = { workspace = true, optional = true }
rust-postgres-native-tls = { workspace = true, optional = true}
+194 -3
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.448.1
version: 1.454.1
title: Windmill API
contact:
@@ -1618,6 +1618,29 @@ paths:
schema:
$ref: "#/components/schemas/User"
/w/{workspace}/workspaces/operator_settings:
post:
operationId: updateOperatorSettings
summary: Update operator settings for a workspace
description: Updates the operator settings for a specific workspace. Requires workspace admin privileges.
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/OperatorSettings"
responses:
'200':
description: Operator settings updated successfully
content:
text/plain:
schema:
type: string
/users/exists/{email}:
get:
summary: exists email
@@ -1745,6 +1768,8 @@ paths:
type: boolean
color:
type: string
operator_settings:
$ref: "#/components/schemas/OperatorSettings"
required:
- code_completion_enabled
- automatic_billing
@@ -7918,6 +7943,39 @@ paths:
schema:
type: string
/w/{workspace}/websocket_triggers/test:
post:
summary: test websocket connection
operationId: testWebsocketConnection
tags:
- websocket_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: test websocket connection
required: true
content:
application/json:
schema:
type: object
properties:
url:
type: string
url_runnable_args:
$ref: "#/components/schemas/ScriptArgs"
can_return_message:
type: boolean
required:
- url
- can_return_message
responses:
"200":
description: successfuly connected to websocket
content:
text/plain:
schema:
type: string
/w/{workspace}/kafka_triggers/create:
post:
summary: create kafka trigger
@@ -8079,6 +8137,34 @@ paths:
schema:
type: string
/w/{workspace}/kafka_triggers/test:
post:
summary: test kafka connection
operationId: testKafkaConnection
tags:
- kafka_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: test kafka connection
required: true
content:
application/json:
schema:
type: object
properties:
connection:
type: object
required:
- connection
responses:
"200":
description: successfuly connected to kafka brokers
content:
text/plain:
schema:
type: string
/w/{workspace}/nats_triggers/create:
post:
summary: create nats trigger
@@ -8242,6 +8328,35 @@ paths:
schema:
type: string
/w/{workspace}/nats_triggers/test:
post:
summary: test NATS connection
operationId: testNatsConnection
tags:
- nats_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: test nats connection
required: true
content:
application/json:
schema:
type: object
properties:
connection:
type: object
required:
- connection
responses:
"200":
description: successfuly connected to NATS servers
content:
text/plain:
schema:
type: string
/w/{workspace}/postgres_triggers/is_valid_postgres_configuration/{path}:
get:
summary: check if postgres configuration is set to logical
@@ -9712,6 +9827,25 @@ paths:
$ref: "#/components/schemas/Capture"
/w/{workspace}/capture/{id}:
get:
summary: get a capture
operationId: getCapture
tags:
- capture
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: capture
content:
application/json:
schema:
$ref: "#/components/schemas/Capture"
delete:
summary: delete a capture
operationId: deleteCapture
@@ -9980,6 +10114,8 @@ paths:
properties:
connection_settings_str:
type: string
azure_container_path:
type: string
required:
- connection_settings_str
@@ -13008,6 +13144,8 @@ components:
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
url_runnable_args:
$ref: "#/components/schemas/ScriptArgs"
can_return_message:
type: boolean
required:
- path
@@ -13021,6 +13159,7 @@ components:
- workspace_id
- enabled
- filters
- can_return_message
NewWebsocketTrigger:
type: object
@@ -13052,6 +13191,8 @@ components:
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
url_runnable_args:
$ref: "#/components/schemas/ScriptArgs"
can_return_message:
type: boolean
required:
- path
@@ -13059,6 +13200,7 @@ components:
- url
- is_flow
- filters
- can_return_message
EditWebsocketTrigger:
type: object
@@ -13088,6 +13230,8 @@ components:
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
url_runnable_args:
$ref: "#/components/schemas/ScriptArgs"
can_return_message:
type: boolean
required:
- path
@@ -13095,7 +13239,7 @@ components:
- url
- is_flow
- filters
- can_return_message
WebsocketTriggerInitialMessage:
anyOf:
- type: object
@@ -13221,6 +13365,9 @@ components:
type: string
error:
type: string
last_server_ping:
type: string
format: date-time
required:
- path
- script_path
@@ -13640,6 +13787,8 @@ components:
type: string
color:
type: string
operator_settings:
$ref: "#/components/schemas/OperatorSettings"
required:
- id
- name
@@ -14498,6 +14647,48 @@ components:
required:
- trigger_kind
OperatorSettings:
nullable: true
type: object
required:
- runs
- schedules
- resources
- variables
- triggers
- audit_logs
- groups
- folders
- workers
properties:
runs:
type: boolean
description: Whether operators can view runs
schedules:
type: boolean
description: Whether operators can view schedules
resources:
type: boolean
description: Whether operators can view resources
variables:
type: boolean
description: Whether operators can view variables
audit_logs:
type: boolean
description: Whether operators can view audit logs
triggers:
type: boolean
description: Whether operators can view triggers
groups:
type: boolean
description: Whether operators can view groups page
folders:
type: boolean
description: Whether operators can view folders page
workers:
type: boolean
description: Whether operators can view workers page
TeamInfo:
type: object
required:
@@ -14518,7 +14709,7 @@ components:
description: List of channels within the team
items:
$ref: '#/components/schemas/ChannelInfo'
ChannelInfo:
type: object
required:
+1 -1
View File
@@ -16,7 +16,6 @@ use crate::{
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
use windmill_common::variables::encrypt;
#[cfg(feature = "parquet")]
use crate::{
job_helpers_ee::{
@@ -54,6 +53,7 @@ use windmill_audit::audit_ee::audit_log;
use windmill_audit::ActionKind;
#[cfg(feature = "parquet")]
use windmill_common::s3_helpers::build_object_store_client;
use windmill_common::variables::encrypt;
use windmill_common::{
apps::{AppScriptId, ListAppQuery},
cache::{self, future::FutureCachedExt},
+22 -19
View File
@@ -35,9 +35,9 @@ use windmill_queue::{PushArgs, PushArgsOwned};
#[cfg(feature = "http_trigger")]
use crate::http_triggers::{build_http_trigger_extra, HttpMethod};
#[cfg(all(feature = "enterprise", feature = "kafka"))]
use crate::kafka_triggers_ee::KafkaResourceSecurity;
use crate::kafka_triggers_ee::KafkaTriggerConfigConnection;
#[cfg(all(feature = "enterprise", feature = "nats"))]
use crate::nats_triggers_ee::NatsResourceAuth;
use crate::nats_triggers_ee::NatsTriggerConfigConnection;
use crate::{
args::WebhookArgs,
db::{ApiAuthed, DB},
@@ -56,6 +56,7 @@ pub fn workspaced_service() -> Router {
.route("/get_configs/:runnable_kind/*path", get(get_configs))
.route("/list/:runnable_kind/*path", get(list_captures))
.route("/:id", delete(delete_capture))
.route("/:id", get(get_capture))
}
pub fn workspaced_unauthed_service() -> Router {
@@ -110,14 +111,6 @@ struct HttpTriggerConfig {
http_method: HttpMethod,
}
#[cfg(all(feature = "enterprise", feature = "kafka"))]
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum KafkaTriggerConfigConnection {
Resource { kafka_resource_path: String },
Static { brokers: Vec<String>, security: KafkaResourceSecurity },
}
#[cfg(all(feature = "enterprise", feature = "kafka"))]
#[derive(Serialize, Deserialize)]
pub struct KafkaTriggerConfig {
@@ -127,14 +120,6 @@ pub struct KafkaTriggerConfig {
pub group_id: String,
}
#[cfg(all(feature = "enterprise", feature = "nats"))]
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum NatsTriggerConfigConnection {
Resource { nats_resource_path: String },
Static { servers: Vec<String>, auth: NatsResourceAuth, require_tls: bool },
}
#[cfg(all(feature = "enterprise", feature = "nats"))]
#[derive(Serialize, Deserialize)]
pub struct NatsTriggerConfig {
@@ -296,7 +281,7 @@ async fn list_captures(
let captures = sqlx::query_as!(
Capture,
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", payload as "payload: _", trigger_extra as "trigger_extra: _"
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", CASE WHEN pg_column_size(payload) < 40000 THEN payload ELSE '"WINDMILL_TOO_BIG"'::jsonb END as "payload!: _", trigger_extra as "trigger_extra: _"
FROM capture
WHERE workspace_id = $1
AND path = $2 AND is_flow = $3
@@ -319,6 +304,24 @@ async fn list_captures(
Ok(Json(captures))
}
async fn get_capture(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, id)): Path<(String, i64)>,
) -> JsonResult<Capture> {
let mut tx = user_db.begin(&authed).await?;
let capture = sqlx::query_as!(
Capture,
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", payload as "payload!: _", trigger_extra as "trigger_extra: _" FROM capture WHERE id = $1 AND workspace_id = $2"#,
id,
&w_id,
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(capture))
}
async fn delete_capture(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
+5 -11
View File
@@ -521,7 +521,7 @@ async fn get_http_route_trigger(
trigger.email.clone(),
&trigger.workspace_id,
&db,
Some(username_override.unwrap_or("anonymous".to_string())),
Some(username_override.unwrap_or(format!("http-{}", trigger.path))),
)
.await?;
@@ -685,12 +685,6 @@ async fn route_job(
.await,
);
let label_prefix = Some(format!(
"http-{}-{}-",
method.as_str().to_lowercase(),
trigger.route_path
));
let run_query = RunJobQuery::default();
if trigger.is_flow {
@@ -703,7 +697,7 @@ async fn route_job(
StripPath(trigger.script_path.to_owned()),
run_query,
args,
label_prefix,
None,
)
.await
.into_response()
@@ -716,7 +710,7 @@ async fn route_job(
user_db,
args,
trigger.workspace_id.clone(),
label_prefix,
None,
)
.await
.into_response()
@@ -731,7 +725,7 @@ async fn route_job(
StripPath(trigger.script_path.to_owned()),
run_query,
args,
label_prefix,
None,
)
.await
.into_response()
@@ -744,7 +738,7 @@ async fn route_job(
user_db,
trigger.workspace_id.clone(),
args,
label_prefix,
None,
)
.await
.into_response()
+121 -74
View File
@@ -9,12 +9,14 @@
use axum::body::Body;
use axum::http::HeaderValue;
use futures::TryFutureExt;
use http::{HeaderMap, HeaderName};
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;
@@ -1966,20 +1968,34 @@ async fn resume_suspended_job_internal(
resume_immediately_if_relevant(parent_flow_info, job_id, &mut tx).await?;
}
let approver = approver.unwrap_or_else(|| "anonymous".to_string());
let audit_author = match authed {
Some(authed) => (&authed).into(),
None => {
let approver = approver.unwrap_or_else(|| "anonymous".to_string());
AuditAuthor { email: approver.clone(), username: approver, username_override: None }
}
None => AuditAuthor {
email: approver.clone(),
username: approver.clone(),
username_override: None,
},
};
audit_log(
&mut *tx,
&audit_author,
"jobs.approved",
"jobs.suspend_resume",
ActionKind::Update,
&w_id,
Some(&job_id.to_string()),
Some(
&serde_json::json!({
"approved": approved,
"job_id": job_id,
"details": if approved {
format!("Approved by {}", &approver)
} else {
format!("Cancelled by {}", &approver)
}
})
.to_string(),
),
None,
)
.await?;
@@ -3522,15 +3538,17 @@ lazy_static::lazy_static! {
pub struct WindmillCompositeResult {
windmill_status_code: Option<u16>,
windmill_content_type: Option<String>,
windmill_headers: Option<HashMap<String, String>>,
result: Option<Box<RawValue>>,
}
pub async fn run_wait_result(
pub async fn run_wait_result_internal(
db: &DB,
uuid: Uuid,
w_id: String,
node_id_for_empty_return: Option<String>,
username: &str,
) -> error::Result<Response> {
) -> error::Result<(Box<RawValue>, bool)> {
let mut result = None;
let mut success = false;
let timeout = TIMEOUT_WAIT_RESULT.read().await.clone().unwrap_or(600);
@@ -3602,81 +3620,110 @@ pub async fn run_wait_result(
};
tokio::time::sleep(core::time::Duration::from_millis(delay)).await;
}
if let Some(result) = result {
g.done = true;
Ok((result, success))
} else {
Err(Error::ExecutionErr(format!("timeout after {}s", timeout)))
}
}
let composite_result = serde_json::from_str::<WindmillCompositeResult>(result.get());
match composite_result {
Ok(WindmillCompositeResult {
windmill_status_code,
windmill_content_type,
result: result_value,
}) => {
if windmill_content_type.is_none() && windmill_status_code.is_none() {
return Ok((
if success {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(result),
)
.into_response());
}
pub async fn run_wait_result(
db: &DB,
uuid: Uuid,
w_id: String,
node_id_for_empty_return: Option<String>,
username: &str,
) -> error::Result<Response> {
let (result, success) =
run_wait_result_internal(db, uuid, w_id, node_id_for_empty_return, username).await?;
let status_code_or_default = windmill_status_code
.map(|val| match StatusCode::from_u16(val) {
Ok(sc) => Ok(sc),
Err(_) => Err(Error::ExecutionErr("Invalid status code".to_string())),
})
.unwrap_or_else(|| {
if !success {
Ok(StatusCode::INTERNAL_SERVER_ERROR)
} else if result_value.is_some() {
Ok(StatusCode::OK)
} else {
Ok(StatusCode::NO_CONTENT)
}
})?;
if windmill_content_type.is_some() {
let serialized_json_result = result_value
.map(|val| val.get().to_owned())
.unwrap_or_else(String::new);
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
// it falls back to the original serialized JSON if it doesn't work.
let serialized_result =
serde_json::from_str::<String>(serialized_json_result.as_str())
.ok()
.unwrap_or(serialized_json_result);
return Ok((
status_code_or_default,
[(
http::header::CONTENT_TYPE,
HeaderValue::from_str(windmill_content_type.unwrap().as_str()).unwrap(),
)],
serialized_result,
)
.into_response());
}
let composite_result = serde_json::from_str::<WindmillCompositeResult>(result.get());
match composite_result {
Ok(WindmillCompositeResult {
windmill_status_code,
windmill_content_type,
windmill_headers,
result: result_value,
}) => {
if windmill_content_type.is_none()
&& windmill_status_code.is_none()
&& windmill_headers.is_none()
{
return Ok((
status_code_or_default,
Json(result_value), // default to JSON result if no content type is provided
if success {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(result),
)
.into_response());
}
_ => Ok((
if success {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(result),
)
.into_response()),
let status_code_or_default = windmill_status_code
.map(|val| match StatusCode::from_u16(val) {
Ok(sc) => Ok(sc),
Err(_) => Err(Error::ExecutionErr("Invalid status code".to_string())),
})
.unwrap_or_else(|| {
if !success {
Ok(StatusCode::INTERNAL_SERVER_ERROR)
} else if result_value.is_some() {
Ok(StatusCode::OK)
} else {
Ok(StatusCode::NO_CONTENT)
}
})?;
let mut headers = HeaderMap::new();
if let Some(windmill_headers) = windmill_headers {
for (k, v) in windmill_headers {
let k = HeaderName::from_str(k.as_str()).map_err(|err| {
Error::InternalErr(format!("Invalid header name {k}: {err}"))
})?;
let v = HeaderValue::from_str(v.as_str()).map_err(|err| {
Error::InternalErr(format!("Invalid header value {v}: {err}"))
})?;
headers.insert(k, v);
}
}
if let Some(content_type) = windmill_content_type {
let serialized_json_result = result_value
.map(|val| val.get().to_owned())
.unwrap_or_else(String::new);
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
// it falls back to the original serialized JSON if it doesn't work.
let serialized_result =
serde_json::from_str::<String>(serialized_json_result.as_str())
.ok()
.unwrap_or(serialized_json_result);
headers.insert(
http::header::CONTENT_TYPE,
HeaderValue::from_str(content_type.as_str()).map_err(|err| {
Error::InternalErr(format!("Invalid content type {content_type}: {err}"))
})?,
);
return Ok((status_code_or_default, headers, serialized_result).into_response());
}
if let Some(result_value) = result_value {
return Ok((status_code_or_default, headers, Json(result_value)).into_response());
} else {
Ok((status_code_or_default, headers).into_response())
}
}
} else {
Err(Error::ExecutionErr(format!("timeout after {}s", timeout)))
_ => Ok((
if success {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(result),
)
.into_response()),
}
}
@@ -9,9 +9,12 @@ pub fn workspaced_service() -> Router {
Router::new()
}
pub async fn start_kafka_consumers(
pub fn start_kafka_consumers(
_db: DB,
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
// implementation is not open source
}
#[derive(Serialize, Deserialize)]
pub enum KafkaTriggerConfigConnection {}
+9 -9
View File
@@ -59,8 +59,6 @@ mod auth;
mod capture;
mod concurrency_groups;
mod configs;
#[cfg(feature = "postgres_trigger")]
mod postgres_triggers;
mod db;
mod drafts;
pub mod ee;
@@ -75,6 +73,8 @@ mod http_triggers;
mod indexer_ee;
mod inputs;
mod integration;
#[cfg(feature = "postgres_trigger")]
mod postgres_triggers;
#[cfg(feature = "enterprise")]
mod apps_ee;
@@ -98,12 +98,12 @@ mod scripts;
mod service_logs;
mod settings;
mod slack_approvals;
#[cfg(feature = "enterprise")]
mod teams_ee;
#[cfg(feature = "smtp")]
mod smtp_server_ee;
mod static_assets;
mod stripe_ee;
#[cfg(feature = "enterprise")]
mod teams_ee;
mod tracing_init;
mod triggers;
mod users;
@@ -296,24 +296,24 @@ pub async fn run_server(
#[cfg(feature = "websocket")]
{
let ws_killpill_rx = rx.resubscribe();
websocket_triggers::start_websockets(db.clone(), ws_killpill_rx).await;
websocket_triggers::start_websockets(db.clone(), ws_killpill_rx);
}
#[cfg(all(feature = "enterprise", feature = "kafka"))]
{
let kafka_killpill_rx = rx.resubscribe();
kafka_triggers_ee::start_kafka_consumers(db.clone(), kafka_killpill_rx).await;
kafka_triggers_ee::start_kafka_consumers(db.clone(), kafka_killpill_rx);
}
#[cfg(all(feature = "enterprise", feature = "nats"))]
{
let nats_killpill_rx = rx.resubscribe();
nats_triggers_ee::start_nats_consumers(db.clone(), nats_killpill_rx).await;
nats_triggers_ee::start_nats_consumers(db.clone(), nats_killpill_rx);
}
#[cfg(feature = "postgres_trigger")]
{
let db_killpill_rx = rx.resubscribe();
postgres_triggers::start_database(db.clone(), db_killpill_rx).await;
postgres_triggers::start_database(db.clone(), db_killpill_rx);
}
}
@@ -454,7 +454,7 @@ pub async fn run_server(
)
.route("/slack", post(slack_approvals::slack_app_callback_handler))
.nest("/teams", {
#[cfg(feature = "enterprise")]
#[cfg(feature = "enterprise")]
{
teams_ee::teams_service()
}
+4 -4
View File
@@ -9,9 +9,9 @@ pub fn workspaced_service() -> Router {
Router::new()
}
pub async fn start_nats_consumers(
_db: DB,
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
pub fn start_nats_consumers(_db: DB, mut _killpill_rx: tokio::sync::broadcast::Receiver<()>) -> () {
// implementation is not open source
}
#[derive(Serialize, Deserialize)]
pub enum NatsTriggerConfigConnection {}
@@ -46,6 +46,7 @@ pub struct Database {
pub host: String,
pub port: u16,
pub dbname: String,
#[serde(default)]
pub sslmode: String,
pub root_certificate_pem: String,
}
@@ -251,6 +252,12 @@ pub async fn create_postgres_trigger(
Path(w_id): Path<String>,
Json(new_postgres_trigger): Json<NewPostgresTrigger>,
) -> error::Result<(StatusCode, String)> {
if *CLOUD_HOSTED {
return Err(error::Error::BadRequest(
"Postgres triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
));
}
let NewPostgresTrigger {
postgres_resource_path,
path,
@@ -262,12 +269,6 @@ pub async fn create_postgres_trigger(
publication,
} = new_postgres_trigger;
if *CLOUD_HOSTED {
return Err(error::Error::BadRequest(
"Postgres triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
));
}
if publication_name.is_none() && publication.is_none() {
return Err(error::Error::BadRequest(
"Publication data is missing".to_string(),
@@ -283,14 +284,14 @@ pub async fn create_postgres_trigger(
if create_publication || create_slot {
let generate_random_string = move || {
let timestamp = Utc::now().timestamp_millis().to_string();
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
let charset = "abcdefghijklmnopqrstuvwxyz0123456789";
let random_part = (0..10)
.map(|_| {
charset
.chars()
.nth(rng.gen_range(0..charset.len()))
.nth(rng.random_range(0..charset.len()))
.unwrap()
})
.collect::<String>();
@@ -110,14 +110,13 @@ async fn run_job(
trigger: &PostgresTrigger,
) -> anyhow::Result<()> {
let args = PushArgsOwned { args: args.unwrap_or_default(), extra };
let label_prefix = Some(format!("db-{}-", trigger.path));
let authed = fetch_api_authed(
trigger.edited_by.clone(),
trigger.email.clone(),
&trigger.workspace_id,
db,
Some("anonymous".to_string()),
Some(format!("postgres-{}", trigger.path)),
)
.await?;
@@ -134,7 +133,7 @@ async fn run_job(
StripPath(trigger.script_path.to_owned()),
run_query,
args,
label_prefix,
None,
)
.await?;
} else {
@@ -146,7 +145,7 @@ async fn run_job(
StripPath(trigger.script_path.to_owned()),
run_query,
args,
label_prefix,
None,
)
.await?;
}
@@ -16,9 +16,11 @@ use crate::{
use bytes::{BufMut, Bytes, BytesMut};
use chrono::TimeZone;
use futures::{pin_mut, SinkExt, StreamExt};
use native_tls::TlsConnector;
use pg_escape::{quote_identifier, quote_literal};
use rand::seq::SliceRandom;
use rust_postgres::{Client, Config, CopyBothDuplex, NoTls, SimpleQueryMessage};
use rust_postgres::{config::SslMode, Client, Config, CopyBothDuplex, SimpleQueryMessage};
use rust_postgres_native_tls::MakeTlsConnector;
use windmill_common::{
db::UserDB, utils::report_critical_error, worker::to_raw_value, INSTANCE_NAME,
};
@@ -60,28 +62,50 @@ impl RowExist for Vec<SimpleQueryMessage> {
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("Error from database: {0}")]
Postgres(rust_postgres::Error),
Postgres(#[from] rust_postgres::Error),
#[error("Error : {0}")]
Common(windmill_common::error::Error),
Common(#[from] windmill_common::error::Error),
#[error("Tls Error: {0}")]
Tls(#[from] native_tls::Error),
}
pub struct PostgresSimpleClient(Client);
impl PostgresSimpleClient {
async fn new(database: &Database) -> Result<Self, Error> {
let ssl_mode = match database.sslmode.as_ref() {
"disable" => SslMode::Disable,
"" | "prefer" | "allow" => SslMode::Prefer,
"require" => SslMode::Require,
"verify-ca" => SslMode::VerifyCa,
"verify-full" => SslMode::VerifyFull,
ssl_mode => {
return Err(Error::Common(windmill_common::error::Error::BadRequest(
format!("Invalid ssl mode for postgres: {}, please put a valid ssl_mode among the following avalible ssl mode: ['disable', 'allow', 'prefer', 'verify-ca', 'verify-full']", ssl_mode),
)))
}
};
let mut config = Config::new();
config
.dbname(&database.dbname)
.host(&database.host)
.port(database.port)
.user(&database.user)
.ssl_mode(ssl_mode)
.replication_mode(rust_postgres::config::ReplicationMode::Logical);
if !database.password.is_empty() {
config.password(&database.password);
}
let (client, connection) = config.connect(NoTls).await.map_err(Error::Postgres)?;
if !database.root_certificate_pem.is_empty() {
config.ssl_root_cert(database.root_certificate_pem.as_bytes());
}
let connector = MakeTlsConnector::new(TlsConnector::new()?);
let (client, connection) = config.connect(connector).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
tracing::debug!("{:#?}", e);
@@ -111,8 +135,7 @@ impl PostgresSimpleClient {
Ok((
self.0
.copy_both_simple::<bytes::Bytes>(query.as_str())
.await
.map_err(Error::Postgres)?,
.await?,
LogicalReplicationSettings::new(false),
))
}
@@ -250,8 +273,7 @@ async fn listen_to_transactions(
&db,
None,
)
.await
.map_err(Error::Common)?;
.await?;
let database = get_database_resource(
authed,
@@ -260,8 +282,7 @@ async fn listen_to_transactions(
&postgres_trigger.postgres_resource_path,
&postgres_trigger.workspace_id,
)
.await
.map_err(Error::Common)?;
.await?;
let client = PostgresSimpleClient::new(&database).await?;
@@ -274,7 +295,6 @@ async fn listen_to_transactions(
Ok::<_, Error>((logical_replication_stream, logical_replication_settings))
};
tokio::select! {
biased;
_ = killpill_rx.recv() => {
@@ -305,7 +325,10 @@ async fn listen_to_transactions(
let message = match message {
Some(message) => message,
None => {
tracing::info!("Stream for postgres trigger {} is empty, leaving....", postgres_trigger.path);
tracing::error!("Stream for postgres trigger {} closed", postgres_trigger.path);
if let None = update_ping(&db, postgres_trigger, Some("Stream closed")).await {
return;
}
return;
}
};
@@ -359,7 +382,7 @@ async fn listen_to_transactions(
Some((update.o_id, relations.body_to_json((update.o_id, update.new_tuple)), "update"))
}
Delete(delete) => {
let body = delete.old_tuple.unwrap_or(delete.key_tuple.unwrap());
let body = delete.old_tuple.unwrap_or_else(|| delete.key_tuple.unwrap());
Some((delete.o_id, relations.body_to_json((delete.o_id, body)), "delete"))
}
};
@@ -389,7 +412,8 @@ async fn listen_to_transactions(
}
}
Err(err) => {
tracing::error!("Postgres trigger error while trying to start_logical_replication_streaming: {}", err.to_string())
tracing::error!("Postgres trigger error while trying to start logical replication streaming: {}", &err);
disable_with_error(&postgres_trigger, &db, err.to_string()).await
}
}
}
@@ -489,7 +513,7 @@ async fn listen_to_unlistened_database_events(
match postgres_triggers {
Ok(mut triggers) => {
triggers.shuffle(&mut rand::thread_rng());
triggers.shuffle(&mut rand::rng());
for trigger in triggers {
try_to_listen_to_database_transactions(
trigger,
@@ -505,7 +529,7 @@ async fn listen_to_unlistened_database_events(
};
}
pub async fn start_database(db: DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>) {
pub fn start_database(db: DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>) {
tokio::spawn(async move {
listen_to_unlistened_database_events(&db, &killpill_rx).await;
loop {
+2 -2
View File
@@ -23,7 +23,7 @@ use windmill_common::{
error::{self, Error},
jobs::JobKind,
scripts::ScriptHash,
variables::{build_crypt, decrypt_value_with_mc},
variables::{build_crypt, decrypt},
};
#[derive(Deserialize, Debug)]
@@ -853,7 +853,7 @@ async fn get_slack_token(db: &DB, slack_resource_path: &str, w_id: &str) -> anyh
if slack_token.is_secret {
let mc = build_crypt(&db, w_id).await?;
let bot_token = decrypt_value_with_mc(slack_token.value, mc).await?;
let bot_token = decrypt(&mc, slack_token.value)?;
Ok(bot_token)
} else {
Ok(slack_token.value)
+1
View File
@@ -1718,6 +1718,7 @@ pub async fn create_session_token<'c>(
if COOKIE_DOMAIN.is_some() {
cookie.set_domain(COOKIE_DOMAIN.clone().unwrap());
}
let mut expire: OffsetDateTime = time::OffsetDateTime::now_utc();
expire += time::Duration::days(3);
cookie.set_expires(expire);
File diff suppressed because it is too large Load Diff
+44 -2
View File
@@ -121,7 +121,8 @@ pub fn workspaced_service() -> Router {
"/critical_alerts/acknowledge_all",
post(acknowledge_all_critical_alerts),
)
.route("/critical_alerts/mute", post(mute_critical_alerts));
.route("/critical_alerts/mute", post(mute_critical_alerts))
.route("/operator_settings", post(update_operator_settings));
#[cfg(feature = "stripe")]
{
@@ -188,6 +189,7 @@ pub struct WorkspaceSettings {
pub default_scripts: Option<serde_json::Value>,
pub mute_critical_alerts: Option<bool>,
pub color: Option<String>,
pub operator_settings: Option<serde_json::Value>,
}
#[derive(FromRow, Serialize, Debug)]
@@ -286,6 +288,7 @@ struct UserWorkspace {
pub name: String,
pub username: String,
pub color: Option<String>,
pub operator_settings: Option<Option<serde_json::Value>>,
}
#[derive(Deserialize)]
@@ -1366,7 +1369,8 @@ async fn user_workspaces(
let mut tx = db.begin().await?;
let workspaces = sqlx::query_as!(
UserWorkspace,
"SELECT workspace.id, workspace.name, usr.username, workspace_settings.color
"SELECT workspace.id, workspace.name, usr.username, workspace_settings.color,
CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings
FROM workspace
JOIN usr ON usr.workspace_id = workspace.id
JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id
@@ -2135,3 +2139,41 @@ async fn mute_critical_alerts(
pub async fn mute_critical_alerts() -> Error {
Error::NotFound("Critical Alerts require EE".to_string())
}
#[derive(Deserialize, Serialize)]
struct ChangeOperatorSettings {
runs: bool,
schedules: bool,
resources: bool,
variables: bool,
triggers: bool,
audit_logs: bool,
groups: bool,
folders: bool,
workers: bool,
}
async fn update_operator_settings(
authed: ApiAuthed,
Path(w_id): Path<String>,
Extension(db): Extension<DB>,
Json(settings): Json<ChangeOperatorSettings>,
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let mut tx = db.begin().await?;
let settings_json = serde_json::json!(settings);
sqlx::query!(
"UPDATE workspace_settings SET operator_settings = $1 WHERE workspace_id = $2",
settings_json,
&w_id
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok("Operator settings updated successfully".to_string())
}
+11 -2
View File
@@ -529,7 +529,7 @@ pub mod script {
lock AS \"lock: String\", \
language AS \"language: Option<ScriptLang>\", \
envs AS \"envs: Vec<String>\", \
codebase AS \"codebase: String\" \
codebase LIKE '%.tar' as use_tar \
FROM script WHERE hash = $1 LIMIT 1",
hash.0
)
@@ -543,7 +543,16 @@ pub mod script {
meta: Some(ScriptMetadata {
language: r.language,
envs: r.envs,
codebase: r.codebase,
codebase: if let Some(use_tar) = r.use_tar {
let sh = hash.to_string();
if use_tar {
Some(format!("{sh}.tar"))
} else {
Some(sh)
}
} else {
None
},
}),
})
});
+2 -2
View File
@@ -161,8 +161,8 @@ impl Retry {
if let Some(random_factor) = exponential.random_factor {
if random_factor > 0 {
let random_component =
rand::thread_rng().gen_range(0..(std::cmp::min(random_factor, 100) as u16));
secs = match rand::thread_rng().gen_bool(1.0 / 2.0) {
rand::rng().random_range(0..(std::cmp::min(random_factor, 100) as u16));
secs = match rand::rng().random_bool(1.0 / 2.0) {
true => secs.saturating_add(secs * random_component / 100),
false => secs.saturating_sub(secs * random_component / 100),
};
+3 -2
View File
@@ -19,7 +19,8 @@ use git_version::git_version;
use chrono::Utc;
use croner::Cron;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rand::distr::Alphanumeric;
use rand::{rng, Rng};
use reqwest::Client;
use semver::Version;
use serde::{Deserialize, Serialize};
@@ -205,7 +206,7 @@ pub async fn http_get_from_hub(
}
pub fn rd_string(len: usize) -> String {
thread_rng()
rng()
.sample_iter(&Alphanumeric)
.take(len)
.map(char::from)
+2 -13
View File
@@ -6,12 +6,11 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::error::Result;
use crate::error;
use crate::{worker::WORKER_GROUP, BASE_URL, DB};
use chrono::{SecondsFormat, Utc};
use magic_crypt::{MagicCrypt256, MagicCryptError, MagicCryptTrait};
use serde::{Deserialize, Serialize};
use crate::error;
lazy_static::lazy_static! {
pub static ref SECRET_SALT: Option<String> = std::env::var("SECRET_SALT").ok();
@@ -134,7 +133,7 @@ pub async fn get_secret_value_as_admin(
let value = variable.value;
if !value.is_empty() {
let mc = build_crypt(db, w_id).await?;
decrypt_value_with_mc(value, mc).await?
decrypt(&mc, value)?
} else {
"".to_string()
}
@@ -145,16 +144,6 @@ pub async fn get_secret_value_as_admin(
Ok(r)
}
pub async fn decrypt_value_with_mc(value: String, mc: MagicCrypt256) -> Result<String> {
mc.decrypt_base64_to_string(value).map_err(|e| match e {
MagicCryptError::DecryptError(_) => crate::error::Error::InternalErr(
"Could not decrypt value. The value may have been encrypted with a different key."
.to_string(),
),
_ => crate::error::Error::InternalErr(e.to_string()),
})
}
pub fn encrypt(mc: &MagicCrypt256, value: &str) -> String {
mc.encrypt_str_to_base64(value)
}
@@ -123,6 +123,13 @@ mount {
is_bind: true
}
mount {
src: "{GLOBAL_SITE_PACKAGES}"
dst: "{GLOBAL_SITE_PACKAGES}"
is_bind: true
mandatory: false
}
{SHARED_MOUNT}
{SHARED_DEPENDENCIES}
+6 -8
View File
@@ -18,7 +18,7 @@ use windmill_common::jobs::ENTRYPOINT_OVERRIDE;
use windmill_common::s3_helpers::{
get_etag_or_empty, LargeFileStorage, ObjectStoreResource, S3Object,
};
use windmill_common::variables::{build_crypt_with_key_suffix, decrypt_value_with_mc};
use windmill_common::variables::{build_crypt_with_key_suffix, decrypt};
use windmill_common::worker::{
to_raw_value, write_file, CLOUD_HOSTED, ROOT_CACHE_DIR, WORKER_CONFIG,
};
@@ -63,10 +63,10 @@ pub fn check_executor_binary_exists(
) -> Result<(), Error> {
if !Path::new(executor_path).exists() {
#[cfg(feature = "enterprise")]
let msg = format!("Couldn't find {executor} at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full-ee` for your instance in order to run {language} jobs.", executor_path);
let msg = format!("Couldn't find {executor} at {}. This probably means that you are not using the windmill-ee-full image. Please use the image `ghcr.io/windmill-labs/windmill-ee-full` for your instance in order to run {language} jobs.", executor_path);
#[cfg(not(feature = "enterprise"))]
let msg = format!("Couldn't find {executor} at {}. This probably means that you are not using the windmill-full image. Please use the image `windmill-full` for your instance in order to run {language} jobs.", executor_path);
let msg = format!("Couldn't find {executor} at {}. This probably means that you are not using the windmill-full image. Please use the image `ghcr.io/windmill-labs/windmill-full` for your instance in order to run {language} jobs.", executor_path);
return Err(Error::NotFound(msg));
}
@@ -255,11 +255,9 @@ pub async fn transform_json_value(
let encrypted = y.strip_prefix("$encrypted:").unwrap();
let mc =
build_crypt_with_key_suffix(&db, &job.workspace_id, &job.id.to_string()).await?;
decrypt_value_with_mc(encrypted.to_string(), mc)
.await
.and_then(|x| {
serde_json::from_str(&x).map_err(|e| Error::InternalErr(e.to_string()))
})
decrypt(&mc, encrypted.to_string()).and_then(|x| {
serde_json::from_str(&x).map_err(|e| Error::InternalErr(e.to_string()))
})
// let path = y.strip_prefix("$res:").unwrap();
}
+24 -7
View File
@@ -72,7 +72,10 @@ pub async fn do_mssql(
let mut config = Config::new();
config.host(database.host);
let host_ref = &database.host;
let port_ref = database.port;
config.host(host_ref.clone());
config.database(database.dbname);
let use_instance_name = database.instance_name.as_ref().is_some_and(|x| x != "");
if use_instance_name {
@@ -101,12 +104,26 @@ pub async fn do_mssql(
};
tcp.set_nodelay(true)?;
// To be able to use Tokio's tcp, we're using the `compat_write` from
// the `TokioAsyncWriteCompatExt` to get a stream compatible with the
// traits from the `futures` crate.
let mut client = Client::connect(config, tcp.compat_write())
.await
.map_err(to_anyhow)?;
// NOTE Azure default behavior with SQL Server is to redirect:
// https://learn.microsoft.com/en-us/azure/azure-sql/database/connectivity-architecture?view=azuresql#connection-policy
// https://github.com/prisma/tiberius?tab=readme-ov-file#redirects
let mut client = match Client::connect(config.clone(), tcp.compat_write()).await {
Ok(client) => {
tracing::debug!("Connected to host: {:#?}, port: {:#?}", host_ref, port_ref);
client
}
Err(tiberius::error::Error::Routing { host, port }) => {
tracing::debug!("Redirecting to host: {:#?}, port: {:#?}", host, port);
config.host(&host);
config.port(port);
let tcp = TcpStream::connect(config.get_addr()).await?;
tcp.set_nodelay(true)?;
Client::connect(config, tcp.compat_write()).await.map_err(to_anyhow)?
}
Err(e) => return Err(Error::Anyhow(to_anyhow(e))),
};
let sig = parse_mssql_sig(&query)
.map_err(|x| Error::ExecutionErr(x.to_string()))?
+76 -25
View File
@@ -36,36 +36,38 @@ use windmill_common::{
use windmill_common::variables::get_secret_value_as_admin;
use windmill_queue::{append_logs, CanceledBy};
use std::env::var;
lazy_static::lazy_static! {
static ref PYTHON_PATH: String =
std::env::var("PYTHON_PATH").unwrap_or_else(|_| "/usr/local/bin/python3".to_string());
var("PYTHON_PATH").unwrap_or_else(|_| "/usr/local/bin/python3".to_string());
static ref UV_PATH: String =
std::env::var("UV_PATH").unwrap_or_else(|_| "/usr/local/bin/uv".to_string());
var("UV_PATH").unwrap_or_else(|_| "/usr/local/bin/uv".to_string());
static ref PY_CONCURRENT_DOWNLOADS: usize =
std::env::var("PY_CONCURRENT_DOWNLOADS").ok().map(|flag| flag.parse().unwrap_or(20)).unwrap_or(20);
var("PY_CONCURRENT_DOWNLOADS").ok().map(|flag| flag.parse().unwrap_or(20)).unwrap_or(20);
static ref FLOCK_PATH: String =
std::env::var("FLOCK_PATH").unwrap_or_else(|_| "/usr/bin/flock".to_string());
var("FLOCK_PATH").unwrap_or_else(|_| "/usr/bin/flock".to_string());
static ref NON_ALPHANUM_CHAR: Regex = regex::Regex::new(r"[^0-9A-Za-z=.-]").unwrap();
static ref PIP_TRUSTED_HOST: Option<String> = std::env::var("PIP_TRUSTED_HOST").ok();
static ref PIP_INDEX_CERT: Option<String> = std::env::var("PIP_INDEX_CERT").ok();
static ref TRUSTED_HOST: Option<String> = var("PY_TRUSTED_HOST").ok().or(var("PIP_TRUSTED_HOST").ok());
static ref INDEX_CERT: Option<String> = var("PY_INDEX_CERT").ok().or(var("PIP_INDEX_CERT").ok());
static ref NATIVE_CERT: bool = var("PY_NATIVE_CERT").ok().or(var("UV_NATIVE_TLS").ok()).map(|flag| flag == "true").unwrap_or(false);
pub static ref USE_SYSTEM_PYTHON: bool = std::env::var("USE_SYSTEM_PYTHON")
pub static ref USE_SYSTEM_PYTHON: bool = var("USE_SYSTEM_PYTHON")
.ok().map(|flag| flag == "true").unwrap_or(false);
pub static ref USE_PIP_COMPILE: bool = std::env::var("USE_PIP_COMPILE")
pub static ref USE_PIP_COMPILE: bool = var("USE_PIP_COMPILE")
.ok().map(|flag| flag == "true").unwrap_or(false);
pub static ref USE_PIP_INSTALL: bool = std::env::var("USE_PIP_INSTALL")
pub static ref USE_PIP_INSTALL: bool = var("USE_PIP_INSTALL")
.ok().map(|flag| flag == "true").unwrap_or(false);
static ref RELATIVE_IMPORT_REGEX: Regex = Regex::new(r#"(import|from)\s(((u|f)\.)|\.)"#).unwrap();
static ref EPHEMERAL_TOKEN_CMD: Option<String> = std::env::var("EPHEMERAL_TOKEN_CMD").ok();
static ref EPHEMERAL_TOKEN_CMD: Option<String> = var("EPHEMERAL_TOKEN_CMD").ok();
}
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
@@ -533,10 +535,10 @@ pub async fn uv_pip_compile(
args.extend(["--index-url", url, "--no-emit-index-url"]);
pip_args.push(format!("--index-url {}", url));
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
if let Some(host) = TRUSTED_HOST.as_ref() {
args.extend(["--trusted-host", host]);
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
if let Some(cert_path) = INDEX_CERT.as_ref() {
args.extend(["--cert", cert_path]);
}
let pip_args_str = pip_args.join(" ");
@@ -622,12 +624,15 @@ pub async fn uv_pip_compile(
if let Some(url) = pip_index_url.as_ref() {
args.extend(["--index-url", url]);
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
if let Some(host) = TRUSTED_HOST.as_ref() {
args.extend(["--trusted-host", host]);
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
if let Some(cert_path) = INDEX_CERT.as_ref() {
args.extend(["--cert", cert_path]);
}
if *NATIVE_CERT {
args.extend(["--native-tls"]);
}
tracing::error!("uv args: {:?}", args);
#[cfg(windows)]
@@ -898,6 +903,8 @@ pub async fn handle_python_job(
tracing::debug!("Finished deps postinstall stage");
}
if no_uv {
append_logs(
&job.id,
@@ -1046,7 +1053,27 @@ except BaseException as e:
let client = client.get_authed().await;
let mut reserved_variables = get_reserved_variables(job, &client.token, db).await?;
let additional_python_paths_folders = additional_python_paths.iter().join(":");
// Add /tmp/windmill/cache/python_xyz/global-site-packages to PYTHONPATH.
// Usefull if certain wheels needs to be preinstalled before execution.
let global_site_packages_path = py_version.to_cache_dir() + "/global-site-packages";
let additional_python_paths_folders = {
let mut paths= additional_python_paths.clone();
if std::fs::metadata(&global_site_packages_path).is_ok() {
// We want global_site_packages_path to be included in additonal_python_paths_folders, but
// we don't want it to be included in global_site_packages_path.
// The reason for this is that additional_python_paths_folders is used to fill PYTHONPATH env variable for jailed script
// When global_site_packages_path used to place mount point of wheels to the jail config.
// Since we handle mount of global_site_packages on our own, we don't want it to be mounted automatically.
// We do this because existence of every wheel in cache is mandatory and if it is not there and nsjail expects it, it is a bug.
// On the other side global_site_packages is purely optional.
// NOTE: This behaviour can be changed in future, so verification of wheels can be offloaded from nsjail to windmill
paths.insert(0, global_site_packages_path.clone());
// ^^^^^^^^
// We also want this be priorotized, that's why we insert it to the beginning
}
paths.iter().join(":")
};
#[cfg(windows)]
let additional_python_paths_folders = additional_python_paths_folders.replace(":", ";");
@@ -1076,6 +1103,7 @@ mount {{
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{SHARED_DEPENDENCIES}", shared_deps.as_str())
.replace("{GLOBAL_SITE_PACKAGES}", &global_site_packages_path)
.replace("{MAIN}", format!("{dirs}/{last}").as_str())
.replace(
"{ADDITIONAL_PYTHON_PATHS}",
@@ -1573,12 +1601,15 @@ async fn spawn_uv_install(
if let Some(url) = pip_index_url.as_ref() {
vars.push(("INDEX_URL", url));
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
vars.push(("PIP_INDEX_CERT", cert_path));
if let Some(cert_path) = INDEX_CERT.as_ref() {
vars.push(("SSL_CERT_FILE", cert_path));
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
if let Some(host) = TRUSTED_HOST.as_ref() {
vars.push(("TRUSTED_HOST", host));
}
if *NATIVE_CERT {
vars.push(("UV_NATIVE_TLS", "true"));
}
let _owner;
if let Some(py_path) = py_path.as_ref() {
_owner = format!(
@@ -1675,18 +1706,26 @@ async fn spawn_uv_install(
});
}
let mut envs = vec![("PATH", PATH_ENV.as_str())];
envs.push(("HOME", HOME_ENV.as_str()));
if let Some(url) = pip_index_url.as_ref() {
command_args.extend(["--index-url", url]);
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
command_args.extend(["--cert", cert_path]);
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
if let Some(host) = TRUSTED_HOST.as_ref() {
command_args.extend(["--trusted-host", &host]);
}
let mut envs = vec![("PATH", PATH_ENV.as_str())];
envs.push(("HOME", HOME_ENV.as_str()));
if *NATIVE_CERT {
command_args.extend(["--native-tls"]);
}
// TODO:
// Track https://github.com/astral-sh/uv/issues/6715
if let Some(cert_path) = INDEX_CERT.as_ref() {
// Once merged --cert can be used instead
//
// command_args.extend(["--cert", cert_path]);
envs.push(("SSL_CERT_FILE", cert_path));
}
tracing::debug!("uv pip install command: {:?}", command_args);
@@ -2159,6 +2198,18 @@ pub async fn handle_python_reqs(
db
).await;
pids.lock().await.get_mut(i).and_then(|e| e.take());
// Create a file to indicate that installation was successfull
let valid_path = venv_p.clone() + "/.valid.windmill";
// This is atomic operation, meaning, that it either completes and wheel is valid,
// or it does not and wheel is invalid and will be reinstalled next run
if let Err(e) = File::create(&valid_path).await{
tracing::error!(
workspace_id = %w_id,
job_id = %job_id,
"Failed to create {}!\n{e}\n
This file needed for python jobs to function", valid_path)
};
return Ok(());
}
}
+1 -1
View File
@@ -1095,7 +1095,7 @@ pub async fn run_worker(
#[cfg(feature = "benchmark")]
let mut infos = BenchmarkInfo::new();
let vacuum_shift = rand::thread_rng().gen_range(0..VACUUM_PERIOD);
let vacuum_shift = rand::rng().random_range(0..VACUUM_PERIOD);
IS_READY.store(true, Ordering::Relaxed);
tracing::info!(
@@ -62,6 +62,8 @@ use windmill_queue::{
type DB = sqlx::Pool<sqlx::Postgres>;
use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_queue::{canceled_job_to_result, push};
// #[instrument(level = "trace", skip_all)]
@@ -1894,6 +1896,12 @@ async fn push_next_flow_job(
))
&& suspend.continue_on_disapprove_timeout.unwrap_or(false);
let audit_author = AuditAuthor {
username: flow_job.permissioned_as.trim_start_matches("u/").to_string(),
email: flow_job.email.clone(),
username_override: None,
};
if can_be_resumed || disapproved_or_timeout_but_continue {
if disapproved_or_timeout_but_continue {
let js = if let Some(disapproved) = is_disapproved.as_ref() {
@@ -1903,7 +1911,18 @@ async fn push_next_flow_job(
};
resume_messages.push(to_raw_value(&js));
audit_log(
&mut *tx,
&audit_author,
"jobs.suspend_resume",
ActionKind::Update,
&flow_job.workspace_id,
Some(&serde_json::json!({"approved": false, "job_id": flow_job.id, "details": "Suspend timed out without approval but can continue".to_string()}).to_string()),
None,
)
.await?;
}
sqlx::query(
"UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'approvers'], $2)
@@ -1971,6 +1990,18 @@ async fn push_next_flow_job(
/* cancelled or we're WaitingForEvents but we don't have enough messages (timed out) */
} else {
if is_disapproved.is_none() {
audit_log(
&mut *tx,
&audit_author,
"jobs.suspend_resume",
ActionKind::Update,
&flow_job.workspace_id,
Some(&serde_json::json!({"approved": false, "job_id": flow_job.id, "details": "Suspend timed out without approval and is cancelled".to_string()}).to_string()),
None,
)
.await?;
}
tx.commit().await?;
let (logs, error_name) = if let Some(disapprover) = is_disapproved {
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.448.1";
export const VERSION = "v1.454.1";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -60,7 +60,7 @@ export {
// }
// });
export const VERSION = "1.448.1";
export const VERSION = "1.454.1";
const command = new Command()
.name("wmill")
+44 -44
View File
@@ -49,7 +49,7 @@ import { FlowFile, replaceInlineScripts } from "./flow.ts";
import { getIsWin } from "./main.ts";
import { FlowValue } from "./gen/types.gen.ts";
export async function generateAllMetadata() {}
export async function generateAllMetadata() { }
function findClosestRawReqs(
lang: "bun" | "python3" | "php" | undefined,
@@ -104,7 +104,8 @@ export async function generateFlowLockInternal(
folder: string,
dryRun: boolean,
workspace: Workspace,
justUpdateMetadataLock?: boolean
justUpdateMetadataLock?: boolean,
noStaleMessage?: boolean
): Promise<string | undefined> {
if (folder.endsWith(SEP)) {
folder = folder.substring(0, folder.length - 1);
@@ -112,7 +113,7 @@ export async function generateFlowLockInternal(
const remote_path = folder
.replaceAll(SEP, "/")
.substring(0, folder.length - ".flow".length);
if (!justUpdateMetadataLock) {
if (!justUpdateMetadataLock && !noStaleMessage) {
log.info(`Generating lock for flow ${folder} at ${remote_path}`);
}
@@ -120,9 +121,11 @@ export async function generateFlowLockInternal(
const conf = await readLockfile();
if (await checkifMetadataUptodate(folder, hashes[TOP_HASH], conf, TOP_HASH)) {
log.info(
colors.green(`Flow ${remote_path} metadata is up-to-date, skipping`)
);
if (!noStaleMessage) {
log.info(
colors.green(`Flow ${remote_path} metadata is up-to-date, skipping`)
);
}
return;
} else if (dryRun) {
return remote_path;
@@ -222,12 +225,7 @@ export async function generateScriptMetadataInternal(
// read script content
const scriptContent = await Deno.readTextFile(scriptPath);
let metadataContent = await Deno.readTextFile(metadataWithType.path);
const c = findCodebase(scriptPath, codebases);
if (c) {
metadataContent += c.digest ?? "";
}
const metadataContent = await Deno.readTextFile(metadataWithType.path);
let hash = await generateScriptHash(rawReqs, scriptContent, metadataContent);
@@ -261,7 +259,9 @@ export async function generateScriptMetadataInternal(
}
if (!opts.schemaOnly && !justUpdateMetadataLock) {
if (!c) {
const hasCodebase = findCodebase(scriptPath, codebases) != undefined;
if (!hasCodebase) {
await updateScriptLock(
workspace,
scriptContent,
@@ -270,16 +270,13 @@ export async function generateScriptMetadataInternal(
metadataParsedContent,
rawReqs
);
metadataParsedContent.codebase = undefined;
} else {
metadataParsedContent.codebase = c.digest;
metadataParsedContent.lock = "";
metadataParsedContent.lock = '';
}
} else {
metadataParsedContent.lock =
"!inline " + remotePath.replaceAll(SEP, "/") + ".script.lock";
}
let metaPath = remotePath + ".script.yaml";
let newMetadataContent = yamlStringify(metadataParsedContent, yamlOptions);
if (metadataWithType.isJson) {
@@ -287,10 +284,8 @@ export async function generateScriptMetadataInternal(
newMetadataContent = JSON.stringify(metadataParsedContent);
}
let metadataContentUsedForHash = newMetadataContent;
if (c) {
metadataContentUsedForHash += c.digest ?? "";
}
const metadataContentUsedForHash = newMetadataContent;
hash = await generateScriptHash(
rawReqs,
scriptContent,
@@ -318,9 +313,9 @@ export async function updateScriptSchema(
path
);
metadataContent.schema = result.schema;
if (result.has_preprocessor != null)
if (result.has_preprocessor == true)
metadataContent.has_preprocessor = result.has_preprocessor;
if (result.no_main_func != null)
if (result.no_main_func === true)
metadataContent.no_main_func = result.no_main_func;
}
@@ -390,7 +385,7 @@ async function updateScriptLock(
if (await Deno.stat(lockPath)) {
await Deno.remove(lockPath);
}
} catch {}
} catch { }
metadataContent.lock = "";
}
} catch (e) {
@@ -431,7 +426,7 @@ export async function updateFlow(
} catch (e) {
try {
responseText = await rawResponse.text();
} catch {}
} catch { }
throw new Error(
`Failed to generate lockfile. Status was: ${rawResponse.statusText}, ${responseText}, ${e}`
);
@@ -528,8 +523,11 @@ export function inferSchema(
};
}
if (!currentSchema) {
currentSchema = {}
}
currentSchema.required = [];
const oldProperties = JSON.parse(JSON.stringify(currentSchema.properties));
const oldProperties = JSON.parse(JSON.stringify(currentSchema?.properties ?? {}));
currentSchema.properties = {};
for (const arg of inferedSchema.args) {
@@ -576,23 +574,23 @@ export function argSigToJsonSchemaType(
| string
| { resource: string | null }
| {
list:
| (string | { object: { key: string; typ: any }[] })
| { str: any }
| { object: { key: string; typ: any }[] }
| null;
}
list:
| (string | { object: { key: string; typ: any }[] })
| { str: any }
| { object: { key: string; typ: any }[] }
| null;
}
| { dynselect: string }
| { str: string[] | null }
| { object: { key: string; typ: any }[] }
| {
oneof: [
{
label: string;
properties: { key: string; typ: any }[];
}
];
},
oneof: [
{
label: string;
properties: { key: string; typ: any }[];
}
];
},
oldS: SchemaProperty
): void {
const newS: SchemaProperty = { type: "" };
@@ -795,10 +793,10 @@ export async function parseMetadataFile(
scriptPath: string,
generateMetadataIfMissing:
| (GlobalOptions & {
path: string;
workspaceRemote: Workspace;
schemaOnly?: boolean;
})
path: string;
workspaceRemote: Workspace;
schemaOnly?: boolean;
})
| undefined,
globalDeps: GlobalDeps,
codebases: SyncCodebase[]
@@ -860,7 +858,9 @@ export async function parseMetadataFile(
scriptInitialMetadata = (await yamlParseFile(
metadataFilePath
)) as ScriptMetadata;
replaceLock(scriptInitialMetadata);
if (!generateMetadataIfMissing.schemaOnly) {
replaceLock(scriptInitialMetadata);
}
} catch (e) {
log.info(
colors.yellow(
+53 -42
View File
@@ -1,5 +1,5 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions, showDiff } from "./types.ts";
import { GlobalOptions } from "./types.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
colors,
@@ -24,7 +24,6 @@ import { Workspace } from "./workspace.ts";
import {
generateScriptMetadataInternal,
parseMetadataFile,
updateScriptSchema,
} from "./metadata.ts";
import {
ScriptLanguage,
@@ -34,6 +33,7 @@ import {
elementsToMap,
findCodebase,
readDirRecursiveWithIgnore,
Skips,
yamlOptions,
} from "./sync.ts";
import { ignoreF } from "./sync.ts";
@@ -119,7 +119,7 @@ export async function findResourceFile(path: string) {
if (validCandidates.length > 1) {
throw new Error(
"Found two resource files for the same resource" +
validCandidates.join(", ")
validCandidates.join(", ")
);
}
if (validCandidates.length < 1) {
@@ -162,7 +162,7 @@ export async function handleFile(
workspace: Workspace,
alreadySynced: string[],
message: string | undefined,
opts: (GlobalOptions & { defaultTs?: "bun" | "deno" }) | undefined,
opts: (GlobalOptions & { defaultTs?: "bun" | "deno" } & Skips) | undefined,
globalDeps: GlobalDeps,
codebases: SyncCodebase[]
): Promise<boolean> {
@@ -238,21 +238,22 @@ export async function handleFile(
}
log.info(`Finished building the bundle for ${path}`);
}
const typed = (
await parseMetadataFile(
remotePath,
opts
? {
let typed =
opts?.skipScriptsMetadata ? undefined :
(await parseMetadataFile(
remotePath,
opts
? {
...opts,
path,
workspaceRemote: workspace,
schemaOnly: codebase ? true : undefined,
}
: undefined,
globalDeps,
codebases
)
)?.payload;
: undefined,
globalDeps,
codebases
)
)?.payload;
const workspaceId = workspace.workspaceId;
@@ -268,20 +269,29 @@ export async function handleFile(
}
const content = await Deno.readTextFile(path);
if (codebase) {
const typedBefore = JSON.parse(JSON.stringify(typed.schema));
await updateScriptSchema(content, language, typed, path);
if (typedBefore != typed.schema) {
log.info(`Updated metadata for bundle ${path}`);
showDiff(
yamlStringify(typedBefore, yamlOptions),
yamlStringify(typed.schema, yamlOptions)
);
await Deno.writeTextFile(
remotePath + ".script.yaml",
yamlStringify(typed as Record<string, any>, yamlOptions)
);
}
if (opts?.skipScriptsMetadata) {
// if (codebase) {
// const typedBefore = JSON.parse(JSON.stringify(typed.schema));
// await updateScriptSchema(content, language, typed, path);
// if (typedBefore != typed.schema) {
// log.info(`Updated metadata for bundle ${path}`);
// showDiff(
// yamlStringify(typedBefore, yamlOptions),
// yamlStringify(typed.schema, yamlOptions)
// );
// await Deno.writeTextFile(
// remotePath + ".script.yaml",
// yamlStringify(typed as Record<string, any>, yamlOptions)
// );
// }
// }
// else {
typed = structuredClone(remote);
// }
}
if (typed && codebase) {
typed.codebase = codebase.digest;
}
const requestBodyCommon: NewScript = {
@@ -312,6 +322,8 @@ export async function handleFile(
on_behalf_of_email: typed?.on_behalf_of_email,
};
// log.info(JSON.stringify(requestBodyCommon, null, 2))
// log.info(JSON.stringify(opts, null, 2))
if (remote) {
if (content === remote.content) {
if (
@@ -327,19 +339,19 @@ export async function handleFile(
deepEqual(typed.schema, remote.schema) &&
typed.tag == remote.tag &&
(typed.ws_error_handler_muted ?? false) ==
remote.ws_error_handler_muted &&
remote.ws_error_handler_muted &&
typed.dedicated_worker == remote.dedicated_worker &&
typed.cache_ttl == remote.cache_ttl &&
typed.concurrency_time_window_s ==
remote.concurrency_time_window_s &&
remote.concurrency_time_window_s &&
typed.concurrent_limit == remote.concurrent_limit &&
Boolean(typed.restart_unless_cancelled) ==
Boolean(remote.restart_unless_cancelled) &&
Boolean(remote.restart_unless_cancelled) &&
Boolean(typed.visible_to_runner_only) ==
Boolean(remote.visible_to_runner_only) &&
Boolean(remote.visible_to_runner_only) &&
Boolean(typed.no_main_func) == Boolean(remote.no_main_func) &&
Boolean(typed.has_preprocessor) ==
Boolean(remote.has_preprocessor) &&
Boolean(remote.has_preprocessor) &&
typed.priority == Boolean(remote.priority) &&
typed.timeout == remote.timeout &&
//@ts-ignore
@@ -412,10 +424,9 @@ async function createScript(
workspace: workspaceId,
requestBody: body,
});
} catch (e) {
} catch (e: any) {
throw Error(
`Script creation for ${body.path} with parent ${
body.parent_hash
`Script creation for ${body.path} with parent ${body.parent_hash
} was not successful: ${e.body ?? e.message}`
);
}
@@ -441,8 +452,7 @@ async function createScript(
});
if (req.status != 201) {
throw Error(
`Script snapshot creation was not successful: ${req.status} - ${
req.statusText
`Script snapshot creation was not successful: ${req.status} - ${req.statusText
} - ${await req.text()}`
);
}
@@ -453,8 +463,8 @@ export async function findContentFile(filePath: string) {
const candidates = filePath.endsWith("script.json")
? exts.map((x) => filePath.replace(".script.json", x))
: filePath.endsWith("script.lock")
? exts.map((x) => filePath.replace(".script.lock", x))
: exts.map((x) => filePath.replace(".script.yaml", x));
? exts.map((x) => filePath.replace(".script.lock", x))
: exts.map((x) => filePath.replace(".script.yaml", x));
const validCandidates = (
await Promise.all(
@@ -473,7 +483,7 @@ export async function findContentFile(filePath: string) {
if (validCandidates.length > 1) {
throw new Error(
"No content path given and more than one candidate found: " +
validCandidates.join(", ")
validCandidates.join(", ")
);
}
if (validCandidates.length < 1) {
@@ -858,6 +868,7 @@ async function generateMetadata(
} & SyncOptions,
scriptPath: string | undefined
) {
log.info("This command only works for workspace scripts, for flows inline scripts use `wmill flow generate-locks`");
if (scriptPath == "") {
scriptPath = undefined;
}
@@ -976,7 +987,7 @@ const command = new Command()
.action(bootstrap as any)
.command(
"generate-metadata",
"re-generate the metadata file updating the lock and the script schema"
"re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate-locks`)"
)
.arguments("[script:file]")
.option("--yes", "Skip confirmation prompt")
+228 -120
View File
@@ -101,8 +101,9 @@ async function addCodebaseDigestIfRelevant(
return content;
}
let isTs = true;
const replacedPath = path.replace(".script.yaml", ".ts");
try {
await Deno.stat(path.replace(".script.yaml", ".ts"));
await Deno.stat(replacedPath);
} catch {
isTs = false;
}
@@ -110,12 +111,12 @@ async function addCodebaseDigestIfRelevant(
return content;
}
if (isTs) {
const c = findCodebase(path, codebases);
const c = findCodebase(replacedPath, codebases);
if (c) {
const parsed: any = yamlParseContent(path, content);
if (parsed && typeof parsed == "object") {
parsed["codebase"] = c.digest;
parsed["lock"] = undefined;
parsed["lock"] = '';
return yamlStringify(parsed, yamlOptions);
} else {
throw Error(
@@ -158,9 +159,8 @@ export async function FSFSElement(
// },
async getContentText(): Promise<string> {
const content = await Deno.readTextFile(localP);
const r = await addCodebaseDigestIfRelevant(localP, content, codebases);
// console.log(r);
const itemPath = localP.substring(p.length + 1)
const r = await addCodebaseDigestIfRelevant(itemPath, content, codebases);
return r;
},
};
@@ -352,12 +352,12 @@ function ZipFSElement(
)
? "flow"
: p.endsWith("app.json")
? "app"
: p.endsWith("script.json")
? "script"
: p.endsWith("resource.json")
? "resource"
: "other";
? "app"
: p.endsWith("script.json")
? "script"
: p.endsWith("resource.json")
? "resource"
: "other";
const isJson = p.endsWith(".json");
@@ -387,7 +387,7 @@ function ZipFSElement(
yield {
isDirectory: false,
path: path.join(finalPath, s.path),
async *getChildren() {},
async *getChildren() { },
// deno-lint-ignore require-await
async getContentText() {
return s.content;
@@ -398,7 +398,7 @@ function ZipFSElement(
yield {
isDirectory: false,
path: path.join(finalPath, "flow.yaml"),
async *getChildren() {},
async *getChildren() { },
// deno-lint-ignore require-await
async getContentText() {
return yamlStringify(flow, yamlOptions);
@@ -414,7 +414,7 @@ function ZipFSElement(
yield {
isDirectory: false,
path: path.join(finalPath, s.path),
async *getChildren() {},
async *getChildren() { },
// deno-lint-ignore require-await
async getContentText() {
return s.content;
@@ -425,7 +425,7 @@ function ZipFSElement(
yield {
isDirectory: false,
path: path.join(finalPath, "app.yaml"),
async *getChildren() {},
async *getChildren() { },
// deno-lint-ignore require-await
async getContentText() {
return yamlStringify(app, yamlOptions);
@@ -490,7 +490,7 @@ function ZipFSElement(
r.push({
isDirectory: false,
path: removeSuffix(finalPath, ".json") + ".lock",
async *getChildren() {},
async *getChildren() { },
// deno-lint-ignore require-await
async getContentText() {
return lock;
@@ -513,7 +513,7 @@ function ZipFSElement(
removeSuffix(finalPath, ".resource.json") +
".resource.file." +
formatExtension,
async *getChildren() {},
async *getChildren() { },
// deno-lint-ignore require-await
async getContentText() {
return fileContent;
@@ -574,19 +574,19 @@ export async function* readDirRecursiveWithIgnore(
// getContentBytes(): Promise<Uint8Array>;
getContentText(): Promise<string>;
}[] = [
{
path: root.path,
ignored: ignore(root.path, root.isDirectory),
isDirectory: root.isDirectory,
c: root.getChildren,
// getContentBytes(): Promise<Uint8Array> {
// throw undefined;
// },
getContentText(): Promise<string> {
throw undefined;
{
path: root.path,
ignored: ignore(root.path, root.isDirectory),
isDirectory: root.isDirectory,
c: root.getChildren,
// getContentBytes(): Promise<Uint8Array> {
// throw undefined;
// },
getContentText(): Promise<string> {
throw undefined;
},
},
},
];
];
while (stack.length > 0) {
const e = stack.pop()!;
@@ -606,7 +606,7 @@ export async function* readDirRecursiveWithIgnore(
type Added = { name: "added"; path: string; content: string };
type Deleted = { name: "deleted"; path: string };
type Edit = { name: "edited"; path: string; before: string; after: string };
type Edit = { name: "edited"; path: string; before: string; after: string; codebase?: string };
type Change = Added | Deleted | Edit;
@@ -649,7 +649,7 @@ export async function elementsToMap(
"js",
"lock",
"rs",
"cs",
"cs",
"yml",
].includes(path.split(".").pop() ?? "") &&
!isFileResource(path)
@@ -677,10 +677,11 @@ export async function elementsToMap(
return map;
}
interface Skips {
export interface Skips {
skipVariables?: boolean | undefined;
skipResources?: boolean | undefined;
skipSecrets?: boolean | undefined;
skipScriptsMetadata?: boolean | undefined;
includeSchedules?: boolean | undefined;
includeUsers?: boolean | undefined;
includeGroups?: boolean | undefined;
@@ -694,13 +695,14 @@ async function compareDynFSElement(
ignore: (path: string, isDirectory: boolean) => boolean,
json: boolean,
skips: Skips,
ignoreMetadataDeletion: boolean
ignoreMetadataDeletion: boolean,
codebases: SyncCodebase[]
): Promise<Change[]> {
const [m1, m2] = els2
? await Promise.all([
elementsToMap(els1, ignore, json, skips),
elementsToMap(els2, ignore, json, skips),
])
elementsToMap(els1, ignore, json, skips),
elementsToMap(els2, ignore, json, skips),
])
: [await elementsToMap(els1, ignore, json, skips), {}];
const changes: Change[] = [];
@@ -734,25 +736,81 @@ async function compareDynFSElement(
return yamlParseContent(k, v);
}
}
for (const [k, v] of Object.entries(m1)) {
const codebaseChanges: Record<string, string> = {};
for (let [k, v] of Object.entries(m1)) {
const isScriptMetadata = k.endsWith(".script.yaml") || k.endsWith(".script.json");
const skipMetadata = skips.skipScriptsMetadata && isScriptMetadata;
if (m2[k] === undefined) {
if (skipMetadata) {
continue;
}
changes.push({ name: "added", path: k, content: v });
} else if (
m2[k] != v &&
(!k.endsWith(".json") || !deepEqual(JSON.parse(v), JSON.parse(m2[k]))) &&
(!k.endsWith(".yaml") || !deepEqual(parseYaml(k, v), parseYaml(k, m2[k])))
) {
changes.push({ name: "edited", path: k, after: v, before: m2[k] });
} else {
if (m2[k] == v) {
continue;
}
else if (k.endsWith(".json")) {
if (deepEqual(JSON.parse(v), JSON.parse(m2[k]))) {
continue;
}
} else if (k.endsWith(".yaml")) {
const before = parseYaml(k, m2[k]);
const after = parseYaml(k, v);
if (deepEqual(before, after)) {
continue;
}
if (before.codebase != undefined) {
delete before.codebase;
m2[k] = yamlStringify(before, yamlOptions);
}
if (after.codebase != undefined) {
if (before.codebase != after.codebase) {
codebaseChanges[k] = after.codebase;
}
delete after.codebase;
v = yamlStringify(after, yamlOptions);
}
if (skipMetadata) {
continue;
}
}
changes.push({ name: "edited", path: k, after: v, before: m2[k], codebase: codebaseChanges[k] });
}
}
const remoteCodebase: Record<string, string> = {};
for (const [k] of Object.entries(m2)) {
if (
m1[k] === undefined &&
(!ignoreMetadataDeletion ||
(!k?.endsWith(".script.yaml") && !k?.endsWith(".script.json")))
) {
changes.push({ name: "deleted", path: k });
if (m1[k] === undefined) {
if (!ignoreMetadataDeletion || (!k?.endsWith(".script.yaml") && !k?.endsWith(".script.json"))) {
changes.push({ name: "deleted", path: k });
} else if (k?.endsWith(".script.yaml")) {
let o = parseYaml(k, m2[k]);
if (o.codebase != undefined) {
remoteCodebase[k] = o.codebase;
}
}
}
}
for (const [k, v] of Object.entries(remoteCodebase)) {
const tsFile = k.replace(".script.yaml", ".ts");
if (changes.find(c => c.path == tsFile && (c.name == "edited" || c.name == "deleted"))) {
continue;
}
let c = findCodebase(tsFile, codebases);
if (c?.digest != v) {
changes.push({ name: "edited", path: tsFile, codebase: v, before: m1[tsFile], after: m2[tsFile] });
}
}
for (const change of changes) {
const codebase = codebaseChanges[change.path];
if (!codebase) continue;
const tsFile = change.path.replace(".script.yaml", ".ts");
if (change.name == "edited" && change.path == tsFile) {
change.codebase = codebase;
}
}
@@ -887,6 +945,58 @@ export async function ignoreF(wmillconf: {
};
}
interface ChangeTracker {
scripts: string[];
flows: string[];
apps: string[];
}
// deno-lint-ignore no-inner-declarations
async function addToChangedIfNotExists(p: string, tracker: ChangeTracker) {
const isScript = exts.some((e) => p.endsWith(e));
if (isScript) {
if (p.includes(".flow" + SEP)) {
const folder =
p.substring(0, p.indexOf(".flow" + SEP)) + ".flow" + SEP;
if (!tracker.flows.includes(folder)) {
tracker.flows.push(folder);
}
} else if (p.includes(".app" + SEP)) {
const folder = p.substring(0, p.indexOf(".app" + SEP)) + ".app" + SEP;
if (!tracker.apps.includes(folder)) {
tracker.apps.push(folder);
}
} else {
if (!tracker.scripts.includes(p)) {
tracker.scripts.push(p);
}
}
} else if (p.endsWith(".script.yaml") || p.endsWith(".script.json")) {
try {
const contentPath = await findContentFile(p);
if (!contentPath) return;
if (tracker.scripts.includes(contentPath)) return;
tracker.scripts.push(contentPath);
} catch {
// ignore
}
}
}
async function buildTracker(changes: Change[]) {
const tracker: ChangeTracker = {
scripts: [],
flows: [],
apps: [],
};
for (const change of changes) {
if (change.name == "added" || change.name == "edited") {
await addToChangedIfNotExists(change.path, tracker);
}
}
return tracker;
}
export async function pull(opts: GlobalOptions & SyncOptions) {
opts = await mergeConfigWithConfigFile(opts);
@@ -940,7 +1050,8 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
await ignoreF(opts),
opts.json ?? false,
opts,
false
false,
codebases
);
log.info(
@@ -959,41 +1070,8 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
}
const conflicts = [];
const changedScripts: string[] = [];
const changedFlows: string[] = [];
const changedApps: string[] = [];
// deno-lint-ignore no-inner-declarations
async function addToChangedIfNotExists(p: string) {
const isScript = exts.some((e) => p.endsWith(e));
if (isScript) {
if (p.includes(".flow" + SEP)) {
const folder =
p.substring(0, p.indexOf(".flow" + SEP)) + ".flow" + SEP;
if (!changedFlows.includes(folder)) {
changedFlows.push(folder);
}
} else if (p.includes(".app" + SEP)) {
const folder = p.substring(0, p.indexOf(".app" + SEP)) + ".app" + SEP;
if (!changedApps.includes(folder)) {
changedApps.push(folder);
}
} else {
if (!changedScripts.includes(p)) {
changedScripts.push(p);
}
}
} else if (p.endsWith(".script.yaml") || p.endsWith(".script.json")) {
try {
const contentPath = await findContentFile(p);
if (!contentPath) return;
if (changedScripts.includes(contentPath)) return;
changedScripts.push(contentPath);
} catch {
// ignore
}
}
}
log.info(colors.gray(`Applying changes to files ...`));
for await (const change of changes) {
@@ -1054,7 +1132,6 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
await ensureDir(path.dirname(stateTarget));
await Deno.copyFile(target, stateTarget);
}
await addToChangedIfNotExists(change.path);
} else if (change.name === "added") {
await ensureDir(path.dirname(target));
if (opts.stateful) {
@@ -1066,7 +1143,6 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
if (opts.stateful) {
await Deno.copyFile(target, stateTarget);
}
await addToChangedIfNotExists(change.path);
} else if (change.name === "deleted") {
try {
log.info(
@@ -1103,7 +1179,9 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
const globalDeps = await findGlobalDeps();
for (const change of changedScripts) {
const tracker: ChangeTracker = await buildTracker(changes);
for (const change of tracker.scripts) {
await generateScriptMetadataInternal(
change,
workspace,
@@ -1115,13 +1193,13 @@ export async function pull(opts: GlobalOptions & SyncOptions) {
true
);
}
for (const change of changedFlows) {
for (const change of tracker.flows) {
log.info(`Updating lock for flow ${change}`);
await generateFlowLockInternal(change, false, workspace, true);
}
if (changedApps.length > 0) {
if (tracker.apps.length > 0) {
log.info(
`Apps ${changedApps.join(
`Apps ${tracker.apps.join(
", "
)} scripts were changed but ignoring for now`
);
@@ -1146,9 +1224,11 @@ function prettyChanges(changes: Change[]) {
);
} else if (change.name === "edited") {
log.info(
colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path)
colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path + (change.codebase ? ` (codebase changed)` : ""))
);
showDiff(change.before, change.after);
if (change.before != change.after) {
showDiff(change.before, change.after);
}
}
}
}
@@ -1238,9 +1318,58 @@ export async function push(opts: GlobalOptions & SyncOptions) {
await ignoreF(opts),
opts.json ?? false,
opts,
true
true,
codebases
);
const globalDeps = await findGlobalDeps();
const tracker: ChangeTracker = await buildTracker(changes);
const staleScripts: string[] = [];
const staleFlows: string[] = [];
for (const change of tracker.scripts) {
const stale = await generateScriptMetadataInternal(
change,
workspace,
opts,
true,
true,
globalDeps,
codebases,
false
);
if (stale) {
staleScripts.push(stale);
}
}
if (staleScripts.length > 0) {
log.info("")
log.warn("Stale scripts metadata found, you may want to update them using 'wmill script generate-metadata' before pushing:");
for (const stale of staleScripts) {
log.warn(stale);
}
log.info("")
}
for (const change of tracker.flows) {
const stale = await generateFlowLockInternal(change, true, workspace, false, true);
if (stale) {
staleFlows.push(stale);
}
}
if (staleFlows.length > 0) {
log.warn("Stale flows locks found, you may want to update them using 'wmill flow generate-locks' before pushing:");
for (const stale of staleFlows) {
log.warn(stale);
}
log.info("")
}
const version = await fetchVersion(workspace.remote);
log.info(colors.gray("Remote version: " + version));
@@ -1264,7 +1393,9 @@ export async function push(opts: GlobalOptions & SyncOptions) {
log.info(colors.gray(`Applying changes to files ...`));
const alreadySynced: string[] = [];
const globalDeps = await findGlobalDeps();
for await (const change of changes) {
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
@@ -1512,24 +1643,13 @@ const command = new Command()
)
.command("pull")
.description("Pull any remote changes and apply them locally.")
.option(
"--fail-conflicts",
"Error on conflicts (both remote and local have changes on the same item)"
)
.option(
"--raw",
"Push without using state, just overwrite. (Default, has no effect)"
)
.option("--yes", "Pull without needing confirmation")
.option(
"--stateful",
"Pull using state tracking (create .wmill folder and needed for --fail-conflicts)"
)
.option("--plain-secrets", "Pull secrets as plain text")
.option("--json", "Use JSON instead of YAML")
.option("--skip-variables", "Skip syncing variables (including secrets)")
.option("--skip-secrets", "Skip syncing only secrets variables")
.option("--skip-resources", "Skip syncing resources")
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
.option("--include-schedules", "Include syncing schedules")
.option("--include-users", "Include syncing users")
.option("--include-groups", "Include syncing groups")
@@ -1551,25 +1671,13 @@ const command = new Command()
.action(pull as any)
.command("push")
.description("Push any local changes and apply them remotely.")
.option(
"--fail-conflicts",
"Error on conflicts (both remote and local have changes on the same item)"
)
.option(
"--raw",
"Push without using state, just overwrite. (Default, has no effect)"
)
.option(
"--stateful",
"Pull using state tracking (use .wmill folder and needed for --fail-conflicts)w"
)
.option("--skip-pull", "(stateful only) Push without pulling first")
.option("--yes", "Push without needing confirmation")
.option("--plain-secrets", "Push secrets as plain text")
.option("--json", "Use JSON instead of YAML")
.option("--skip-variables", "Skip syncing variables (including secrets)")
.option("--skip-secrets", "Skip syncing only secrets variables")
.option("--skip-resources", "Skip syncing resources")
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
.option("--include-schedules", "Include syncing schedules")
.option("--include-users", "Include syncing users")
.option("--include-groups", "Include syncing groups")
+4 -3
View File
@@ -79,7 +79,8 @@ export function showDiff(local: string, remote: string) {
log.info("Diff too large to display");
return;
}
for (const part of Diff.diffLines(local, remote)) {
for (const part of Diff.diffLines(local ?? '', remote ?? '')) {
if (part.removed) {
// print red if removed without newline
finalString += `\x1b[31m${part.value}\x1b[0m`;
@@ -157,8 +158,8 @@ export function parseFromPath(p: string, content: string): any {
return p.endsWith(".yaml")
? yamlParseContent(p, content)
: p.endsWith(".json")
? JSON.parse(content)
: content;
? JSON.parse(content)
: content;
}
export function parseFromFile(p: string): any {
if (p.endsWith(".json")) {
-2
View File
@@ -1,2 +0,0 @@
windmill-api/
node_modules
-5
View File
@@ -1,5 +0,0 @@
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": true
}
-7
View File
@@ -1,7 +0,0 @@
# windmill-deno-client
Deno client for Windmill
## Howto build locally
run `./build.sh`
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
set -e
npm ci --ignore-scripts
npx --yes openapi-typescript-codegen --input ../backend/windmill-api/openapi.yaml \
--output ./src --useOptions \
&& sed -i '213 i \\ request.referrerPolicy = \"no-referrer\"\n' src/core/request.ts
npx --yes denoify
rm -rf windmill-api
mv deno_dist windmill-api
sed -i 's/buffer DENOIFY: UNKNOWN NODE BUILTIN/node:buffer/' ./windmill-api/core/request.ts
rm -rf src/
-319
View File
@@ -1,319 +0,0 @@
import {
JobService,
ResourceService,
VariableService,
} from "./windmill-api/index.ts";
import { OpenAPI } from "./windmill-api/index.ts";
export {
AdminService,
AuditService,
FlowService,
GranularAclService,
GroupService,
JobService,
ResourceService,
VariableService,
ScriptService,
ScheduleService,
SettingsService,
UserService,
WorkspaceService,
} from "./windmill-api/index.ts";
export type Sql = string;
export type Email = string;
export type Base64 = string;
export type Resource<S extends string> = any;
export const SHARED_FOLDER = "/shared";
export function setClient(token: string, baseUrl: string) {
OpenAPI.WITH_CREDENTIALS = true;
OpenAPI.TOKEN = token;
OpenAPI.BASE = baseUrl + "/api";
}
setClient(
Deno.env.get("WM_TOKEN") ?? "no_token",
Deno.env.get("BASE_INTERNAL_URL") ??
Deno.env.get("BASE_URL") ??
"http://localhost:8000"
);
/**
* Create a client configuration from env variables
* @returns client configuration
*/
export function getWorkspace(): string {
return Deno.env.get("WM_WORKSPACE") ?? "no_workspace";
}
/**
* Get a resource value by path
* @param path path of the resource, default to internal state path
* @param undefinedIfEmpty if the resource does not exist, return undefined instead of throwing an error
* @returns resource value
*/
export async function getResource(
path?: string,
undefinedIfEmpty?: boolean
): Promise<any> {
const workspace = getWorkspace();
path = path ?? getStatePath();
try {
return await ResourceService.getResourceValueInterpolated({
workspace,
path,
});
} catch (e: any) {
if (undefinedIfEmpty && e.status === 404) {
return undefined;
} else {
throw Error(`Resource not found at ${path} or not visible to you`);
}
}
}
/**
* Resolve a resource value in case the default value was picked because the input payload was undefined
* @param obj resource value or path of the resource under the format `$res:path`
* @returns resource value
*/
export async function resolveDefaultResource(obj: any): Promise<any> {
if (typeof obj === "string" && obj.startsWith("$res:")) {
return await getResource(obj.substring(5), true);
} else {
return obj;
}
}
export function getStatePath(): string {
const state_path = Deno.env.get("WM_STATE_PATH_NEW") ?? Deno.env.get("WM_STATE_PATH");
if (state_path === undefined) {
throw Error("State path not set");
}
return state_path;
}
/**
* Set a resource value by path
* @param value new value of the resource to set
* @param path path of the resource to set, default to state path
* @param initializeToTypeIfNotExist if the resource does not exist, initialize it with this type
*/
export async function setResource(
value: any,
path?: string,
initializeToTypeIfNotExist?: string
): Promise<void> {
path = path ?? getStatePath();
const workspace = getWorkspace();
if (await ResourceService.existsResource({ workspace, path })) {
await ResourceService.updateResourceValue({
workspace,
path,
requestBody: { value },
});
} else if (initializeToTypeIfNotExist) {
await ResourceService.createResource({
workspace,
requestBody: { path, value, resource_type: initializeToTypeIfNotExist },
});
} else {
throw Error(
`Resource at path ${path} does not exist and no type was provided to initialize it`
);
}
}
/**
* Set the state
* @param state state to set
* @deprecated use setState instead
*/
export async function setInternalState(state: any): Promise<void> {
await setResource(state, undefined, "state");
}
/**
* Set the state
* @param state state to set
*/
export async function setState(state: any): Promise<void> {
await setResource(state, undefined, "state");
}
/**
* Set the shared state
* @param state state to set
*/
export async function setSharedState(
state: any,
path = "state.json"
): Promise<void> {
await Deno.writeTextFile(SHARED_FOLDER + "/" + path, JSON.stringify(state));
}
/**
* Get the shared state
* @param state state to set
*/
export async function getSharedState(path = "state.json"): Promise<any> {
return JSON.parse(await Deno.readTextFile(SHARED_FOLDER + "/" + path));
}
/**
* Get the internal state
* @deprecated use getState instead
*/
export async function getInternalState(): Promise<any> {
return await getResource(getStatePath(), true);
}
/**
* Get the state shared across executions
*/
export async function getState(): Promise<any> {
return await getResource(getStatePath(), true);
}
/**
* Get a variable by path
* @param path path of the variable
* @returns variable value
*/
export async function getVariable(path: string): Promise<string | undefined> {
const workspace = getWorkspace();
try {
return await VariableService.getVariableValue({ workspace, path });
} catch (e: any) {
throw Error(`Variable not found at ${path} or not visible to you`);
}
}
/**
* Set a variable by path, create if not exist
* @param path path of the variable
* @param value value of the variable
* @param isSecretIfNotExist if the variable does not exist, create it as secret or not (default: false)
* @param descriptionIfNotExist if the variable does not exist, create it with this description (default: "")
*/
export async function setVariable(
path: string,
value: string,
isSecretIfNotExist?: boolean,
descriptionIfNotExist?: string
): Promise<void> {
const workspace = getWorkspace();
if (await VariableService.existsVariable({ workspace, path })) {
await VariableService.updateVariable({
workspace,
path,
requestBody: { value },
});
} else {
await VariableService.createVariable({
workspace,
requestBody: {
path,
value,
is_secret: isSecretIfNotExist ?? false,
description: descriptionIfNotExist ?? "",
},
});
}
}
export async function databaseUrlFromResource(path: string): Promise<string> {
const resource = await getResource(path);
return `postgresql://${resource.user}:${resource.password}@${resource.host}:${resource.port}/${resource.dbname}?sslmode=${resource.sslmode}`;
}
/**
* Get URLs needed for resuming a flow after this step
* @param approver approver name
* @returns approval page UI URL, resume and cancel API URLs for resumeing the flow
*/
export async function getResumeUrls(approver?: string): Promise<{
approvalPage: string;
resume: string;
cancel: string;
}> {
const nonce = Math.floor(Math.random() * 4294967295);
const workspace = getWorkspace();
return await JobService.getResumeUrls({
workspace,
resumeId: nonce,
approver,
id: Deno.env.get("WM_JOB_ID") ?? "NO_JOB_ID",
});
}
/**
* @deprecated use getResumeUrls instead
*/
export function getResumeEndpoints(approver?: string): Promise<{
approvalPage: string;
resume: string;
cancel: string;
}> {
return getResumeUrls(approver);
}
export function base64ToUint8Array(data: string): Uint8Array {
return Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
}
export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string {
let base64 = "";
const encodings =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const bytes = new Uint8Array(arrayBuffer);
const byteLength = bytes.byteLength;
const byteRemainder = byteLength % 3;
const mainLength = byteLength - byteRemainder;
let a, b, c, d;
let chunk;
// Main loop deals with bytes in chunks of 3
for (let i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder == 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + "==";
} else if (byteRemainder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + "=";
}
return base64;
}
-1512
View File
File diff suppressed because it is too large Load Diff
-12
View File
@@ -1,12 +0,0 @@
{
"denoify": {
"index": "index.ts"
},
"dependencies": {
"denoify": "^1.3.1",
"openapi-typescript-codegen": "^0.23.0"
},
"overrides": {
"evt": "2.4.6"
}
}
-15
View File
@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"module": "ES6",
"lib": ["es7", "es6", "dom"],
"declaration": true,
"outDir": "dist",
"strict": true,
"esModuleInterop": true
},
"exclude": [
"node_modules",
"dist"
]
}
+2
View File
@@ -159,6 +159,7 @@ services:
# Configure the mounted Caddyfile and the exposed ports or use another reverse proxy if needed
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
# - ./certs:/certs # Provide custom certificate files like cert.pem and key.pem to enable HTTPS - See the corresponding section in the Caddyfile
ports:
# To change the exposed port, simply change 80:80 to <desired_port>:80. No other changes needed
@@ -176,3 +177,4 @@ volumes:
worker_logs: null
windmill_index: null
lsp_cache: null
caddy_data: null
+21 -1
View File
@@ -1,3 +1,22 @@
FROM alpine:3.14 AS oracledb-client
ARG TARGETPLATFORM
# Oracle DB Client for amd64
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/amd64/lib
# Oracle DB Client for arm64
RUN mkdir -p /opt/oracle/23/arm64 \
&& cd /opt/oracle/23/arm64 \
&& wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linux-arm64.zip \
&& unzip instantclient-basiclite-linux-arm64.zip && rm instantclient-basiclite-linux-arm64.zip && mv instantclient* ./lib
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
mv /opt/oracle/23/arm64/lib /opt/oracle/23/lib; \
else \
mv /opt/oracle/23/amd64/lib /opt/oracle/23/lib; \
fi
FROM ghcr.io/windmill-labs/windmill-ee:dev
COPY --from=rust:1.81.0 /usr/local/cargo /usr/local/cargo
@@ -5,11 +24,12 @@ COPY --from=rust:1.81.0 /usr/local/rustup /usr/local/rustup
RUN pip3 install ansible
# dotnet SDK
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
ENV DOTNET_ROOT="/opt/dotnet-sdk/bin"
# Oracle DB Client
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/lib
COPY --from=oracledb-client /opt/oracle/23/lib /opt/oracle/23/lib
RUN apt-get -y update && apt-get install -y libaio1
RUN echo /opt/oracle/23/lib > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
+21 -1
View File
@@ -21,6 +21,25 @@ RUN apt-get -y update \
RUN git clone -b master --single-branch https://github.com/google/nsjail.git . && git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800
RUN make
FROM alpine:3.14 AS oracledb-client
ARG TARGETPLATFORM
# Oracle DB Client for amd64
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/amd64/lib
# Oracle DB Client for arm64
RUN mkdir -p /opt/oracle/23/arm64 \
&& cd /opt/oracle/23/arm64 \
&& wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linux-arm64.zip \
&& unzip instantclient-basiclite-linux-arm64.zip && rm instantclient-basiclite-linux-arm64.zip && mv instantclient* ./lib
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
mv /opt/oracle/23/arm64/lib /opt/oracle/23/lib; \
else \
mv /opt/oracle/23/amd64/lib /opt/oracle/23/lib; \
fi
FROM ghcr.io/windmill-labs/windmill-ee:dev
RUN apt-get update && apt-get install -y libprotobuf-dev libnl-route-3-dev
@@ -30,12 +49,13 @@ COPY --from=rust:1.80.1 /usr/local/rustup /usr/local/rustup
RUN pip3 install ansible
# dotnet SDK
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
ENV DOTNET_ROOT="/opt/dotnet-sdk/bin"
# Oracle DB Client
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/lib
COPY --from=oracledb-client /opt/oracle/23/lib /opt/oracle/23/lib
RUN apt-get -y update && apt-get install -y libaio1
RUN echo /opt/oracle/23/lib > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
+6 -2
View File
@@ -10,6 +10,7 @@
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ (import rust-overlay) ];
};
rust = pkgs.rust-bin.stable.latest.default.override {
@@ -49,7 +50,7 @@
devShell = pkgs.mkShell {
buildInputs = buildInputs ++ (with pkgs; [
git xcaddy sqlx-cli flock sccache
deno python3 python3Packages.pip go bun uv
deno python3 python3Packages.pip go bun uv dotnet-sdk_9 oracle-instantclient
]);
packages = [
(pkgs.writeScriptBin "wm-caddy" ''
@@ -102,12 +103,15 @@
UV_PATH = "${pkgs.uv}/bin/uv";
FLOCK_PATH = "${pkgs.flock}/bin/flock";
CARGO_PATH = "${rust}/bin/cargo";
DOTNET_PATH = "${pkgs.dotnet-sdk_9}/bin/dotnet";
DOTNET_ROOT = "${pkgs.dotnet-sdk_9}/share/dotnet";
ORACLE_LIB_DIR = "${pkgs.oracle-instantclient.lib}/lib";
};
packages.default = self.packages.${system}.windmill;
packages.windmill-client = pkgs.buildNpmPackage {
name = "windmill-client";
version = (pkgs.lib.strings.trim (builtins.readFile ./version.txt));
src = pkgs.nix-gitignore.gitignoreSource [] ./frontend;
nativeBuildInputs = with pkgs; [ pkg-config ];
buildInputs = with pkgs; [ nodejs pixman cairo pango ];
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.448.1",
"version": "1.454.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.448.1",
"version": "1.454.1",
"license": "AGPL-3.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.448.1",
"version": "1.454.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -4,12 +4,13 @@
import { base } from '$lib/base'
import { emptySchema, emptyString } from '$lib/utils'
import SchemaForm from './SchemaForm.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import type SimpleEditor from './SimpleEditor.svelte'
import Toggle from './Toggle.svelte'
import TestConnection from './TestConnection.svelte'
import SupabaseIcon from './icons/SupabaseIcon.svelte'
import Popup from './common/popup/Popup.svelte'
import Button from './common/button/Button.svelte'
import { Loader2 } from 'lucide-svelte'
export let resourceType: string
export let resourceTypeInfo: ResourceType | undefined
@@ -195,13 +196,17 @@
>{error}</span
>{:else}<div class="py-2" />{/if}
<div class="h-full w-full border p-1 rounded">
<SimpleEditor
bind:this={rawCodeEditor}
autoHeight
lang="json"
bind:code={rawCode}
fixedOverflowWidgets={false}
/>
{#await import('$lib/components/SimpleEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:this={rawCodeEditor}
autoHeight
lang="json"
bind:code={rawCode}
fixedOverflowWidgets={false}
/>
{/await}
</div>
{:else if resourceTypeInfo?.format_extension}
<h5 class="mt-4 inline-flex items-center gap-4">
@@ -209,13 +214,17 @@
</h5>
<div class="py-2" />
<div class="h-full w-full border p-1 rounded">
<SimpleEditor
bind:this={rawCodeEditor}
autoHeight
lang={resourceTypeInfo.format_extension}
bind:code={textFileContent}
fixedOverflowWidgets={false}
/>
{#await import('$lib/components/SimpleEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:this={rawCodeEditor}
autoHeight
lang={resourceTypeInfo.format_extension}
bind:code={textFileContent}
fixedOverflowWidgets={false}
/>
{/await}
</div>
{:else}
<SchemaForm
@@ -174,13 +174,21 @@
}
function popupListener(event) {
console.log('Received oauth popup message')
let data = event.data
if (event.origin == null || event.origin !== window.location.origin) {
console.log(
'Received oauth popup message from different origin',
event.origin,
window.location.origin
)
return
}
window.removeEventListener('message', popupListener)
processPopupData(data)
if (data.type == 'success' || data.type == 'error') {
window.removeEventListener('message', popupListener)
processPopupData(data)
}
}
function handleStorageEvent(event) {
@@ -205,6 +213,7 @@
})
function processPopupData(data) {
console.log('Processing oauth popup data')
if (data.type === 'error') {
sendUserToast(data.error, true)
step = 2
+122 -74
View File
@@ -6,18 +6,17 @@
emptyString,
getSchemaFromProperties
} from '$lib/utils'
import { DollarSign, Pipette, Plus, X, Check } from 'lucide-svelte'
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
import { createEventDispatcher, onMount, tick } from 'svelte'
import Multiselect from 'svelte-multiselect'
import { fade } from 'svelte/transition'
import JsonEditor from './apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
import { Button, SecondsInput } from './common'
import FieldHeader from './FieldHeader.svelte'
import type ItemPicker from './ItemPicker.svelte'
import ObjectResourceInput from './ObjectResourceInput.svelte'
import Range from './Range.svelte'
import ResourcePicker from './ResourcePicker.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import type SimpleEditor from './SimpleEditor.svelte'
import Toggle from './Toggle.svelte'
import type VariableEditor from './VariableEditor.svelte'
import { twMerge } from 'tailwind-merge'
@@ -38,6 +37,7 @@
import DynSelect from './DynSelect.svelte'
import type { Script } from '$lib/gen'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
import type { ComponentCustomCSS } from './apps/types'
export let label: string = ''
export let value: any
@@ -101,6 +101,20 @@
export let hideNested = false
export let nestedParent: { label: string; nestedParent: any | undefined } | undefined = undefined
export let nestedClasses = ''
export let displayType = true
export let css: ComponentCustomCSS<'schemaformcomponent'> | undefined = undefined
export let appPath: string | undefined = undefined
export let computeS3ForceViewerPolicies:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined = undefined
export let workspace: string | undefined = undefined
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
@@ -354,8 +368,9 @@
<!-- svelte-ignore a11y-autofocus -->
<div
class={twMerge(
'flex flex-col w-full rounded-md px-2 relative',
'flex flex-col w-full rounded-md relative',
minW ? 'min-w-[250px]' : '',
diffStatus?.diff ? 'px-2' : '',
diffStatus?.diff == 'added'
? 'bg-green-300 dark:bg-green-800'
: diffStatus?.diff === 'removed'
@@ -366,11 +381,11 @@
? 'border-2 border-green-500 bg-surface'
: ''
)}
data-schema-picker
>
{#if diffStatus && typeof diffStatus === 'object' && diffStatus.diff !== 'same'}
<div
class="absolute top-0 right-2 rounded-md rounded-t-none flex flex-row overflow-hidden bg-surface"
data-schema-picker
>
<button
class="p-1 bg-green-500 text-white hover:bg-green-600"
@@ -401,11 +416,13 @@
{format}
{simpleTooltip}
{lightHeader}
{displayType}
labelClass={css?.label?.class}
/>
{/if}
{#if description}
<div class="text-xs italic pb-1 text-secondary">
<div class={twMerge('text-xs italic pb-1 text-secondary', css?.description?.class)}>
<pre class="font-main whitespace-normal">{description}</pre>
</div>
{/if}
@@ -511,7 +528,11 @@
multiple={false}
/>
{:else if itemsType?.type == 'object' && itemsType?.resourceType === undefined && itemsType?.properties === undefined}
<JsonEditor code={JSON.stringify(v, null, 2)} bind:value={v} />
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default code={JSON.stringify(v, null, 2)} bind:value={v} />
{/await}
{:else if Array.isArray(itemsType?.enum)}
<ArgEnum
required
@@ -538,17 +559,21 @@
defaultValue={undefined}
/>
{:else if itemsType?.type == 'resource'}
<JsonEditor
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={JSON.stringify(v, null, 2)}
bind:value={v}
/>
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={JSON.stringify(v, null, 2)}
bind:value={v}
/>
{/await}
{:else if itemsType?.type === 'object' && itemsType?.properties}
<div class="p-8 border rounded-md w-full">
<SchemaForm
@@ -669,17 +694,21 @@
options={{ left: 'Raw S3 object input' }}
/>
{#if s3FileUploadRawMode}
<JsonEditor
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={JSON.stringify(value ?? defaultValue ?? { s3: '' }, null, 2)}
bind:value
/>
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={JSON.stringify(value ?? defaultValue ?? { s3: '' }, null, 2)}
bind:value
/>
{/await}
<Button
variant="border"
color="light"
@@ -694,6 +723,9 @@
</Button>
{:else}
<FileUpload
{appPath}
computeForceViewerPolicies={computeS3ForceViewerPolicies}
{workspace}
allowMultiple={false}
randomFileKey={true}
on:addition={(evt) => {
@@ -787,33 +819,41 @@
{:else if disabled}
<textarea disabled />
{:else}
<JsonEditor
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={rawValue}
bind:value
/>
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={rawValue}
bind:value
/>
{/await}
{/if}
{/if}
{:else if disabled}
<textarea disabled />
{:else}
<JsonEditor
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={rawValue}
bind:value
/>
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={rawValue}
bind:value
/>
{/await}
{/if}
</div>
{:else if properties && Object.keys(properties).length > 0 && inputCat !== 'list'}
@@ -884,17 +924,21 @@
{:else if disabled}
<textarea disabled />
{:else}
<JsonEditor
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={rawValue}
bind:value
/>
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={rawValue}
bind:value
/>
{/await}
{/if}
{#if inputCat == 'list'}
<div class="block">
@@ -937,18 +981,22 @@
{/if}
{:else if inputCat == 'sql' || inputCat == 'yaml'}
<div class="border my-1 mb-4 w-full border-primary">
<SimpleEditor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
bind:this={editor}
lang={inputCat}
bind:code={value}
autoHeight
/>
{#await import('$lib/components/SimpleEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
bind:this={editor}
lang={inputCat}
bind:code={value}
autoHeight
/>
{/await}
</div>
{:else if inputCat == 'base64'}
<div class="flex flex-col my-6 w-full">
@@ -1,18 +0,0 @@
<script lang="ts">
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
export let flowPath: string | null = null
export let headless: boolean = true
export let addButton: boolean = false
</script>
<div class="h-full">
<CaptureTable
path={flowPath ?? ''}
on:select
isFlow={true}
{headless}
{addButton}
on:openTriggers
/>
</div>
@@ -7,7 +7,6 @@
import { sendUserToast } from '$lib/utils'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import GraphqlSchemaViewer from './GraphqlSchemaViewer.svelte'
import { Loader2, RefreshCcw } from 'lucide-svelte'
import {
formatGraphqlSchema,
@@ -93,7 +92,11 @@
</ToggleButtonGroup>
{/if}
{#if dbSchema.lang === 'graphql'}
<GraphqlSchemaViewer code={formatGraphqlSchema(dbSchema.schema)} class="h-full" />
{#await import('$lib/components/GraphqlSchemaViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default code={formatGraphqlSchema(dbSchema.schema)} class="h-full" />
{/await}
{:else}
<ObjectViewer json={formatSchema(dbSchema)} pureViewer collapseLevel={1} />
{/if}
+6 -2
View File
@@ -6,7 +6,7 @@
import DarkModeObserver from './DarkModeObserver.svelte'
import ResultJobLoader from './ResultJobLoader.svelte'
import Tooltip from './Tooltip.svelte'
import JsonEditor from './apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
import { Loader2 } from 'lucide-svelte'
export let value: any = undefined
export let helperScript:
@@ -149,6 +149,10 @@
<div class="text-xs text-tertiary"
>Dynamic Select is not available in this mode, write value directly</div
>
<JsonEditor code={rawCode} bind:value />
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default code={rawCode} bind:value />
{/await}
</div>
{/if}
@@ -35,7 +35,6 @@
export let noPreview: boolean = false
export let jsonEnabled: boolean = true
export let isAppInput: boolean = false
export let lightweightMode: boolean = false
export let displayWebhookWarning: boolean = false
export let dndType: string | undefined = undefined
export let editTab:
@@ -52,6 +51,7 @@
export let diff: Record<string, SchemaDiff> = {}
export let disableDnd: boolean = false
export let shouldDispatchChanges: boolean = false
export let isValid: boolean = true
const dispatch = createEventDispatcher()
@@ -295,7 +295,6 @@
schema = schema
dispatch('change', schema)
}}
{lightweightMode}
prettifyHeader={isAppInput}
disabled={!!previewSchema}
{diff}
@@ -305,6 +304,7 @@
dispatch('change', schema)
}}
{shouldDispatchChanges}
bind:isValid
/>
<slot name="runButton" />
@@ -557,7 +557,6 @@
bind:defaultValue={schema.properties[argName].default}
{variableEditor}
{itemPicker}
{lightweightMode}
bind:nullable={schema.properties[argName].nullable}
bind:disabled={schema.properties[argName].disabled}
type={schema.properties[argName].type}
@@ -1,32 +1,25 @@
<script lang="ts">
import { type Job, JobService, type Flow, type RestartedFrom, type OpenFlow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Badge, Button, Drawer, Popup } from './common'
import { Badge, Button, Popup } from './common'
import { createEventDispatcher, getContext } from 'svelte'
import type { FlowEditorContext } from './flows/types'
import { runFlowPreview } from './flows/utils'
import SchemaForm from './SchemaForm.svelte'
import SchemaFormWithArgPicker from './SchemaFormWithArgPicker.svelte'
import FlowStatusViewer from '../components/FlowStatusViewer.svelte'
import FlowProgressBar from './flows/FlowProgressBar.svelte'
import {
AlertTriangle,
ArrowRight,
CornerDownLeft,
Play,
RefreshCw,
X,
ArrowLeftIcon
} from 'lucide-svelte'
import { AlertTriangle, ArrowRight, CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
import { emptyString, sendUserToast } from '$lib/utils'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import SavedInputs from './SavedInputs.svelte'
import { dfs } from './flows/dfs'
import { sliceModules } from './flows/flowStateUtils'
import CapturesInputs from '$lib/components/CapturesInputs.svelte'
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
import Toggle from './Toggle.svelte'
import JsonInputs from './JsonInputs.svelte'
export let previewMode: 'upTo' | 'whole'
export let open: boolean
export let preventEscape = false
export let jobId: string | undefined = undefined
export let job: Job | undefined = undefined
@@ -39,6 +32,10 @@
let isRunning: boolean = false
let jobProgressReset: () => void
let jsonView: boolean = false
let jsonEditor: JsonInputs
let schemaHeight = 0
let isValid: boolean = true
export function test() {
renderCount++
@@ -83,6 +80,10 @@
const newFlow = extractFlow(previewMode)
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
isRunning = true
if (inputSelected) {
savedArgs = $previewArgs
inputSelected = undefined
}
} catch (e) {
sendUserToast('Could not run preview', true, undefined, e.toString())
isRunning = false
@@ -99,6 +100,14 @@
runPreview($previewArgs, undefined)
}
break
case 'Escape':
if (preventEscape) {
selectInput(undefined)
event.preventDefault()
event.stopPropagation
}
break
}
}
}
@@ -124,97 +133,40 @@
}
}
let savedArgs = $previewArgs
let inputSelected: 'captures' | 'history' | 'saved' | undefined = undefined
async function selectInput(input, type?: 'captures' | 'history' | 'saved' | undefined) {
if (!input) {
$previewArgs = savedArgs
inputSelected = undefined
setTimeout(() => {
preventEscape = false
}, 100)
} else {
$previewArgs = input
inputSelected = type
preventEscape = true
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
}
}
export function refresh() {
renderCount++
}
$: if (job?.type === 'CompletedJob') {
isRunning = false
}
$: selectedJobStep !== undefined && onSelectedJobStepChange()
let inputLibraryDrawer: Drawer
let captureLibraryDrawer: Drawer
let renderCount: number = 0
let selectedCapture: Record<string, any> | undefined = undefined
</script>
<svelte:window on:keydown={onKeyDown} />
<Drawer bind:this={inputLibraryDrawer}>
<DrawerContent title="Input library {initialPath}" on:close={inputLibraryDrawer?.toggleDrawer}>
<SavedInputs
flowPath={initialPath}
isValid={true}
args={$previewArgs}
on:selected_args={(e) => {
$previewArgs = JSON.parse(JSON.stringify(e.detail))
inputLibraryDrawer?.closeDrawer()
renderCount++
}}
/>
</DrawerContent>
</Drawer>
<Drawer bind:this={captureLibraryDrawer}>
<DrawerContent
title="Trigger captures library {initialPath}"
on:close={captureLibraryDrawer?.toggleDrawer}
>
<div class="h-full flex flex-col gap-2">
<div class="min-h-0 grow h-full">
<CapturesInputs
flowPath={$pathStore}
headless={false}
addButton={true}
on:select={(e) => {
selectedCapture = e.detail
}}
on:openTriggers={(e) => {
dispatch('openTriggers', e.detail)
captureLibraryDrawer?.closeDrawer()
}}
/>
</div>
<div class="w-full flex flex-col gap-2 px-2 py-2 h-[50%]">
<div class="w-full flex flex-col">
<Button
color="blue"
btnClasses="w-full"
size="sm"
spacingSize="xl"
on:click={async () => {
$previewArgs = JSON.parse(JSON.stringify(selectedCapture))
captureLibraryDrawer?.closeDrawer()
renderCount++
}}
disabled={!selectedCapture}
>
<ArrowLeftIcon class="w-4 h-4 mr-2" />
Use input
</Button>
</div>
<div class="w-full min-h-0 grow overflow-auto">
{#if typeof selectedCapture == 'string' && selectedCapture == 'WINDMILL_TOO_BIG'}
<div class="text-secondary mt-2">
Payload too big to preview but can still be loaded</div
>
{:else if Object.keys(selectedCapture || {}).length > 0}
<div class=" overflow-auto h-full p-2">
<ObjectViewer json={selectedCapture} />
</div>
{:else}
<div class="text-center text-tertiary">
Select an Input to preview scripts arguments
</div>
{/if}
</div>
</div>
</div>
</DrawerContent>
</Drawer>
<div class="flex flex-col space-y-2 h-screen bg-surface px-6 py-2 w-full" id="flow-preview-content">
<div class="flex flex-row justify-between w-full items-center gap-x-2">
<div class="flex flex-col space-y-2 h-screen bg-surface px-4 py-2 w-full" id="flow-preview-content">
<div class="flex flex-row w-full items-center gap-x-2">
<div class="w-8">
<Button
on:click={() => dispatch('close')}
@@ -227,28 +179,30 @@
</div>
{#if isRunning}
<Button
color="red"
on:click={async () => {
isRunning = false
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
id: jobId,
requestBody: {}
}))
} catch {}
}}
size="sm"
btnClasses="w-full max-w-lg"
loading={true}
clickableWhileLoading
>
Cancel
</Button>
<div class="mx-auto">
<Button
color="red"
on:click={async () => {
isRunning = false
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
id: jobId,
requestBody: {}
}))
} catch {}
}}
size="sm"
btnClasses="w-full max-w-lg"
loading={true}
clickableWhileLoading
>
Cancel
</Button>
</div>
{:else}
<div class="flex flex-row gap-4">
<div class="grow justify-center flex flex-row gap-4">
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel}
{#if selectedJobStepType == 'single'}
<Button
@@ -352,26 +306,6 @@
</Button>
</div>
{/if}
<div class="flex gap-2">
<Button
btnClasses="h-full truncate"
size="sm"
variant="border"
on:click={() => {
captureLibraryDrawer?.openDrawer()
}}>Trigger captures library</Button
>
{#if initialPath != ''}
<Button
btnClasses="h-full truncate"
size="sm"
variant="border"
on:click={() => {
inputLibraryDrawer?.openDrawer()
}}>Past runs/Input library</Button
>
{/if}
</div>
</div>
<div class="w-full flex flex-col gap-y-1">
{#if lastPreviewFlow && JSON.stringify($flowStore) != lastPreviewFlow}
@@ -387,17 +321,68 @@
<FlowProgressBar {job} bind:reset={jobProgressReset} />
</div>
<div class="overflow-y-auto grow flex flex-col pr-4">
<div class="overflow-y-auto grow flex flex-col pt-4">
<div class="border-b">
{#key renderCount}
<SchemaForm
noVariablePicker
compact
class="py-4 max-w-3xl"
schema={$flowStore.schema}
bind:args={$previewArgs}
/>
{/key}
<SchemaFormWithArgPicker
runnableId={initialPath == '' ? $pathStore : initialPath}
runnableType={'FlowPath'}
previewArgs={$previewArgs}
on:openTriggers
on:select={(e) => {
selectInput(e.detail.payload, e.detail?.type)
}}
{isValid}
{jsonView}
>
<div class="w-full flex flex-row justify-between">
<InputSelectedBadge {inputSelected} />
<div class="flex flex-row gap-2">
<Toggle
bind:checked={jsonView}
label="JSON View"
size="xs"
options={{
right: 'JSON',
rightTooltip: 'Fill args from JSON'
}}
lightMode
on:change={(e) => {
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
refresh()
}}
/>
</div>
</div>
{#if jsonView}
<div class="py-2" style="height: {Math.max(schemaHeight, 100)}px" data-schema-picker>
<JsonInputs
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
$previewArgs = e.detail
}
}}
updateOnBlur={false}
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}`}
/>
</div>
{:else}
{#key renderCount}
<div bind:clientHeight={schemaHeight} class="min-h-[40vh]">
<SchemaForm
noVariablePicker
compact
schema={$flowStore.schema}
bind:args={$previewArgs}
on:change={() => {
savedArgs = $previewArgs
}}
bind:isValid
/>
</div>
{/key}
{/if}
</SchemaFormWithArgPicker>
</div>
<div class="pt-4 flex flex-col grow">
{#if jobId}
@@ -821,7 +821,7 @@
As a non logged in user, you can only see jobs ran by anonymous users like you
</Alert>
{:else if job}
<div class="flow-root w-full space-y-4 {wideResults ? '' : 'max-w-7xl'} mx-auto px-4">
<div class="flow-root w-full space-y-4 {wideResults ? '' : 'max-w-7xl px-4'} mx-auto">
<!-- {#if innerModules.length > 0 && true}
<h3 class="text-md leading-6 font-bold text-primay border-b pb-2">Flow result</h3>
{:else}
@@ -5,9 +5,9 @@
import { sendUserToast } from '$lib/toast'
import { X } from 'lucide-svelte'
import DisplayResult from './DisplayResult.svelte'
import LightweightSchemaForm from './LightweightSchemaForm.svelte'
import Tooltip from './Tooltip.svelte'
import { Button } from './common'
import SchemaForm from './SchemaForm.svelte'
export let isOwner: boolean
export let workspaceId: string | undefined
@@ -145,7 +145,7 @@
{#if job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema}
<div class="w-full border rounded-lg p-2">
<LightweightSchemaForm bind:args={default_payload} {defaultValues} {schema} />
<SchemaForm onlyMaskPassword bind:args={default_payload} {defaultValues} {schema} />
</div>
{/if}
<Tooltip
@@ -10,32 +10,20 @@
import { clickOutside } from '$lib/utils'
import InfiniteList from './InfiniteList.svelte'
export let scriptHash: string | null = null
export let scriptPath: string | null = null
export let flowPath: string | null = null
export let runnableId: string | undefined = undefined
export let runnableType: RunnableType | undefined = undefined
export let loading: boolean = false
const dispatch = createEventDispatcher()
let jobs: Job[] = []
let loading: boolean = false
let hasMoreCurrentRuns = false
let page = 1
let infiniteList: InfiniteList | undefined = undefined
let loadInputsPageFn: ((page: number, perPage: number) => Promise<any>) | undefined = undefined
let runnableType: RunnableType | undefined = undefined
$: runnableType = scriptHash
? 'ScriptHash'
: scriptPath
? 'ScriptPath'
: flowPath
? 'FlowPath'
: undefined
let runnableId: string | undefined = undefined
let cachedArgs: Record<string, any> = {}
function initLoadInputs() {
runnableId = scriptHash || scriptPath || flowPath || undefined
loadInputsPageFn = async (page: number, perPage: number) => {
const inputs = await InputService.getInputHistory({
workspace: $workspaceStore!,
@@ -48,7 +36,21 @@
const inputsWithPayload = await Promise.all(
inputs.map(async (input) => {
if (cachedArgs[input.id]) {
return {
...input,
payloadData: cachedArgs[input.id]
}
}
const payloadData = await loadArgsFromHistory(input.id, undefined, false)
if (payloadData === 'WINDMILL_TOO_BIG') {
return {
...input,
payloadData: 'WINDMILL_TOO_BIG',
getFullPayload: () => loadArgsFromHistory(input.id, undefined, true)
}
}
cachedArgs[input.id] = payloadData
return {
...input,
payloadData
@@ -60,21 +62,24 @@
infiniteList?.setLoader(loadInputsPageFn)
}
function handleSelected(data: any) {
async function handleSelected(data: any) {
if (selected === data.id) {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
return
}
selected = data.id
dispatch('select', data.payloadData)
if (data.payloadData === 'WINDMILL_TOO_BIG') {
const fullPayload = await data.getFullPayload?.()
dispatch('select', fullPayload)
} else {
dispatch('select', structuredClone(data.payloadData))
}
}
let selected: string | undefined = undefined
onDestroy(() => {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
})
async function getPropPickerElements(): Promise<HTMLElement[]> {
@@ -100,8 +105,7 @@
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && selected) {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
event.stopPropagation()
event.preventDefault()
}
@@ -115,14 +119,21 @@
let jobHovered: string | undefined = undefined
function refresh() {
export function refresh() {
if (infiniteList) {
infiniteList.loadData('refresh')
}
}
export function resetSelected(dispatchEvent?: boolean) {
selected = undefined
if (dispatchEvent) {
dispatch('select', undefined)
}
}
$: !loading && refresh()
$: $workspaceStore && (scriptHash || scriptPath || flowPath) && infiniteList && initLoadInputs()
$: $workspaceStore && runnableId && runnableType && infiniteList && initLoadInputs()
</script>
<svelte:window on:keydown={handleKeydown} />
@@ -153,19 +164,18 @@
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
on:click_outside={() => {
if (selected) {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
}
}}
>
<div class="grow-0">
<div class="grow-0" data-schema-picker>
<DataTable size="xs" bind:currentPage={page} hasMore={hasMoreCurrentRuns} tableFixed={true}>
{#if loading && (jobs == undefined || jobs?.length == 0)}
<div class="text-center text-tertiary text-xs py-2">Loading current runs...</div>
{:else if jobs?.length > 0}
<colgroup>
<col class="w-8" />
<col class="w-20" />
<col class="w-16" />
<col />
</colgroup>
@@ -190,7 +200,7 @@
</DataTable>
</div>
<div class="min-h-0 grow">
<div class="min-h-0 grow" data-schema-picker>
<InfiniteList
bind:this={infiniteList}
selectedItemId={selected}
@@ -200,7 +210,7 @@
<svelte:fragment slot="columns">
<colgroup>
<col class="w-8" />
<col class="w-20" />
<col class="w-16" />
<col />
</colgroup>
</svelte:fragment>
@@ -213,7 +223,9 @@
/>
</svelte:fragment>
<svelte:fragment slot="empty">
<div class="text-center text-tertiary text-xs py-2">No previous Runs</div>
<div class="text-center text-tertiary text-xs py-2">
{runnableId ? 'No previous inputs' : 'Save draft to see previous runs'}
</div>
</svelte:fragment>
</InfiniteList>
</div>
@@ -56,10 +56,14 @@
isNew: false
}))
}
}, 1000)
}, 2000)
page = Math.floor(items.length / perPage) + 1
hasMore = items.length === perPage * (page - 1)
page = Math.ceil(items.length / perPage)
hasMore = items.length === perPage * page
if (hasMore) {
const potentialNewItems = await loadInputs(page + 1, perPage)
hasMore = potentialNewItems.length > 0
}
initLoad = true
isEmpty = items.length === 0
length = items.length
@@ -112,6 +116,7 @@
on:loadMore={() => {
loadData()
}}
{loading}
>
<slot name="columns" />
File diff suppressed because it is too large Load Diff

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