Compare commits

..

2 Commits

Author SHA1 Message Date
Ruben Fiszel
06898b9837 all 2025-06-16 15:16:15 +02:00
Ruben Fiszel
8306e6e5d2 all 2025-06-16 15:13:06 +02:00
697 changed files with 14813 additions and 25073 deletions

View File

@@ -20,7 +20,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.88.0
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -44,7 +44,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.88.0
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -81,7 +81,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.88.0
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
@@ -121,7 +121,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.88.0
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend

View File

@@ -45,7 +45,7 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.1.43
- uses: astral-sh/setup-uv@v6.2.1
- uses: astral-sh/setup-uv@v6
with:
version: "0.6.2"
- uses: actions-rust-lang/setup-rust-toolchain@v1

View File

@@ -9,14 +9,7 @@ jobs:
runs-on: ubicloud
container: node:18
steps:
- uses: actions/create-github-app-token@v2
id: app
with:
app-id: ${{ vars.INTERNAL_APP_ID }}
private-key: ${{ secrets.INTERNAL_APP_KEY }}
- uses: actions/checkout@v4
with:
token: ${{ steps.app.outputs.token }}
- run: git config --system --add safe.directory /__w/windmill/windmill
- name: Change versions
run: ./.github/change-versions.sh "$(cat version.txt)"
@@ -28,8 +21,3 @@ jobs:
cd backend
cargo generate-lockfile
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_user_name: windmill-internal-app[bot]
commit_user_email: windmill-internal-app[bot]@users.noreply.github.com
env:
GITHUB_TOKEN: ${{ steps.app.outputs.token }}

View File

@@ -1,60 +0,0 @@
name: Check Organization Membership
on:
workflow_call:
inputs:
commenter:
required: true
type: string
description: 'The username to check for organization membership'
organization:
required: false
type: string
default: 'windmill-labs'
description: 'The organization to check membership for'
trusted_bot:
required: false
type: string
default: 'windmill-internal-app[bot]'
description: 'The trusted bot username to allow'
secrets:
access_token:
required: true
description: 'The access token to use for org membership check'
outputs:
is_member:
description: 'Whether the user is an organization member or trusted bot'
value: ${{ jobs.check-membership.outputs.is_member }}
jobs:
check-membership:
runs-on: ubicloud-standard-2
outputs:
is_member: ${{ steps.check-membership.outputs.is_member }}
steps:
- name: Check organization membership
id: check-membership
env:
ORG_ACCESS_TOKEN: ${{ secrets.access_token }}
COMMENTER: ${{ inputs.commenter }}
ORG: ${{ inputs.organization }}
TRUSTED_BOT: ${{ inputs.trusted_bot }}
run: |
# 1. Allow the trusted bot straight away
if [[ "$COMMENTER" == "$TRUSTED_BOT" ]]; then
echo "is_member=true" >> $GITHUB_OUTPUT
exit 0
fi
# 2. Otherwise fall back to the org-membership check
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $ORG_ACCESS_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/orgs/$ORG/members/$COMMENTER")
if [ "$STATUS" -eq 204 ]; then
echo "is_member=true" >> $GITHUB_OUTPUT
else
echo "is_member=false" >> $GITHUB_OUTPUT
fi

View File

@@ -11,40 +11,45 @@ on:
types: [submitted]
jobs:
determine-commenter:
check-membership:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai')) ||
(github.event_name == 'issues' && contains(github.event.issue.body, '/ai'))
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai') && !contains(github.event.review.user.login, '[bot]')) ||
(github.event_name == 'issues' && contains(github.event.issue.body, '/ai') && !contains(github.event.issue.user.login, '[bot]'))
runs-on: ubicloud-standard-2
outputs:
commenter: ${{ steps.determine-commenter.outputs.commenter }}
is_member: ${{ steps.check-membership.outputs.is_member }}
steps:
- name: Determine commenter
id: determine-commenter
- name: Check organization membership
id: check-membership
env:
ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
run: |
# Work out who wrote the comment / review
if [[ "${{ github.event_name }}" == "issue_comment" || \
"${{ github.event_name }}" == "pull_request_review_comment" ]]; then
ORG="windmill-labs"
if [[ "${{ github.event_name }}" == "issue_comment" || "${{ github.event_name }}" == "pull_request_review_comment" ]]; then
COMMENTER="${{ github.event.comment.user.login }}"
elif [[ "${{ github.event_name }}" == "pull_request_review" ]]; then
COMMENTER="${{ github.event.review.user.login }}"
else
COMMENTER="${{ github.event.issue.user.login }}"
fi
echo "commenter=$COMMENTER" >> $GITHUB_OUTPUT
check-membership:
needs: determine-commenter
uses: ./.github/workflows/check-org-membership.yml
with:
commenter: ${{ needs.determine-commenter.outputs.commenter }}
secrets:
access_token: ${{ secrets.ORG_ACCESS_TOKEN }}
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $ORG_ACCESS_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/orgs/$ORG/members/$COMMENTER")
if [ "$STATUS" -eq 204 ]; then
echo "is_member=true" >> $GITHUB_OUTPUT
else
echo "is_member=false" >> $GITHUB_OUTPUT
fi
claude-code-action:
needs: [determine-commenter, check-membership]
needs: check-membership
if: |
needs.check-membership.outputs.is_member == 'true'
runs-on: ubicloud-standard-8
@@ -59,69 +64,22 @@ jobs:
with:
fetch-depth: 1
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Run npm install and generate-backend-client
working-directory: ./frontend
run: |
# add a build directory for cargo check
mkdir -p build
npm install
npm run generate-backend-client
- name: install xmlsec1
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.85.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: backend
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
run: |
SQLX_OFFLINE=true cargo check --features $(./all_features_oss.sh)
- name: Run Claude PR Action
uses: anthropics/claude-code-action@beta
env:
SQLX_OFFLINE: true
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
allowed_tools: "mcp__github__create_pull_request,Bash"
custom_instructions: |
## IMPORTANT INSTRUCTIONS
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main.
## Code Quality Requirements
After making any code changes, you MUST run the appropriate validation commands:
**Frontend Changes:**
- Run: `npm run check` in the frontend directory
- Fix all warnings and errors before proceeding
**Backend Changes:**
- Run: `cargo check --features $(./all_features_oss.sh)` in the backend directory
- Fix all warnings and errors before proceeding
**Pull Request Creation:**
- DO NOT FORGET TO OPEN A DRAFT PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
## Available Tools
- mcp__github__create_pull_request: Create PRs from branches
- Bash: Full access to run validation commands and git operations
allowed_tools: "mcp__github__create_pull_request,Bash(npm run check),Bash(npm install),Bash(cargo check),Bash(curl https://sh.rustup.rs -sSf | sh -s -- -y)"
custom_instructions: "IMPORTANT INSTRUCTIONS:
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main, with the title starting with [Claude PR].
- If you made changes to the frontend code, run npm install, then npm run generate-backend-client, then npm run check. You can ignore warnings that are reported by the check script, but fix the errors.
- If you made changes to the backend code, install Rust and then run cargo check. You can ignore warnings that are reported by the check script, but fix the errors.
- DO NOT FORGET TO OPEN A DRAFT PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
AVAILABLE TOOLS:
- mcp__github__create_pull_request: Create a PR from a branch to main
- Bash(npm run check): Run the check script. You should run this tool after making changes to the frontend code.
- Bash(npm install): Install dependencies. You need this to run npm run check.
- Bash(npm run generate-backend-client): Generate the backend client. You need this to run npm run check.
- Bash(cargo check): Run the cargo check script. You should run this tool after making changes to the backend code.
- Bash(curl https://sh.rustup.rs -sSf | sh -s -- -y): Install Rust. You need this to run cargo check."
trigger_phrase: "/ai"

View File

@@ -4,36 +4,38 @@ on:
jobs:
check-membership:
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') }}
uses: ./.github/workflows/check-org-membership.yml
with:
commenter: ${{ github.event.comment.user.login }}
secrets:
access_token: ${{ secrets.ORG_ACCESS_TOKEN }}
generate-token:
needs: check-membership
if: ${{ needs.check-membership.outputs.is_member == 'true' }}
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') && github.event.comment.user.type != 'Bot' }}
runs-on: ubicloud-standard-2
outputs:
app_token: ${{ steps.app.outputs.token }}
is_member: ${{ steps.check-membership.outputs.is_member }}
steps:
- name: Generate an installation token
id: app
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.INTERNAL_APP_ID }}
private-key: ${{ secrets.INTERNAL_APP_KEY }}
owner: windmill-labs
- name: Check organization membership
id: check-membership
env:
ORG_ACCESS_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
COMMENTER: ${{ github.event.comment.user.login }}
run: |
ORG="windmill-labs"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $ORG_ACCESS_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/orgs/$ORG/members/$COMMENTER")
if [ "$STATUS" -eq 204 ]; then
echo "is_member=true" >> $GITHUB_OUTPUT
else
echo "is_member=false" >> $GITHUB_OUTPUT
fi
trigger-docs:
needs: [generate-token, check-membership]
if: ${{ needs.check-membership.outputs.is_member == 'true' }}
needs: check-membership
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/docs') && needs.check-membership.outputs.is_member == 'true' }}
uses: windmill-labs/windmilldocs/.github/workflows/create-docs.yml@main
with:
pr_number: ${{ github.event.issue.number }}
repo: ${{ github.event.repository.name }}
comment_text: ${{ github.event.comment.body }}
secrets:
DOCS_TOKEN: ${{ needs.generate-token.outputs.app_token }}
DOCS_TOKEN: ${{ secrets.DOCS_TOKEN }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}

View File

@@ -24,7 +24,7 @@ jobs:
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
merge_success_emoji:
if: github.event.action == 'closed'
if: github.event.pull_request.merged == true
uses: ./.github/workflows/shareable-discord-notification.yml
with:
PR_STATUS: "merged"

View File

@@ -1,175 +0,0 @@
name: Git commands
on:
issue_comment:
types: [created]
jobs:
update-sqlx:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/updatesqlx')
runs-on: ubicloud-standard-8
permissions:
contents: write
pull-requests: write
issues: write
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: windmill
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Comment on PR - Starting
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Starting sqlx update...'
})
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: ${{ github.event.issue.pull_request.head.ref }}
fetch-depth: 0
- name: Checkout windmill-ee-private
uses: actions/checkout@v3
with:
repository: windmill-labs/windmill-ee-private
path: windmill-ee-private
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
# Cache rust dependencies
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: "./backend -> target"
- name: Install xmlsec build-time deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
pkg-config libxml2-dev libssl-dev \
xmlsec1 libxmlsec1-dev libxmlsec1-openssl
- name: Run update-sqlx script
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/windmill
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=${{ github.event.issue.number }}
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
echo "Checking out PR branch: $BRANCH_NAME"
git checkout $BRANCH_NAME
mkdir frontend/build
cd backend
cargo install sqlx-cli --version 0.8.5
sqlx migrate run
./update_sqlx.sh --dir ./windmill-ee-private
# Pass the branch name to the next step
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
- name: Commit changes if any
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add backend/.sqlx
git commit -m "Update SQLx metadata"
git push origin ${{ env.BRANCH_NAME }}
- name: Comment on PR - Completed
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Successfully ran sqlx update'
})
update-ee-ref:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/eeref')
runs-on: ubicloud-standard-2
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Comment on PR - Starting
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Starting ee ref update...'
})
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: ${{ github.event.issue.pull_request.head.ref }}
fetch-depth: 0
- name: Checkout windmill-ee-private
uses: actions/checkout@v3
with:
repository: windmill-labs/windmill-ee-private
path: windmill-ee-private
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
- name: Get last commit hash of private-repo
id: get-commit-hash
run: |
cd windmill-ee-private
COMMIT_HASH=$(git rev-parse HEAD)
echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
echo "Latest commit hash: $COMMIT_HASH"
- name: Update ee-repo-ref.txt
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "${{ steps.get-commit-hash.outputs.commit_hash }}" > backend/ee-repo-ref.txt
echo "Updated backend/ee-repo-ref.txt with commit hash: ${{ steps.get-commit-hash.outputs.commit_hash }}"
# commit and push the changes
PR_NUMBER=${{ github.event.issue.number }}
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
echo "Checking out PR branch: $BRANCH_NAME"
git checkout $BRANCH_NAME
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add backend/ee-repo-ref.txt
git commit -m "Update ee-repo-ref.txt" || echo "No changes to commit"
git push origin $BRANCH_NAME
- name: Comment on PR - Completed
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Successfully updated ee-repo-ref.txt'
})

View File

@@ -9,19 +9,11 @@ jobs:
runs-on: ubicloud-standard-2
steps:
- name: Generate an installation token
id: app
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.INTERNAL_APP_ID }}
private-key: ${{ secrets.INTERNAL_APP_KEY }}
owner: windmill-labs
- name: Checkout on helm repository
uses: actions/checkout@v3
with:
repository: windmill-labs/windmill-helm-charts
token: ${{ steps.app.outputs.token }}
token: ${{ secrets.HELM_CHART_TOKEN }}
- name: Get version
id: get_version
@@ -57,23 +49,6 @@ jobs:
APP_VERSION=${APP_VERSION%/}
sed -i "s/appVersion: .*/appVersion: $APP_VERSION/" ./charts/windmill/Chart.yaml
- name: Close existing bump-helm PRs
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
run: |
# List open PR numbers whose title starts with the prefix
prs=$(gh pr list \
--state open \
--search '"helm: bump version to" in:title' \
--json number \
-q '.[].number')
for pr in $prs; do
echo "Closing outdated bump PR #$pr"
gh pr close "$pr" \
--comment "Closed automatically superseded by a newer Helm-chart bump PR."
done
- name: Commit and push
run: |
git add .
@@ -82,7 +57,7 @@ jobs:
- name: Create PR
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
GH_TOKEN: ${{ secrets.HELM_CHART_TOKEN }}
run: |
gh pr create \
--title "helm: bump version to ${{ env.VERSION }}" \

View File

@@ -1,41 +0,0 @@
name: Claude Auto Review
on:
pull_request:
types: [ready_for_review, opened]
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
auto-review:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false || github.event.pull_request.ready_for_review == true
permissions:
contents: read
pull-requests: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Automatic PR Review
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
direct_prompt: |
Please review this pull request and provide comprehensive feedback.
Focus on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security implications
Provide constructive feedback with specific suggestions for improvement.
Use inline comments to highlight specific areas of concern.
allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"

3
.gitignore vendored
View File

@@ -11,5 +11,4 @@ CaddyfileRemoteMalo
.dev-docker-wrapper*
backend/.minio-data
.aider*
!.aiderignore
rust-client/Cargo.toml
!.aiderignore

View File

@@ -1,182 +1,5 @@
# Changelog
## [1.502.2](https://github.com/windmill-labs/windmill/compare/v1.502.1...v1.502.2) (2025-07-01)
### Bug Fixes
* bad spacing ai chat context elements ([#6111](https://github.com/windmill-labs/windmill/issues/6111)) ([2fb912b](https://github.com/windmill-labs/windmill/commit/2fb912b78c90d9e70d3db4b0c3c473831161c8b4))
* **frontend:** improve step job load ([#6109](https://github.com/windmill-labs/windmill/issues/6109)) ([0afe3f9](https://github.com/windmill-labs/windmill/commit/0afe3f9691d93f837b10b29a0cf125eaa175589d))
* **frontend:** only show test button for script modules ([#6107](https://github.com/windmill-labs/windmill/issues/6107)) ([7042a6f](https://github.com/windmill-labs/windmill/commit/7042a6f52db823d6b9b5ad14fa83af36880bd2d5))
## [1.502.1](https://github.com/windmill-labs/windmill/compare/v1.502.0...v1.502.1) (2025-07-01)
### Bug Fixes
* **frontend:** update test job logs ([#6102](https://github.com/windmill-labs/windmill/issues/6102)) ([a4c295b](https://github.com/windmill-labs/windmill/commit/a4c295b5e857314d78de6bb6ab942dc60ff99279))
## [1.502.0](https://github.com/windmill-labs/windmill/compare/v1.501.4...v1.502.0) (2025-06-30)
### Features
* kafka better retry and errors ([#6067](https://github.com/windmill-labs/windmill/issues/6067)) ([8edf4b2](https://github.com/windmill-labs/windmill/commit/8edf4b2b92fe77ad86d96d41095b05540176e783))
* use FIM for code autocomplete ([#6081](https://github.com/windmill-labs/windmill/issues/6081)) ([431437c](https://github.com/windmill-labs/windmill/commit/431437c3449ddcd8c45bacd696a4db209577773b))
### Bug Fixes
* add support for GCS object storage ([#6083](https://github.com/windmill-labs/windmill/issues/6083)) ([c51e128](https://github.com/windmill-labs/windmill/commit/c51e128920801ec7199033c59655a0bcdd5341ba))
* fix critical alerts flapping on low disk ([#6075](https://github.com/windmill-labs/windmill/issues/6075)) ([bcba462](https://github.com/windmill-labs/windmill/commit/bcba46225f094e60bb7e77ed74fc060ffaccb6c6))
* fix s3 settings reset ([8ba3959](https://github.com/windmill-labs/windmill/commit/8ba3959adac955cd4ec5cbebd357703992c68baa))
* **frontend:** improve flow editor settings bar UX ([#6049](https://github.com/windmill-labs/windmill/issues/6049)) ([ded54f2](https://github.com/windmill-labs/windmill/commit/ded54f2e68da09618c377cd699e0a2eaa53a63a8))
* optimize public apps rendering ([a7e78f0](https://github.com/windmill-labs/windmill/commit/a7e78f01f1697b8a4a3c61cd4377bc34b8077d38))
* public url in app menu ([ca368ab](https://github.com/windmill-labs/windmill/commit/ca368aba7a334efc9963f69b3a4462d9972997f4))
* test up to broken due to mutable flow ai chat preview ([#6096](https://github.com/windmill-labs/windmill/issues/6096)) ([805a8b5](https://github.com/windmill-labs/windmill/commit/805a8b574c057b911bff5d335e0c63051c6587ee))
## [1.501.4](https://github.com/windmill-labs/windmill/compare/v1.501.3...v1.501.4) (2025-06-26)
### Bug Fixes
* add windows paths to uv install to find git/ssh ([#6063](https://github.com/windmill-labs/windmill/issues/6063)) ([835f1d2](https://github.com/windmill-labs/windmill/commit/835f1d2ec945145942deaa41cb3bd176ed276279))
* optionally enable CSP headers ([#6033](https://github.com/windmill-labs/windmill/issues/6033)) ([d933648](https://github.com/windmill-labs/windmill/commit/d933648d3666b2ca9d813e04b9f19ddc3c7efda3))
* schemaform reorder ([#6069](https://github.com/windmill-labs/windmill/issues/6069)) ([1a4b096](https://github.com/windmill-labs/windmill/commit/1a4b096f3ce40e238f1724aa4fd26649d70cb62a))
## [1.501.3](https://github.com/windmill-labs/windmill/compare/v1.501.2...v1.501.3) (2025-06-25)
### Bug Fixes
* **backend:** return correct content-type for openapi spec ([#6045](https://github.com/windmill-labs/windmill/issues/6045)) ([44457c7](https://github.com/windmill-labs/windmill/commit/44457c72cf75c969de97c39bb23f57acad268e10))
* **frontend:** load all flow jobs on page load ([#6029](https://github.com/windmill-labs/windmill/issues/6029)) ([dc5e764](https://github.com/windmill-labs/windmill/commit/dc5e764d9db9251dc356094d6ac47c45fdf72c74))
* ignore type only imports when computing ts lockfiles ([900c8ed](https://github.com/windmill-labs/windmill/commit/900c8edd7b35802e23a1359029da8ddbfb783753))
* improve ordering of forms for non complete ordering + array schema fix ([18ee03a](https://github.com/windmill-labs/windmill/commit/18ee03a32371885f5e608cb306b5ccbccc31dac5))
* missing static_asset_config from api call ([#6058](https://github.com/windmill-labs/windmill/issues/6058)) ([395f1ff](https://github.com/windmill-labs/windmill/commit/395f1ff8ba05020d72d1d8b34bd6bb32517b7aec))
## [1.501.2](https://github.com/windmill-labs/windmill/compare/v1.501.1...v1.501.2) (2025-06-24)
### Bug Fixes
* improve schema form handling of inconsistent order and properties ([3daf79f](https://github.com/windmill-labs/windmill/commit/3daf79ffbc45ca32ff443e5521a67d62528665db))
## [1.501.1](https://github.com/windmill-labs/windmill/compare/v1.501.0...v1.501.1) (2025-06-24)
### Bug Fixes
* optimize jobs list run incremental refresh performance ([1bdd00a](https://github.com/windmill-labs/windmill/commit/1bdd00a3e4a94ecb23efb9614c341c64a67ac389))
* pwsh skip already installed modules outside of cache ([#6037](https://github.com/windmill-labs/windmill/issues/6037)) ([29f6fab](https://github.com/windmill-labs/windmill/commit/29f6fab60c6f8cf251182a56c09bac7692868bae))
## [1.501.0](https://github.com/windmill-labs/windmill/compare/v1.500.3...v1.501.0) (2025-06-24)
### Features
* ai flow chat prompt and UX improvements ([#5942](https://github.com/windmill-labs/windmill/issues/5942)) ([5722014](https://github.com/windmill-labs/windmill/commit/57220146513444436faff95f58c1b36481d1fa1d))
### Bug Fixes
* improve reactivity of apps ([27e12a1](https://github.com/windmill-labs/windmill/commit/27e12a1527c41ac801042038b707a94897e718f8))
## [1.500.3](https://github.com/windmill-labs/windmill/compare/v1.500.2...v1.500.3) (2025-06-23)
### Bug Fixes
* fix conditional wrappre ([6f3cb5e](https://github.com/windmill-labs/windmill/commit/6f3cb5eabb7b2224d04ec10f151f67c0955a5cfd))
## [1.500.2](https://github.com/windmill-labs/windmill/compare/v1.500.1...v1.500.2) (2025-06-20)
### Bug Fixes
* consistency of root job propagation fixing cases where runFlow in scripts would fail ([9c2f6a7](https://github.com/windmill-labs/windmill/commit/9c2f6a757fb168c7305c991c9fdbf78acd856a1c))
## [1.500.1](https://github.com/windmill-labs/windmill/compare/v1.500.0...v1.500.1) (2025-06-20)
### Bug Fixes
* git repository resource picker effect loop ([#6017](https://github.com/windmill-labs/windmill/issues/6017)) ([1b1bee5](https://github.com/windmill-labs/windmill/commit/1b1bee5b53d78e4407b684b567d0fddd2b5283f3))
## [1.500.0](https://github.com/windmill-labs/windmill/compare/v1.499.0...v1.500.0) (2025-06-20)
### Features
* add typescript client context to ai chat system prompt ([#6004](https://github.com/windmill-labs/windmill/issues/6004)) ([3e82282](https://github.com/windmill-labs/windmill/commit/3e822823519d1d5c22e422e4bd1ad4d37b6428b6))
* blacklist remote agent worker token ([#5985](https://github.com/windmill-labs/windmill/issues/5985)) ([86eb907](https://github.com/windmill-labs/windmill/commit/86eb9074cc94f309f17ea72e9cecd0d502ffd2be))
* **frontend:** run steps from graph ([#5915](https://github.com/windmill-labs/windmill/issues/5915)) ([67e6bce](https://github.com/windmill-labs/windmill/commit/67e6bce9b2eba1653450921afab3eabbd41fc715))
### Bug Fixes
* ai button in inline script editor to open AI chat in flow builder ([#5989](https://github.com/windmill-labs/windmill/issues/5989)) ([4ae5928](https://github.com/windmill-labs/windmill/commit/4ae5928788831196672e212b32ca410afab640e0))
* improve piptar upload - sequential uploads via background task queue ([#5994](https://github.com/windmill-labs/windmill/issues/5994)) ([c4adaee](https://github.com/windmill-labs/windmill/commit/c4adaeeabd287ca1c4f3522bcd8bcea30b00fe6d))
* new MultiSelect component ([#5979](https://github.com/windmill-labs/windmill/issues/5979)) ([fa8d1b4](https://github.com/windmill-labs/windmill/commit/fa8d1b47db19e15fe854e01f9987c8f97cb45b44))
* replace worker tags to listen multiselect ([#5997](https://github.com/windmill-labs/windmill/issues/5997)) ([e4255e6](https://github.com/windmill-labs/windmill/commit/e4255e6276565c4a45b1f45a5d627bcfb5369270))
## [1.499.0](https://github.com/windmill-labs/windmill/compare/v1.498.0...v1.499.0) (2025-06-18)
### Features
* devOps role can edit worker groups ([#5984](https://github.com/windmill-labs/windmill/issues/5984)) ([b1c4f8b](https://github.com/windmill-labs/windmill/commit/b1c4f8b29d0fb4cad76853110b84a87892b54661))
### Bug Fixes
* prevent keypress events from bubbling in decision tree drawer ([#5993](https://github.com/windmill-labs/windmill/issues/5993)) ([2a33442](https://github.com/windmill-labs/windmill/commit/2a334421e85abf046784aab57522582439ef2901))
## [1.498.0](https://github.com/windmill-labs/windmill/compare/v1.497.2...v1.498.0) (2025-06-17)
### Features
* use provider api to list available AI models in workspace settings ([#5947](https://github.com/windmill-labs/windmill/issues/5947)) ([7490e88](https://github.com/windmill-labs/windmill/commit/7490e883d747a7f65b2fefd3ec14b1cfc3d9bbd4))
* windmill http triggers and webhooks to openapi spec ([#5918](https://github.com/windmill-labs/windmill/issues/5918)) ([aba8c01](https://github.com/windmill-labs/windmill/commit/aba8c01d7f44ba4be369a3c711be9e156d6bf215))
## [1.497.2](https://github.com/windmill-labs/windmill/compare/v1.497.1...v1.497.2) (2025-06-17)
### Bug Fixes
* always rm containers in docker mode ([38eb71b](https://github.com/windmill-labs/windmill/commit/38eb71bdf55ee2f606d1d2ad2e987d5af16d88c0))
* flow steps use their tags if any specific when used as subflow ([26bec05](https://github.com/windmill-labs/windmill/commit/26bec054a3447a91c5d5f56d8b98717c06496087))
## [1.497.1](https://github.com/windmill-labs/windmill/compare/v1.497.0...v1.497.1) (2025-06-16)
### Bug Fixes
* fix mcp server initialization ([1c6a7c8](https://github.com/windmill-labs/windmill/commit/1c6a7c8cd0bd8396f158e3cb0583b927ce957f12))
## [1.497.0](https://github.com/windmill-labs/windmill/compare/v1.496.3...v1.497.0) (2025-06-16)
### Features
* add api tools to ai chat ([#5921](https://github.com/windmill-labs/windmill/issues/5921)) ([f7a83c0](https://github.com/windmill-labs/windmill/commit/f7a83c03c12b8ae70179fb228e0e2391b6ea2858))
* **backend:** use streamable http in favor of sse for MCP ([#5910](https://github.com/windmill-labs/windmill/issues/5910)) ([d47c078](https://github.com/windmill-labs/windmill/commit/d47c078bb5ab86d82d9cbbce3c55c89c0c20d809))
* better graph layout algorithm + migrate to svelte 5 almost everywhere + xyflow 1.0 ([23920ae](https://github.com/windmill-labs/windmill/commit/23920aee84fdca4a557a34ff2d66a0bb7bdca605))
* fill runnable inputs with AI chat ([#5887](https://github.com/windmill-labs/windmill/issues/5887)) ([b4a6a7e](https://github.com/windmill-labs/windmill/commit/b4a6a7e72429617d420af85a9de35bb13adfc6fb))
* **go:** local go.mod ([#5929](https://github.com/windmill-labs/windmill/issues/5929)) ([0b89260](https://github.com/windmill-labs/windmill/commit/0b89260540b307c6d614ca4275dd038fbfdac33c))
* multiple azure models support ([#5920](https://github.com/windmill-labs/windmill/issues/5920)) ([f412ede](https://github.com/windmill-labs/windmill/commit/f412ede6ed48e9a492f39582ac70a5584477529e))
* **rust:** add rust sdk ([#5909](https://github.com/windmill-labs/windmill/issues/5909)) ([332f66e](https://github.com/windmill-labs/windmill/commit/332f66e3483abbeacd4e7c1b74c94c5265314882))
### Bug Fixes
* ai chat tooltip + user settings autocomplete issue ([#5917](https://github.com/windmill-labs/windmill/issues/5917)) ([6f907c7](https://github.com/windmill-labs/windmill/commit/6f907c79b4cf6279bd52e35a3ee96e0d021422f5))
* audit logs for token refresh + consider refresh for active users ([#5930](https://github.com/windmill-labs/windmill/issues/5930)) ([cf2d09e](https://github.com/windmill-labs/windmill/commit/cf2d09e7a8c5d2472af0d483689c3fcfa2976117))
* fix input with wrong height on first render ([#5935](https://github.com/windmill-labs/windmill/issues/5935)) ([1a6283b](https://github.com/windmill-labs/windmill/commit/1a6283b42a6a514ab2e05160855cdc0f70b61d0e))
* flow step missing input warnings ([#5916](https://github.com/windmill-labs/windmill/issues/5916)) ([f077849](https://github.com/windmill-labs/windmill/commit/f077849b8f7c1916fd420e85b4844a5c5e93a139))
* **frontend:** use correct kind for flow insert module btn ([#5938](https://github.com/windmill-labs/windmill/issues/5938)) ([17c8c8a](https://github.com/windmill-labs/windmill/commit/17c8c8a5616ab8656799cea3fc5bc7cfaedc4995))
## [1.496.3](https://github.com/windmill-labs/windmill/compare/v1.496.2...v1.496.3) (2025-06-09)

View File

@@ -1,10 +1,3 @@
# Windmill Development Guide
## Overview
Windmill is an open-source developer platform for building internal tools, workflows, API integrations, background jobs, workflows, and user interfaces. See @windmill-overview.mdc for full platform details.
## Language-Specific Guides
- Backend (Rust): @backend/rust-best-practices.mdc + @backend/summarized_schema.txt
- Frontend (Svelte 5): @frontend/svelte5-best-practices.mdc
To have an overview of what this app does, see @.cursor/rules/windmill-overview.mdc
For backend modifications, follow the rules mentioned here @.cursor/rules/rust-best-practices.mdc
For frontend modifications, follow the rules mentioned here @.cursor/rules/svelte5-best-practices.mdc

View File

@@ -1,5 +1,5 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.88-slim-bookworm
ARG RUST_IMAGE=rust:1.86-slim-bookworm
FROM ${RUST_IMAGE} AS rust_base

View File

@@ -367,11 +367,10 @@ you to have it being synced automatically everyday.
## Run a local dev setup
Using [Nix](./frontend/README_DEV.md#nix) (Recommended).
See the [./frontend/README_DEV.md](./frontend/README_DEV.md) file for all
running options.
Using [Nix](./frontend/README_DEV.md#nix).
### only Frontend

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM app WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "08c827d9b2de0b77ce0ea2653760751615112c501b35e931ed817dbefd7c6bdb"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n summary,\n description,\n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [
{
@@ -35,21 +35,11 @@
},
{
"ordinal": 6,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 9,
"ordinal": 7,
"name": "http_method: _",
"type_info": {
"Custom": {
@@ -67,32 +57,32 @@
}
},
{
"ordinal": 10,
"ordinal": 8,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 11,
"ordinal": 9,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 12,
"ordinal": 10,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 13,
"ordinal": 11,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"ordinal": 12,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 15,
"ordinal": 13,
"name": "authentication_method: _",
"type_info": {
"Custom": {
@@ -111,27 +101,27 @@
}
},
{
"ordinal": 16,
"ordinal": 14,
"name": "static_asset_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 15,
"name": "is_static_website",
"type_info": "Bool"
},
{
"ordinal": 18,
"ordinal": 16,
"name": "authentication_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 19,
"ordinal": 17,
"name": "wrap_body",
"type_info": "Bool"
},
{
"ordinal": 20,
"ordinal": 18,
"name": "raw_string",
"type_info": "Bool"
}
@@ -149,8 +139,6 @@
false,
false,
false,
true,
true,
false,
false,
false,
@@ -166,5 +154,5 @@
false
]
},
"hash": "39401cb0db8d367b5beb2be0c13aa7595adae0eac4e4e3a888cb12b972d1a7ce"
"hash": "144e4eccfd1c1e729e3c864bd5dc3316248719dfa8a6c9e1d15a7931638e86db"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n summary = $16,\n description = $17,\n edited_at = now(), \n is_static_website = $18\n WHERE \n workspace_id = $19 AND \n path = $20\n ",
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n edited_at = now(), \n is_static_website = $16\n WHERE \n workspace_id = $17 AND \n path = $18\n ",
"describe": {
"columns": [],
"parameters": {
@@ -47,8 +47,6 @@
}
}
},
"Varchar",
"Text",
"Bool",
"Text",
"Text"
@@ -56,5 +54,5 @@
},
"nullable": []
},
"hash": "3f05e6186050a7ce6d8efb41067d3c5282319fe7e041f114e02fb22b91716637"
"hash": "187e8f85a71dea958e89fdfdf96c913a19eef8678dc7890c2f0e1ef8758ec43b"
}

View File

@@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token, expires_at, blacklisted_at, blacklisted_by \n FROM agent_token_blacklist \n ORDER BY blacklisted_at DESC",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamp"
},
{
"ordinal": 2,
"name": "blacklisted_at",
"type_info": "Timestamp"
},
{
"ordinal": 3,
"name": "blacklisted_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "1c5d3556fc8436ddd294f39c5431e1f501a821d6143c5d8aece20814237a6b86"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "CREATE INDEX CONCURRENTLY idx_audit_recent_login_activities \nON audit (timestamp, username) \nWHERE operation IN ('users.login', 'oauth.login', 'users.token.refresh');",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "222e29b89d10f3840d4e9b9ab63207df3cbab63c83d4a6374e72a11893841653"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM agent_token_blacklist WHERE token = $1 AND expires_at > $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Timestamp"
]
},
"nullable": [
null
]
},
"hash": "2bf99d540365c228e1776ee5d2ba01ebe289183526afab19c1390bbf5082f019"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM token WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "2f30274b0fe89aa1579b252b990876e5035ca5b31a68fcf08701102a6457e5c4"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path,\n summary,\n description\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2 AND\n archived is FALSE\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "33367c42e87e78ae987c0966dc4d445c5eff75b2e2843ffd7a46b03cbaea9ae8"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM raw_app WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "3b5295a7c4b99aefa52c9a8ae1e0dd12bf4a0be1bf755caf7a1fa863e7950562"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n id As \"id!\",\n flow_status->'restarted_from'->'flow_job_id' AS \"restarted_from: Json<Uuid>\"\n FROM v2_job_status\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id",
"query": "SELECT\n id As \"id!\",\n flow_status->'restarted_from'->'flow_job_id' AS \"restarted_from: Json<Uuid>\"\n FROM v2_as_queue\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $1), $1) = id AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -16,13 +16,14 @@
],
"parameters": {
"Left": [
"Uuid"
"Uuid",
"Text"
]
},
"nullable": [
false,
true,
null
]
},
"hash": "019100d178129340a7c35d60ab61f983c8a9cb810db4369554bf26c6b0d6003d"
"hash": "3c0b2a840102b12864c5d721b8e0142602ab37f3e1a95d39b3c7cbd7ff34d0b2"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM flow WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "52032730f2eeaaeab55305f72bea5481d1c50c2eaa92a97a078239430f0d6c13"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM agent_token_blacklist WHERE token = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "54fee31b61d62598c89cf7d0729079ac1721fe7bd1844f339236379211defc78"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -45,41 +45,31 @@
},
{
"ordinal": 8,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 11,
"ordinal": 9,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"ordinal": 10,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 13,
"ordinal": 11,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"ordinal": 12,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 15,
"ordinal": 13,
"name": "authentication_method: _",
"type_info": {
"Custom": {
@@ -98,7 +88,7 @@
}
},
{
"ordinal": 16,
"ordinal": 14,
"name": "http_method: _",
"type_info": {
"Custom": {
@@ -116,22 +106,22 @@
}
},
{
"ordinal": 17,
"ordinal": 15,
"name": "static_asset_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 18,
"ordinal": 16,
"name": "is_static_website",
"type_info": "Bool"
},
{
"ordinal": 19,
"ordinal": 17,
"name": "wrap_body",
"type_info": "Bool"
},
{
"ordinal": 20,
"ordinal": 18,
"name": "raw_string",
"type_info": "Bool"
}
@@ -150,8 +140,6 @@
true,
false,
false,
true,
true,
false,
false,
false,
@@ -165,5 +153,5 @@
false
]
},
"hash": "4228b098883408323bd8413ee094454b95962047458a6927d19ac0d3e7b3f0fa"
"hash": "56c2522a12f91515e38290e4680a55a4727195125cd49a2f92f89bcdf74dc364"
}

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM variable WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "5a31b32659a0ac6a6ad0e122a4d475787240d6714ddadf16296d2b7bd5fdcb52"
}

View File

@@ -1,93 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route,\n summary,\n description,\n authentication_method AS \"authentication_method: _\",\n authentication_resource_path\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "route_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "http_method: _",
"type_info": {
"Custom": {
"name": "http_method",
"kind": {
"Enum": [
"get",
"post",
"put",
"delete",
"patch"
]
}
}
}
},
{
"ordinal": 2,
"name": "is_async",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "workspaced_route",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "authentication_method: _",
"type_info": {
"Custom": {
"name": "authentication_method",
"kind": {
"Enum": [
"none",
"windmill",
"api_key",
"basic_http",
"custom_script",
"signature"
]
}
}
}
},
{
"ordinal": 7,
"name": "authentication_resource_path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"TextArray",
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true,
false,
true
]
},
"hash": "714fb0f66ceb536aee8cb9ae0144757b999d25870fda37fe904e09dd5c742015"
}

View File

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

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM resource WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "7c765f50c67b0ef751bafc1bf9279c4cb8a851dfab406ba7611f77773663e9f3"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n summary,\n description,\n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19\n )\n ",
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -14,8 +14,6 @@
"Bool",
"Bool",
"Varchar",
"Varchar",
"Text",
"Bool",
"Bool",
{
@@ -55,5 +53,5 @@
},
"nullable": []
},
"hash": "ed99d4d088d0fd0c01f29803b12e99ae0a53d0b1feaa67737da409c51c1b6751"
"hash": "8c30e91c2486f7511563621e7e805d0588a9ec8bbea9db10e95783e27e35bc12"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM agent_token_blacklist WHERE expires_at <= now() RETURNING token",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "995b194da28092d5aa053df936e7a9ee4b80cf3ade038a032c57ecff8fa3c6cf"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM workspace WHERE owner = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "9d488c5ba4b9f5203692721d76ec831f5954861a5576e0d8c1c42a9eca90927f"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT account FROM variable WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "account",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "c925264b7b0fd44ea7ab01c9af1514b9a9f2200e5a5db0a741697b28cd8b505f"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO agent_token_blacklist (token, expires_at, blacklisted_by) \n VALUES ($1, $2, $3) \n ON CONFLICT (token) DO UPDATE SET \n expires_at = EXCLUDED.expires_at,\n blacklisted_at = NOW(),\n blacklisted_by = EXCLUDED.blacklisted_by",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Timestamp",
"Varchar"
]
},
"nullable": []
},
"hash": "c9c040ec228a8fe4fda08439420141bee63339d4e3d5e2d68aabb12009f691c6"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM script WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "cb8bde4d92a020278cbae79c5c01a766c198392aceb38fb27e57b73de8f7f279"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26,\n CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $21, $22, $23, $24, $25, $26,\n CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
"describe": {
"columns": [],
"parameters": {
@@ -97,11 +97,10 @@
"Bool",
"Bool",
"JsonbArray",
"TextArray",
"Uuid"
"TextArray"
]
},
"nullable": []
},
"hash": "b7c3a66c3831eb5d145ff00807badae57bef81be051f150df754fd1444d7356d"
"hash": "cccdcb7fe7968eadfc04d8957a8e98b2f2d92a6d7f687a9dd5a70edb3d5a63e6"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow_leaf_jobs->$1::text AS \"leaf_jobs: Json<Box<RawValue>>\", v2_job.parent_job\n FROM v2_job_status\n LEFT JOIN v2_job ON v2_job.id = v2_job_status.id AND v2_job.workspace_id = $3\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = v2_job_status.id",
"query": "SELECT leaf_jobs->$1::text AS \"leaf_jobs: Json<Box<RawValue>>\", parent_job\n FROM v2_as_queue\n WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $2), $2) = id AND workspace_id = $3",
"describe": {
"columns": [
{
@@ -26,5 +26,5 @@
true
]
},
"hash": "b46a0fbebdc8e5e9852a06444b0aeaa4eaf67959e68b69eb2f0896ebe9244691"
"hash": "cf12a70e7b75ae471a0944de34502384be156cf25129f9c52bda34b240cf469a"
}

View File

@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token, expires_at, blacklisted_at, blacklisted_by \n FROM agent_token_blacklist \n WHERE expires_at > $1 \n ORDER BY blacklisted_at DESC",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamp"
},
{
"ordinal": 2,
"name": "blacklisted_at",
"type_info": "Timestamp"
},
{
"ordinal": 3,
"name": "blacklisted_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Timestamp"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "d56722c25877222af9affd5da5bb83b28fa8cbb528a2cfc90684cb10a69e4375"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n path,\n summary,\n description\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2 AND\n archived is FALSE\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "summary",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "dc36b46b9eb80cb7c92fa72519d117eda99a6f482a073ccd36a6431ef689a3fd"
}

View File

@@ -1,12 +0,0 @@
# Backend Development (Rust)
## Core Principles
- Follow @rust-best-practices.mdc for detailed guidelines
- Database schema reference: @summarized_schema.txt
- The API routes prefixes are all listed in windmill-api/src/lib.rs
## Adding New Features
1. Update database schema with migration if necessary
2. Update backend/windmill-api/openapi.yaml after modifying API endpoints

912
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.502.2"
version = "1.496.3"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.502.2"
version = "1.496.3"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -130,7 +130,6 @@ prometheus = { workspace = true, optional = true }
uuid.workspace = true
gethostname.workspace = true
serde_json.workspace = true
serde_yml.workspace = true
serde.workspace = true
deno_core = { workspace = true, optional = true }
object_store = { workspace = true, optional = true }
@@ -202,7 +201,6 @@ tower-http = { version = "^0.6", features = ["trace", "cors"] }
tower-cookies = "^0.10"
serde = "^1"
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
serde_yml = "0.0.12"
uuid = { version = "^1", features = ["serde", "v4"] }
thiserror = "^2"
anyhow = "^1"
@@ -232,7 +230,7 @@ php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb
cron = "^0"
mail-send = { version = "0.4.0", features = ["builder"], default-features=false }
urlencoding = "^2"
url = { version = "^2" , features = ["serde"]}
url = "^2"
async-oauth2 = "^0"
reqwest = { version = "^0.12", features = ["json", "stream", "gzip"] }
time = "^0"
@@ -329,7 +327,7 @@ jsonwebtoken = "8.3.0"
pem = "3.0.1"
nix = { version = "0.27.1", features = ["process", "signal"] }
tinyvector = { git = "https://github.com/windmill-labs/tinyvector", rev = "20823b94c20f2b9093f318badd24026cf54dcc85" }
hf-hub = "0.4.3"
hf-hub = "0.3.2"
tokenizers = "0.14.1"
candle-core = "0.9.1"
candle-transformers = "0.9.1"
@@ -348,7 +346,7 @@ nkeys = "0.4.4"
nu-parser = { version = "0.101.0", default-features = false }
datafusion = "47.0.0"
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure", "gcp"] }
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7", features = ["aws", "azure"] }
openidconnect = { version = "4.0.0-rc.1" }
aws-config = "^1"
aws-sdk-sqs = "1.57.0"

View File

@@ -1 +1 @@
21aabec96e91c8075dd637d1e32af90e495082fc
2c3e21f4573486628e0b8969ff478c237bd0283f

View File

@@ -4,4 +4,4 @@ DROP TYPE http_method;
ALTER TABLE script DROP COLUMN has_preprocessor;
DROP FUNCTION prevent_route_path_change();
DROP FUNCTION prevent_route_path_change();

View File

@@ -1,3 +0,0 @@
-- Remove email and span columns from audit table
ALTER TABLE audit DROP COLUMN email;
ALTER TABLE audit DROP COLUMN span;

View File

@@ -1,3 +0,0 @@
-- Add email and span columns to audit table
ALTER TABLE audit ADD COLUMN email VARCHAR(255);
ALTER TABLE audit ADD COLUMN span VARCHAR(255);

View File

@@ -1,4 +0,0 @@
-- Add down migration script here
ALTER TABLE http_trigger
DROP COLUMN summary,
DROP COLUMN description;

View File

@@ -1,5 +0,0 @@
-- Add up migration script here
ALTER TABLE http_trigger
ADD COLUMN summary VARCHAR(512) NULL,
ADD COLUMN description TEXT NULL;

View File

@@ -1,2 +0,0 @@
-- Remove agent token blacklist table
DROP TABLE IF EXISTS agent_token_blacklist;

View File

@@ -1,14 +0,0 @@
-- Add agent token blacklist table
CREATE TABLE agent_token_blacklist (
token VARCHAR PRIMARY KEY,
expires_at TIMESTAMP NOT NULL,
blacklisted_at TIMESTAMP NOT NULL DEFAULT NOW(),
blacklisted_by VARCHAR NOT NULL
);
-- Add index for efficient expiry cleanup
CREATE INDEX idx_agent_token_blacklist_expires_at ON agent_token_blacklist(expires_at);
-- Grant permissions to windmill users
GRANT ALL ON agent_token_blacklist TO windmill_user;
GRANT ALL ON agent_token_blacklist TO windmill_admin;

View File

@@ -30,6 +30,8 @@ use windmill_common::{
worker::PythonAnnotations,
};
const DEF_MAIN: &str = "def main(";
fn replace_import(x: String) -> String {
SHORT_IMPORTS_MAP
.get(&x)
@@ -46,9 +48,6 @@ lazy_static! {
static ref RE: Regex = Regex::new(r"^\#\s?(\S+)\s*$").unwrap();
static ref PIN_RE: Regex = Regex::new(r"(?:\s*#\s*(pin|repin):\s*)(\S*)").unwrap();
static ref PKG_RE: Regex = Regex::new(r"^([^!=<>]+)(?:[!=<>]|$)").unwrap();
// Regex to properly match main function definition at line start,
// capturing both sync and async variants
static ref DEF_MAIN_RE: Regex = Regex::new(r"(?m)^(async\s+)?def\s+main\s*\(").unwrap();
}
fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<NImport> {
@@ -144,12 +143,7 @@ struct ImportPin {
}
fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<NImport>> {
// Use regex to safely find the main function definition
let mut code = DEF_MAIN_RE
.split(code)
.next()
.unwrap_or_default()
.to_string();
let mut code = code.split(DEF_MAIN).next().unwrap_or("").to_string();
// remove main function decorator from end of file if it exists
if code
@@ -166,17 +160,10 @@ fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<NImport>>
+ "\n";
}
// Add a fake main function to ensure the parser can process the code correctly
// This is needed because we've split off the real main function above
let code_with_fake_main = format!("{}\n\ndef main(): pass", code);
let ast = Suite::parse(&code_with_fake_main, "main.py").map_err(|e| {
let ast = Suite::parse(&code, "main.py").map_err(|e| {
error::Error::ExecutionErr(format!("Error parsing code for imports: {}", e.to_string()))
})?;
// Note: We're still using the original code for finding pins,
// as the TextRange values from the parsed AST would be based on code_with_fake_main
// but we want to match against the original code
let find_pin = |range: TextRange, key: String| {
let hs = code
.chars()
@@ -391,7 +378,7 @@ async fn parse_python_imports_inner(
})
.join("\n")
.parse::<toml::Table>()
.map_err(to_anyhow)?;
.map_err(to_anyhow)?;
{
if let Some(v) = metadata.get("requires-python").and_then(|v| v.as_str()) {

View File

@@ -232,14 +232,7 @@ fn parse_typ(id: &str) -> Typ {
x @ _ if x.starts_with("DynSelect_") => {
Typ::DynSelect(x.strip_prefix("DynSelect_").unwrap().to_string())
}
_ => Typ::Resource(map_resource_name(id)),
}
}
fn map_resource_name(x: &str) -> String {
match x {
"S3Object" => "s3_object".to_string(),
_ => x.to_string(),
_ => Typ::Resource(id.to_string()),
}
}
@@ -475,7 +468,7 @@ def main(test1: str,
Arg {
otyp: None,
name: "s3o".to_string(),
typ: Typ::Resource("s3_object".to_string()),
typ: Typ::Resource("S3Object".to_string()),
default: None,
has_default: false,
oidx: None

View File

@@ -29,36 +29,12 @@ use wasm_bindgen::prelude::*;
struct ImportsFinder {
imports: HashSet<String>,
skip_type_only: bool,
}
impl Visit for ImportsFinder {
noop_visit_type!();
fn visit_import_decl(&mut self, n: &swc_ecma_ast::ImportDecl) {
if self.skip_type_only {
if n.type_only {
return;
}
if n.specifiers.len() > 0 {
let mut is_type_only = true;
for specifier in n.specifiers.iter() {
match specifier {
swc_ecma_ast::ImportSpecifier::Named(
swc_ecma_ast::ImportNamedSpecifier { is_type_only, .. },
) if *is_type_only => (),
_ => {
is_type_only = false;
break;
}
}
}
if is_type_only {
return;
}
}
}
if let Some(ref s) = n.src.raw {
let s = s.to_string();
if s.starts_with("'") && s.ends_with("'") {
@@ -70,7 +46,7 @@ impl Visit for ImportsFinder {
}
}
pub fn parse_expr_for_imports(code: &str, skip_type_only: bool) -> anyhow::Result<Vec<String>> {
pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<String>> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()).into(), code.into());
let mut tss = TsSyntax::default();
@@ -96,7 +72,7 @@ pub fn parse_expr_for_imports(code: &str, skip_type_only: bool) -> anyhow::Resul
anyhow::anyhow!("Error while parsing code, it is invalid TypeScript: {err_s}, {e:?}")
})?;
let mut visitor = ImportsFinder { imports: HashSet::new(), skip_type_only };
let mut visitor = ImportsFinder { imports: HashSet::new() };
visitor.visit_module(&expr);
let mut imports: Vec<_> = visitor.imports.into_iter().collect();
@@ -342,7 +318,7 @@ lazy_static::lazy_static! {
}
pub fn remove_pinned_imports(code: &str) -> anyhow::Result<String> {
let mut imports = parse_expr_for_imports(code, false)?;
let mut imports = parse_expr_for_imports(code)?;
imports.sort_by_key(|f| 0 - (f.len() as i32));
let mut content = code.to_string();
for import in imports {

View File

@@ -1,19 +0,0 @@
mod tests {
use windmill_parser_ts::parse_expr_for_imports;
#[test]
fn test_imports() {
let code = r#"
import { foo } from "bar";
import type { foo } from "bar2";
import { type foo, bar } from "bar3";
import { bar, type foo } from "bar7";
import { type foo, type bar } from "bar4";
import * as foo from "bar5";
import foo from "bar6";
"#;
let imports = parse_expr_for_imports(code, true).unwrap();
assert_eq!(imports, vec!["bar", "bar3", "bar5", "bar6", "bar7"]);
}
}

View File

@@ -41,7 +41,7 @@ pub fn parse_outputs(code: &str) -> String {
#[cfg(feature = "ts-parser")]
#[wasm_bindgen]
pub fn parse_ts_imports(code: &str) -> String {
let parsed = parse_expr_for_imports(code, false);
let parsed = parse_expr_for_imports(code);
let r = if let Ok(parsed) = parsed {
json!({ "imports": parsed })
} else {

View File

@@ -339,7 +339,7 @@ fn test_parse_imports() -> anyhow::Result<()> {
import { bar } from \"bar/foo/d\";
import { bar as baroof } from \"bar\";
";
let mut l = parse_expr_for_imports(code, false)?;
let mut l = parse_expr_for_imports(code)?;
l.sort();
assert_eq!(
l,
@@ -360,7 +360,7 @@ fn test_parse_imports_dts() -> anyhow::Result<()> {
let code = "
export type foo = number
";
let mut l = parse_expr_for_imports(code, false)?;
let mut l = parse_expr_for_imports(code)?;
l.sort();
assert_eq!(l, vec![] as Vec<String>);

View File

@@ -92,12 +92,6 @@ mod test {
fn test_snake_case() {
assert_eq!("s3", to_snake_case("S3"));
assert_eq!("s3", to_snake_case("s3"));
assert_eq!("s3_object", to_snake_case("S3Object"));
assert_eq!("s3_object", to_snake_case("S3object"));
assert_eq!("s3_object", to_snake_case("s3object"));
assert_eq!("abc", to_snake_case("ABC"));
assert_eq!("aa_bc", to_snake_case("AaBC"));
assert_eq!("a_b_c", to_snake_case("A_B_C"));
assert_eq!("s_3", to_snake_case("S_3"));
assert_eq!("type_name_here", to_snake_case("typeNameHere"));
}

View File

@@ -54,8 +54,7 @@ use windmill_common::{
stats_oss::schedule_stats,
triggers::TriggerKind,
utils::{
create_worker_suffix,
worker_name_with_suffix,
create_default_worker_suffix, create_ssh_agent_worker_suffix, worker_name_with_suffix,
Mode, GIT_VERSION, HOSTNAME, MODE_AND_ADDONS,
},
worker::{
@@ -348,7 +347,7 @@ async fn windmill_main() -> anyhow::Result<()> {
"Creating http client for cluster using base internal url {}",
std::env::var("BASE_INTERNAL_URL").unwrap_or_default()
);
let suffix = create_worker_suffix(&hostname);
let suffix = create_ssh_agent_worker_suffix(&hostname);
(
Connection::Http(build_agent_http_client(&suffix)),
Some(suffix),
@@ -688,7 +687,7 @@ Windmill Community Edition {GIT_VERSION}
let suffix = if i == 0 && first_suffix.is_some() {
first_suffix.as_ref().unwrap().clone()
} else {
create_worker_suffix(&hostname)
create_default_worker_suffix(&hostname)
};
let worker_conn = WorkerConn {
@@ -1103,9 +1102,6 @@ Windmill Community Edition {GIT_VERSION}
_ = tokio::time::sleep(Duration::from_secs(12 * 60 * 60)) => {
tracing::info!("Reloading config after 12 hours");
initial_load(&conn, tx.clone(), worker_mode, server_mode, #[cfg(feature = "parquet")] disable_s3_store).await;
if let Err(e) = reload_license_key(&conn).await {
tracing::error!("Failed to reload license key on agent: {e:#}");
}
#[cfg(feature = "enterprise")]
ee_oss::verify_license_key().await;
}

View File

@@ -840,24 +840,6 @@ pub async fn delete_expired_items(db: &DB) -> () {
tracing::error!("Error deleting audit log on CE: {:?}", e);
}
match sqlx::query_scalar!(
"DELETE FROM agent_token_blacklist WHERE expires_at <= now() RETURNING token",
)
.fetch_all(db)
.await
{
Ok(deleted_tokens) => {
if deleted_tokens.len() > 0 {
tracing::info!(
"deleted {} expired blacklisted agent tokens: {:?}",
deleted_tokens.len(),
deleted_tokens
);
}
}
Err(e) => tracing::error!("Error deleting expired blacklisted agent tokens: {:?}", e),
}
let job_retention_secs = *JOB_RETENTION_SECS.read().await;
if job_retention_secs > 0 {
match db.begin().await {
@@ -1926,7 +1908,6 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
&job.email,
&job.id,
None,
Some(format!("handle_zombie_jobs")),
)
.await
.expect("could not create job token");

View File

@@ -1,154 +0,0 @@
# This script is used to summarize the database schema.
# You can use pg_dump to dump the schema to a file.
# pg_dump --file "schema.sql" --host "localhost" --port "5432" --username "postgres" --no-password --format=c --large-objects --schema-only --no-owner --no-privileges --no-tablespaces --no-unlogged-table-data --no-comments --no-publications --no-subscriptions --no-security-labels --no-toast-compression --no-table-access-method --verbose --schema "public" "windmill"
# Then you can run python summarize_schema.py schema.sql to get the summarized schema.
import re
import sys
from collections import defaultdict
def summarize_schema(file_path):
"""
Parses a PostgreSQL dump file and extracts a summarized schema.
"""
tables = defaultdict(lambda: {'columns': [], 'pks': set(), 'fks': [], 'indexes': []})
enums = defaultdict(list)
# Use state variables to parse multi-line definitions
current_table = None
current_enum = None
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
# --- State Resets ---
if line.startswith(');'):
current_table = None
current_enum = None
continue
# --- Parse ENUM definitions ---
match_enum = re.match(r"CREATE TYPE public\.(\w+) AS ENUM \($", line)
if match_enum:
current_enum = match_enum.group(1)
continue
if current_enum:
# Extract enum values, which are typically like 'value',
value = line.strip("',")
if value and not value.startswith('--'):
enums[current_enum].append(value)
continue
# --- Parse TABLE definitions ---
match_table = re.match(r"CREATE TABLE public\.(\w+) \($", line)
if match_table:
current_table = match_table.group(1)
continue
if current_table:
# Parse columns within a CREATE TABLE block
# e.g., "column_name type NOT NULL,"
# e.g., "id bigint NOT NULL,"
match_column = re.match(r'^"?(\w+)"?\s+([\w\d\.\[\]\(\)]+)', line)
if match_column:
col_name = match_column.group(1)
col_type = match_column.group(2)
tables[current_table]['columns'].append(f"{col_name} ({col_type})")
# Parse PRIMARY KEY defined inside the table
match_pk = re.search(r"CONSTRAINT \w+ PRIMARY KEY \((.+)\)", line)
if match_pk:
# Handle multiple PK columns: "col1, col2, col3"
pk_cols = [p.strip().strip('"') for p in match_pk.group(1).split(',')]
tables[current_table]['pks'].update(pk_cols)
continue
# --- Parse Foreign Keys (defined outside CREATE TABLE) ---
match_fk = re.match(r"ALTER TABLE ONLY public\.(\w+)\s+ADD CONSTRAINT \w+ FOREIGN KEY \(([\w,\s\"]+)\) REFERENCES public\.(\w+)\(([\w,\s\"]+)\);", line)
if match_fk:
from_table, from_cols, to_table, to_cols = match_fk.groups()
# Clean up column names
from_cols_clean = ', '.join([c.strip().strip('"') for c in from_cols.split(',')])
to_cols_clean = ', '.join([c.strip().strip('"') for c in to_cols.split(',')])
fk_string = f"({from_cols_clean}) -> {to_table}({to_cols_clean})"
tables[from_table]['fks'].append(fk_string)
# --- Parse Index definitions ---
match_index = re.match(r"CREATE (UNIQUE )?INDEX (\w+) ON public\.(\w+) USING (\w+) \((.+)\);", line)
if match_index:
is_unique = match_index.group(1) is not None
index_name = match_index.group(2)
table_name = match_index.group(3)
index_type = match_index.group(4)
columns = match_index.group(5)
# Clean up column expressions
columns_clean = columns.replace('"', '')
unique_str = "UNIQUE " if is_unique else ""
index_string = f"{unique_str}INDEX {index_name} ({index_type}) ON ({columns_clean})"
tables[table_name]['indexes'].append(index_string)
return enums, tables
def format_output(enums, tables):
"""
Formats the parsed schema data into a clean, readable string.
"""
output = []
output.append("### Simplified Database Schema ###")
output.append("\n--- Custom Data Types (ENUMs) ---\n")
if not enums:
output.append("No custom ENUM types found.")
else:
for name, values in sorted(enums.items()):
output.append(f"{name}:")
for v in values:
output.append(f" - {v}")
output.append("")
output.append("\n--- Tables and Relationships ---\n")
if not tables:
output.append("No tables found.")
else:
for name, data in sorted(tables.items()):
output.append(f"TABLE: {name}")
for col in data['columns']:
col_name = col.split(' ')[0]
marker = " (PK)" if col_name in data['pks'] else ""
output.append(f" - {col}{marker}")
if data['fks']:
output.append(" Relationships:")
for fk in data['fks']:
output.append(f" - {fk}")
if data['indexes']:
output.append(" Indexes:")
for idx in data['indexes']:
output.append(f" - {idx}")
output.append("-" * 20)
return "\n".join(output)
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: python {sys.argv[0]} <path_to_dump.sql>")
sys.exit(1)
input_file = sys.argv[1]
try:
enums_data, tables_data = summarize_schema(input_file)
formatted_summary = format_output(enums_data, tables_data)
print(formatted_summary)
except FileNotFoundError:
print(f"Error: The file '{input_file}' was not found.")
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred: {e}")
sys.exit(1)

File diff suppressed because it is too large Load Diff

View File

@@ -322,7 +322,7 @@ mod suspend_resume {
let second = completed.next().await.unwrap();
// print_job(second, &db).await;
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None, None).await.unwrap();
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None).await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
))
@@ -427,7 +427,7 @@ mod suspend_resume {
/* ... and send a request resume it. */
let second = completed.next().await.unwrap();
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None, None).await.unwrap();
let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None).await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
))
@@ -935,7 +935,6 @@ impl RunJob {
/* user */ "test-user",
/* email */ "test@windmill.dev",
/* permissioned_as */ "u/test-user".to_string(),
/* token_prefix */ None,
/* scheduled_for_o */ None,
/* schedule_path */ None,
/* parent_job */ None,
@@ -2090,89 +2089,6 @@ def main():
assert_eq!(result, serde_json::json!("hello world"));
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base"))]
async fn test_python_global_site_packages(db: Pool<Postgres>) {
use windmill_common::{cache::concatcp, worker::ROOT_CACHE_DIR};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
// Shared for all 3.12.*
let path = concatcp!(ROOT_CACHE_DIR, "python_3_12/global-site-packages").to_owned();
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path + "/my_global_site_package_3_12_any.py", "").unwrap();
// 3.12
{
let content = r#"# py: ==3.12
#requirements:
#
import my_global_site_package_3_12_any
def main():
return "hello world"
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Python3,
lock: None,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
});
let result = run_job_in_new_worker_until_complete(&db, job, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("hello world"));
}
// 3.12.1
{
let content = r#"# py: ==3.12.1
#requirements:
#
import my_global_site_package_3_12_any
def main():
return "hello world"
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Python3,
lock: None,
custom_concurrency_key: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
});
let result = run_job_in_new_worker_until_complete(&db, job, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("hello world"));
}
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base"))]
async fn test_python_job_heavy_dep(db: Pool<Postgres>) {
@@ -4210,7 +4126,6 @@ async fn test_result_format(db: Pool<Postgres>) {
"",
&Uuid::nil(),
None,
None,
)
.await
.unwrap();

View File

@@ -1,18 +1,4 @@
#!/bin/bash
# Default directory
EE_DIR="../windmill-ee-private"
# Parse arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
--dir) EE_DIR="$2"; shift ;;
*) echo "Unknown parameter: $1"; exit 1 ;;
esac
shift
done
./substitute_ee_code.sh --dir "$EE_DIR"
./substitute_ee_code.sh --dir ../windmill-ee-private
# Check if running on macOS
if [[ "$(uname)" == "Darwin" ]]; then
@@ -32,4 +18,4 @@ if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' 's/^#samael = { version="0.0.14", features = \["xmlsec"\] }/samael = { version="0.0.14", features = ["xmlsec"] }/' Cargo.toml
# Comment out the git-based samael dependency
sed -i '' 's/^\(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/# \1/' Cargo.toml
fi
fi

View File

@@ -17,7 +17,7 @@ agent_worker_server = []
enterprise_saml = ["dep:samael", "dep:libxml"]
benchmark = []
embedding = ["dep:tinyvector", "dep:hf-hub", "dep:tokenizers", "dep:candle-core", "dep:candle-transformers", "dep:candle-nn"]
parquet = ["dep:datafusion", "dep:object_store", "windmill-common/parquet", "windmill-worker/parquet"]
parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/parquet", "windmill-worker/parquet"]
prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus", "windmill-worker/prometheus"]
openidconnect = ["dep:openidconnect", "windmill-common/openidconnect"]
tantivy = ["dep:windmill-indexer"]
@@ -76,7 +76,6 @@ hex.workspace = true
base64.workspace = true
base32.workspace = true
serde_urlencoded.workspace = true
serde_yml.workspace = true
cron.workspace = true
mime_guess.workspace = true
rust-embed = { workspace = true, optional = true }
@@ -103,7 +102,6 @@ prometheus = { workspace = true, optional = true }
async_zip = { workspace = true, optional = true }
regex.workspace = true
bytes.workspace = true
url.workspace = true
samael = { workspace = true, optional = true }
libxml = { workspace = true, optional = true }
async-recursion.workspace = true
@@ -118,6 +116,7 @@ candle-nn = { workspace = true, optional = true}
datafusion = { workspace = true, optional = true}
object_store = { workspace = true, optional = true}
openidconnect = { workspace = true, optional = true}
url = { workspace = true, optional = true}
jsonwebtoken = { workspace = true }
matchit = { workspace = true, optional = true }
tokio-tungstenite = { workspace = true, optional = true}
@@ -127,7 +126,6 @@ nkeys = { workspace = true, optional = true }
const_format.workspace = true
pin-project.workspace = true
http.workspace = true
indexmap.workspace = true
async-stream.workspace = true
ulid.workspace = true
rust-postgres = { workspace = true, optional = true }

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.502.2
version: 1.496.3
title: Windmill API
contact:
@@ -8451,51 +8451,6 @@ paths:
"201":
description: default error handler set
/w/{workspace}/openapi/generate:
post:
summary: generate openapi spec from http routes/webhook
operationId: generateOpenapiSpec
tags:
- openapi
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: openapi spec info and url
content:
application/json:
schema:
$ref: "#/components/schemas/GenerateOpenapiSpec"
responses:
"200":
description: openapi spec
content:
text/plain:
schema:
type: string
/w/{workspace}/openapi/download:
post:
summary: Download the OpenAPI v3.1 spec as a file
operationId: DownloadOpenapiSpec
tags:
- openapi
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: openapi spec info and url
content:
application/json:
schema:
$ref: "#/components/schemas/GenerateOpenapiSpec"
responses:
"200":
description: Downloaded OpenAPI spec
content:
application/octet-stream:
schema:
type: string
format: binary
/w/{workspace}/http_triggers/create_many:
post:
summary: create many HTTP triggers
@@ -11240,98 +11195,6 @@ paths:
schema:
type: string
/agent_workers/blacklist_token:
post:
summary: blacklist agent token (requires super admin)
operationId: blacklistAgentToken
tags:
- agent_workers
requestBody:
description: token to blacklist
required: true
content:
application/json:
schema:
type: object
properties:
token:
type: string
description: The agent token to blacklist
expires_at:
type: string
format: date-time
description: Optional expiration date for the blacklist entry
required:
- token
responses:
"200":
description: token blacklisted successfully
/agent_workers/remove_blacklist_token:
post:
summary: remove agent token from blacklist (requires super admin)
operationId: removeBlacklistAgentToken
tags:
- agent_workers
requestBody:
description: token to remove from blacklist
required: true
content:
application/json:
schema:
type: object
properties:
token:
type: string
description: The agent token to remove from blacklist
required:
- token
responses:
"200":
description: token removed from blacklist successfully
/agent_workers/list_blacklisted_tokens:
get:
summary: list blacklisted agent tokens (requires super admin)
operationId: listBlacklistedAgentTokens
tags:
- agent_workers
parameters:
- name: include_expired
in: query
description: Whether to include expired blacklisted tokens
schema:
type: boolean
default: false
responses:
"200":
description: list of blacklisted tokens
content:
application/json:
schema:
type: array
items:
type: object
properties:
token:
type: string
description: The blacklisted token (without prefix)
expires_at:
type: string
format: date-time
description: When the blacklist entry expires
blacklisted_at:
type: string
format: date-time
description: When the token was blacklisted
blacklisted_by:
type: string
description: Email of the user who blacklisted the token
required:
- token
- expires_at
- blacklisted_at
- blacklisted_by
/w/{workspace}/acls/get/{kind}/{path}:
get:
@@ -14304,8 +14167,6 @@ components:
type: string
parameters:
type: object
span:
type: string
required:
- id
- timestamp
@@ -14893,106 +14754,6 @@ components:
- custom_script
- signature
RunnableKind:
type: string
enum:
- script
- flow
OpenapiSpecFormat:
type: string
enum:
- yaml
- json
OpenapiHttpRouteFilters:
type: object
properties:
folder_regex:
type: string
path_regex:
type: string
route_path_regex:
type: string
required:
- folder_regex
- path_regex
- route_path_regex
WebhookFilters:
type: object
properties:
user_or_folder_regex:
type: string
enum:
- "*"
- u
- f
user_or_folder_regex_value:
type: string
path:
type: string
runnable_kind:
$ref: "#/components/schemas/RunnableKind"
required:
- user_or_folder_regex
- user_or_folder_regex_value
- path
- runnable_kind
OpenapiV3Info:
type: object
properties:
title:
type: string
version:
type: string
description:
type: string
terms_of_service:
type: string
contact:
type: object
properties:
name:
type: string
url:
type: string
email:
type: string
license:
type: object
properties:
name:
type: string
identifier:
type: string
url:
type: string
required:
- name
required:
- title
- version
GenerateOpenapiSpec:
type: object
properties:
info:
$ref: "#/components/schemas/OpenapiV3Info"
url:
type: string
openapi_spec_format:
$ref: "#/components/schemas/OpenapiSpecFormat"
http_route_filters:
type: array
items:
$ref: "#/components/schemas/OpenapiHttpRouteFilters"
webhook_filters:
type: array
items:
$ref: "#/components/schemas/WebhookFilters"
HttpMethod:
type: string
enum:
@@ -15024,10 +14785,6 @@ components:
$ref: "#/components/schemas/HttpMethod"
authentication_resource_path:
type: string
summary:
type: string
description:
type: string
is_async:
type: boolean
authentication_method:
@@ -15062,10 +14819,6 @@ components:
type: string
workspaced_route:
type: boolean
summary:
type: string
description:
type: string
static_asset_config:
type: object
properties:
@@ -15113,10 +14866,6 @@ components:
type: string
route_path:
type: string
summary:
type: string
description:
type: string
workspaced_route:
type: boolean
static_asset_config:
@@ -16565,13 +16314,11 @@ components:
properties:
type:
type: string
enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc", "GoogleCloudStorage"]
enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"]
s3_resource_path:
type: string
azure_blob_resource_path:
type: string
gcs_resource_path:
type: string
public_resource:
type: boolean
secondary_storage:
@@ -16582,13 +16329,11 @@ components:
type:
type: string
enum:
["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc", "GoogleCloudStorage"]
["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"]
s3_resource_path:
type: string
azure_blob_resource_path:
type: string
gcs_resource_path:
type: string
public_resource:
type: boolean

View File

@@ -16,6 +16,9 @@ use crate::db::DB;
#[cfg(not(feature = "private"))]
use axum::Router;
#[cfg(not(feature = "private"))]
use serde::{Deserialize, Serialize};
#[cfg(not(feature = "private"))]
pub fn global_service() -> Router {
Router::new()
@@ -41,6 +44,15 @@ pub fn workspaced_service(
(router, vec![], Some(job_completed_tx))
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg(not(feature = "private"))]
pub struct AgentAuth {
pub worker_group: String,
pub suffix: Option<String>,
pub tags: Vec<String>,
pub exp: Option<usize>,
}
#[cfg(not(feature = "private"))]
pub struct AgentCache {}

View File

@@ -4,7 +4,7 @@ use crate::{
};
use axum::{body::Bytes, extract::Path, response::IntoResponse, routing::post, Extension, Router};
use http::{HeaderMap, Method};
use http::HeaderMap;
use quick_cache::sync::Cache;
use reqwest::{Client, RequestBuilder};
use serde::{Deserialize, Serialize};
@@ -141,8 +141,6 @@ impl AIRequestConfig {
self,
provider: &AIProvider,
path: &str,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> Result<RequestBuilder> {
let body = if let Some(user) = self.user {
@@ -155,9 +153,8 @@ impl AIRequestConfig {
let is_azure = matches!(provider, AIProvider::OpenAI) && base_url != OPENAI_BASE_URL
|| matches!(provider, AIProvider::AzureOpenAI);
let is_anthropic = matches!(provider, AIProvider::Anthropic);
let url = if is_azure && method != Method::GET {
let url = if is_azure {
if base_url.ends_with("/deployments") {
let model = Self::get_azure_model(&body)?;
format!("{}/{}/{}", base_url, model, path)
@@ -174,16 +171,9 @@ impl AIRequestConfig {
tracing::debug!("AI request URL: {}", url);
let mut request = HTTP_CLIENT
.request(method, url)
.header("content-type", "application/json");
for (header_name, header_value) in headers.iter() {
if header_name.to_string().starts_with("anthropic-") {
request = request.header(header_name, header_value);
}
}
request = request.body(body);
.post(url)
.header("content-type", "application/json")
.body(body);
if is_azure {
request = request.query(&[("api-version", AZURE_API_VERSION)])
@@ -191,12 +181,9 @@ impl AIRequestConfig {
if let Some(api_key) = self.api_key {
if is_azure {
request = request.header("api-key", api_key.clone())
request = request.header("api-key", api_key)
} else {
request = request.header("authorization", format!("Bearer {}", api_key.clone()))
}
if is_anthropic {
request = request.header("X-API-Key", api_key);
request = request.header("authorization", format!("Bearer {}", api_key))
}
}
@@ -352,18 +339,17 @@ pub struct AIConfig {
}
pub fn global_service() -> Router {
Router::new().route("/proxy/*ai", post(global_proxy).get(global_proxy))
Router::new().route("/proxy/*ai", post(global_proxy))
}
pub fn workspaced_service() -> Router {
Router::new().route("/proxy/*ai", post(proxy).get(proxy))
Router::new().route("/proxy/*ai", post(proxy))
}
async fn global_proxy(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(ai_path): Path<String>,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
@@ -388,7 +374,7 @@ async fn global_proxy(
let url = format!("{}/{}", base_url, ai_path);
let request = HTTP_CLIENT
.request(method, url)
.post(url)
.header("content-type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(body);
@@ -424,7 +410,6 @@ async fn proxy(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path((w_id, ai_path)): Path<(String, String)>,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
@@ -507,7 +492,7 @@ async fn proxy(
}
};
let request = request_config.prepare_request(&provider, &ai_path, method, headers, body)?;
let request = request_config.prepare_request(&provider, &ai_path, body)?;
let response = request.send().await.map_err(to_anyhow)?;

View File

@@ -4,7 +4,6 @@ use uuid::Uuid;
use std::str::FromStr;
use regex::Regex;
use serde_json::Value;
use crate::auth::OptTokened;
use crate::db::{ApiAuthed, DB};
use crate::jobs::{cancel_suspended_job, resume_suspended_job, QueryApprover, QueryOrBody, ResumeUrls, get_resume_urls_internal};
use axum::{extract::{Path, Query}, Extension};
@@ -102,7 +101,6 @@ pub fn extract_w_id_from_resume_url(resume_url: &str) -> Result<&str, Error> {
pub async fn handle_resume_action(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
db: DB,
resume_url: &str,
form_data: Value,
@@ -137,7 +135,6 @@ pub async fn handle_resume_action(
let res = if action == "resume" {
resume_suspended_job(
authed,
opt_tokened,
Extension(db.clone()),
Path((
w_id.to_string(),
@@ -152,7 +149,6 @@ pub async fn handle_resume_action(
} else {
cancel_suspended_job(
authed,
opt_tokened,
Extension(db.clone()),
Path((
w_id.to_string(),

View File

@@ -9,11 +9,10 @@ use std::{collections::HashMap, sync::Arc};
*/
use crate::{
auth::OptTokened,
db::{ApiAuthed, DB},
resources::get_resource_value_interpolated_internal,
users::{require_owner_of_path, OptAuthed},
utils::WithStarredInfoQuery,
utils::{RunnableKind, WithStarredInfoQuery},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
@@ -53,7 +52,6 @@ use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
apps::{AppScriptId, ListAppQuery},
auth::TOKEN_PREFIX_LEN,
cache::{self, future::FutureCachedExt},
db::UserDB,
error::{to_anyhow, Error, JsonResult, Result},
@@ -61,7 +59,7 @@ use windmill_common::{
users::username_to_permissioned_as,
utils::{
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin,
Pagination, RunnableKind, StripPath,
Pagination, StripPath,
},
variables::{build_crypt, build_crypt_with_key_suffix, encrypt},
worker::{to_raw_value, CLOUD_HOSTED},
@@ -919,23 +917,6 @@ async fn create_app_internal<'a>(
raw_app: bool,
mut app: CreateApp,
) -> Result<(sqlx::Transaction<'a, sqlx::Postgres>, String, i64)> {
if *CLOUD_HOSTED {
let nb_apps =
sqlx::query_scalar!("SELECT COUNT(*) FROM app WHERE workspace_id = $1", &w_id)
.fetch_one(&db)
.await?;
if nb_apps.unwrap_or(0) >= 1000 {
return Err(Error::BadRequest(
"You have reached the maximum number of apps (1000) on cloud. Contact support@windmill.dev to increase the limit"
.to_string(),
));
}
if app.summary.len() > 300 {
return Err(Error::BadRequest(
"Summary must be less than 300 characters on cloud".to_string(),
));
}
}
let mut tx = user_db.clone().begin(&authed).await?;
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
app.policy.on_behalf_of_email = Some(authed.email.clone());
@@ -1042,7 +1023,6 @@ async fn create_app_internal<'a>(
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -1414,7 +1394,6 @@ async fn update_app_internal<'a>(
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -1521,7 +1500,6 @@ fn empty_triggerables(mut policy: Policy) -> Policy {
async fn execute_component(
OptAuthed(opt_authed): OptAuthed,
tokened: OptTokened,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -1724,10 +1702,6 @@ async fn execute_component(
&username,
email,
permissioned_as,
opt_authed
.and_then(|a| a.token_prefix)
.or_else(|| tokened.token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string()))
.as_deref(),
None,
None,
None,
@@ -2037,7 +2011,7 @@ async fn upload_s3_file_from_app(
if !has_unnamed_policy {
return Err(Error::BadRequest(
"no policy found for unnamed s3 file upload".to_string(),
"no policy found for unnamed s3 file uplooad".to_string(),
));
}

View File

@@ -21,7 +21,7 @@ use std::sync::{
use tokio::sync::RwLock;
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, TOKEN_PREFIX_LEN},
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims},
jwt,
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
};
@@ -34,11 +34,10 @@ lazy_static::lazy_static! {
// Global function to invalidate a specific token from cache
pub fn invalidate_token_from_cache(token: &str) {
// Remove all cache entries for this token (across all workspaces)
AUTH_CACHE.retain(|(_workspace_id, cached_token), _cached_value| cached_token != token);
tracing::info!(
"Invalidated token from auth cache: {}...",
&token[..token.len().min(8)]
);
AUTH_CACHE.retain(|(_workspace_id, cached_token), _cached_value| {
cached_token != token
});
tracing::info!("Invalidated token from auth cache: {}...", &token[..token.len().min(8)]);
}
#[derive(Clone)]
@@ -134,7 +133,6 @@ impl AuthCache {
folders: claims.folders,
scopes: None,
username_override,
token_prefix: claims.audit_span,
};
AUTH_CACHE.insert(
@@ -218,7 +216,6 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
} else {
let groups = vec![name.to_string()];
@@ -240,7 +237,6 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
} else {
@@ -255,7 +251,6 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
}
@@ -303,7 +298,6 @@ impl AuthCache {
folders,
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
None if super_admin => Some(ApiAuthed {
@@ -315,7 +309,6 @@ impl AuthCache {
folders: vec![],
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
}),
None => None,
}
@@ -329,7 +322,6 @@ impl AuthCache {
folders: Vec::new(),
scopes,
username_override,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
}
}
@@ -362,7 +354,6 @@ impl AuthCache {
folders: Vec::new(),
scopes: None,
username_override: None,
token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()),
})
} else {
None
@@ -491,27 +482,6 @@ where
}
}
fn maybe_get_workspace_id_from_path(path_vec: &[&str]) -> Option<String> {
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[1] == "api"
&& path_vec[2] == "mcp"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_owned())
} else {
if path_vec.len() >= 5 && path_vec[0] == "" && path_vec[2] == "srch" && path_vec[3] == "w" {
Some(path_vec[4].to_owned())
} else {
None
}
};
workspace_id
}
#[async_trait]
impl<S> FromRequestParts<S> for ApiAuthed
where
@@ -524,62 +494,85 @@ where
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
if parts.method == http::Method::OPTIONS {
return Ok(ApiAuthed::default());
return Ok(ApiAuthed {
email: "".to_owned(),
username: "".to_owned(),
is_admin: false,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
});
};
let already_authed = parts.extensions.get::<ApiAuthed>();
if let Some(authed) = already_authed {
return Ok(authed.clone());
}
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
Ok(authed.clone())
} else {
extract_token(parts, state).await
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(parts, state).await
};
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[1] == "api"
&& path_vec[2] == "mcp"
&& path_vec[3] == "w"
{
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = maybe_get_workspace_id_from_path(&path_vec);
Some(path_vec[4].to_string())
} else {
if path_vec.len() >= 5
&& path_vec[0] == ""
&& path_vec[2] == "srch"
&& path_vec[3] == "w"
{
Some(path_vec[4].to_string())
} else {
None
}
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
parts.extensions.insert(authed.clone());
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
}
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
if authed.scopes.as_ref().is_some_and(|scopes| {
scopes
.iter()
.any(|s| s.starts_with("jobs:") || s.starts_with("run:"))
}) && (path_vec.len() < 3
|| (path_vec[4] != "jobs" && path_vec[4] != "jobs_u"))
{
BRUTE_FORCE_COUNTER.increment().await;
return Err((
StatusCode::UNAUTHORIZED,
format!("Unauthorized scoped token: {:?}", authed.scopes),
));
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
}
parts.extensions.insert(authed.clone());
Span::current().record("username", &authed.username.as_str());
Span::current().record("email", &authed.email);
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
return Ok(authed);
}
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
BRUTE_FORCE_COUNTER.increment().await;
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
}
}

View File

@@ -64,6 +64,7 @@ use crate::{
args::RawWebhookArgs,
db::{ApiAuthed, DB},
users::fetch_api_authed,
utils::RunnableKind,
};
use axum::{
@@ -81,7 +82,7 @@ use windmill_common::{
db::UserDB,
error::{JsonResult, Result},
triggers::{RunnableFormat, RunnableFormatVersion, TriggerKind},
utils::{not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
utils::{not_found_if_none, paginate, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};

View File

@@ -21,7 +21,7 @@ use windmill_common::{
DB,
};
use crate::{db::ApiAuthed, utils::{require_devops_role}};
use crate::{db::ApiAuthed, utils::require_super_admin};
pub fn global_service() -> Router {
Router::new()
@@ -103,7 +103,7 @@ async fn get_config(
Path(name): Path<String>,
Extension(db): Extension<DB>,
) -> error::JsonResult<Option<serde_json::Value>> {
require_devops_role(&db, &authed.email).await?;
require_super_admin(&db, &authed.email).await?;
let config = sqlx::query_as!(Config, "SELECT * FROM config WHERE name = $1", name)
.fetch_optional(&db)
@@ -119,7 +119,7 @@ async fn update_config(
authed: ApiAuthed,
Json(config): Json<serde_json::Value>,
) -> error::Result<String> {
require_devops_role(&db, &authed.email).await?;
require_super_admin(&db, &authed.email).await?;
#[cfg(not(feature = "enterprise"))]
if name.starts_with("worker__") {
@@ -157,7 +157,7 @@ async fn delete_config(
Extension(db): Extension<DB>,
authed: ApiAuthed,
) -> error::Result<String> {
require_devops_role(&db, &authed.email).await?;
require_super_admin(&db, &authed.email).await?;
let mut tx = db.begin().await?;
@@ -232,7 +232,7 @@ async fn list_configs(
authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> error::JsonResult<Vec<Config>> {
require_devops_role(&db, &authed.email).await?;
require_super_admin(&db, &authed.email).await?;
let configs = sqlx::query_as!(Config, "SELECT name, config FROM config")
.fetch_all(&db)
.await?;

View File

@@ -812,20 +812,10 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
.execute(db)
.await?;
});
run_windmill_migration!("audit_recent_login_activities", db, |tx| {
sqlx::query!(
"CREATE INDEX CONCURRENTLY idx_audit_recent_login_activities
ON audit (timestamp, username)
WHERE operation IN ('users.login', 'oauth.login', 'users.token.refresh');"
)
.execute(db)
.await?;
});
Ok(())
}
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq)]
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct ApiAuthed {
pub email: String,
pub username: String,
@@ -836,7 +826,6 @@ pub struct ApiAuthed {
pub folders: Vec<(String, bool, bool)>,
pub scopes: Option<Vec<String>>,
pub username_override: Option<String>,
pub token_prefix: Option<String>,
}
impl From<ApiAuthed> for Authed {
@@ -849,7 +838,6 @@ impl From<ApiAuthed> for Authed {
groups: value.groups,
folders: value.folders,
scopes: value.scopes,
token_prefix: value.token_prefix,
}
}
}
@@ -860,7 +848,6 @@ impl From<&ApiAuthed> for AuditAuthor {
email: value.email.clone(),
username: value.username.clone(),
username_override: value.username_override.clone(),
token_prefix: value.token_prefix.clone(),
}
}
}
@@ -881,9 +868,6 @@ impl AuditAuthorable for ApiAuthed {
fn username_override(&self) -> Option<&str> {
self.username_override.as_deref()
}
fn token_prefix(&self) -> Option<&str> {
self.token_prefix.as_deref()
}
}
impl Authable for ApiAuthed {

View File

@@ -24,7 +24,7 @@ use candle_nn::VarBuilder;
#[cfg(feature = "embedding")]
use candle_transformers::models::bert::{BertModel, Config, DTYPE};
#[cfg(feature = "embedding")]
use hf_hub::api::tokio::Api;
use hf_hub::{api::sync::Api, Cache, Repo};
#[cfg(feature = "embedding")]
use serde::Deserialize;
#[cfg(feature = "embedding")]
@@ -158,22 +158,63 @@ pub struct ModelInstance {
#[cfg(feature = "embedding")]
impl ModelInstance {
pub async fn load_model_files() -> Result<(PathBuf, PathBuf, PathBuf)> {
let api = Api::new()?;
let repo_api = api.model("thenlper/gte-small".to_string());
let repo = Repo::model("thenlper/gte-small".to_string());
let (config_filename, tokenizer_filename, weights_filename) =
(
repo_api
.get("config.json")
.await
.map_err(|e| anyhow!("Failed to get config.json from hugging face: {}", e))?,
repo_api.get("tokenizer.json").await.map_err(|e| {
anyhow!("Failed to get tokenizer.json from hugging face: {}", e)
})?,
repo_api.get("model.safetensors").await.map_err(|e| {
anyhow!("Failed to get model.safetensors from hugging face: {}", e)
})?,
);
let cache = Cache::default().repo(repo.clone());
let api = Api::new()?;
let api = api.repo(repo);
let (config_filename, tokenizer_filename, weights_filename) = (
cache
.get("config.json")
.or_else(|| {
api.get("config.json")
.or_else(|e| {
tracing::error!("Failed to get config.json from hugging face: {}", e);
return Err(e);
})
.ok()
})
.ok_or(Error::msg("could not get config.json"))?,
cache
.get("tokenizer.json")
.or_else(|| {
api.get("tokenizer.json")
.or_else(|e| {
tracing::error!(
"Failed to get tokenizer.json from hugging face: {}",
e
);
return Err(e);
})
.ok()
})
.ok_or(Error::msg("could not get tokenizer.json"))?,
cache
.get("model.safetensors")
.and_then(|p| {
tracing::info!("Found embedding model in cache");
Some(p)
})
.or_else(|| {
tracing::info!("Downloading embedding model...");
api.get("model.safetensors")
.or_else(|e| {
tracing::error!(
"Failed to get model.safetensors from hugging face: {}",
e
);
return Err(e);
})
.ok()
.and_then(|p| {
tracing::info!("Downloaded embedding model");
Some(p)
})
})
.ok_or(Error::msg("could not get model.safetensors"))?,
);
Ok((config_filename, tokenizer_filename, weights_filename))
}

View File

@@ -12,7 +12,7 @@ use crate::db::ApiAuthed;
use crate::triggers::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
};
use crate::utils::WithStarredInfoQuery;
use crate::utils::{RunnableKind, WithStarredInfoQuery};
use crate::{
db::DB,
schedule::clear_schedule,
@@ -34,7 +34,7 @@ use sqlx::{FromRow, Postgres, Transaction};
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::utils::query_elems_from_hub;
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
use windmill_common::worker::to_raw_value;
use windmill_common::HUB_BASE_URL;
use windmill_common::{
db::UserDB,
@@ -43,7 +43,7 @@ use windmill_common::{
jobs::JobPayload,
schedule::Schedule,
scripts::Schema,
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, StripPath},
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel};
@@ -358,32 +358,6 @@ async fn create_flow(
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> {
if *CLOUD_HOSTED {
let nb_flows =
sqlx::query_scalar!("SELECT COUNT(*) FROM flow WHERE workspace_id = $1", &w_id)
.fetch_one(&db)
.await?;
if nb_flows.unwrap_or(0) >= 1000 {
return Err(Error::BadRequest(
"You have reached the maximum number of flows (1000) on cloud. Contact support@windmill.dev to increase the limit"
.to_string(),
));
}
if nf.summary.len() > 300 {
return Err(Error::BadRequest(
"Summary must be less than 300 characters on cloud".to_string(),
));
}
if nf
.description
.as_ref()
.is_some_and(|desc| desc.len() > 3000)
{
return Err(Error::BadRequest(
"Description must be less than 3000 characters on cloud".to_string(),
));
}
}
#[cfg(not(feature = "enterprise"))]
if nf
.value
@@ -494,7 +468,6 @@ async fn create_flow(
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -504,7 +477,7 @@ async fn create_flow(
false,
None,
true,
None,
nf.tag,
None,
None,
None,
@@ -936,7 +909,6 @@ async fn update_flow(
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,

View File

@@ -441,8 +441,8 @@ pub struct BasicAuthAuthentication {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ApiKeyAuthentication {
pub api_key_header: String,
pub api_key_secret: String,
api_key_header: String,
api_key_secret: String,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize)]

View File

@@ -44,7 +44,7 @@ use windmill_common::{
error::{self, JsonResult},
s3_helpers::S3Object,
triggers::TriggerKind,
utils::{empty_as_none, not_found_if_none, paginate, require_admin, Pagination, StripPath},
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::CLOUD_HOSTED,
};
use windmill_git_sync::handle_deployment_metadata;
@@ -114,8 +114,6 @@ struct NewTrigger {
static_asset_config: Option<sqlx::types::Json<S3Object>>,
http_method: HttpMethod,
workspaced_route: Option<bool>,
summary: Option<String>,
description: Option<String>,
is_static_website: bool,
wrap_body: Option<bool>,
raw_string: Option<bool>,
@@ -136,8 +134,6 @@ pub struct HttpTrigger {
pub is_async: bool,
pub authentication_method: AuthenticationMethod,
pub http_method: HttpMethod,
pub summary: Option<String>,
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub static_asset_config: Option<sqlx::types::Json<S3Object>>,
pub is_static_website: bool,
@@ -157,8 +153,6 @@ struct EditTrigger {
authentication_method: AuthenticationMethod,
#[serde(deserialize_with = "non_empty_str")]
authentication_resource_path: Option<String>,
summary: Option<String>,
description: Option<String>,
http_method: HttpMethod,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
workspaced_route: Option<bool>,
@@ -173,7 +167,6 @@ pub struct ListTriggerQuery {
pub per_page: Option<usize>,
pub path: Option<String>,
pub is_flow: Option<bool>,
#[serde(default, deserialize_with = "empty_as_none")]
pub path_start: Option<String>,
}
@@ -195,8 +188,6 @@ async fn list_triggers(
"wrap_body",
"raw_string",
"script_path",
"summary",
"description",
"is_flow",
"http_method",
"edited_by",
@@ -251,8 +242,6 @@ async fn get_trigger(
route_path_key,
workspaced_route,
script_path,
summary,
description,
is_flow,
http_method as "http_method: _",
edited_by,
@@ -328,8 +317,6 @@ async fn create_trigger_inner(
wrap_body,
raw_string,
script_path,
summary,
description,
is_flow,
is_async,
authentication_method,
@@ -341,7 +328,7 @@ async fn create_trigger_inner(
is_static_website
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, now(), $19
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17
)
"#,
w_id,
@@ -353,8 +340,6 @@ async fn create_trigger_inner(
new_http_trigger.wrap_body.unwrap_or(false),
new_http_trigger.raw_string.unwrap_or(false),
new_http_trigger.script_path,
new_http_trigger.summary,
new_http_trigger.description,
new_http_trigger.is_flow,
new_http_trigger.is_async,
new_http_trigger.authentication_method as _,
@@ -390,11 +375,7 @@ fn check_no_duplicates<'trigger>(
let mut seen = HashSet::with_capacity(new_http_triggers.len());
for (i, trigger) in new_http_triggers.iter().enumerate() {
if !seen.insert((
&route_path_key[i],
trigger.http_method,
trigger.workspaced_route,
)) {
if !seen.insert((&route_path_key[i], trigger.http_method, trigger.workspaced_route)) {
return Err(Error::BadRequest(format!(
"Duplicate HTTP route detected: '{}'. Each HTTP route must have a unique 'route_path'.",
&trigger.route_path
@@ -613,13 +594,11 @@ async fn update_trigger(
email = $13,
is_async = $14,
authentication_method = $15,
summary = $16,
description = $17,
edited_at = now(),
is_static_website = $18
is_static_website = $16
WHERE
workspace_id = $19 AND
path = $20
workspace_id = $17 AND
path = $18
"#,
route_path,
&route_path_key,
@@ -636,8 +615,6 @@ async fn update_trigger(
&authed.email,
ct.is_async,
ct.authentication_method as _,
ct.summary,
ct.description,
ct.is_static_website,
w_id,
path,
@@ -1170,8 +1147,8 @@ async fn route_job(
.map_err(|e| e.into_response())?;
if trigger.script_path.is_empty() && trigger.static_asset_config.is_none() {
return Err(Error::NotFound(format!(
"Runnable path of HTTP route at path: {}",
return Err(Error::BadRequest(format!(
"Script path of HTTP route at path: {} must not be empty",
trigger.path
))
.into_response());
@@ -1221,7 +1198,7 @@ async fn route_job(
let auth_method = try_get_resource_from_db_as::<
crate::http_trigger_auth::AuthenticationMethod,
>(
&authed,
authed.clone(),
Some(user_db.clone()),
&db,
&resource_path,

View File

@@ -20,20 +20,22 @@ 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;
#[cfg(feature = "prometheus")]
use tokio::time::Instant;
use tower::ServiceBuilder;
use windmill_common::auth::{is_super_admin_email, TOKEN_PREFIX_LEN};
use windmill_common::auth::is_super_admin_email;
use windmill_common::error::JsonResult;
use windmill_common::flow_status::{JobResult, RestartedFrom};
use windmill_common::jobs::{format_completed_job_result, format_result, ENTRYPOINT_OVERRIDE};
use windmill_common::utils::WarnAfterExt;
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
use windmill_common::variables::get_workspace_key;
use crate::add_webhook_allowed_origin;
use crate::auth::{OptTokened, Tokened};
use crate::concurrency_groups::join_concurrency_key;
use crate::db::ApiAuthed;
@@ -82,6 +84,9 @@ use windmill_common::{
},
};
#[cfg(feature = "prometheus")]
use windmill_common::{METRICS_DEBUG_ENABLED, METRICS_ENABLED};
use windmill_common::{
get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path,
get_script_info_for_hash, FlowVersionInfo, ScriptHashInfo, BASE_URL,
@@ -91,6 +96,36 @@ use windmill_queue::{
PushArgsOwned, PushIsolationLevel,
};
#[cfg(feature = "prometheus")]
type Histo = prometheus::Histogram;
#[cfg(not(feature = "prometheus"))]
type Histo = ();
#[cfg(feature = "prometheus")]
fn setup_list_jobs_debug_metrics() -> Option<Histo> {
let api_list_jobs_query_duration = if METRICS_DEBUG_ENABLED.load(Ordering::Relaxed)
&& METRICS_ENABLED.load(Ordering::Relaxed)
{
Some(
prometheus::register_histogram!(prometheus::HistogramOpts::new(
"api_list_jobs_query_duration",
"Duration of listing jobs (query)",
))
.expect("register prometheus metric"),
)
} else {
None
};
api_list_jobs_query_duration
}
#[cfg(not(feature = "prometheus"))]
fn setup_list_jobs_debug_metrics() -> Option<Histo> {
None
}
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST])
@@ -101,6 +136,8 @@ pub fn workspaced_service() -> Router {
let ce_headers =
ServiceBuilder::new().layer(axum::middleware::from_fn(add_webhook_allowed_origin));
let api_list_jobs_query_duration = setup_list_jobs_debug_metrics();
Router::new()
.route(
"/run/f/*script_path",
@@ -175,7 +212,10 @@ pub fn workspaced_service() -> Router {
)
.route("/add_batch_jobs/:n", post(add_batch_jobs))
.route("/run/preview_flow", post(run_preview_flow_job))
.route("/list", get(list_jobs))
.route(
"/list",
get(list_jobs).layer(Extension(api_list_jobs_query_duration)),
)
.route(
"/list_selected_job_groups",
// We use post because sending a huge array as a query param can produce
@@ -294,7 +334,6 @@ struct JsonPath {
}
async fn get_result_by_id(
authed: ApiAuthed,
tokened: Tokened,
Extension(db): Extension<DB>,
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>,
Query(JsonPath { json_path, .. }): Query<JsonPath>,
@@ -303,7 +342,7 @@ async fn get_result_by_id(
windmill_queue::get_result_by_id(db.clone(), w_id.clone(), flow_id, node_id, json_path)
.await?;
log_job_view(&db, Some(&authed), Some(&tokened.token), &w_id, &flow_id).await?;
log_job_view(&db, Some(&authed), &w_id, &flow_id).await?;
Ok(Json(res))
}
@@ -339,7 +378,6 @@ async fn get_db_clock(Extension(db): Extension<DB>) -> windmill_common::error::J
async fn cancel_job_api(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Json(CancelJob { reason }): Json<CancelJob>,
@@ -352,7 +390,6 @@ async fn cancel_job_api(
username: "anonymous".to_string(),
username_override: None,
email: "anonymous".to_string(),
token_prefix: opt_tokened.token.map(|s| s[0..TOKEN_PREFIX_LEN].to_string()),
},
};
let (mut tx, job_option) = tokio::time::timeout(
@@ -451,7 +488,6 @@ async fn cancel_persistent_script_api(
async fn force_cancel(
OptAuthed(opt_authed): OptAuthed,
tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Json(CancelJob { reason }): Json<CancelJob>,
@@ -464,7 +500,6 @@ async fn force_cancel(
username: "anonymous".to_string(),
username_override: None,
email: "anonymous".to_string(),
token_prefix: tokened.token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string()),
},
};
@@ -516,7 +551,6 @@ async fn force_cancel(
async fn get_flow_job_debug_info(
OptAuthed(opt_authed): OptAuthed,
tokened_o: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
@@ -568,7 +602,7 @@ async fn get_flow_job_debug_info(
}
}
log_job_view(&db, opt_authed.as_ref(), tokened_o.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(jobs).into_response())
} else {
@@ -628,7 +662,6 @@ struct GetJobQuery {
async fn get_job(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(GetJobQuery { no_logs }): Query<GetJobQuery>,
@@ -648,7 +681,7 @@ async fn get_job(
let mut job = get.fetch(&db, id, &w_id).await?;
job.fetch_outstanding_wait_time(&db).await?;
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(job).into_response())
}
@@ -1096,7 +1129,6 @@ async fn get_logs_from_disk(
async fn get_job_logs(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
@@ -1134,7 +1166,7 @@ async fn get_job_logs(
}
let logs = record.logs.unwrap_or_default();
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(r) = get_logs_from_store(record.log_offset, &logs, &record.log_file_index).await
@@ -1171,7 +1203,7 @@ async fn get_job_logs(
}
let logs = text.logs.unwrap_or_default();
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(r) =
@@ -1195,7 +1227,6 @@ async fn get_job_logs(
async fn get_args(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> JsonResult<Box<RawValue>> {
@@ -1221,7 +1252,7 @@ async fn get_args(
));
}
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(record.args.map(|x| x.0).unwrap_or_default()))
} else {
@@ -1242,7 +1273,7 @@ async fn get_args(
));
}
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(record.args.map(|x| x.0).unwrap_or_default()))
}
@@ -1895,6 +1926,7 @@ async fn list_jobs(
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListCompletedQuery>,
Extension(_api_list_jobs_query_duration): Extension<Option<Histo>>,
) -> error::JsonResult<Vec<Job>> {
check_scopes(&authed, || format!("jobs:listjobs"))?;
@@ -1958,12 +1990,24 @@ async fn list_jobs(
};
let mut tx: Transaction<'_, Postgres> = user_db.begin(&authed).await?;
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql)
.fetch_all(&mut *tx)
.warn_after_seconds_with_sql(5, format!("list_jobs: {}", sql))
.await?;
#[cfg(feature = "prometheus")]
let start = Instant::now();
#[cfg(feature = "prometheus")]
if _api_list_jobs_query_duration.is_some() || true {
tracing::info!("list_jobs query: {}", sql);
}
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql).fetch_all(&mut *tx).await?;
tx.commit().await?;
#[cfg(feature = "prometheus")]
if let Some(api_list_jobs_query_duration) = _api_list_jobs_query_duration {
let duration = start.elapsed().as_secs_f64();
api_list_jobs_query_duration.observe(duration);
tracing::info!("list_jobs query took {}s: {}", duration, sql);
}
Ok(Json(jobs.into_iter().map(From::from).collect()))
}
@@ -2003,23 +2047,13 @@ pub async fn resume_suspended_flow_as_owner(
pub async fn resume_suspended_job(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
resume_suspended_job_internal(
value,
db,
w_id,
job_id,
resume_id,
approver,
secret,
authed,
opt_tokened,
true,
value, db, w_id, job_id, resume_id, approver, secret, authed, true,
)
.await
}
@@ -2033,7 +2067,6 @@ async fn resume_suspended_job_internal(
approver: QueryApprover,
secret: String,
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
approved: bool,
) -> Result<StatusCode, Error> {
let value = value.unwrap_or(serde_json::Value::Null);
@@ -2112,7 +2145,6 @@ async fn resume_suspended_job_internal(
email: approver.clone(),
username: approver.clone(),
username_override: None,
token_prefix: opt_tokened.token.map(|s| s[0..TOKEN_PREFIX_LEN].to_string()),
},
};
audit_log(
@@ -2282,14 +2314,13 @@ async fn get_suspended_flow_info<'c>(
pub async fn cancel_suspended_job(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
resume_suspended_job_internal(
value, db, w_id, job_id, resume_id, approver, secret, authed, opt_tokened, false,
value, db, w_id, job_id, resume_id, approver, secret, authed, false,
)
.await
}
@@ -2307,7 +2338,6 @@ pub struct QueryApprover {
pub async fn get_suspended_job_flow(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>,
@@ -2374,7 +2404,7 @@ pub async fn get_suspended_job_flow(
approvers_from_status
};
log_job_view(&db, authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &job).await?;
log_job_view(&db, authed.as_ref(), &w_id, &job).await?;
Ok(Json(SuspendedJobFlow { job: flow, approvers }).into_response())
}
@@ -3531,11 +3561,10 @@ pub async fn run_flow_by_path_inner(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
run_query.root_job,
run_query.root_job.or(run_query.parent_job),
run_query.job_id,
false,
false,
@@ -3625,11 +3654,10 @@ pub async fn restart_flow(
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
run_query.root_job,
run_query.root_job.or(run_query.parent_job),
run_query.job_id,
false,
false,
@@ -3718,11 +3746,10 @@ pub async fn run_script_by_path_inner(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
run_query.root_job,
run_query.root_job.or(run_query.parent_job),
run_query.job_id,
false,
false,
@@ -3864,7 +3891,6 @@ pub async fn run_workflow_as_code(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
Some(job_id),
@@ -4280,7 +4306,6 @@ impl JobViewCache {
async fn log_job_view(
db: &DB,
opt_authed: Option<&ApiAuthed>,
opt_token: Option<&str>,
w_id: &str,
job_id: &Uuid,
) -> error::Result<()> {
@@ -4291,7 +4316,6 @@ async fn log_job_view(
username: "anonymous".to_string(),
username_override: None,
email: "anonymous".to_string(),
token_prefix: opt_token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string())
},
};
if JOB_VIEW_CACHE
@@ -4390,11 +4414,10 @@ pub async fn run_wait_result_job_by_path_get(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
None,
None,
run_query.parent_job,
run_query.root_job,
run_query.root_job.or(run_query.parent_job),
run_query.job_id,
false,
false,
@@ -4531,11 +4554,10 @@ pub async fn run_wait_result_script_by_path_internal(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
None,
None,
run_query.parent_job,
run_query.root_job,
run_query.root_job.or(run_query.parent_job),
run_query.job_id,
false,
false,
@@ -4645,11 +4667,10 @@ pub async fn run_wait_result_script_by_hash(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
None,
None,
run_query.parent_job,
run_query.root_job,
run_query.root_job.or(run_query.parent_job),
run_query.job_id,
false,
false,
@@ -4760,11 +4781,10 @@ pub async fn run_wait_result_flow_by_path_internal(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
run_query.root_job,
run_query.root_job.or(run_query.parent_job),
run_query.job_id,
false,
false,
@@ -4831,7 +4851,6 @@ async fn run_preview_script(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
scheduled_for,
None,
None,
@@ -4920,7 +4939,6 @@ async fn run_bundle_preview_script(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
scheduled_for,
None,
None,
@@ -5088,7 +5106,6 @@ async fn run_dependencies_job(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -5146,7 +5163,6 @@ async fn run_flow_dependencies_job(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
None,
None,
None,
@@ -5487,7 +5503,6 @@ async fn run_preview_flow_job(
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
authed.token_prefix.as_deref(),
scheduled_for,
None,
None,
@@ -5607,11 +5622,10 @@ pub async fn run_job_by_hash_inner(
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
run_query.root_job,
run_query.root_job.or(run_query.parent_job),
run_query.job_id,
false,
false,
@@ -5700,7 +5714,6 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R
async fn get_job_update(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, job_id)): Path<(String, Uuid)>,
Query(JobUpdateQuery { log_offset, get_progress, running }): Query<JobUpdateQuery>,
@@ -5757,7 +5770,7 @@ async fn get_job_update(
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
));
}
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &job_id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &job_id).await?;
Ok(Json(JobUpdate {
running: record.running,
completed: record.completed,
@@ -5862,7 +5875,6 @@ pub fn filter_list_completed_query(
sqlb.and_where_le("started_at", "?".bind(&dt.to_rfc3339()));
}
if let Some(dt) = &lq.created_or_started_after {
sqlb.and_where_ge("created_at", "?".bind(&dt.to_rfc3339()));
sqlb.and_where_ge("started_at", "?".bind(&dt.to_rfc3339()));
}
@@ -6045,7 +6057,6 @@ async fn list_completed_jobs(
async fn get_completed_job<'a>(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
@@ -6071,7 +6082,7 @@ async fn get_completed_job<'a>(
// .fetch_optional(db)
// .await.ok().flatten().flatten();
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(response)
}
@@ -6085,7 +6096,6 @@ pub struct RawResult {
async fn get_completed_job_result(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(JsonPath { json_path, suspended_job, approver, resume_id, secret }): Query<JsonPath>,
@@ -6174,7 +6184,7 @@ async fn get_completed_job_result(
raw_result.result.as_mut(),
);
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(raw_result.result).into_response())
}
@@ -6232,7 +6242,6 @@ struct GetCompletedJobQuery {
async fn get_completed_job_result_maybe(
OptAuthed(opt_authed): OptAuthed,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(GetCompletedJobQuery { get_started }): Query<GetCompletedJobQuery>,
@@ -6265,7 +6274,7 @@ async fn get_completed_job_result_maybe(
));
}
log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?;
log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?;
Ok(Json(CompletedJobResult {
started: Some(true),
@@ -6303,7 +6312,6 @@ async fn get_completed_job_result_maybe(
async fn delete_completed_job<'a>(
authed: ApiAuthed,
Tokened { token }: Tokened,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
@@ -6353,11 +6361,5 @@ async fn delete_completed_job<'a>(
.await?;
tx.commit().await?;
return get_completed_job(
OptAuthed(Some(authed)),
OptTokened { token: Some(token) },
Extension(db),
Path((w_id, id)),
)
.await;
return get_completed_job(OptAuthed(Some(authed)), Extension(db), Path((w_id, id))).await;
}

View File

@@ -36,10 +36,8 @@ use anyhow::Context;
use argon2::Argon2;
use axum::extract::DefaultBodyLimit;
use axum::{middleware::from_extractor, routing::get, routing::post, Extension, Router};
use axum::response::Response;
use axum::http::HeaderValue;
use axum::body::Body;
use db::DB;
use http::HeaderValue;
use reqwest::Client;
#[cfg(feature = "oauth2")]
use std::collections::HashMap;
@@ -105,8 +103,6 @@ mod integration;
#[cfg(feature = "postgres_trigger")]
mod postgres_triggers;
pub mod openapi;
mod approvals;
#[cfg(all(feature = "enterprise", feature = "private"))]
pub mod apps_ee;
@@ -238,7 +234,6 @@ lazy_static::lazy_static! {
}
// Compliance with cloud events spec.
pub async fn add_webhook_allowed_origin(
req: axum::extract::Request,
@@ -261,7 +256,6 @@ pub async fn add_webhook_allowed_origin(
next.run(req).await
}
#[cfg(not(feature = "tantivy"))]
type IndexReader = ();
@@ -601,7 +595,6 @@ pub async fn run_server(
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc_oss::workspaced_service())
.nest("/openapi", openapi::openapi_service())
.nest("/http_triggers", http_triggers_service)
.nest("/websocket_triggers", websocket_triggers_service)
.nest("/kafka_triggers", kafka_triggers_service)
@@ -894,18 +887,12 @@ async fn ee_license() -> String {
}
}
async fn openapi() -> Response {
Response::builder()
.header("content-type", "application/yaml")
.body(Body::from(include_str!("../openapi-deref.yaml")))
.unwrap()
async fn openapi() -> &'static str {
include_str!("../openapi-deref.yaml")
}
async fn openapi_json() -> Response {
Response::builder()
.header("content-type", "application/json")
.body(Body::from(include_str!("../openapi-deref.json")))
.unwrap()
async fn openapi_json() -> &'static str {
include_str!("../openapi-deref.json")
}
pub async fn migrate_db(db: &DB) -> anyhow::Result<Option<JoinHandle<()>>> {

View File

@@ -387,8 +387,7 @@ impl Runner {
item_type: &str,
) -> Result<Vec<T>, Error> {
let mut sqlb = SqlBuilder::select_from(&format!("{} as o", item_type));
let fields = vec!["o.path", "o.summary", "o.description", "o.schema"];
sqlb.fields(&fields);
sqlb.fields(&["o.path", "o.summary", "o.description", "o.schema"]);
if scope_type == "favorites" {
sqlb.join("favorite")
.on("favorite.favorite_kind = ? AND favorite.workspace_id = o.workspace_id AND favorite.path = o.path AND favorite.usr = ?".bind(&item_type)
@@ -396,21 +395,16 @@ impl Runner {
}
sqlb.and_where("o.workspace_id = ?".bind(&workspace_id))
.and_where("o.archived = false")
.and_where("o.draft_only IS NOT TRUE");
if item_type == "script" {
sqlb.and_where("(o.no_main_func IS NOT TRUE OR o.no_main_func IS NULL)");
}
sqlb.order_by(
if item_type == "flow" {
"o.edited_at"
} else {
"o.created_at"
},
false,
)
.limit(100);
.and_where("o.draft_only IS NOT TRUE")
.order_by(
if item_type == "flow" {
"o.edited_at"
} else {
"o.created_at"
},
false,
)
.limit(100);
let sql = sqlb.sql().map_err(|_e| {
tracing::error!("failed to build sql: {}", _e);
Error::internal_error("failed to build sql", None)
@@ -1183,11 +1177,7 @@ pub async fn extract_and_store_workspace_id(
pub async fn setup_mcp_server() -> anyhow::Result<(Router, Arc<LocalSessionManager>)> {
let session_manager = Arc::new(LocalSessionManager::default());
let service_config = Default::default();
let service = StreamableHttpService::new(
|| Ok(Runner::new()),
session_manager.clone(),
service_config,
);
let service = StreamableHttpService::new(Runner::new, session_manager.clone(), service_config);
let router = axum::Router::new().nest_service("/", service);
Ok((router, session_manager))

View File

@@ -478,7 +478,7 @@ pub async fn test_mqtt_connection(
test_postgres;
let mqtt_resource = try_get_resource_from_db_as::<MqttResource>(
&authed,
authed,
Some(user_db),
&db,
&mqtt_resource_path,
@@ -1253,7 +1253,7 @@ impl MqttConfig {
}
}
let mqtt_resource = try_get_resource_from_db_as::<MqttResource>(
&authed,
authed,
Some(UserDB::new(db.clone())),
db,
mqtt_resource_path,

View File

@@ -1,982 +0,0 @@
use std::{
collections::{HashMap, HashSet},
fmt::Display,
};
use anyhow::anyhow;
use axum::{
body::Body, extract::Path, http, response::Response, routing::post, Extension, Json, Router,
};
use http::{header, HeaderValue, Method, StatusCode};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::{to_value, Map, Value};
use sqlx::PgConnection;
use url::Url;
use windmill_common::{
db::UserDB,
error::{Error, Result},
utils::{deserialize_url, empty_as_none, is_empty, RunnableKind},
DB,
};
use crate::db::ApiAuthed;
#[cfg(feature = "http_trigger")]
use {
crate::{
http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication,
http_triggers::AuthenticationMethod, resources::try_get_resource_from_db_as,
},
itertools::Itertools,
};
lazy_static::lazy_static! {
static ref DEFAULT_OPENAPI_INFO_OBJECT: Info = Info {
title: "Windmill API".to_string(),
version: "1.0.0".to_string(),
..Default::default()
};
}
const DEFAULT_OPENAPI_GENERATED_VERSION: &'static str = "3.1.0";
const JWT_SECURITY_SCHEME: &'static str = "JwtAuth";
const BASIC_HTTP_AUTH_SCHEME: &'static str = "BasicHttp";
const DEFAULT_REQUEST_KEY: &'static str = "defaultRequest";
const DEFAULT_ASYNC_RESPONSE_KEY: &'static str = "AsyncResponse";
const DEFAULT_SYNC_RESPONSE_KEY: &'static str = "SyncResponse";
const DEFAULT_PAYLOAD_PARAM_KEY: &'static str = "PayloadParam";
pub fn openapi_service() -> Router {
Router::new()
.route("/generate", post(generate_openapi_spec))
.route("/download", post(download_spec))
}
#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum Format {
JSON,
YAML,
}
impl Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let format = match self {
Format::JSON => "json",
Format::YAML => "yaml",
};
write!(f, "{}", format)
}
}
impl Default for Format {
fn default() -> Self {
Self::YAML
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
struct Contact {
#[serde(skip_serializing_if = "is_empty")]
name: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_url",
skip_serializing_if = "Option::is_none"
)]
url: Option<Url>,
#[serde(skip_serializing_if = "is_empty")]
email: Option<String>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
struct License {
name: String,
#[serde(skip_serializing_if = "is_empty")]
identifier: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_url",
skip_serializing_if = "Option::is_none"
)]
url: Option<Url>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Info {
title: String,
version: String,
#[serde(skip_serializing_if = "is_empty")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
contact: Option<Contact>,
#[serde(skip_serializing_if = "Option::is_none")]
license: Option<License>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Server {
url: String,
#[serde(skip_serializing_if = "is_empty")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
variables: Option<HashMap<String, Value>>,
}
#[derive(Debug)]
pub enum SecurityScheme {
BearerJwt,
BasicHttp,
ApiKey(String),
}
#[derive(Debug)]
pub struct WebhookConfig {
runnable_kind: RunnableKind,
}
impl WebhookConfig {
pub fn new(runnable_kind: RunnableKind) -> Self {
Self { runnable_kind }
}
}
#[derive(Debug)]
pub struct HttpRouteConfig {
method: Method,
}
impl HttpRouteConfig {
pub fn new(method: Method) -> Self {
Self { method }
}
}
#[derive(Debug)]
pub enum Kind {
Webhook(WebhookConfig),
HttpRoute(HttpRouteConfig),
}
#[derive(Debug)]
pub struct FuturePath {
route_path: String,
kind: Kind,
is_async: Option<bool>,
summary: Option<String>,
description: Option<String>,
security_scheme: Option<SecurityScheme>,
}
impl FuturePath {
pub fn new(
route_path: String,
kind: Kind,
is_async: Option<bool>,
summary: Option<String>,
description: Option<String>,
security_scheme: Option<SecurityScheme>,
) -> FuturePath {
FuturePath { route_path, kind, is_async, summary, description, security_scheme }
}
}
fn from_route_path_to_openapi_path(
route_path: &str,
kind: &Kind,
) -> Result<(Vec<String>, Option<Value>)> {
let mut openapi_path = String::new();
let mut parameters = Vec::new();
for segment in route_path.split('/') {
if segment.starts_with(':') {
let param_name = &segment[1..];
if param_name.is_empty() {
return Err(anyhow!("Empty parameter name in path: {}", route_path).into());
}
openapi_path.push_str(&format!("/{{{}}}", param_name));
parameters.push(serde_json::json!({
"name": param_name,
"in": "path",
"required": true,
"schema": { "type": "string" }
}));
} else if !segment.is_empty() {
openapi_path.push('/');
openapi_path.push_str(segment);
} else {
openapi_path.push('/');
}
}
let parameters_json = if parameters.is_empty() {
None
} else {
Some(Value::Array(parameters))
};
let prefix = match kind {
Kind::HttpRoute(_) => "",
Kind::Webhook(WebhookConfig { runnable_kind }) => match runnable_kind {
RunnableKind::Script => "p",
RunnableKind::Flow => "f",
},
};
let normalized_path = if openapi_path.starts_with('/') {
format!("{prefix}{openapi_path}")
} else {
format!("{}/{}", prefix, openapi_path)
};
let route_paths = if prefix.is_empty() {
vec![normalized_path]
} else {
vec![
format!("/run/{}", &normalized_path),
format!("/run_wait_result/{}", &normalized_path),
]
};
Ok((route_paths, parameters_json))
}
fn get_servers_component(url: &str, kind: &Kind) -> Server {
let url = url.trim_end_matches('/');
let server = match kind {
Kind::HttpRoute(_) => {
Server { url: format!("{}/api/r", url), description: None, variables: None }
}
Kind::Webhook(_) => Server {
url: format!("{}/api/w/{{workspace}}/jobs", url),
variables: Some(HashMap::from([(
"workspace".to_string(),
serde_json::json!({
"default": "test",
"description": "Workspace identifier"
}),
)])),
description: None,
},
};
server
}
fn generate_paths(
paths: Vec<FuturePath>,
url: Option<&Url>,
) -> Result<IndexMap<String, IndexMap<String, Value>>> {
let mut map: IndexMap<String, IndexMap<String, Value>> = IndexMap::new();
let generate_default_request = || {
serde_json::json!({
"$ref": format!("#/components/requestBodies/{DEFAULT_REQUEST_KEY}")
})
};
let generate_response = |is_async: bool| {
let responses = if is_async {
serde_json::json!({
"200": {
"$ref": format!("#/components/responses/{DEFAULT_ASYNC_RESPONSE_KEY}")
}
})
} else {
serde_json::json!(serde_json::json!({
"200": {
"$ref": format!("#/components/responses/{DEFAULT_SYNC_RESPONSE_KEY}")
}
}))
};
responses
};
let get_security_scheme = |security_scheme: Option<&SecurityScheme>| -> Vec<Value> {
if let Some(security_scheme) = security_scheme {
let scheme = match security_scheme {
SecurityScheme::ApiKey(api_key) => header_to_pascal_case(&api_key),
SecurityScheme::BearerJwt => JWT_SECURITY_SCHEME.to_owned(),
SecurityScheme::BasicHttp => BASIC_HTTP_AUTH_SCHEME.to_owned(),
};
vec![serde_json::json!({
scheme: []
})]
} else {
vec![]
}
};
let mut webhooks = HashSet::new();
for path in paths {
if let Kind::Webhook(WebhookConfig { runnable_kind }) = &path.kind {
if !webhooks.insert((path.route_path.clone(), runnable_kind.to_owned())) {
continue;
}
}
let (route_paths, parameters) =
from_route_path_to_openapi_path(&path.route_path, &path.kind)?;
for route_path in route_paths {
let path_object = map.entry(route_path.clone()).or_insert_with(|| {
let mut path_object = IndexMap::new();
if let Some(url) = url {
let servers = get_servers_component(url.as_str(), &path.kind);
path_object.insert("servers".to_string(), to_value(vec![servers]).unwrap());
}
if parameters.is_some() {
path_object.insert(
"parameters".to_string(),
to_value(parameters.clone()).unwrap(),
);
}
path_object
});
let is_async;
let (methods, is_webhook) = match &path.kind {
Kind::Webhook(_) => {
is_async = route_path.starts_with("/run/");
let methods = if is_async {
vec![Method::POST]
} else {
vec![Method::GET, Method::POST]
};
(methods, true)
}
Kind::HttpRoute(HttpRouteConfig { method }) => {
if path_object.get(&method.to_string()).is_some() {
return Err(anyhow!(
"Found duplicate {} method, for route at path: {}",
method,
path.route_path
)
.into());
}
is_async = path.is_async.unwrap_or(true);
(vec![method.to_owned()], false)
}
};
for method in methods {
let mut method_map = IndexMap::new();
if let Some(summary) = path.summary.as_ref().filter(|s| !s.is_empty()) {
method_map.insert("summary", Value::String(summary.to_owned()));
}
if let Some(description) = path.description.as_ref().filter(|s| !s.is_empty()) {
method_map.insert("description", Value::String(description.to_owned()));
}
method_map.insert(
"security",
to_value(get_security_scheme(path.security_scheme.as_ref()))?,
);
if method != Method::GET {
method_map.insert("requestBody", generate_default_request());
} else if is_webhook {
method_map.insert(
"parameters",
Value::Array(vec![serde_json::json!({
"$ref": format!("#/components/parameters/{DEFAULT_PAYLOAD_PARAM_KEY}")
})]),
);
}
method_map.insert("responses", generate_response(is_async));
path_object.insert(method.to_string().to_lowercase(), to_value(&method_map)?);
}
}
}
return Ok(map);
}
pub fn transform_to_minified_postgres_regex(glob: &str) -> String {
let mut regex = String::from("^");
for ch in glob.chars() {
match ch {
'*' => regex.push_str(".*"),
'.' | '+' | '(' | ')' | '|' | '^' | '$' | '{' | '}' | '[' | ']' | '\\' => {
regex.push('\\');
regex.push(ch);
}
_ => regex.push(ch),
}
}
regex.push('$');
regex
}
#[derive(Debug, Default)]
pub struct ServerToSet {
pub http_route: bool,
pub webhook_flow: bool,
pub webhook_script: bool,
}
impl ServerToSet {
pub fn new(http_route: bool, webhook_flow: bool, webhook_script: bool) -> ServerToSet {
ServerToSet { http_route, webhook_flow, webhook_script }
}
}
fn header_to_pascal_case(header: &str) -> String {
header
.split(|c: char| c == '-' || c == '_' || c == ' ')
.filter(|s| !s.is_empty())
.map(|s| {
let mut chars = s.chars();
match chars.next() {
Some(first) => {
first.to_ascii_uppercase().to_string()
+ chars.as_str().to_ascii_lowercase().as_str()
}
None => String::new(),
}
})
.collect::<String>()
}
#[derive(Debug, Default)]
struct SecuritySchemeToAdd {
basic_http: bool,
bearer_jwt: bool,
api_keys: Vec<(String, Value)>,
}
fn generate_all_security_schemes(future_paths: &[FuturePath]) -> SecuritySchemeToAdd {
let mut to_add = SecuritySchemeToAdd::default();
let mut set = HashSet::new();
for future_path in future_paths {
if !to_add.basic_http
&& matches!(future_path.security_scheme, Some(SecurityScheme::BasicHttp))
{
to_add.basic_http = true
} else if !to_add.bearer_jwt
&& matches!(future_path.security_scheme, Some(SecurityScheme::BearerJwt))
{
to_add.bearer_jwt = true
} else if let Some(SecurityScheme::ApiKey(api_key)) = future_path.security_scheme.as_ref() {
let pascal_case_header = header_to_pascal_case(&api_key);
if !set.insert(pascal_case_header.clone()) {
continue;
}
let scheme = serde_json::json!({
"type": "apiKey",
"in": "header",
"name": api_key
});
to_add.api_keys.push((pascal_case_header, scheme));
}
}
to_add
}
fn generate_components(future_paths: &[FuturePath]) -> Map<String, Value> {
let mut components = Map::new();
if future_paths
.iter()
.any(|path| matches!(path.kind, Kind::Webhook(_)))
{
components.insert(
"parameters".to_owned(),
serde_json::json!({
"PayloadParam": {
"name": "payload",
"in": "query",
"required": true,
"description": "A URL-safe base64-encoded JSON string payload.",
"schema": {
"type": "string"
}
}
}),
);
}
{
let mut security_scheme = Map::new();
let SecuritySchemeToAdd { basic_http, bearer_jwt, api_keys } =
generate_all_security_schemes(future_paths);
if basic_http {
security_scheme.insert(
BASIC_HTTP_AUTH_SCHEME.to_owned(),
serde_json::json!({
"type": "http",
"scheme": "basic"
}),
);
}
if bearer_jwt {
security_scheme.insert(
JWT_SECURITY_SCHEME.to_owned(),
serde_json::json!({
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}),
);
}
for (key, value) in api_keys {
security_scheme.insert(key, value);
}
components.insert("securitySchemes".to_owned(), Value::Object(security_scheme));
}
components.insert("requestBodies".to_owned(), serde_json::json!({
DEFAULT_REQUEST_KEY: {
"description": "This route may or may not require a request body, but its structure and content type are unknown.",
"required": false,
"content": {
"application/json": {}
}
}
}));
components.insert("responses".to_owned(), serde_json::json!({
DEFAULT_ASYNC_RESPONSE_KEY: {
"description": "Returns a job ID as a UUID string.",
"content": {
"text/plain": {
"schema": {
"type": "string",
"format": "uuid",
"examples": [ "550e8400-e29b-41d4-a716-446655440000" ]
}
}
}
},
DEFAULT_SYNC_RESPONSE_KEY: {
"description": "This route may return a response, but its structure and content type are unknown.",
"content": {
"application/octet-stream": {}
}
},
}));
components
}
pub fn generate_openapi_document(
info: Option<&Info>,
url: Option<&Url>,
paths: Vec<FuturePath>,
format: Format,
) -> Result<String> {
let mut openapi_doc: IndexMap<&'static str, Value> = IndexMap::new();
openapi_doc.insert("openapi", to_value(&DEFAULT_OPENAPI_GENERATED_VERSION)?);
openapi_doc.insert(
"info",
to_value(info.unwrap_or(&DEFAULT_OPENAPI_INFO_OBJECT))?,
);
openapi_doc.insert("components", Value::Object(generate_components(&paths)));
openapi_doc.insert("paths", to_value(generate_paths(paths, url)?)?);
let openapi_document = match format {
Format::YAML => serde_yml::to_string(&openapi_doc).map_err(|err| {
anyhow!(
"Could not generate OpenAPI document in YAML format: {}",
err
)
})?,
Format::JSON => serde_json::to_string_pretty(&openapi_doc).map_err(|err| {
anyhow!(
"Could not generate OpenAPI document in JSON format: {}",
err
)
})?,
};
Ok(openapi_document)
}
#[allow(unused)]
#[derive(Debug, Deserialize)]
struct HttpRouteFilter {
folder_regex: String,
path_regex: String,
route_path_regex: String,
}
#[derive(Debug, Deserialize)]
struct WebhookFilter {
user_or_folder_regex: String,
user_or_folder_regex_value: String,
path: String,
runnable_kind: RunnableKind,
}
#[derive(Debug, Deserialize)]
struct GenerateOpenAPI {
info: Option<Info>,
url: Option<Url>,
#[serde(default, deserialize_with = "empty_as_none")]
http_route_filters: Option<Vec<HttpRouteFilter>>,
#[serde(default, deserialize_with = "empty_as_none")]
webhook_filters: Option<Vec<WebhookFilter>>,
#[serde(default)]
openapi_spec_format: Format,
}
#[cfg(feature = "http_trigger")]
async fn http_routes_to_future_paths(
db: &DB,
user_db: UserDB,
authed: &ApiAuthed,
pg_pool: &mut PgConnection,
http_route_filters: Option<&[HttpRouteFilter]>,
w_id: &str,
) -> Result<Vec<FuturePath>> {
let mut http_routes = Vec::new();
if let Some(http_route_filters) = http_route_filters {
let path_regex = http_route_filters
.iter()
.map(|filter| {
transform_to_minified_postgres_regex(&format!(
"f/{}/{}",
filter.folder_regex, filter.path_regex
))
})
.collect_vec();
let route_path_regex = http_route_filters
.iter()
.map(|filter| transform_to_minified_postgres_regex(&filter.route_path_regex))
.collect_vec();
#[derive(Debug, Deserialize)]
struct MinifiedHttpTrigger {
route_path: String,
http_method: HttpMethod,
is_async: bool,
workspaced_route: bool,
summary: Option<String>,
description: Option<String>,
authentication_method: AuthenticationMethod,
authentication_resource_path: Option<String>,
}
http_routes = sqlx::query_as!(
MinifiedHttpTrigger,
r#"
SELECT
route_path,
http_method AS "http_method: _",
is_async,
workspaced_route,
summary,
description,
authentication_method AS "authentication_method: _",
authentication_resource_path
FROM
http_trigger
WHERE
path ~ ANY($1) AND
route_path ~ ANY($2) AND
workspace_id = $3
"#,
&path_regex,
&route_path_regex,
&w_id
)
.fetch_all(pg_pool)
.await?;
}
let mut openapi_future_paths = Vec::with_capacity(http_routes.len());
for http_route in http_routes {
let auth_method = match http_route.authentication_method {
AuthenticationMethod::BasicHttp => Some(SecurityScheme::BasicHttp),
AuthenticationMethod::Windmill => Some(SecurityScheme::BearerJwt),
AuthenticationMethod::ApiKey => {
let resource_path = match http_route.authentication_resource_path {
Some(resource_path) => resource_path,
None => {
return Err(Error::BadRequest(
"Missing authentication resource path".to_string(),
));
}
};
let api = try_get_resource_from_db_as::<ApiKeyAuthentication>(
authed,
Some(user_db.clone()),
db,
&resource_path,
w_id,
)
.await?;
Some(SecurityScheme::ApiKey(api.api_key_header))
}
_ => None,
};
let route_path = if http_route.workspaced_route {
format!("{}/{}", w_id, http_route.route_path.trim_start_matches('/'))
} else {
http_route.route_path.clone()
};
let method = match http_route.http_method {
HttpMethod::Get => Method::GET,
HttpMethod::Post => Method::POST,
HttpMethod::Put => Method::PUT,
HttpMethod::Patch => Method::PATCH,
HttpMethod::Delete => Method::DELETE,
};
let future_path = FuturePath::new(
route_path,
Kind::HttpRoute(HttpRouteConfig::new(method)),
Some(http_route.is_async),
http_route.summary,
http_route.description,
auth_method,
);
openapi_future_paths.push(future_path);
}
Ok(openapi_future_paths)
}
#[cfg(not(feature = "http_trigger"))]
async fn http_routes_to_future_paths(
_db: &DB,
_user_db: UserDB,
_authed: &ApiAuthed,
_pg_pool: &mut PgConnection,
_http_route_filters: Option<&[HttpRouteFilter]>,
_w_id: &str,
) -> Result<Vec<FuturePath>> {
Ok(Vec::new())
}
async fn webhook_to_future_paths(
pg_pool: &mut PgConnection,
webhook_filters: Option<&[WebhookFilter]>,
w_id: &str,
) -> Result<Vec<FuturePath>> {
let mut openapi_future_paths = Vec::new();
if let Some(webhook_filters) = webhook_filters {
let mut script_webhook_filter = Vec::new();
let mut flow_webhook_filter = Vec::new();
for webhook in webhook_filters {
let full_regex = transform_to_minified_postgres_regex(&format!(
"{}/{}/{}",
&webhook.user_or_folder_regex, &webhook.user_or_folder_regex_value, &webhook.path
));
match webhook.runnable_kind {
RunnableKind::Script => {
script_webhook_filter.push(full_regex);
}
RunnableKind::Flow => {
flow_webhook_filter.push(full_regex);
}
}
}
#[derive(Debug, Deserialize, Clone, Hash)]
struct MinifiedWebhook {
path: String,
description: Option<String>,
summary: Option<String>,
}
let webhook_scripts = sqlx::query_as!(
MinifiedWebhook,
r#"SELECT
path,
summary,
description
FROM
script
WHERE
path ~ ANY($1) AND
workspace_id = $2 AND
archived is FALSE
"#,
&script_webhook_filter,
&w_id
)
.fetch_all(&mut *pg_pool)
.await?;
let webhook_flows = sqlx::query_as!(
MinifiedWebhook,
r#"SELECT
path,
summary,
description
FROM
flow
WHERE
path ~ ANY($1) AND
workspace_id = $2 AND
archived is FALSE
"#,
&flow_webhook_filter,
&w_id
)
.fetch_all(&mut *pg_pool)
.await?;
openapi_future_paths.reserve_exact(webhook_scripts.len() + webhook_flows.len());
for webhook in webhook_scripts {
openapi_future_paths.push(FuturePath::new(
webhook.path,
Kind::Webhook(WebhookConfig::new(RunnableKind::Script)),
None,
webhook.summary,
webhook.description,
Some(SecurityScheme::BearerJwt),
));
}
for webhook in webhook_flows {
openapi_future_paths.push(FuturePath::new(
webhook.path,
Kind::Webhook(WebhookConfig::new(RunnableKind::Flow)),
None,
webhook.summary,
webhook.description,
Some(SecurityScheme::BearerJwt),
));
}
}
Ok(openapi_future_paths)
}
async fn generate_openapi_future_path(
db: &DB,
user_db: UserDB,
authed: &ApiAuthed,
http_route_filters: Option<&[HttpRouteFilter]>,
webhook_filters: Option<&[WebhookFilter]>,
w_id: &str,
) -> Result<Vec<FuturePath>> {
if http_route_filters.is_none() && webhook_filters.is_none() {
return Err(Error::BadRequest(
"Expected http route filter and/or webhook filters".to_string(),
));
}
let mut tx = user_db.clone().begin(authed).await?;
let mut openapi_future_paths =
http_routes_to_future_paths(db, user_db, authed, &mut tx, http_route_filters, w_id).await?;
openapi_future_paths
.append(&mut webhook_to_future_paths(&mut tx, webhook_filters, w_id).await?);
tx.commit().await?;
if openapi_future_paths.is_empty() {
return Err(Error::NotFound(
"No match for the current filter".to_string(),
));
}
Ok(openapi_future_paths)
}
async fn generate_openapi_spec(
Extension(authed): Extension<ApiAuthed>,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(generate_openapi): Json<GenerateOpenAPI>,
) -> Result<String> {
let openapi_future_paths = generate_openapi_future_path(
&db,
user_db,
&authed,
generate_openapi.http_route_filters.as_deref(),
generate_openapi.webhook_filters.as_deref(),
&w_id,
)
.await?;
let openapi_document = generate_openapi_document(
generate_openapi.info.as_ref(),
generate_openapi.url.as_ref(),
openapi_future_paths,
generate_openapi.openapi_spec_format,
);
openapi_document
}
async fn download_spec(
Extension(authed): Extension<ApiAuthed>,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(generate_openapi): Json<GenerateOpenAPI>,
) -> Result<Response> {
let openapi_future_paths = generate_openapi_future_path(
&db,
user_db,
&authed,
generate_openapi.http_route_filters.as_deref(),
generate_openapi.webhook_filters.as_deref(),
&w_id,
)
.await?;
let openapi_document = generate_openapi_document(
generate_openapi.info.as_ref(),
generate_openapi.url.as_ref(),
openapi_future_paths,
generate_openapi.openapi_spec_format,
)?;
let response = Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"),
)
.body(Body::from(openapi_document))
.unwrap();
Ok(response)
}

View File

@@ -160,7 +160,7 @@ pub async fn get_pg_connection(
logical_mode: bool,
) -> Result<Client> {
let database =
try_get_resource_from_db_as::<Postgres>(&authed, user_db, db, postgres_resource_path, w_id)
try_get_resource_from_db_as::<Postgres>(authed, user_db, db, postgres_resource_path, w_id)
.await?;
Ok(get_raw_postgres_connection(&database, logical_mode).await?)

View File

@@ -417,7 +417,7 @@ impl PostgresConfig {
};
let database = try_get_resource_from_db_as::<Postgres>(
&authed,
authed,
Some(UserDB::new(db.clone())),
&db,
postgres_resource_path,

View File

@@ -29,7 +29,6 @@ use windmill_common::{
db::UserDB,
error::{Error, JsonResult, Result},
utils::{not_found_if_none, paginate, Pagination, StripPath},
worker::CLOUD_HOSTED,
};
pub fn workspaced_service() -> Router {
@@ -150,29 +149,9 @@ async fn create_app(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
if *CLOUD_HOSTED {
let nb_apps = sqlx::query_scalar!(
"SELECT COUNT(*) FROM raw_app WHERE workspace_id = $1",
&w_id
)
.fetch_one(&db)
.await?;
if nb_apps.unwrap_or(0) >= 1000 {
return Err(Error::BadRequest(
"You have reached the maximum number of apps (1000) on cloud. Contact support@windmill.dev to increase the limit"
.to_string(),
));
}
if app.summary.len() > 300 {
return Err(Error::BadRequest(
"Summary must be less than 300 characters on cloud".to_string(),
));
}
}
let mut tx = user_db.begin(&authed).await?;
if &app.path == "" {
return Err(Error::BadRequest("App path cannot be empty".to_string()));

View File

@@ -33,7 +33,6 @@ use windmill_common::{
error::{Error, JsonResult, Result},
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
variables,
worker::CLOUD_HOSTED,
};
pub fn workspaced_service() -> Router {
@@ -508,7 +507,6 @@ pub async fn transform_json_value<'c>(
email: "backend".to_string(),
username: "backend".to_string(),
username_override: None,
token_prefix: None,
}),
)
.await?;
@@ -585,6 +583,7 @@ pub async fn transform_json_value<'c>(
job.schedule_path.clone(),
job.flow_step_id.clone(),
job.root_job.map(|x| x.to_string()),
None,
Some(job.scheduled_for.clone()),
)
.await;
@@ -658,20 +657,6 @@ async fn create_resource(
Query(q): Query<CreateResourceQuery>,
Json(resource): Json<CreateResource>,
) -> Result<(StatusCode, String)> {
if *CLOUD_HOSTED {
let nb_resources = sqlx::query_scalar!(
"SELECT COUNT(*) FROM resource WHERE workspace_id = $1",
&w_id
)
.fetch_one(&db)
.await?;
if nb_resources.unwrap_or(0) >= 10000 {
return Err(Error::BadRequest(
"You have reached the maximum number of resources (10000) on cloud. Contact support@windmill.dev to increase the limit"
.to_string(),
));
}
}
let authed = maybe_refresh_folders(&resource.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
@@ -1232,7 +1217,7 @@ async fn update_resource_type(
)
))]
pub async fn try_get_resource_from_db_as<T>(
authed: &ApiAuthed,
authed: ApiAuthed,
user_db: Option<UserDB>,
db: &DB,
resource_path: &str,

View File

@@ -7,7 +7,10 @@
*/
use crate::{
db::{ApiAuthed, DB}, settings::{delete_global_setting, set_global_setting_internal}, users::maybe_refresh_folders, utils::require_super_admin
db::{ApiAuthed, DB},
settings::{delete_global_setting, set_global_setting_internal},
users::maybe_refresh_folders,
utils::require_super_admin,
};
use axum::{
extract::{Extension, Path, Query},
@@ -22,7 +25,11 @@ use std::str::FromStr;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
db::UserDB, error::{Error, JsonResult, Result}, schedule::Schedule, utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath}, worker::to_raw_value
db::UserDB,
error::{Error, JsonResult, Result},
schedule::Schedule,
utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath},
worker::to_raw_value,
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::schedule::push_scheduled_job;

View File

@@ -42,7 +42,7 @@ use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_worker::process_relative_imports;
use windmill_common::{error::to_anyhow, worker::CLOUD_HOSTED};
use windmill_common::error::to_anyhow;
use windmill_common::{
db::UserDB,
@@ -520,29 +520,6 @@ async fn create_script_internal<'c>(
.to_string(),
));
}
if *CLOUD_HOSTED {
let nb_scripts =
sqlx::query_scalar!("SELECT COUNT(*) FROM script WHERE workspace_id = $1", &w_id)
.fetch_one(&db)
.await?;
if nb_scripts.unwrap_or(0) >= 5000 {
return Err(Error::BadRequest(
"You have reached the maximum number of scripts (5000) on cloud. Contact support@windmill.dev to increase the limit"
.to_string(),
));
}
if ns.summary.len() > 300 {
return Err(Error::BadRequest(
"Summary must be less than 300 characters on cloud".to_string(),
));
}
if ns.description.len() > 3000 {
return Err(Error::BadRequest(
"Description must be less than 3000 characters on cloud".to_string(),
));
}
}
let script_path = ns.path.clone();
let hash = ScriptHash(hash_script(&ns));
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
@@ -941,7 +918,6 @@ async fn create_script_internal<'c>(
&authed.username,
&authed.email,
permissioned_as,
authed.token_prefix.as_deref(),
None,
None,
None,

View File

@@ -11,11 +11,11 @@ use std::collections::HashMap;
use windmill_common::error::Error;
use windmill_common::variables::get_secret_value_as_admin;
use crate::{approvals::{
use crate::approvals::{
extract_w_id_from_resume_url, handle_resume_action, ApprovalFormDetails, FieldType,
MessageFormat, QueryDefaultArgsJson, QueryDynamicEnumJson, QueryFlowStepId, QueryMessage,
ResumeFormField, ResumeSchema,
}, auth::OptTokened};
};
use crate::db::{ApiAuthed, DB};
use crate::jobs::{QueryApprover, ResumeUrls};
@@ -116,7 +116,6 @@ struct PrivateMetadata {
pub async fn slack_app_callback_handler(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Form(form_data): Form<SlackFormData>,
) -> Result<StatusCode, Error> {
@@ -125,8 +124,8 @@ pub async fn slack_app_callback_handler(
tracing::debug!("Payload: {:#?}", payload);
match payload.r#type {
PayloadType::ViewSubmission => handle_submission(authed, opt_tokened, db, &payload, "resume").await?,
PayloadType::ViewClosed => handle_submission(authed, opt_tokened, db, &payload, "cancel").await?,
PayloadType::ViewSubmission => handle_submission(authed, db, &payload, "resume").await?,
PayloadType::ViewClosed => handle_submission(authed, db, &payload, "cancel").await?,
_ => {
if let Some(actions) = &payload.actions {
if let Some(action) = actions.first() {
@@ -257,7 +256,6 @@ pub async fn request_slack_approval(
async fn handle_submission(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
db: DB,
payload: &Payload,
action: &str,
@@ -296,7 +294,7 @@ async fn handle_submission(
}
// Use the common handler to process the resume/cancel action
handle_resume_action(authed, opt_tokened, db.clone(), &resume_url, state_json, action).await?;
handle_resume_action(authed, db.clone(), &resume_url, state_json, action).await?;
let w_id = extract_w_id_from_resume_url(&resume_url)?;
let slack_token = get_slack_token(&db, &resource_path, w_id).await?;

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