mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 16:02:47 +00:00
Merge remote-tracking branch 'origin/main' into glm/select-users
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
---
|
||||
description:
|
||||
globs: frontend/src/**/*.svelte
|
||||
alwaysApply: false
|
||||
name: svelte-frontend
|
||||
description: Svelte coding guidelines for the Windmill frontend. MUST use when writing or modifying code in the frontend directory.
|
||||
---
|
||||
|
||||
# Svelte 5 Best Practices
|
||||
|
||||
This guide outlines best practices for developing with Svelte 5, incorporating the new Runes API and other modern Svelte features. These rules MUST NOT be applied on svelte 4 files unless explicitly asked to do so.
|
||||
@@ -4,9 +4,10 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commenter:
|
||||
required: true
|
||||
required: false
|
||||
type: string
|
||||
description: 'The username to check for organization membership'
|
||||
default: ''
|
||||
description: 'The username to check. Auto-detected from the event context if not provided.'
|
||||
organization:
|
||||
required: false
|
||||
type: string
|
||||
@@ -32,11 +33,27 @@ jobs:
|
||||
outputs:
|
||||
is_member: ${{ steps.check-membership.outputs.is_member }}
|
||||
steps:
|
||||
- name: Determine commenter
|
||||
id: determine-commenter
|
||||
run: |
|
||||
COMMENTER="${{ inputs.commenter }}"
|
||||
if [[ -z "$COMMENTER" ]]; then
|
||||
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
|
||||
fi
|
||||
echo "commenter=$COMMENTER" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check organization membership
|
||||
id: check-membership
|
||||
env:
|
||||
ORG_ACCESS_TOKEN: ${{ secrets.access_token }}
|
||||
COMMENTER: ${{ inputs.commenter }}
|
||||
COMMENTER: ${{ steps.determine-commenter.outputs.commenter }}
|
||||
ORG: ${{ inputs.organization }}
|
||||
TRUSTED_BOT: ${{ inputs.trusted_bot }}
|
||||
run: |
|
||||
|
||||
@@ -11,40 +11,18 @@ on:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
determine-commenter:
|
||||
check-membership:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai-fast')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai-fast')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai-fast')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '/ai-fast'))
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
commenter: ${{ steps.determine-commenter.outputs.commenter }}
|
||||
steps:
|
||||
- name: Determine commenter
|
||||
id: determine-commenter
|
||||
run: |
|
||||
# Work out who wrote the comment / review
|
||||
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 }}
|
||||
|
||||
claude-code-action:
|
||||
needs: [determine-commenter, check-membership]
|
||||
needs: check-membership
|
||||
if: |
|
||||
needs.check-membership.outputs.is_member == 'true'
|
||||
runs-on: ubicloud-standard-8
|
||||
|
||||
@@ -11,40 +11,18 @@ on:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
determine-commenter:
|
||||
check-membership:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/plan')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/plan')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/plan')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '/plan'))
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
commenter: ${{ steps.determine-commenter.outputs.commenter }}
|
||||
steps:
|
||||
- name: Determine commenter
|
||||
id: determine-commenter
|
||||
run: |
|
||||
# Work out who wrote the comment / review
|
||||
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 }}
|
||||
|
||||
claude-plan-action:
|
||||
needs: [determine-commenter, check-membership]
|
||||
needs: check-membership
|
||||
if: |
|
||||
needs.check-membership.outputs.is_member == 'true'
|
||||
runs-on: ubicloud-standard-4
|
||||
|
||||
@@ -11,40 +11,18 @@ 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'))
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
commenter: ${{ steps.determine-commenter.outputs.commenter }}
|
||||
steps:
|
||||
- name: Determine commenter
|
||||
id: determine-commenter
|
||||
run: |
|
||||
# Work out who wrote the comment / review
|
||||
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 }}
|
||||
|
||||
claude-code-action:
|
||||
needs: [determine-commenter, check-membership]
|
||||
needs: check-membership
|
||||
if: |
|
||||
needs.check-membership.outputs.is_member == 'true'
|
||||
runs-on: ubicloud-standard-8
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
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' }}
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
app_token: ${{ steps.app.outputs.token }}
|
||||
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
|
||||
|
||||
trigger-docs:
|
||||
needs: [generate-token, check-membership]
|
||||
if: ${{ 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 }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
@@ -5,8 +5,21 @@ on:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-membership:
|
||||
if: >-
|
||||
github.event.issue.pull_request && (
|
||||
startsWith(github.event.comment.body, '/updatesqlx') ||
|
||||
startsWith(github.event.comment.body, '/demo') ||
|
||||
startsWith(github.event.comment.body, '/eeref') ||
|
||||
startsWith(github.event.comment.body, '/docs')
|
||||
)
|
||||
uses: ./.github/workflows/check-org-membership.yml
|
||||
secrets:
|
||||
access_token: ${{ secrets.ORG_ACCESS_TOKEN }}
|
||||
|
||||
update-sqlx:
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/updatesqlx')
|
||||
needs: check-membership
|
||||
if: needs.check-membership.outputs.is_member == 'true' && startsWith(github.event.comment.body, '/updatesqlx')
|
||||
runs-on: ubicloud-standard-8
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -121,7 +134,8 @@ jobs:
|
||||
})
|
||||
|
||||
demo:
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/demo')
|
||||
needs: check-membership
|
||||
if: needs.check-membership.outputs.is_member == 'true' && startsWith(github.event.comment.body, '/demo')
|
||||
runs-on: ubicloud-standard-2
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -200,7 +214,8 @@ jobs:
|
||||
fi
|
||||
|
||||
update-ee-ref:
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/eeref')
|
||||
needs: check-membership
|
||||
if: needs.check-membership.outputs.is_member == 'true' && startsWith(github.event.comment.body, '/eeref')
|
||||
runs-on: ubicloud-standard-2
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -283,3 +298,35 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
body: 'Successfully updated ee-repo-ref.txt'
|
||||
})
|
||||
|
||||
update-docs:
|
||||
needs: check-membership
|
||||
if: needs.check-membership.outputs.is_member == 'true' && startsWith(github.event.comment.body, '/docs')
|
||||
runs-on: ubicloud-standard-2
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: app
|
||||
with:
|
||||
app-id: ${{ vars.INTERNAL_APP_ID }}
|
||||
private-key: ${{ secrets.INTERNAL_APP_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: |
|
||||
windmilldocs
|
||||
|
||||
- name: Trigger docs update
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app.outputs.token }}
|
||||
COMMENT_TEXT: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
jq -n \
|
||||
--argjson pr_number ${{ github.event.issue.number }} \
|
||||
--arg repo "${{ github.event.repository.name }}" \
|
||||
--arg comment "$COMMENT_TEXT" \
|
||||
'{event_type: "create-docs", client_payload: {pr_number: $pr_number, repo: $repo, comment_text: $comment}}' | \
|
||||
gh api repos/windmill-labs/windmilldocs/dispatches \
|
||||
--method POST \
|
||||
--input -
|
||||
|
||||
@@ -13,38 +13,16 @@ on:
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
determine-commenter:
|
||||
check-membership:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/spawnbackend')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/spawnbackend'))
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
commenter: ${{ steps.determine-commenter.outputs.commenter }}
|
||||
steps:
|
||||
- name: Determine commenter
|
||||
id: determine-commenter
|
||||
run: |
|
||||
# Work out who wrote the comment / review
|
||||
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 }}
|
||||
|
||||
spawn-backend:
|
||||
needs: [determine-commenter, check-membership]
|
||||
needs: check-membership
|
||||
# Only run on PR comments that contain /spawn-backend, or manual dispatch
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
"svelte": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.svelte.dev/mcp"
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
}
|
||||
}
|
||||
}
|
||||
+144
@@ -1,5 +1,149 @@
|
||||
# Changelog
|
||||
|
||||
## [1.635.1](https://github.com/windmill-labs/windmill/compare/v1.635.0...v1.635.1) (2026-02-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pin strum_macros to 0.27 to match strum version ([#7957](https://github.com/windmill-labs/windmill/issues/7957)) ([68f766e](https://github.com/windmill-labs/windmill/commit/68f766e1ae54dbe2fe42769559d81d4d76a409ef))
|
||||
|
||||
## [1.635.0](https://github.com/windmill-labs/windmill/compare/v1.634.6...v1.635.0) (2026-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Kubernetes operator and instance settings YAML editor ([#7836](https://github.com/windmill-labs/windmill/issues/7836)) ([82e5f6d](https://github.com/windmill-labs/windmill/commit/82e5f6de48e246a49b25e7d4ea7be65122e8772c))
|
||||
* add maven settings.xml support for java private registries ([#7940](https://github.com/windmill-labs/windmill/issues/7940)) ([581dde8](https://github.com/windmill-labs/windmill/commit/581dde8d0bc4428a5e95fcb5341239231ab36ef6))
|
||||
* **cli:** add `lint` command ([#7917](https://github.com/windmill-labs/windmill/issues/7917)) ([37d1277](https://github.com/windmill-labs/windmill/commit/37d1277b91d1b8a03e327b0585f547037482498d))
|
||||
* handle $var: and $res: in arrays for transform_json_value ([#7949](https://github.com/windmill-labs/windmill/issues/7949)) ([e4a34d0](https://github.com/windmill-labs/windmill/commit/e4a34d031b2bdb1b73a2a7ca68544fa34f83ed0f))
|
||||
* IaC hints, YAML editor for worker configs ([#7956](https://github.com/windmill-labs/windmill/issues/7956)) ([8b8e33e](https://github.com/windmill-labs/windmill/commit/8b8e33e2dc1a2b4c0effab70463f6d4b402a0f7f))
|
||||
* open-source worker group configuration UI ([#7954](https://github.com/windmill-labs/windmill/issues/7954)) ([6cf3f5f](https://github.com/windmill-labs/windmill/commit/6cf3f5f4a35a6139b5cdf9f44af29c3941f19645))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow renaming of backend runnables in the UI ([6215760](https://github.com/windmill-labs/windmill/commit/6215760b1294d55245909a1c1de6c4cc8cef320a))
|
||||
* **go:** preserve proxy envs for go mod tidy/download ([#7946](https://github.com/windmill-labs/windmill/issues/7946)) ([8410b59](https://github.com/windmill-labs/windmill/commit/8410b59a8f23d62c57e497d170449643b46595a0))
|
||||
* Missing app policy for datatable ([#7944](https://github.com/windmill-labs/windmill/issues/7944)) ([a9dbd1f](https://github.com/windmill-labs/windmill/commit/a9dbd1f73fca9100b64106281802c43881181e78))
|
||||
* strip slack_oauth_client_secret from get_settings for non-admins ([#7950](https://github.com/windmill-labs/windmill/issues/7950)) ([43218c6](https://github.com/windmill-labs/windmill/commit/43218c62852490d0efafa8f94385bfe0e8f2ad82))
|
||||
|
||||
## [1.634.6](https://github.com/windmill-labs/windmill/compare/v1.634.5...v1.634.6) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* full build fix with new rustup config ([caccdd5](https://github.com/windmill-labs/windmill/commit/caccdd553ad72ff26c2c7c45f0ff3a25bd19a49f))
|
||||
|
||||
## [1.634.5](https://github.com/windmill-labs/windmill/compare/v1.634.4...v1.634.5) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* rust + java works with just /tmp mounted ([e144432](https://github.com/windmill-labs/windmill/commit/e144432a168178a531aa146def0aff478f3d1586))
|
||||
|
||||
## [1.634.4](https://github.com/windmill-labs/windmill/compare/v1.634.3...v1.634.4) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve style panel reactivity and CSS defaults ([#7935](https://github.com/windmill-labs/windmill/issues/7935)) ([eacbee3](https://github.com/windmill-labs/windmill/commit/eacbee38cb51d11b051612ac66994e6444e81bf2))
|
||||
* java + rust only relies on /tmp, + https proxy improvement for java ([791cb3e](https://github.com/windmill-labs/windmill/commit/791cb3e225ebda4b2f6f7181bc8265c378150d4e))
|
||||
|
||||
## [1.634.3](https://github.com/windmill-labs/windmill/compare/v1.634.2...v1.634.3) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix incorrect oauth base url refresh error ([b3a1629](https://github.com/windmill-labs/windmill/commit/b3a1629e56217605d059d1dceca43c9999a58592))
|
||||
|
||||
## [1.634.2](https://github.com/windmill-labs/windmill/compare/v1.634.1...v1.634.2) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix hub schedule not set at on-boarding ([beeb19d](https://github.com/windmill-labs/windmill/commit/beeb19db04e8e9059c63007d2402981c3e81f1e2))
|
||||
|
||||
## [1.634.1](https://github.com/windmill-labs/windmill/compare/v1.634.0...v1.634.1) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* conditionally skip relock on dep job ([#7860](https://github.com/windmill-labs/windmill/issues/7860)) ([d6c72df](https://github.com/windmill-labs/windmill/commit/d6c72df99a0a500bdd925fcdcba8abd8bbe537f5))
|
||||
* improve on-boarding experience ([4e38a4f](https://github.com/windmill-labs/windmill/commit/4e38a4f1083d880b0814e336d5e27cb40187fc28))
|
||||
|
||||
## [1.634.0](https://github.com/windmill-labs/windmill/compare/v1.633.1...v1.634.0) (2026-02-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add force_sandboxing global setting and #sandbox bash annotation ([#7816](https://github.com/windmill-labs/windmill/issues/7816)) ([2646629](https://github.com/windmill-labs/windmill/commit/2646629194f260d0be3a809be421bbab1307f927))
|
||||
* support for datatables in App Db studio ([#7930](https://github.com/windmill-labs/windmill/issues/7930)) ([6cee34a](https://github.com/windmill-labs/windmill/commit/6cee34a81da389faebb1474957a3989c4aadb00f))
|
||||
|
||||
## [1.633.1](https://github.com/windmill-labs/windmill/compare/v1.633.0...v1.633.1) (2026-02-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add private registries support for RUST + java home nit ([e2c28e4](https://github.com/windmill-labs/windmill/commit/e2c28e42dbda0f7bf119efc8d587da0d30636a44))
|
||||
|
||||
## [1.633.0](https://github.com/windmill-labs/windmill/compare/v1.632.0...v1.633.0) (2026-02-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* /health endpoints ([#7727](https://github.com/windmill-labs/windmill/issues/7727)) ([7df4aa4](https://github.com/windmill-labs/windmill/commit/7df4aa4fec021fc728950fc10db4d0401a205087))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* save deployment msg in CE ([#7923](https://github.com/windmill-labs/windmill/issues/7923)) ([e9be616](https://github.com/windmill-labs/windmill/commit/e9be616d3c079a0c6fd98733c66560f2cc1ee40d))
|
||||
|
||||
## [1.632.0](https://github.com/windmill-labs/windmill/compare/v1.631.2...v1.632.0) (2026-02-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ai:** add AWS bedrock session token support ([#7908](https://github.com/windmill-labs/windmill/issues/7908)) ([d95e4db](https://github.com/windmill-labs/windmill/commit/d95e4db8f31e70a645a5f41e287557933b257db8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add kafka kerberos runtime packages ([#7918](https://github.com/windmill-labs/windmill/issues/7918)) ([22f22c2](https://github.com/windmill-labs/windmill/commit/22f22c26612b904c2b82b415ec9337213ef593c3))
|
||||
* **frontend:** redesign instance settings ([#7916](https://github.com/windmill-labs/windmill/issues/7916)) ([dd419ad](https://github.com/windmill-labs/windmill/commit/dd419ade94a992073dfd1a979bfeb8a5fadfb051))
|
||||
* hash long dedicated worker tags ([#7914](https://github.com/windmill-labs/windmill/issues/7914)) ([aaa1b92](https://github.com/windmill-labs/windmill/commit/aaa1b92300bdc0794de5356ef6750bab5d8d81a0))
|
||||
|
||||
## [1.631.2](https://github.com/windmill-labs/windmill/compare/v1.631.1...v1.631.2) (2026-02-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** revert CloseButton refactor that broke tag removal in MultiSelect ([#7909](https://github.com/windmill-labs/windmill/issues/7909)) ([b11d6ed](https://github.com/windmill-labs/windmill/commit/b11d6ed7940faddfe74a22b25bcb132527cbcec8))
|
||||
* nix flake libz.so for deno_core ([#7905](https://github.com/windmill-labs/windmill/issues/7905)) ([900c76c](https://github.com/windmill-labs/windmill/commit/900c76ccad09f58fc6adcfa8151db780249617f6))
|
||||
* strip unsupported schema fields for Google AI ([#7894](https://github.com/windmill-labs/windmill/issues/7894)) ([5effb87](https://github.com/windmill-labs/windmill/commit/5effb87a36793e20d17e678619091ef458fe8f0a)), closes [#7759](https://github.com/windmill-labs/windmill/issues/7759)
|
||||
|
||||
## [1.631.1](https://github.com/windmill-labs/windmill/compare/v1.631.0...v1.631.1) (2026-02-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add kafka-gssapi support to ee builds ([2815cfa](https://github.com/windmill-labs/windmill/commit/2815cfae1a5eefb2c553e893dc926e62ad1df528))
|
||||
|
||||
## [1.631.0](https://github.com/windmill-labs/windmill/compare/v1.630.2...v1.631.0) (2026-02-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ai:** support 1M context window for Anthropic resources ([#7891](https://github.com/windmill-labs/windmill/issues/7891)) ([f22eb96](https://github.com/windmill-labs/windmill/commit/f22eb964e47defe9922ecdb6ab471dd4ca267952))
|
||||
* **uv:** index resolve strategy ([#7885](https://github.com/windmill-labs/windmill/issues/7885)) ([097d928](https://github.com/windmill-labs/windmill/commit/097d9288c58076882f1991e2fb33e4441fe332d3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** improve time picker ([#7893](https://github.com/windmill-labs/windmill/issues/7893)) ([31bfccc](https://github.com/windmill-labs/windmill/commit/31bfccc74588af10fc11fbbb0bc4833d65ff6421))
|
||||
* otel gracefully handle no native ts ([92c6018](https://github.com/windmill-labs/windmill/commit/92c601860f1e1211aa34838cd08900ba7334a20c))
|
||||
* waitJob getJob and streamJob in raw apps ([#7901](https://github.com/windmill-labs/windmill/issues/7901)) ([754b48c](https://github.com/windmill-labs/windmill/commit/754b48cb898dffe196339cea1c1598c9e1765cdc))
|
||||
* worker do not apply migrations anymore but wait for servers to do so ([7eb239f](https://github.com/windmill-labs/windmill/commit/7eb239f1e2eb1b71234a8d4265c7c5813e5861ae))
|
||||
|
||||
## [1.630.2](https://github.com/windmill-labs/windmill/compare/v1.630.1...v1.630.2) (2026-02-11)
|
||||
|
||||
|
||||
|
||||
@@ -17,19 +17,52 @@ When implementing new features in Windmill, follow these best practices:
|
||||
|
||||
## Language-Specific Guides
|
||||
|
||||
- Backend (Rust): @backend/rust-best-practices.mdc + @backend/summarized_schema.txt
|
||||
- Frontend (Svelte 5): @frontend/svelte5-best-practices.mdc
|
||||
- Backend (Rust): see `backend/CLAUDE.md` and the `rust-backend` skill: `.claude/skills/rust-backend/SKILL.md`
|
||||
- Frontend (Svelte 5): see `frontend/CLAUDE.md` and the `svelte-frontend` skill: `.claude/skills/svelte-frontend/SKILL.md`
|
||||
|
||||
## Dev Environment
|
||||
|
||||
- **Backend**: `cargo run` from `backend/` (API at http://localhost:8000)
|
||||
- **Frontend**: `REMOTE=http://localhost:8000 npm run dev` from `frontend/`
|
||||
- The `REMOTE` env var configures the Vite proxy target. Without it, API calls proxy to `https://app.windmill.dev` instead of the local backend.
|
||||
- The dev server starts on port 3000 (or 3001+ if 3000 is in use).
|
||||
- **Default login**: `admin@windmill.dev` / `changeme`
|
||||
- **Instance settings**: navigate to `/#superadmin-settings` (opens the drawer overlay)
|
||||
|
||||
## UI Testing with Playwright MCP
|
||||
|
||||
When testing the frontend with the Playwright MCP tools:
|
||||
|
||||
1. **Start servers**: Launch backend (`cargo run`) and frontend (`REMOTE=http://localhost:8000 npm run dev`) as background tasks
|
||||
2. **Wait for readiness**: Backend takes ~60s to compile; check output for `health check completed`. Frontend starts in ~5s.
|
||||
3. **Login flow**: Navigate to `/user/login`, click "Log in without third-party", fill email/password, submit
|
||||
4. **Instance settings drawer**: Navigate to `/#superadmin-settings` to open the drawer directly
|
||||
5. **Toggle components**: The YAML toggle uses a custom `<Toggle>` component where the checkbox is visually hidden (`sr-only`). Click the wrapper `<label>` element (the parent container with `cursor=pointer`), not the checkbox ref directly.
|
||||
6. **Console errors to ignore**: `critical_alerts` 404s are expected on CE builds (EE-only endpoint). VSCode worker 404s are dev-mode artifacts.
|
||||
|
||||
## Code Validation (MUST DO)
|
||||
|
||||
After making code changes, you MUST run the appropriate checks and fix all errors before considering the work done:
|
||||
|
||||
- **Backend**: Run `cargo check` from the `backend/` directory. Only enable the feature flags needed for the code you changed — check `backend/Cargo.toml` `[features]` section to identify which flags gate the crates/modules you modified. For example: `cargo check --features enterprise,parquet` if you only touched enterprise and parquet code.
|
||||
- **Frontend**: Run `npm run check` from the `frontend/` directory.
|
||||
|
||||
## Querying the Database
|
||||
|
||||
To query the database directly, use psql with the following connection string:
|
||||
`backend/summarized_schema.txt` provides a compact overview of all tables, columns, types, ENUMs, and foreign keys. Use it to quickly understand the data model and relationships. Note: this file is a simplified summary — it omits indexes, constraints details, and other metadata.
|
||||
|
||||
For exact table definitions (indexes, constraints, column defaults, etc.), query the database directly:
|
||||
|
||||
```bash
|
||||
psql postgres://postgres:changeme@localhost:5432/windmill
|
||||
```
|
||||
|
||||
This can be helpful for:
|
||||
Useful psql commands:
|
||||
- `\d <table_name>` — full table definition with indexes and constraints
|
||||
- `\di <table_name>*` — list indexes for a table
|
||||
- `\d+ <table_name>` — extended table info including storage and descriptions
|
||||
|
||||
This is also helpful for:
|
||||
- Inspecting database state during development
|
||||
- Testing queries before implementing them in Rust
|
||||
- Debugging data-related issues
|
||||
|
||||
+6
-4
@@ -132,6 +132,7 @@ ARG WITH_POWERSHELL=true
|
||||
ARG WITH_KUBECTL=true
|
||||
ARG WITH_HELM=true
|
||||
ARG WITH_GIT=true
|
||||
ARG features=""
|
||||
|
||||
# To change latest stable version:
|
||||
# 1. Change placeholder in instanceSettings.ts
|
||||
@@ -149,7 +150,8 @@ ENV PATH /usr/local/bin:/root/.local/bin:/tmp/.local/bin:$PATH
|
||||
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends netbase tzdata ca-certificates wget curl jq unzip build-essential unixodbc xmlsec1 software-properties-common tini libsasl2-modules-gssapi-mit \
|
||||
&& apt-get install -y --no-install-recommends netbase tzdata ca-certificates wget curl jq unzip build-essential unixodbc xmlsec1 software-properties-common tini \
|
||||
&& if echo "$features" | grep -q "ee"; then apt-get install -y --no-install-recommends libsasl2-modules-gssapi-mit krb5-user; fi \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -239,7 +241,7 @@ RUN mkdir -p /tmp/windmill/cache && \
|
||||
cp -r /tmp/build_cache/* /tmp/windmill/cache/ && \
|
||||
chmod -R a+rw /tmp/windmill/cache && \
|
||||
rm -rf /tmp/build_cache && \
|
||||
mkdir -p -m 777 /tmp/windmill/cache/uv /tmp/windmill/cache/go
|
||||
mkdir -p -m 777 /tmp/windmill/cache/uv /tmp/windmill/cache/go /tmp/windmill/cache/rustup /tmp/windmill/cache/cargo
|
||||
|
||||
# Runtime cache locations
|
||||
ENV UV_CACHE_DIR=/tmp/windmill/cache/uv
|
||||
@@ -262,8 +264,8 @@ COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer
|
||||
# add the docker client to call docker from a worker if enabled
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
ENV RUSTUP_HOME="/usr/local/rustup"
|
||||
ENV CARGO_HOME="/usr/local/cargo"
|
||||
ENV RUSTUP_HOME="/tmp/windmill/cache/rustup"
|
||||
ENV CARGO_HOME="/tmp/windmill/cache/cargo"
|
||||
ENV LD_LIBRARY_PATH="."
|
||||
|
||||
# nsjail runtime deps and binary
|
||||
|
||||
@@ -31,7 +31,7 @@ Scripts are turned into sharable UIs automatically, and can be composed together
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://app.windmill.dev">Try it</a> - <a href="https://www.windmill.dev/docs/intro/">Docs</a> - <a href="https://discord.gg/V7PM2YHsPB">Discord</a> - <a href="https://hub.windmill.dev">Hub</a> - <a href="https://www.windmill.dev/docs/misc/contributing">Contributor's guide</a>
|
||||
<a href="https://app.windmill.dev">Try it</a> - <a href="https://www.windmill.dev/">Website</a> - <a href="https://www.windmill.dev/docs/intro/">Docs</a> - <a href="https://discord.gg/V7PM2YHsPB">Discord</a> - <a href="https://hub.windmill.dev">Hub</a> - <a href="https://www.windmill.dev/docs/misc/contributing">Contributor's guide</a>
|
||||
</p>
|
||||
|
||||
# Windmill - Developer platform for APIs, background jobs, workflows and UIs
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n path,\n custom_path\n FROM \n app\n WHERE \n custom_path IN (\n SELECT \n custom_path\n FROM \n app\n GROUP \n BY custom_path\n HAVING COUNT(*) > 1\n )\n ORDER BY custom_path\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "custom_path",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "11e24f758a70cd5f3a240bc81a05f40754826db0ee1194409227597a98603e92"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM resource_type WHERE workspace_id = 'admins' AND name = $1 AND schema IS NOT DISTINCT FROM $2 AND description IS NOT DISTINCT FROM $3)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1ea97f9085ec018f779e77e0fdbda3d4ecd67b3fbee9a58228ef577f846607ae"
|
||||
}
|
||||
+2
-1
@@ -16,7 +16,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n path,\n custom_path\n FROM\n app\n WHERE\n custom_path IN (\n SELECT\n custom_path\n FROM\n app\n GROUP\n BY custom_path\n HAVING COUNT(*) > 1\n )\n ORDER BY custom_path\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "custom_path",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "26b35cf50959b1b1fd7e1cb33c65da40d29e20fd16b02355ba073f420c03a767"
|
||||
}
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT logs FROM job_logs WHERE created_at > $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "logs",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "3cc7ecab48c379cd845b22012ef1fe1573fc332c9f1c44960084f3784ddb3f54"
|
||||
}
|
||||
+4
-2
@@ -16,7 +16,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -51,7 +52,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO resource_type (workspace_id, name, schema, description, edited_at)\n VALUES ('admins', $1, $2, $3, now())\n ON CONFLICT (workspace_id, name) DO UPDATE\n SET schema = EXCLUDED.schema, description = EXCLUDED.description, edited_at = now()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4b93550c7836fd3643180ade3548faa875e471d3f9ca37fc669f359e7a1818bb"
|
||||
}
|
||||
+2
-1
@@ -16,7 +16,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path, importer_node_id, imported_lockfile_hash)\n VALUES ($1, $2, $3::text::IMPORTER_KIND, $4, $5, $6)\n ON CONFLICT (workspace_id, importer_node_id, importer_kind, importer_path, imported_path)\n DO UPDATE SET imported_lockfile_hash = EXCLUDED.imported_lockfile_hash",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "55a7460901c8ccdb478a2b7a3d4bd9d367838fe80844b5a14d2ef66452f36297"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT hash FROM script WHERE path = 'f/rel/leaf_2' AND workspace_id = 'test-workspace' AND archived = false ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hash",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5f6d0d1b24693db22230ec8322b3174ecc195242a1205131f5d0d32148c0280a"
|
||||
}
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO workspace_dependencies(name, workspace_id, content, language, description)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ",
|
||||
"query": "\n WITH ins AS (\n INSERT INTO workspace_dependencies(name, workspace_id, content, language, description)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ), lock_ins AS (\n INSERT INTO lock_hash (workspace_id, path, lockfile_hash)\n VALUES ($2, $6, $7)\n ON CONFLICT (workspace_id, path) DO UPDATE SET lockfile_hash = $7\n )\n SELECT id FROM ins\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -46,12 +46,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Text"
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6c10c38a5d5e560d8d76159ce0ae118cf7191b06d72285e71256dd6d70c19451"
|
||||
"hash": "8acc8c47123af84d2308042dcd34d04b941e928c7621e66d3bdc510c335f8c20"
|
||||
}
|
||||
+2
-1
@@ -17,7 +17,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n COUNT(*) > 0\n AND BOOL_AND(\n CASE\n -- For dependencies/: use IS NOT DISTINCT FROM (NULL = NULL is true for legacy)\n -- And the reason for this, is that for every script we also add dependency on default workspace dependencies by default\n -- that default/unnamed workspace dependencies may not exist, but we still do this.\n -- it is needed for windmill to know what to redeploy when default workspace dependencies are being added\n\n -- naturally for non-existant entries we have no hash of it\n -- so if we compared hash with '=' (instead of IS NOT DISTINCT FROM), it would give false on NULL = NULL,\n -- which would mean that this entire query returns false, which means relock skip cannot happen\n\n -- The solution is to say if both: referenced and current hash are NULLs we treat it as true, so it becomes no longer a blocker from skip.\n --\n -- It is backed up by the fact that server is responsible for deploying new workspace dependencies\n -- so if one was to deploy a new wdeps, server would assign it new hash, and this expression would be invalid and would not approve skip\n WHEN dm.imported_path LIKE 'dependencies/%'\n THEN dm.imported_lockfile_hash IS NOT DISTINCT FROM lh.lockfile_hash\n\n -- For scripts: use = with COALESCE (NULL = NULL becomes false)\n -- unlike w deps, we can't do IS NOT DISTINCT FROM here\n -- the reason is that scripts deployments are issued by other workers instead of the server\n -- which would mean that there is no guarantee that new d job will also write it's lock's hash to the `lock_hash`\n -- which could lead to false positives\n ELSE COALESCE(dm.imported_lockfile_hash = lh.lockfile_hash, false)\n END\n )\n FROM dependency_map dm\n LEFT JOIN lock_hash lh\n ON lh.workspace_id = dm.workspace_id\n AND lh.path = dm.imported_path\n WHERE dm.workspace_id = $1\n AND dm.importer_path = $2\n AND dm.importer_node_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9ca267c5a88f0cf3a4821e7514fe585131a42136c522a0fdde47c63cc0c6bc5a"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, deployment_msg) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL DO UPDATE SET deployment_msg = EXCLUDED.deployment_msg",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9f07510019ebe6f0c5fa17bf31c2d14755474cba82b3b388a47585a8bb325b1a"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM app WHERE path = 'g/all/setup_app')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a264bbd8dbabb03854bd25350a7aeda0704770eb200bae635f1933eece90c9d6"
|
||||
}
|
||||
+4
-2
@@ -21,7 +21,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -71,7 +72,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM worker_ping WHERE ping_at > now() - interval '5 minutes'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "bbf166552ba15ce7cd76f812cdf7223414fa5c8a4860f3fe829d649baa1c5465"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM dependency_map\n WHERE workspace_id = $1\n AND importer_path = $2\n AND importer_kind = $3::text::IMPORTER_KIND\n AND importer_node_id = $4\n AND imported_path = $5\n ",
|
||||
"query": "\n DELETE FROM dependency_map\n WHERE workspace_id = $1\n AND importer_path = $2\n AND importer_kind = $3::text::IMPORTER_KIND\n AND importer_node_id = $4\n AND imported_path = $5\n AND imported_lockfile_hash IS NOT DISTINCT FROM $6 -- we don't want to delete other entries with other lockfile hash.\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -9,10 +9,11 @@
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ead84a63cb965e36155605434c9e3670e50344f39b0d61eef8e60c2cdb57ae1f"
|
||||
"hash": "c8dee3bd1ce03d8ee3c05345a0bdd5bdbd75396fe5bc24fd2d42edc79261169c"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT lock FROM script WHERE path = $1 AND workspace_id = $2 AND lock IS NOT NULL\n AND deleted = false ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "lock",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cbbe20745c2b743acc814e8e77c0e4e154b511af676e085bbe88e72e7d14aee3"
|
||||
}
|
||||
+2
-1
@@ -11,7 +11,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d697b7311430e7bd5375ec5494179f4071e3bfe123d9600249cfa80c1103edd8"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "e004ebd5b5532a4b85984a62f8ad48a81aa3460c1ca07701f386135d72cdecf5"
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path, importer_node_id)\n VALUES ($1, $2, $3::text::IMPORTER_KIND, $4, $5) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e32d6c6ae4e0d824c4cf19128182d67f36d1fd87fe4cf4f002e18367088c497c"
|
||||
}
|
||||
+4
-2
@@ -21,7 +21,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -71,7 +72,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -21,7 +21,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -71,7 +72,8 @@
|
||||
"name": "native_trigger_service",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"nextcloud"
|
||||
"nextcloud",
|
||||
"google"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH update_lock AS (\n UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3\n )\n INSERT INTO lock_hash (workspace_id, path, lockfile_hash)\n VALUES ($3, $4, $5)\n ON CONFLICT (workspace_id, path) DO UPDATE SET lockfile_hash = $5",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f8c08997d7fd0158b4d3cc093fb0a2aadd4473e98873d5a7873f4fa944b99d58"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n worker_group,\n wm_version\n FROM worker_ping\n WHERE ping_at > now() - interval '5 minutes'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "worker_group",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "wm_version",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f912b91c940900b9961920ac0f71b7ba1e8cd50ce90c0c17336e9dd9c684f348"
|
||||
}
|
||||
@@ -49,6 +49,28 @@ Windmill uses a workspace-based architecture with multiple crates:
|
||||
- Use feature flags: `#[cfg(feature = "enterprise")]`
|
||||
- Isolate enterprise code in separate modules
|
||||
|
||||
## Code Validation (MUST DO)
|
||||
|
||||
After making backend changes, you MUST run `cargo check` and fix all errors and warnings before considering the work done.
|
||||
|
||||
Only enable the feature flags relevant to your changes — do NOT use `all_sqlx_features` as it compiles the entire codebase and is very slow. Check the `[features]` section in `Cargo.toml` to identify which flags gate the crates/modules you modified.
|
||||
|
||||
Examples:
|
||||
```bash
|
||||
# Changed core code (no feature-gated modules)
|
||||
cargo check
|
||||
|
||||
# Changed code behind the enterprise feature
|
||||
cargo check --features enterprise
|
||||
|
||||
# Changed kafka trigger code
|
||||
cargo check --features kafka
|
||||
```
|
||||
|
||||
## Git Workflow
|
||||
|
||||
- **Never push directly to main** — always create a branch and open a pull request
|
||||
|
||||
## Testing
|
||||
|
||||
- Write unit tests for core functionality
|
||||
|
||||
Generated
+351
-259
File diff suppressed because it is too large
Load Diff
+27
-13
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.630.2"
|
||||
version = "1.635.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -9,6 +9,7 @@ resolver = "2"
|
||||
members = [
|
||||
"./windmill-api",
|
||||
"./windmill-api-scripts",
|
||||
"./windmill-api-flows",
|
||||
"./windmill-api-users",
|
||||
"./windmill-api-workspaces",
|
||||
"./windmill-api-groups",
|
||||
@@ -51,6 +52,7 @@ members = [
|
||||
"./windmill-audit",
|
||||
"./windmill-git-sync",
|
||||
"./windmill-autoscaling",
|
||||
"./windmill-operator",
|
||||
"./windmill-indexer",
|
||||
"./windmill-macros",
|
||||
"./windmill-oauth",
|
||||
@@ -66,12 +68,14 @@ members = [
|
||||
"./parsers/windmill-parser-py",
|
||||
"./parsers/windmill-parser-py-imports",
|
||||
"./parsers/windmill-sql-datatype-parser-wasm",
|
||||
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu"
|
||||
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu",
|
||||
"./windmill-test-utils",
|
||||
"./windmill-api-integration-tests",
|
||||
]
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.630.2"
|
||||
version = "1.635.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -92,8 +96,8 @@ lto = "thin"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
private = ["windmill-api/private", "windmill-api-agent-workers?/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"]
|
||||
agent_worker_server = ["windmill-api/agent_worker_server", "dep:windmill-api-agent-workers"]
|
||||
private = ["windmill-api/private", "windmill-api-agent-workers?/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-operator?/private", "windmill-queue/private", "windmill-worker/private", "windmill-test-utils/private"]
|
||||
agent_worker_server = ["windmill-api/agent_worker_server", "dep:windmill-api-agent-workers", "windmill-test-utils/agent_worker_server"]
|
||||
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-api-agent-workers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
|
||||
local_reports = ["windmill-common/local_reports"]
|
||||
enterprise_saml = ["windmill-api/enterprise_saml", "oauth2"]
|
||||
@@ -109,7 +113,7 @@ cloud = ["windmill-queue/cloud", "windmill-worker/cloud", "windmill-common/cloud
|
||||
jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-ctl"]
|
||||
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy", "windmill-indexer/enterprise", "windmill-indexer/parquet", "windmill-common/tantivy", "enterprise", "parquet"]
|
||||
sqlx = ["windmill-worker/sqlx"]
|
||||
deno_core = ["windmill-worker/deno_core", "dep:windmill-runtime-nativets"]
|
||||
deno_core = ["windmill-worker/deno_core", "dep:windmill-runtime-nativets", "windmill-test-utils/deno_core"]
|
||||
deno_core_mac = ["deno_core", "windmill-worker/libffi_mac"]
|
||||
kafka = ["windmill-api/kafka"]
|
||||
kafka-gssapi = ["windmill-api/kafka-gssapi"]
|
||||
@@ -132,9 +136,11 @@ zip = ["windmill-api/zip"]
|
||||
static_frontend = ["windmill-api/static_frontend"]
|
||||
scoped_cache = ["windmill-common/scoped_cache"]
|
||||
no_auth = ["windmill-api/no_auth"]
|
||||
operator = ["dep:windmill-operator"]
|
||||
test_job_debouncing = []
|
||||
private_registry_test = []
|
||||
# Languages
|
||||
python = ["windmill-worker/python", "windmill-api/python"]
|
||||
python = ["windmill-worker/python", "windmill-api/python", "windmill-test-utils/python"]
|
||||
rust = ["windmill-worker/rust"]
|
||||
mysql = ["windmill-worker/mysql"]
|
||||
oracledb = ["windmill-worker/oracledb"]
|
||||
@@ -160,11 +166,11 @@ oss_core = [
|
||||
"static_frontend", "mcp", "bedrock", "inline_preview",
|
||||
"quickjs"
|
||||
]
|
||||
ce_core = ["oss_core", "private"]
|
||||
ce_core = ["oss_core", "private", "operator"]
|
||||
ee_core = [
|
||||
"enterprise", "stripe", "prometheus", "cloud",
|
||||
"kafka", "sqs_trigger", "nats", "gcp_trigger",
|
||||
"jemalloc", "otel"
|
||||
"jemalloc", "otel", "operator"
|
||||
]
|
||||
ee_server = ["enterprise_saml", "tantivy", "agent_worker_server", "local_reports"]
|
||||
# Edition meta-features: CE variants
|
||||
@@ -172,13 +178,14 @@ oss = ["oss_core", "all_languages", "no_auth"]
|
||||
ce_rpi = ["ce_core", "all_languages"]
|
||||
ce = ["ce_rpi", "jemalloc", "dind", "agent_worker_server"]
|
||||
# Edition meta-features: EE variants
|
||||
ee = ["ce", "ee_core", "ee_server"]
|
||||
ee_rhel = ["ce_core", "ee_core", "all_languages"]
|
||||
ee = ["ce", "ee_core", "ee_server", "kafka-gssapi"]
|
||||
ee_rhel = ["ce_core", "ee_core", "kafka-gssapi", "all_languages"]
|
||||
ee_windows = ["ce_core", "ee_core", "all_languages_windows"]
|
||||
all_sqlx_features = ["all_languages", "enterprise", "enterprise_saml", "embedding", "parquet", "prometheus", "flow_testing",
|
||||
"openidconnect", "cloud", "jemalloc", "tantivy", "sqlx", "kafka", "kafka-gssapi", "nats", "otel", "dind", "websocket", "http_trigger",
|
||||
"postgres_trigger", "mcp", "mqtt_trigger", "sqs_trigger", "gcp_trigger", "smtp", "stripe",
|
||||
"license", "oauth2", "zip", "static_frontend", "scoped_cache", "agent_worker_server", "bedrock", "native_trigger", "quickjs"]
|
||||
"license", "oauth2", "zip", "static_frontend", "scoped_cache", "agent_worker_server", "bedrock", "native_trigger", "quickjs",
|
||||
"windmill-git-sync/all_sqlx_features"]
|
||||
|
||||
[patch.crates-io]
|
||||
object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7" }
|
||||
@@ -199,8 +206,10 @@ windmill-api-settings.workspace = true
|
||||
windmill-worker.workspace = true
|
||||
windmill-indexer = { workspace = true, optional = true }
|
||||
windmill-autoscaling = { workspace = true, optional = true }
|
||||
windmill-operator = { workspace = true, optional = true }
|
||||
futures.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
sqlx.workspace = true
|
||||
sql-builder.workspace = true
|
||||
rand.workspace = true
|
||||
@@ -239,6 +248,7 @@ serde_json.workspace = true
|
||||
reqwest.workspace = true
|
||||
windmill-queue = { workspace = true, features = ["failpoints"] }
|
||||
windmill-dep-map.workspace = true
|
||||
windmill-test-utils.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
windmill-api-client.workspace = true
|
||||
@@ -261,12 +271,14 @@ windmill-common = { path = "./windmill-common", default-features = false }
|
||||
windmill-audit = { path = "./windmill-audit" }
|
||||
windmill-git-sync = { path = "./windmill-git-sync" }
|
||||
windmill-autoscaling = { path = "./windmill-autoscaling" }
|
||||
windmill-operator = { path = "./windmill-operator" }
|
||||
windmill-indexer = {path = "./windmill-indexer"}
|
||||
windmill-mcp = {path = "./windmill-mcp"}
|
||||
windmill-oauth = {path = "./windmill-oauth"}
|
||||
windmill-macros = {path = "./windmill-macros"}
|
||||
windmill-api-auth = { path = "./windmill-api-auth" }
|
||||
windmill-api-scripts = { path = "./windmill-api-scripts" }
|
||||
windmill-api-flows = { path = "./windmill-api-flows" }
|
||||
windmill-api-users = { path = "./windmill-api-users" }
|
||||
windmill-api-workspaces = { path = "./windmill-api-workspaces" }
|
||||
windmill-api-groups = { path = "./windmill-api-groups" }
|
||||
@@ -315,6 +327,7 @@ windmill-parser-php = { path = "./parsers/windmill-parser-php" }
|
||||
windmill-jseval = { path = "./windmill-jseval" }
|
||||
windmill-runtime-nativets = { path = "./windmill-runtime-nativets" }
|
||||
windmill-api-client = { path = "./windmill-api-client" }
|
||||
windmill-test-utils = { path = "./windmill-test-utils" }
|
||||
|
||||
reqwest-retry = "^0"
|
||||
reqwest-middleware = { version = "^0", features = ["json"] }
|
||||
@@ -551,6 +564,7 @@ backon = "1.3.0"
|
||||
|
||||
flume = { version = "0.11.1", features = ["async"] }
|
||||
kube = { version = "1.1.0", features = ["runtime", "derive"] }
|
||||
schemars = "0.8"
|
||||
k8s-openapi = { version = "0.25.0", features = ["latest"] }
|
||||
libloading = "0.8.8"
|
||||
|
||||
@@ -570,7 +584,7 @@ tree-sitter-ruby = "0.23.0"
|
||||
oracle = { version = "0.6.3", features = ["chrono"] }
|
||||
rumqttc = { version = "0.24.0", features = ["use-native-tls"]}
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
strum_macros = "^0"
|
||||
strum_macros = "0.27"
|
||||
hudsucker = { version = "0.22", features = ["rcgen-ca", "native-tls-client"] }
|
||||
hyper-http-proxy = { version = "1", default-features = false, features = ["native-tls"] }
|
||||
rcgen = "0.13"
|
||||
|
||||
@@ -1 +1 @@
|
||||
b2bd490cd14e7794bebbd8737c2833ea671370b3
|
||||
c2f12c6989e9f069fd274085aefde29264f73c8c
|
||||
@@ -0,0 +1,3 @@
|
||||
DROP TABLE IF EXISTS lock_hash;
|
||||
DROP INDEX IF EXISTS dependency_map_importer_path_idx;
|
||||
ALTER TABLE dependency_map DROP COLUMN IF EXISTS imported_lockfile_hash;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Add column to track lockfile hash of imported scripts
|
||||
-- Used to skip re-locking when imports' lockfiles haven't changed
|
||||
ALTER TABLE dependency_map ADD COLUMN imported_lockfile_hash BIGINT;
|
||||
|
||||
-- Index for queries filtering by importer (skip-relock check, load(), clear_map_for_item)
|
||||
CREATE INDEX IF NOT EXISTS dependency_map_importer_path_idx ON dependency_map (workspace_id, importer_path);
|
||||
|
||||
-- Stores lockfile/content hashes to detect when imports' locks have changed
|
||||
CREATE TABLE lock_hash (
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
path VARCHAR(255) NOT NULL,
|
||||
lockfile_hash BIGINT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, path)
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
-- The setup_app was a complex app created via multiple migrations.
|
||||
-- Restoring it would require re-running the original creation and update migrations.
|
||||
-- This is a no-op down migration since the app is no longer needed.
|
||||
@@ -0,0 +1 @@
|
||||
DELETE FROM app WHERE workspace_id = 'admins' AND path = 'g/all/setup_app';
|
||||
@@ -328,6 +328,7 @@ pub static FULL_IMPORTS_MAP: PyMap = phf_map! {
|
||||
"azure.keyvault.secrets" => "azure-keyvault-secrets",
|
||||
"azure.storage.blob" => "azure-storage-blob",
|
||||
"azure.storage.filedatalake" => "azure-storage-file-datalake",
|
||||
"azure.identity" => "azure-identity",
|
||||
// Add new entry here ^
|
||||
};
|
||||
|
||||
|
||||
+73
-23
@@ -10,10 +10,11 @@ use monitor::{
|
||||
load_base_url, load_otel, reload_critical_alerts_on_db_oversize,
|
||||
reload_delete_logs_periodically_setting, reload_indexer_config,
|
||||
reload_instance_python_version_setting, reload_maven_repos_setting,
|
||||
reload_no_default_maven_setting, reload_nuget_config_setting,
|
||||
reload_powershell_repo_pat_setting, reload_powershell_repo_url_setting,
|
||||
reload_ruby_repos_setting, reload_timeout_wait_result_setting,
|
||||
send_current_log_file_to_object_store, send_logs_to_object_store, WORKERS_NAMES,
|
||||
reload_maven_settings_xml_setting, reload_no_default_maven_setting,
|
||||
reload_nuget_config_setting, reload_powershell_repo_pat_setting,
|
||||
reload_powershell_repo_url_setting, reload_ruby_repos_setting,
|
||||
reload_timeout_wait_result_setting, send_current_log_file_to_object_store,
|
||||
send_logs_to_object_store, WORKERS_NAMES,
|
||||
};
|
||||
use rand::Rng;
|
||||
use sqlx::{Pool, Postgres};
|
||||
@@ -42,15 +43,15 @@ use windmill_common::{
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
|
||||
INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING,
|
||||
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
|
||||
INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING,
|
||||
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING,
|
||||
MAVEN_SETTINGS_XML_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
|
||||
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING,
|
||||
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
|
||||
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING,
|
||||
POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
RUBY_REPOS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING,
|
||||
TIMEOUT_WAIT_RESULT_SETTING,
|
||||
TIMEOUT_WAIT_RESULT_SETTING, UV_INDEX_STRATEGY_SETTING,
|
||||
},
|
||||
scripts::ScriptLang,
|
||||
stats_oss::schedule_stats,
|
||||
@@ -100,10 +101,10 @@ use crate::monitor::{
|
||||
reload_bunfig_install_scopes_setting, reload_critical_alert_mute_ui_setting,
|
||||
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
|
||||
reload_hub_api_secret_setting, reload_hub_base_url_setting, reload_job_default_timeout_setting,
|
||||
reload_jwt_secret_setting, reload_license_key, reload_npm_config_registry_setting,
|
||||
reload_otel_tracing_proxy_setting, reload_pip_index_url_setting,
|
||||
reload_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
|
||||
reload_worker_config, MonitorIteration,
|
||||
reload_job_isolation_setting, reload_jwt_secret_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_otel_tracing_proxy_setting,
|
||||
reload_pip_index_url_setting, reload_retention_period_setting, reload_scim_token_setting,
|
||||
reload_smtp_config, reload_uv_index_strategy_setting, reload_worker_config, MonitorIteration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -483,6 +484,9 @@ fn print_help() {
|
||||
println!(" version Show Windmill version and exit");
|
||||
println!(" cache [hubPaths.json] Pre-cache hub scripts (default: ./hubPaths.json)");
|
||||
println!(" cache-rt Pre-cache hub resource types");
|
||||
println!(" sync-config <file> Sync instance config from a YAML file to the database");
|
||||
println!(" operator Run the Kubernetes operator (watches WindmillInstance CRDs)");
|
||||
println!(" operator crd Print the WindmillInstance CRD YAML to stdout");
|
||||
println!();
|
||||
println!("Environment variables (name = default):");
|
||||
println!(" DATABASE_URL = <required> The Postgres database url.");
|
||||
@@ -606,6 +610,43 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
cache_hub_resource_types().await?;
|
||||
return Ok(());
|
||||
}
|
||||
"sync-config" => {
|
||||
tracing_subscriber::fmt::init();
|
||||
let path = std::env::args().nth(2).unwrap_or_else(|| {
|
||||
eprintln!("Usage: windmill sync-config <file>");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let contents = tokio::fs::read_to_string(&path)
|
||||
.await
|
||||
.with_context(|| format!("Could not read config file: {path}"))?;
|
||||
let mut config: windmill_common::instance_config::InstanceConfig =
|
||||
serde_yml::from_str(&contents)
|
||||
.with_context(|| format!("Could not parse YAML from: {path}"))?;
|
||||
windmill_common::instance_config::resolve_env_refs(&mut config.global_settings)
|
||||
.map_err(|var| anyhow::anyhow!("environment variable '{var}' not found"))?;
|
||||
|
||||
tracing::info!("Connecting to database...");
|
||||
let db = crate::db_connect::initial_connection().await?;
|
||||
config.sync_to_db(&db).await?;
|
||||
tracing::info!("Synced instance config from {path}");
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(feature = "operator")]
|
||||
"operator" => {
|
||||
let sub_arg = std::env::args().nth(2).unwrap_or_default();
|
||||
if sub_arg == "crd" {
|
||||
windmill_operator::print_crd_yaml();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing_subscriber::fmt::init();
|
||||
tracing::info!("Starting Windmill Kubernetes operator...");
|
||||
tracing::info!("Connecting to database...");
|
||||
let db = crate::db_connect::initial_connection().await?;
|
||||
tracing::info!("Database connected. Starting controller...");
|
||||
windmill_operator::run(db).await?;
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1216,11 +1257,15 @@ Windmill Community Edition {GIT_VERSION}
|
||||
last_settings_reload = Instant::now();
|
||||
}
|
||||
|
||||
if server_mode {
|
||||
if !*windmill_common::QUIET_LOGS {
|
||||
tracing::info!("monitor task started");
|
||||
}
|
||||
}
|
||||
let monitor_start = Instant::now();
|
||||
let warn_handle = if server_mode {
|
||||
Some(tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
tracing::warn!("monitor task has been running for more than 5s");
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
monitor_db(
|
||||
&conn,
|
||||
&base_internal_url,
|
||||
@@ -1235,10 +1280,12 @@ Windmill Community Edition {GIT_VERSION}
|
||||
)
|
||||
.await;
|
||||
monitor_iteration += 1;
|
||||
if server_mode {
|
||||
if !*windmill_common::QUIET_LOGS {
|
||||
tracing::info!("monitor task finished");
|
||||
}
|
||||
if let Some(handle) = warn_handle {
|
||||
handle.abort();
|
||||
}
|
||||
let elapsed = monitor_start.elapsed();
|
||||
if server_mode && elapsed >= Duration::from_secs(5) {
|
||||
tracing::info!("monitor task finished in {elapsed:.1?}");
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1555,6 +1602,7 @@ async fn process_notify_event(
|
||||
reload_delete_logs_periodically_setting(conn).await
|
||||
}
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING => reload_job_default_timeout_setting(conn).await,
|
||||
JOB_ISOLATION_SETTING => reload_job_isolation_setting(conn).await,
|
||||
#[cfg(feature = "parquet")]
|
||||
OBJECT_STORE_CONFIG_SETTING => {
|
||||
if !disable_s3_store {
|
||||
@@ -1564,6 +1612,7 @@ async fn process_notify_event(
|
||||
SCIM_TOKEN_SETTING => reload_scim_token_setting(conn).await,
|
||||
EXTRA_PIP_INDEX_URL_SETTING => reload_extra_pip_index_url_setting(conn).await,
|
||||
PIP_INDEX_URL_SETTING => reload_pip_index_url_setting(conn).await,
|
||||
UV_INDEX_STRATEGY_SETTING => reload_uv_index_strategy_setting(conn).await,
|
||||
INSTANCE_PYTHON_VERSION_SETTING => {
|
||||
reload_instance_python_version_setting(conn).await
|
||||
}
|
||||
@@ -1573,6 +1622,7 @@ async fn process_notify_event(
|
||||
POWERSHELL_REPO_URL_SETTING => reload_powershell_repo_url_setting(conn).await,
|
||||
POWERSHELL_REPO_PAT_SETTING => reload_powershell_repo_pat_setting(conn).await,
|
||||
MAVEN_REPOS_SETTING => reload_maven_repos_setting(conn).await,
|
||||
MAVEN_SETTINGS_XML_SETTING => reload_maven_settings_xml_setting(conn).await,
|
||||
NO_DEFAULT_MAVEN_SETTING => reload_no_default_maven_setting(conn).await,
|
||||
RUBY_REPOS_SETTING => reload_ruby_repos_setting(conn).await,
|
||||
HUB_API_SECRET_SETTING => reload_hub_api_secret_setting(conn).await,
|
||||
|
||||
+95
-9
@@ -54,12 +54,14 @@ use windmill_common::{
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING,
|
||||
NUGET_CONFIG_SETTING, OTEL_SETTING, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING,
|
||||
POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
|
||||
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING,
|
||||
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
|
||||
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
|
||||
UV_INDEX_STRATEGY_SETTING,
|
||||
},
|
||||
indexer::load_indexer_config,
|
||||
jwt::JWT_SECRET,
|
||||
@@ -84,11 +86,12 @@ use windmill_common::{
|
||||
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
|
||||
use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload};
|
||||
use windmill_worker::{
|
||||
result_processor::handle_job_error, JobCompletedSender, OtelTracingProxySettings,
|
||||
SameWorkerSender, BUNFIG_INSTALL_SCOPES, INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT,
|
||||
KEEP_JOB_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NUGET_CONFIG,
|
||||
OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, POWERSHELL_REPO_PAT,
|
||||
POWERSHELL_REPO_URL,
|
||||
result_processor::handle_job_error, JobCompletedSender, JobIsolationLevel,
|
||||
OtelTracingProxySettings, SameWorkerSender, BUNFIG_INSTALL_SCOPES, CARGO_REGISTRIES,
|
||||
INSTANCE_PYTHON_VERSION, JAVA_HOME_DIR, JOB_DEFAULT_TIMEOUT, JOB_ISOLATION, KEEP_JOB_DIR,
|
||||
MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NSJAIL_AVAILABLE,
|
||||
NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL,
|
||||
POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UV_INDEX_STRATEGY,
|
||||
};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -315,8 +318,10 @@ pub async fn initial_load(
|
||||
|
||||
if worker_mode {
|
||||
reload_job_default_timeout_setting(&conn).await;
|
||||
reload_job_isolation_setting(&conn).await;
|
||||
reload_extra_pip_index_url_setting(&conn).await;
|
||||
reload_pip_index_url_setting(&conn).await;
|
||||
reload_uv_index_strategy_setting(&conn).await;
|
||||
reload_npm_config_registry_setting(&conn).await;
|
||||
reload_bunfig_install_scopes_setting(&conn).await;
|
||||
reload_instance_python_version_setting(&conn).await;
|
||||
@@ -324,8 +329,10 @@ pub async fn initial_load(
|
||||
reload_powershell_repo_url_setting(&conn).await;
|
||||
reload_powershell_repo_pat_setting(&conn).await;
|
||||
reload_maven_repos_setting(&conn).await;
|
||||
reload_maven_settings_xml_setting(&conn).await;
|
||||
reload_no_default_maven_setting(&conn).await;
|
||||
reload_ruby_repos_setting(&conn).await;
|
||||
reload_cargo_registries_setting(&conn).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1250,6 +1257,16 @@ pub async fn reload_pip_index_url_setting(conn: &Connection) {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_uv_index_strategy_setting(conn: &Connection) {
|
||||
reload_option_setting_with_tracing(
|
||||
conn,
|
||||
UV_INDEX_STRATEGY_SETTING,
|
||||
"UV_INDEX_STRATEGY",
|
||||
UV_INDEX_STRATEGY.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_instance_python_version_setting(conn: &Connection) {
|
||||
reload_option_setting_with_tracing(
|
||||
conn,
|
||||
@@ -1320,6 +1337,39 @@ pub async fn reload_maven_repos_setting(conn: &Connection) {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_maven_settings_xml_setting(conn: &Connection) {
|
||||
reload_option_setting_with_tracing(
|
||||
conn,
|
||||
windmill_common::global_settings::MAVEN_SETTINGS_XML_SETTING,
|
||||
"MAVEN_SETTINGS_XML",
|
||||
MAVEN_SETTINGS_XML.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if !cfg!(feature = "enterprise") {
|
||||
return;
|
||||
}
|
||||
|
||||
let settings_xml = MAVEN_SETTINGS_XML.read().await.clone();
|
||||
match settings_xml {
|
||||
Some(ref content) if !content.trim().is_empty() => {
|
||||
let m2_dir = format!("{JAVA_HOME_DIR}/.m2");
|
||||
if let Err(e) = tokio::fs::create_dir_all(&m2_dir).await {
|
||||
tracing::error!("Failed to create .m2 directory: {e:#}");
|
||||
return;
|
||||
}
|
||||
let settings_path = format!("{m2_dir}/settings.xml");
|
||||
if let Err(e) = tokio::fs::write(&settings_path, content).await {
|
||||
tracing::error!("Failed to write Maven settings.xml: {e:#}");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let settings_path = format!("{JAVA_HOME_DIR}/.m2/settings.xml");
|
||||
let _ = tokio::fs::remove_file(&settings_path).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload_no_default_maven_setting(conn: &Connection) {
|
||||
let value = load_value_from_global_settings_with_conn(
|
||||
conn,
|
||||
@@ -1346,6 +1396,16 @@ pub async fn reload_ruby_repos_setting(conn: &Connection) {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_cargo_registries_setting(conn: &Connection) {
|
||||
reload_option_setting_with_tracing(
|
||||
conn,
|
||||
windmill_common::global_settings::CARGO_REGISTRIES_SETTING,
|
||||
"CARGO_REGISTRIES",
|
||||
CARGO_REGISTRIES.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_hub_api_secret_setting(conn: &Connection) {
|
||||
reload_option_setting_with_tracing(
|
||||
conn,
|
||||
@@ -1395,6 +1455,32 @@ pub async fn reload_job_default_timeout_setting(conn: &Connection) {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_job_isolation_setting(conn: &Connection) {
|
||||
let value =
|
||||
match load_value_from_global_settings_with_conn(conn, JOB_ISOLATION_SETTING, true).await {
|
||||
Ok(Some(v)) => JobIsolationLevel::from_str(v.as_str().unwrap_or("")),
|
||||
Ok(None) => JobIsolationLevel::Undefined,
|
||||
Err(e) => {
|
||||
tracing::error!("Error reloading job_isolation setting: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let old_value = JobIsolationLevel::from_u8(JOB_ISOLATION.swap(value as u8, Ordering::Relaxed));
|
||||
if old_value != value {
|
||||
tracing::info!(
|
||||
"job_isolation setting changed from {:?} to {:?}",
|
||||
old_value,
|
||||
value
|
||||
);
|
||||
}
|
||||
if value == JobIsolationLevel::NsjailSandboxing && NSJAIL_AVAILABLE.is_none() {
|
||||
tracing::error!(
|
||||
"job_isolation is set to nsjail_sandboxing but nsjail is not available on this worker. \
|
||||
All jobs will fail until nsjail is installed or the setting is changed."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload_request_size(conn: &Connection) {
|
||||
if let Err(e) = reload_setting(
|
||||
conn,
|
||||
|
||||
@@ -11,12 +11,10 @@ EE_CODE_DIR="../windmill-ee-private/"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-r|--revert)
|
||||
# If EE files have been substituted, this will revert them to their initial content.
|
||||
# This relies on `git restore` so the EE files must not be committed to the repo for
|
||||
# this to work (commit hooks should prevent this from happening, as well as the fact
|
||||
# that we're using symlinks by default).
|
||||
# Removes all _ee.rs files from the backend directory. Symlinks are deleted.
|
||||
# Regular files with content differing from windmill-ee-private are moved back
|
||||
# to the EE repo so nothing is lost.
|
||||
REVERT="YES"
|
||||
MOVE_NEW_FILES="YES"
|
||||
shift
|
||||
;;
|
||||
-c|--copy)
|
||||
@@ -66,12 +64,28 @@ if [ ! -d "${EE_CODE_DIR}" ]; then
|
||||
fi
|
||||
|
||||
if [ "$REVERT" == "YES" ]; then
|
||||
for ee_file in $(find ${EE_CODE_DIR} -name "*ee.rs"); do
|
||||
ce_file="${ee_file/${EE_CODE_DIR}/}"
|
||||
ce_file="${root_dirpath}/backend/${ce_file}"
|
||||
rm ${ce_file} || true
|
||||
backend_dirpath="${root_dirpath}/backend/"
|
||||
for ce_file in $(find "${root_dirpath}/backend" -name "*_ee.rs"); do
|
||||
if [ -L "${ce_file}" ]; then
|
||||
rm "${ce_file}"
|
||||
echo "Deleted symlink '${ce_file}'"
|
||||
else
|
||||
ee_file="${ce_file/${backend_dirpath}/}"
|
||||
ee_file="${EE_CODE_DIR}${ee_file}"
|
||||
if [ ! -f "${ee_file}" ] || ! diff -q "${ce_file}" "${ee_file}" > /dev/null 2>&1; then
|
||||
mkdir -p "$(dirname "${ee_file}")"
|
||||
mv "${ce_file}" "${ee_file}"
|
||||
echo "Moved '${ce_file}' -->> '${ee_file}'"
|
||||
else
|
||||
rm "${ce_file}"
|
||||
echo "Deleted '${ce_file}' (identical to EE)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
elif [ "$MOVE_NEW_FILES" == "NO" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$MOVE_NEW_FILES" == "NO" ]; then
|
||||
# This replaces all files in current repo with alternative EE files in windmill-ee-private
|
||||
for ee_file in $(find "${EE_CODE_DIR}" -name "*ee.rs"); do
|
||||
ce_file="${ee_file/${EE_CODE_DIR}/}"
|
||||
|
||||
+74
-35
@@ -1,6 +1,6 @@
|
||||
# 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"
|
||||
# pg_dump --file "schema.sql" --format=p --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" "postgresql://postgres:changeme@localhost:5432/windmill"
|
||||
# Then you can run python summarize_schema.py schema.sql to get the summarized schema.
|
||||
|
||||
import re
|
||||
@@ -17,6 +17,7 @@ def summarize_schema(file_path):
|
||||
# Use state variables to parse multi-line definitions
|
||||
current_table = None
|
||||
current_enum = None
|
||||
pending_alter_table = None # For multi-line ALTER TABLE ... ADD CONSTRAINT
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
@@ -54,6 +55,9 @@ def summarize_schema(file_path):
|
||||
match_column = re.match(r'^"?(\w+)"?\s+([\w\d\.\[\]\(\)]+)', line)
|
||||
if match_column:
|
||||
col_name = match_column.group(1)
|
||||
# Skip CONSTRAINT definitions (e.g., CHECK, UNIQUE) mistakenly matched as columns
|
||||
if col_name.upper() == 'CONSTRAINT':
|
||||
continue
|
||||
col_type = match_column.group(2)
|
||||
tables[current_table]['columns'].append(f"{col_name} ({col_type})")
|
||||
|
||||
@@ -65,16 +69,22 @@ def summarize_schema(file_path):
|
||||
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 Foreign Keys (defined outside CREATE TABLE, may span 2 lines) ---
|
||||
match_alter = re.match(r"ALTER TABLE ONLY public\.(\w+)$", line)
|
||||
if match_alter:
|
||||
pending_alter_table = match_alter.group(1)
|
||||
continue
|
||||
|
||||
if pending_alter_table:
|
||||
match_fk = re.match(r"ADD CONSTRAINT \w+ FOREIGN KEY \(([\w,\s\"]+)\) REFERENCES public\.(\w+)\(([\w,\s\"]+)\)", line)
|
||||
if match_fk:
|
||||
from_cols, to_table, to_cols = match_fk.groups()
|
||||
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[pending_alter_table]['fks'].append(fk_string)
|
||||
pending_alter_table = None
|
||||
continue
|
||||
|
||||
# --- Parse Index definitions ---
|
||||
match_index = re.match(r"CREATE (UNIQUE )?INDEX (\w+) ON public\.(\w+) USING (\w+) \((.+)\);", line)
|
||||
@@ -94,44 +104,73 @@ def summarize_schema(file_path):
|
||||
|
||||
return enums, tables
|
||||
|
||||
TYPE_ABBREVIATIONS = {
|
||||
'character': 'char',
|
||||
'integer': 'int',
|
||||
'bigint': 'bigint',
|
||||
'smallint': 'smallint',
|
||||
'boolean': 'bool',
|
||||
'timestamp': 'ts',
|
||||
'bytea': 'bytes',
|
||||
'real': 'float',
|
||||
'json': 'json',
|
||||
'jsonb': 'jsonb',
|
||||
'text': 'text',
|
||||
'uuid': 'uuid',
|
||||
'bit(64)': 'bit64',
|
||||
}
|
||||
|
||||
def shorten_type(col_str):
|
||||
"""Shorten a 'name (type)' string to 'name(short_type)'."""
|
||||
match = re.match(r'^(\w+) \((.+)\)$', col_str)
|
||||
if not match:
|
||||
return col_str
|
||||
name, typ = match.group(1), match.group(2)
|
||||
# Strip public. prefix from enum types
|
||||
typ = re.sub(r'^public\.', '', typ)
|
||||
# Apply abbreviations (prefix-based to handle parametrized types like character(64) and array types like integer[])
|
||||
for prefix, abbr in TYPE_ABBREVIATIONS.items():
|
||||
if typ.startswith(prefix):
|
||||
typ = abbr + typ[len(prefix):]
|
||||
break
|
||||
return f"{name}({typ})"
|
||||
|
||||
def format_output(enums, tables):
|
||||
"""
|
||||
Formats the parsed schema data into a clean, readable string.
|
||||
Formats the parsed schema data into a compact, LLM-friendly string.
|
||||
"""
|
||||
output = []
|
||||
|
||||
output.append("### Simplified Database Schema ###")
|
||||
output.append("\n--- Custom Data Types (ENUMs) ---\n")
|
||||
|
||||
output.append("# Database Schema")
|
||||
output.append("")
|
||||
output.append("## ENUMs")
|
||||
if not enums:
|
||||
output.append("No custom ENUM types found.")
|
||||
output.append("(none)")
|
||||
else:
|
||||
for name, values in sorted(enums.items()):
|
||||
output.append(f"{name}:")
|
||||
for v in values:
|
||||
output.append(f" - {v}")
|
||||
output.append("")
|
||||
output.append(f"{name}: {', '.join(values)}")
|
||||
|
||||
output.append("\n--- Tables and Relationships ---\n")
|
||||
output.append("")
|
||||
output.append("## Tables")
|
||||
if not tables:
|
||||
output.append("No tables found.")
|
||||
output.append("(none)")
|
||||
else:
|
||||
for name, data in sorted(tables.items()):
|
||||
output.append(f"TABLE: {name}")
|
||||
# Columns: inline, comma-separated, with PK marker and shortened types
|
||||
cols = []
|
||||
for col in data['columns']:
|
||||
col_short = shorten_type(col)
|
||||
col_name = col.split(' ')[0]
|
||||
marker = " (PK)" if col_name in data['pks'] else ""
|
||||
output.append(f" - {col}{marker}")
|
||||
|
||||
if col_name in data['pks']:
|
||||
col_short += " PK"
|
||||
cols.append(col_short)
|
||||
output.append(f"{name}: {', '.join(cols)}")
|
||||
|
||||
# Foreign keys on one indented line
|
||||
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)
|
||||
output.append(f" FK: {' | '.join(data['fks'])}")
|
||||
|
||||
# Indexes omitted for brevity — query the database for exact index info
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
+192
-1086
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
||||
#![cfg(all(feature = "private", feature = "agent_worker_server"))]
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
mod common;
|
||||
use crate::common::*;
|
||||
use windmill_test_utils::*;
|
||||
use sqlx::postgres::Postgres;
|
||||
use sqlx::Pool;
|
||||
use windmill_common::jobs::{JobPayload, RawCode};
|
||||
@@ -860,7 +859,7 @@ export function main() {
|
||||
// ============================================================================
|
||||
|
||||
mod dedicated_worker_protocol {
|
||||
use crate::common::{parse_dedicated_worker_line, DedicatedWorkerResult};
|
||||
use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
use windmill_worker::{
|
||||
|
||||
@@ -2,8 +2,9 @@ use sqlx::{Pool, Postgres};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use windmill_api_client::types::NewScript;
|
||||
mod common;
|
||||
use common::{in_test_worker, init_client, listen_for_completed_jobs, ApiServer};
|
||||
use windmill_test_utils::{
|
||||
in_test_worker, init_client, listen_for_completed_jobs, rebuild_dmap, ApiServer,
|
||||
};
|
||||
|
||||
mod dependency_map {
|
||||
use super::*;
|
||||
@@ -170,7 +171,7 @@ mod dependency_map {
|
||||
let (client, _port, _s) = init(db.clone()).await;
|
||||
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
|
||||
// rebuild map
|
||||
assert!(common::rebuild_dmap(&client).await);
|
||||
assert!(rebuild_dmap(&client).await);
|
||||
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -183,7 +184,7 @@ mod dependency_map {
|
||||
// Spawn first rebuild
|
||||
let handle = {
|
||||
let client = client.clone();
|
||||
tokio::spawn(async move { common::rebuild_dmap(&client).await })
|
||||
tokio::spawn(async move { rebuild_dmap(&client).await })
|
||||
};
|
||||
|
||||
// Immidiately spawn another
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
/// Test that workspace error handler can be set and removed via database operations
|
||||
#[cfg(feature = "deno_core")]
|
||||
|
||||
@@ -25,8 +25,7 @@ use windmill_common::{
|
||||
scripts::ScriptLang,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
/// Helper to create a FlowModule with default fields
|
||||
fn flow_module(id: &str, value: FlowModuleValue) -> FlowModule {
|
||||
|
||||
@@ -0,0 +1,994 @@
|
||||
/*!
|
||||
* Integration tests for windmill-common instance_config module.
|
||||
*
|
||||
* Tests verify the DB-level operations:
|
||||
* - `InstanceConfig::from_db()` reads global_settings + worker configs
|
||||
* - `apply_settings_diff()` applies upserts and deletes to global_settings
|
||||
* - `apply_configs_diff()` applies upserts and deletes to config table
|
||||
* - Full roundtrip: write → read → modify → diff → apply → read → verify
|
||||
*
|
||||
* Note: the test DB is created from migrations which seed default settings
|
||||
* and worker configs. Tests either clean up first or assert on specific keys
|
||||
* rather than exact counts.
|
||||
*/
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::instance_config::{
|
||||
apply_configs_diff, apply_settings_diff, diff_global_settings, diff_worker_configs, ApplyMode,
|
||||
ConfigsDiff, InstanceConfig, SettingsDiff,
|
||||
};
|
||||
|
||||
// ========================================================================
|
||||
// Helpers
|
||||
// ========================================================================
|
||||
|
||||
async fn get_global_setting(db: &Pool<Postgres>, name: &str) -> Option<serde_json::Value> {
|
||||
sqlx::query_as::<_, (serde_json::Value,)>("SELECT value FROM global_settings WHERE name = $1")
|
||||
.bind(name)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.expect("query should succeed")
|
||||
.map(|(v,)| v)
|
||||
}
|
||||
|
||||
async fn get_config(db: &Pool<Postgres>, name: &str) -> Option<serde_json::Value> {
|
||||
sqlx::query_as::<_, (serde_json::Value,)>("SELECT config FROM config WHERE name = $1")
|
||||
.bind(name)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.expect("query should succeed")
|
||||
.map(|(v,)| v)
|
||||
}
|
||||
|
||||
async fn insert_global_setting(db: &Pool<Postgres>, name: &str, value: serde_json::Value) {
|
||||
sqlx::query(
|
||||
"INSERT INTO global_settings (name, value) VALUES ($1, $2) \
|
||||
ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(&value)
|
||||
.execute(db)
|
||||
.await
|
||||
.expect("insert should succeed");
|
||||
}
|
||||
|
||||
async fn insert_config(db: &Pool<Postgres>, name: &str, config: serde_json::Value) {
|
||||
sqlx::query(
|
||||
"INSERT INTO config (name, config) VALUES ($1, $2) \
|
||||
ON CONFLICT (name) DO UPDATE SET config = EXCLUDED.config",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(&config)
|
||||
.execute(db)
|
||||
.await
|
||||
.expect("insert should succeed");
|
||||
}
|
||||
|
||||
async fn count_global_settings(db: &Pool<Postgres>) -> i64 {
|
||||
sqlx::query_as::<_, (i64,)>("SELECT COUNT(*) FROM global_settings")
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.expect("count query should succeed")
|
||||
.0
|
||||
}
|
||||
|
||||
/// Clear all migration-seeded data so tests start from a clean slate.
|
||||
async fn clear_settings_and_configs(db: &Pool<Postgres>) {
|
||||
sqlx::query("DELETE FROM global_settings")
|
||||
.execute(db)
|
||||
.await
|
||||
.expect("clear global_settings should succeed");
|
||||
sqlx::query("DELETE FROM config WHERE name LIKE 'worker__%'")
|
||||
.execute(db)
|
||||
.await
|
||||
.expect("clear worker configs should succeed");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// InstanceConfig::from_db() tests
|
||||
// ========================================================================
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_empty(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db)
|
||||
.await
|
||||
.expect("from_db should succeed on empty DB");
|
||||
assert!(config.global_settings.base_url.is_none());
|
||||
assert!(config.global_settings.license_key.is_none());
|
||||
assert!(config.worker_configs.is_empty());
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_with_typed_global_settings(db: Pool<Postgres>) {
|
||||
insert_global_setting(&db, "base_url", serde_json::json!("https://windmill.test")).await;
|
||||
insert_global_setting(&db, "retention_period_secs", serde_json::json!(86400)).await;
|
||||
insert_global_setting(&db, "expose_metrics", serde_json::json!(true)).await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert_eq!(
|
||||
config.global_settings.base_url.as_deref(),
|
||||
Some("https://windmill.test")
|
||||
);
|
||||
assert_eq!(config.global_settings.retention_period_secs, Some(86400));
|
||||
assert_eq!(config.global_settings.expose_metrics, Some(true));
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_with_structured_settings(db: Pool<Postgres>) {
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"smtp_settings",
|
||||
serde_json::json!({
|
||||
"smtp_host": "mail.test.com",
|
||||
"smtp_port": 587,
|
||||
"smtp_tls_implicit": false
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"otel",
|
||||
serde_json::json!({
|
||||
"metrics_enabled": true,
|
||||
"logs_enabled": false,
|
||||
"otel_exporter_otlp_endpoint": "http://otel:4317"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
|
||||
let smtp = config.global_settings.smtp_settings.as_ref().unwrap();
|
||||
assert_eq!(smtp.smtp_host.as_deref(), Some("mail.test.com"));
|
||||
assert_eq!(smtp.smtp_port, Some(587));
|
||||
assert_eq!(smtp.smtp_tls_implicit, Some(false));
|
||||
|
||||
let otel = config.global_settings.otel.as_ref().unwrap();
|
||||
assert_eq!(otel.metrics_enabled, Some(true));
|
||||
assert_eq!(otel.logs_enabled, Some(false));
|
||||
assert_eq!(
|
||||
otel.otel_exporter_otlp_endpoint.as_deref(),
|
||||
Some("http://otel:4317")
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_unknown_settings_go_to_extra(db: Pool<Postgres>) {
|
||||
insert_global_setting(&db, "future_setting_xyz", serde_json::json!({"nested": 42})).await;
|
||||
insert_global_setting(&db, "another_unknown", serde_json::json!("hello")).await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert_eq!(
|
||||
config.global_settings.extra["future_setting_xyz"],
|
||||
serde_json::json!({"nested": 42})
|
||||
);
|
||||
assert_eq!(
|
||||
config.global_settings.extra["another_unknown"],
|
||||
serde_json::json!("hello")
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_with_worker_configs(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__default",
|
||||
serde_json::json!({"init_bash": "echo default"}),
|
||||
)
|
||||
.await;
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__gpu",
|
||||
serde_json::json!({
|
||||
"dedicated_worker": "ws:f/gpu_script",
|
||||
"worker_tags": ["gpu", "cuda"]
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert_eq!(config.worker_configs.len(), 2);
|
||||
assert_eq!(
|
||||
config.worker_configs["default"].init_bash.as_deref(),
|
||||
Some("echo default")
|
||||
);
|
||||
assert_eq!(
|
||||
config.worker_configs["gpu"].dedicated_worker.as_deref(),
|
||||
Some("ws:f/gpu_script")
|
||||
);
|
||||
assert_eq!(
|
||||
config.worker_configs["gpu"].worker_tags.as_ref().unwrap(),
|
||||
&["gpu", "cuda"]
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_ignores_non_worker_configs(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
// Insert a config without worker__ prefix — should not appear
|
||||
insert_config(&db, "server_config", serde_json::json!({"important": true})).await;
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__actual",
|
||||
serde_json::json!({"init_bash": "echo hi"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert_eq!(config.worker_configs.len(), 1);
|
||||
assert!(config.worker_configs.contains_key("actual"));
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_worker_config_prefix_stripping(db: Pool<Postgres>) {
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__my_group_name",
|
||||
serde_json::json!({"cache_clear": 5}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert!(
|
||||
config.worker_configs.contains_key("my_group_name"),
|
||||
"worker__ prefix should be stripped"
|
||||
);
|
||||
assert_eq!(config.worker_configs["my_group_name"].cache_clear, Some(5));
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_worker_config_unknown_fields_in_extra(db: Pool<Postgres>) {
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__test_extra",
|
||||
serde_json::json!({
|
||||
"init_bash": "echo hello",
|
||||
"future_field": 999,
|
||||
"another_future": {"nested": true}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
let wc = &config.worker_configs["test_extra"];
|
||||
assert_eq!(wc.init_bash.as_deref(), Some("echo hello"));
|
||||
assert_eq!(wc.extra["future_field"], serde_json::json!(999));
|
||||
assert_eq!(
|
||||
wc.extra["another_future"],
|
||||
serde_json::json!({"nested": true})
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// apply_settings_diff() tests
|
||||
// ========================================================================
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_settings_diff_upserts_only(db: Pool<Postgres>) {
|
||||
let diff = SettingsDiff {
|
||||
upserts: {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert("key_a".to_string(), serde_json::json!("val_a"));
|
||||
m.insert("key_b".to_string(), serde_json::json!(123));
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
};
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "key_a").await,
|
||||
Some(serde_json::json!("val_a"))
|
||||
);
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "key_b").await,
|
||||
Some(serde_json::json!(123))
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_settings_diff_deletes_only(db: Pool<Postgres>) {
|
||||
insert_global_setting(&db, "to_delete_1", serde_json::json!("bye")).await;
|
||||
insert_global_setting(&db, "to_delete_2", serde_json::json!("gone")).await;
|
||||
insert_global_setting(&db, "to_keep", serde_json::json!("stay")).await;
|
||||
|
||||
let diff = SettingsDiff {
|
||||
upserts: BTreeMap::new(),
|
||||
deletes: vec!["to_delete_1".to_string(), "to_delete_2".to_string()],
|
||||
};
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
assert!(get_global_setting(&db, "to_delete_1").await.is_none());
|
||||
assert!(get_global_setting(&db, "to_delete_2").await.is_none());
|
||||
assert!(get_global_setting(&db, "to_keep").await.is_some());
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_settings_diff_upserts_and_deletes(db: Pool<Postgres>) {
|
||||
insert_global_setting(&db, "old_key", serde_json::json!("old")).await;
|
||||
|
||||
let diff = SettingsDiff {
|
||||
upserts: {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert("new_key".to_string(), serde_json::json!("new"));
|
||||
m
|
||||
},
|
||||
deletes: vec!["old_key".to_string()],
|
||||
};
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
assert!(get_global_setting(&db, "old_key").await.is_none());
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "new_key").await,
|
||||
Some(serde_json::json!("new"))
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_settings_diff_empty_noop(db: Pool<Postgres>) {
|
||||
insert_global_setting(&db, "preexisting", serde_json::json!("value")).await;
|
||||
|
||||
let diff = SettingsDiff::default();
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "preexisting").await,
|
||||
Some(serde_json::json!("value"))
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_settings_diff_upsert_overwrites(db: Pool<Postgres>) {
|
||||
insert_global_setting(&db, "overwrite_me", serde_json::json!("old_value")).await;
|
||||
|
||||
let diff = SettingsDiff {
|
||||
upserts: {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert("overwrite_me".to_string(), serde_json::json!("new_value"));
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
};
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "overwrite_me").await,
|
||||
Some(serde_json::json!("new_value"))
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_settings_diff_complex_json(db: Pool<Postgres>) {
|
||||
let complex_value = serde_json::json!({
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"auth": {"user": "admin", "pass": "secret"},
|
||||
"tags": [1, 2, 3],
|
||||
"nested": {"deep": {"value": null}}
|
||||
});
|
||||
|
||||
let diff = SettingsDiff {
|
||||
upserts: {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert("complex_setting".to_string(), complex_value.clone());
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
};
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
let stored = get_global_setting(&db, "complex_setting").await.unwrap();
|
||||
assert_eq!(stored, complex_value);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_settings_diff_delete_nonexistent_is_noop(db: Pool<Postgres>) {
|
||||
let diff =
|
||||
SettingsDiff { upserts: BTreeMap::new(), deletes: vec!["does_not_exist".to_string()] };
|
||||
|
||||
// Should not error
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// apply_configs_diff() tests
|
||||
// ========================================================================
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_configs_diff_upserts_with_prefix(db: Pool<Postgres>) {
|
||||
let diff = ConfigsDiff {
|
||||
upserts: {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert(
|
||||
"mygroup".to_string(),
|
||||
serde_json::json!({"init_bash": "echo hi"}),
|
||||
);
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
};
|
||||
|
||||
apply_configs_diff(&db, &diff).await.unwrap();
|
||||
|
||||
let stored = get_config(&db, "worker__mygroup").await.unwrap();
|
||||
assert_eq!(stored["init_bash"], "echo hi");
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_configs_diff_deletes_with_prefix(db: Pool<Postgres>) {
|
||||
insert_config(&db, "worker__to_remove", serde_json::json!({"a": 1})).await;
|
||||
|
||||
let diff = ConfigsDiff { upserts: BTreeMap::new(), deletes: vec!["to_remove".to_string()] };
|
||||
|
||||
apply_configs_diff(&db, &diff).await.unwrap();
|
||||
|
||||
assert!(get_config(&db, "worker__to_remove").await.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_configs_diff_upsert_overwrites(db: Pool<Postgres>) {
|
||||
insert_config(&db, "worker__grp", serde_json::json!({"old": true})).await;
|
||||
|
||||
let diff = ConfigsDiff {
|
||||
upserts: {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert("grp".to_string(), serde_json::json!({"new": true}));
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
};
|
||||
|
||||
apply_configs_diff(&db, &diff).await.unwrap();
|
||||
|
||||
let stored = get_config(&db, "worker__grp").await.unwrap();
|
||||
assert_eq!(stored, serde_json::json!({"new": true}));
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_configs_diff_empty_noop(db: Pool<Postgres>) {
|
||||
insert_config(&db, "worker__keep", serde_json::json!({"keep": true})).await;
|
||||
|
||||
let diff = ConfigsDiff::default();
|
||||
apply_configs_diff(&db, &diff).await.unwrap();
|
||||
|
||||
assert!(get_config(&db, "worker__keep").await.is_some());
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_apply_configs_diff_does_not_touch_non_worker_configs(db: Pool<Postgres>) {
|
||||
insert_config(&db, "server_config", serde_json::json!({"x": 1})).await;
|
||||
|
||||
let diff = ConfigsDiff { upserts: BTreeMap::new(), deletes: vec!["server_config".to_string()] };
|
||||
|
||||
apply_configs_diff(&db, &diff).await.unwrap();
|
||||
|
||||
// The delete targets "worker__server_config", not "server_config"
|
||||
assert!(
|
||||
get_config(&db, "server_config").await.is_some(),
|
||||
"Non-worker config should not be affected"
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Full roundtrip tests
|
||||
// ========================================================================
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_roundtrip_write_read_modify_apply(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
// Step 1: Seed initial state
|
||||
insert_global_setting(&db, "base_url", serde_json::json!("https://v1.test")).await;
|
||||
insert_global_setting(&db, "retention_period_secs", serde_json::json!(3600)).await;
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__default",
|
||||
serde_json::json!({"init_bash": "echo v1"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Step 2: Read via from_db
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert_eq!(
|
||||
config.global_settings.base_url.as_deref(),
|
||||
Some("https://v1.test")
|
||||
);
|
||||
assert_eq!(config.global_settings.retention_period_secs, Some(3600));
|
||||
assert_eq!(config.worker_configs.len(), 1);
|
||||
|
||||
// Step 3: Modify — change base_url, add a new setting, remove retention
|
||||
let mut desired_settings = config.global_settings.clone();
|
||||
desired_settings.base_url = Some("https://v2.test".to_string());
|
||||
desired_settings.expose_metrics = Some(true);
|
||||
desired_settings.retention_period_secs = None;
|
||||
|
||||
let current_map = config.global_settings.to_settings_map();
|
||||
let desired_map = desired_settings.to_settings_map();
|
||||
|
||||
// Step 4: Diff + apply (Merge mode — no deletes)
|
||||
let diff = diff_global_settings(¤t_map, &desired_map, ApplyMode::Merge);
|
||||
assert!(diff.upserts.contains_key("base_url"));
|
||||
assert!(diff.upserts.contains_key("expose_metrics"));
|
||||
assert!(diff.deletes.is_empty());
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
// Step 5: Read back and verify
|
||||
let config2 = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert_eq!(
|
||||
config2.global_settings.base_url.as_deref(),
|
||||
Some("https://v2.test")
|
||||
);
|
||||
assert_eq!(config2.global_settings.expose_metrics, Some(true));
|
||||
// retention_period_secs still in DB because Merge mode doesn't delete
|
||||
assert_eq!(config2.global_settings.retention_period_secs, Some(3600));
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_roundtrip_replace_mode_deletes(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
// Seed
|
||||
insert_global_setting(&db, "base_url", serde_json::json!("https://old.test")).await;
|
||||
insert_global_setting(&db, "retention_period_secs", serde_json::json!(7200)).await;
|
||||
insert_global_setting(&db, "expose_metrics", serde_json::json!(false)).await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
let current_map = config.global_settings.to_settings_map();
|
||||
|
||||
// Desired: only base_url — retention and expose_metrics should be deleted
|
||||
let mut desired_map = BTreeMap::new();
|
||||
desired_map.insert(
|
||||
"base_url".to_string(),
|
||||
serde_json::json!("https://old.test"),
|
||||
);
|
||||
|
||||
let diff = diff_global_settings(¤t_map, &desired_map, ApplyMode::Replace);
|
||||
assert!(diff.upserts.is_empty());
|
||||
assert!(diff.deletes.contains(&"retention_period_secs".to_string()));
|
||||
assert!(diff.deletes.contains(&"expose_metrics".to_string()));
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
assert!(get_global_setting(&db, "retention_period_secs")
|
||||
.await
|
||||
.is_none());
|
||||
assert!(get_global_setting(&db, "expose_metrics").await.is_none());
|
||||
assert!(get_global_setting(&db, "base_url").await.is_some());
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_roundtrip_worker_configs(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
// Seed two worker configs
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__default",
|
||||
serde_json::json!({"init_bash": "echo default"}),
|
||||
)
|
||||
.await;
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__legacy",
|
||||
serde_json::json!({"init_bash": "echo legacy"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Read
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert_eq!(config.worker_configs.len(), 2);
|
||||
|
||||
// Desired: replace default, add gpu, remove legacy
|
||||
let current_map: BTreeMap<String, serde_json::Value> = config
|
||||
.worker_configs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), serde_json::to_value(v).unwrap()))
|
||||
.collect();
|
||||
|
||||
let mut desired_map = BTreeMap::new();
|
||||
desired_map.insert(
|
||||
"default".to_string(),
|
||||
serde_json::json!({"init_bash": "echo default v2"}),
|
||||
);
|
||||
desired_map.insert(
|
||||
"gpu".to_string(),
|
||||
serde_json::json!({"dedicated_worker": "ws:f/gpu"}),
|
||||
);
|
||||
|
||||
let diff = diff_worker_configs(¤t_map, &desired_map, ApplyMode::Replace);
|
||||
assert!(diff.upserts.contains_key("default")); // changed
|
||||
assert!(diff.upserts.contains_key("gpu")); // new
|
||||
assert_eq!(diff.deletes, vec!["legacy".to_string()]);
|
||||
|
||||
apply_configs_diff(&db, &diff).await.unwrap();
|
||||
|
||||
// Verify
|
||||
let config2 = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert_eq!(config2.worker_configs.len(), 2);
|
||||
assert_eq!(
|
||||
config2.worker_configs["default"].init_bash.as_deref(),
|
||||
Some("echo default v2")
|
||||
);
|
||||
assert_eq!(
|
||||
config2.worker_configs["gpu"].dedicated_worker.as_deref(),
|
||||
Some("ws:f/gpu")
|
||||
);
|
||||
assert!(!config2.worker_configs.contains_key("legacy"));
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_roundtrip_to_settings_map_from_db_consistency(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
// Write a GlobalSettings via to_settings_map + apply, then read back via from_db
|
||||
let original = windmill_common::instance_config::GlobalSettings {
|
||||
base_url: Some("https://roundtrip.test".to_string()),
|
||||
retention_period_secs: Some(43200),
|
||||
expose_metrics: Some(true),
|
||||
smtp_settings: Some(windmill_common::instance_config::SmtpSettings {
|
||||
smtp_host: Some("smtp.roundtrip.test".to_string()),
|
||||
smtp_port: Some(465),
|
||||
..Default::default()
|
||||
}),
|
||||
custom_tags: Some(vec!["tag1".to_string(), "tag2".to_string()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let map = original.to_settings_map();
|
||||
let diff = SettingsDiff { upserts: map.into_iter().collect(), deletes: vec![] };
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
assert_eq!(config.global_settings.base_url, original.base_url);
|
||||
assert_eq!(
|
||||
config.global_settings.retention_period_secs,
|
||||
original.retention_period_secs
|
||||
);
|
||||
assert_eq!(
|
||||
config.global_settings.expose_metrics,
|
||||
original.expose_metrics
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.global_settings
|
||||
.smtp_settings
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.smtp_host,
|
||||
original.smtp_settings.as_ref().unwrap().smtp_host
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.global_settings
|
||||
.smtp_settings
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.smtp_port,
|
||||
original.smtp_settings.as_ref().unwrap().smtp_port
|
||||
);
|
||||
assert_eq!(config.global_settings.custom_tags, original.custom_tags);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_idempotent_apply(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
let diff = SettingsDiff {
|
||||
upserts: {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert("idem_key".to_string(), serde_json::json!("idem_value"));
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
};
|
||||
|
||||
// Apply twice
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "idem_key").await,
|
||||
Some(serde_json::json!("idem_value"))
|
||||
);
|
||||
assert_eq!(count_global_settings(&db).await, 1);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_mixed_typed_and_extra(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
// Insert both typed and untyped settings
|
||||
insert_global_setting(&db, "base_url", serde_json::json!("https://mixed.test")).await;
|
||||
insert_global_setting(&db, "expose_metrics", serde_json::json!(true)).await;
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"unknown_future_setting",
|
||||
serde_json::json!({"key": "val"}),
|
||||
)
|
||||
.await;
|
||||
insert_global_setting(&db, "another_custom", serde_json::json!([1, 2, 3])).await;
|
||||
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
|
||||
// Typed fields
|
||||
assert_eq!(
|
||||
config.global_settings.base_url.as_deref(),
|
||||
Some("https://mixed.test")
|
||||
);
|
||||
assert_eq!(config.global_settings.expose_metrics, Some(true));
|
||||
|
||||
// Extra fields
|
||||
assert_eq!(
|
||||
config.global_settings.extra["unknown_future_setting"],
|
||||
serde_json::json!({"key": "val"})
|
||||
);
|
||||
assert_eq!(
|
||||
config.global_settings.extra["another_custom"],
|
||||
serde_json::json!([1, 2, 3])
|
||||
);
|
||||
|
||||
// Roundtrip: to_settings_map should include everything
|
||||
let map = config.global_settings.to_settings_map();
|
||||
assert!(map.contains_key("base_url"));
|
||||
assert!(map.contains_key("expose_metrics"));
|
||||
assert!(map.contains_key("unknown_future_setting"));
|
||||
assert!(map.contains_key("another_custom"));
|
||||
assert_eq!(map.len(), 4);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_full_config_roundtrip(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
// Seed a realistic configuration
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"base_url",
|
||||
serde_json::json!("https://prod.windmill.dev"),
|
||||
)
|
||||
.await;
|
||||
insert_global_setting(&db, "license_key", serde_json::json!("prod-license-key")).await;
|
||||
insert_global_setting(&db, "retention_period_secs", serde_json::json!(2592000)).await;
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"smtp_settings",
|
||||
serde_json::json!({
|
||||
"smtp_host": "smtp.prod.com",
|
||||
"smtp_port": 587,
|
||||
"smtp_from": "noreply@prod.com",
|
||||
"smtp_tls_implicit": true
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"critical_error_channels",
|
||||
serde_json::json!([
|
||||
{"email": "admin@prod.com"},
|
||||
{"slack_channel": "#prod-alerts"}
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"otel",
|
||||
serde_json::json!({
|
||||
"metrics_enabled": true,
|
||||
"tracing_enabled": true,
|
||||
"otel_exporter_otlp_endpoint": "http://otel-collector:4317"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__default",
|
||||
serde_json::json!({
|
||||
"init_bash": "apt-get update",
|
||||
"worker_tags": ["default", "deno", "bun"],
|
||||
"cache_clear": 7
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__gpu",
|
||||
serde_json::json!({
|
||||
"dedicated_worker": "ws:f/gpu_inference",
|
||||
"autoscaling": {
|
||||
"enabled": true,
|
||||
"min_workers": 0,
|
||||
"max_workers": 4,
|
||||
"integration": {"type": "kubernetes"}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Read full config
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.global_settings.base_url.as_deref(),
|
||||
Some("https://prod.windmill.dev")
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.global_settings
|
||||
.license_key
|
||||
.as_ref()
|
||||
.and_then(|v| v.as_literal()),
|
||||
Some("prod-license-key")
|
||||
);
|
||||
assert_eq!(config.global_settings.retention_period_secs, Some(2592000));
|
||||
|
||||
let smtp = config.global_settings.smtp_settings.as_ref().unwrap();
|
||||
assert_eq!(smtp.smtp_host.as_deref(), Some("smtp.prod.com"));
|
||||
assert_eq!(smtp.smtp_from.as_deref(), Some("noreply@prod.com"));
|
||||
|
||||
let channels = config
|
||||
.global_settings
|
||||
.critical_error_channels
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
assert_eq!(channels.len(), 2);
|
||||
|
||||
let otel = config.global_settings.otel.as_ref().unwrap();
|
||||
assert_eq!(otel.metrics_enabled, Some(true));
|
||||
assert_eq!(otel.tracing_enabled, Some(true));
|
||||
|
||||
assert_eq!(config.worker_configs.len(), 2);
|
||||
assert_eq!(config.worker_configs["default"].cache_clear, Some(7));
|
||||
let gpu_auto = config.worker_configs["gpu"].autoscaling.as_ref().unwrap();
|
||||
assert!(gpu_auto.enabled);
|
||||
assert_eq!(gpu_auto.min_workers, Some(0));
|
||||
assert_eq!(gpu_auto.max_workers, Some(4));
|
||||
|
||||
// Verify settings count matches
|
||||
let settings_map = config.global_settings.to_settings_map();
|
||||
let db_count = count_global_settings(&db).await;
|
||||
assert_eq!(
|
||||
settings_map.len() as i64,
|
||||
db_count,
|
||||
"to_settings_map should produce same count as DB rows"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_diff_apply_only_touches_changed_rows(db: Pool<Postgres>) {
|
||||
clear_settings_and_configs(&db).await;
|
||||
|
||||
// Seed 3 settings
|
||||
insert_global_setting(&db, "unchanged_1", serde_json::json!("val1")).await;
|
||||
insert_global_setting(&db, "unchanged_2", serde_json::json!("val2")).await;
|
||||
insert_global_setting(&db, "to_change", serde_json::json!("old")).await;
|
||||
|
||||
let mut current = BTreeMap::new();
|
||||
current.insert("unchanged_1".to_string(), serde_json::json!("val1"));
|
||||
current.insert("unchanged_2".to_string(), serde_json::json!("val2"));
|
||||
current.insert("to_change".to_string(), serde_json::json!("old"));
|
||||
|
||||
let mut desired = current.clone();
|
||||
desired.insert("to_change".to_string(), serde_json::json!("new"));
|
||||
desired.insert("added".to_string(), serde_json::json!("fresh"));
|
||||
|
||||
let diff = diff_global_settings(¤t, &desired, ApplyMode::Merge);
|
||||
|
||||
// Only "to_change" and "added" should be in upserts
|
||||
assert_eq!(diff.upserts.len(), 2);
|
||||
assert!(diff.upserts.contains_key("to_change"));
|
||||
assert!(diff.upserts.contains_key("added"));
|
||||
assert!(!diff.upserts.contains_key("unchanged_1"));
|
||||
assert!(!diff.upserts.contains_key("unchanged_2"));
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
// Verify all 4 settings present
|
||||
assert_eq!(count_global_settings(&db).await, 4);
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "to_change").await,
|
||||
Some(serde_json::json!("new"))
|
||||
);
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "added").await,
|
||||
Some(serde_json::json!("fresh"))
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_from_db_reads_migration_defaults(db: Pool<Postgres>) {
|
||||
// Verify that from_db correctly reads the migration-seeded state
|
||||
// without clearing — tests that pre-existing data is properly deserialized
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
|
||||
// Migrations seed at least base_url, license_key, etc.
|
||||
// Just verify from_db doesn't error and returns a populated struct
|
||||
let map = config.global_settings.to_settings_map();
|
||||
assert!(
|
||||
!map.is_empty(),
|
||||
"Migration-seeded DB should produce non-empty settings"
|
||||
);
|
||||
assert!(
|
||||
!config.worker_configs.is_empty(),
|
||||
"Migration-seeded DB should have worker configs"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_replace_mode_protects_settings_in_integration(db: Pool<Postgres>) {
|
||||
// Seed a protected setting
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"ducklake_settings",
|
||||
serde_json::json!({"ducklakes": {}}),
|
||||
)
|
||||
.await;
|
||||
insert_global_setting(&db, "ducklake_user_pg_pwd", serde_json::json!("secret_pwd")).await;
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"custom_instance_pg_databases",
|
||||
serde_json::json!({"databases": {}}),
|
||||
)
|
||||
.await;
|
||||
insert_global_setting(&db, "normal_setting", serde_json::json!("will_be_deleted")).await;
|
||||
|
||||
// Read current state
|
||||
let config = InstanceConfig::from_db(&db).await.unwrap();
|
||||
let current_map = config.global_settings.to_settings_map();
|
||||
|
||||
// Desired: only keep_me — everything else should be deleted except protected
|
||||
let mut desired_map = BTreeMap::new();
|
||||
desired_map.insert("keep_me".to_string(), serde_json::json!("yes"));
|
||||
|
||||
let diff = diff_global_settings(¤t_map, &desired_map, ApplyMode::Replace);
|
||||
|
||||
// Protected keys should NOT be in deletes
|
||||
assert!(
|
||||
!diff.deletes.contains(&"ducklake_settings".to_string()),
|
||||
"ducklake_settings is protected"
|
||||
);
|
||||
assert!(
|
||||
!diff.deletes.contains(&"ducklake_user_pg_pwd".to_string()),
|
||||
"ducklake_user_pg_pwd is protected"
|
||||
);
|
||||
assert!(
|
||||
!diff
|
||||
.deletes
|
||||
.contains(&"custom_instance_pg_databases".to_string()),
|
||||
"custom_instance_pg_databases is protected"
|
||||
);
|
||||
// But normal_setting should be deleted
|
||||
assert!(diff.deletes.contains(&"normal_setting".to_string()));
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
// Verify protected settings survived
|
||||
assert!(get_global_setting(&db, "ducklake_settings").await.is_some());
|
||||
assert!(get_global_setting(&db, "ducklake_user_pg_pwd")
|
||||
.await
|
||||
.is_some());
|
||||
assert!(get_global_setting(&db, "custom_instance_pg_databases")
|
||||
.await
|
||||
.is_some());
|
||||
// Normal setting is gone
|
||||
assert!(get_global_setting(&db, "normal_setting").await.is_none());
|
||||
// New setting is present
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "keep_me").await,
|
||||
Some(serde_json::json!("yes"))
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
mod common;
|
||||
|
||||
mod job_payload {
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
@@ -8,7 +6,7 @@ mod job_payload {
|
||||
use windmill_common::scripts::{ScriptHash, ScriptLang};
|
||||
use windmill_common::flow_status::RestartedFrom;
|
||||
|
||||
use crate::common::*;
|
||||
use windmill_test_utils::*;
|
||||
use windmill_common::min_version::{
|
||||
MIN_VERSION, MIN_VERSION_IS_AT_LEAST_1_427, MIN_VERSION_IS_AT_LEAST_1_432,
|
||||
MIN_VERSION_IS_AT_LEAST_1_440,
|
||||
|
||||
@@ -7,8 +7,7 @@ use windmill_common::{
|
||||
scripts::ScriptLang,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ListJobsResponse {
|
||||
|
||||
@@ -14,10 +14,7 @@
|
||||
*/
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
use futures::StreamExt;
|
||||
|
||||
@@ -11,10 +11,7 @@
|
||||
*/
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
/*!
|
||||
* Integration tests for windmill-operator db_sync module.
|
||||
*
|
||||
* Tests verify full declarative sync of global_settings and worker configs:
|
||||
* - Upsert desired settings into DB
|
||||
* - Delete settings present in DB but absent from desired state
|
||||
* - Protect certain internal settings from deletion
|
||||
*/
|
||||
|
||||
#[cfg(feature = "operator")]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
// ========================================================================
|
||||
// Helpers
|
||||
// ========================================================================
|
||||
|
||||
async fn get_global_setting(db: &Pool<Postgres>, name: &str) -> Option<serde_json::Value> {
|
||||
sqlx::query_as::<_, (serde_json::Value,)>(
|
||||
"SELECT value FROM global_settings WHERE name = $1",
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.expect("query should succeed")
|
||||
.map(|(v,)| v)
|
||||
}
|
||||
|
||||
async fn get_config(db: &Pool<Postgres>, name: &str) -> Option<serde_json::Value> {
|
||||
sqlx::query_as::<_, (serde_json::Value,)>("SELECT config FROM config WHERE name = $1")
|
||||
.bind(name)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.expect("query should succeed")
|
||||
.map(|(v,)| v)
|
||||
}
|
||||
|
||||
async fn insert_global_setting(db: &Pool<Postgres>, name: &str, value: serde_json::Value) {
|
||||
sqlx::query(
|
||||
"INSERT INTO global_settings (name, value) VALUES ($1, $2) \
|
||||
ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(&value)
|
||||
.execute(db)
|
||||
.await
|
||||
.expect("insert should succeed");
|
||||
}
|
||||
|
||||
async fn insert_config(db: &Pool<Postgres>, name: &str, config: serde_json::Value) {
|
||||
sqlx::query(
|
||||
"INSERT INTO config (name, config) VALUES ($1, $2) \
|
||||
ON CONFLICT (name) DO UPDATE SET config = EXCLUDED.config",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(&config)
|
||||
.execute(db)
|
||||
.await
|
||||
.expect("insert should succeed");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// sync_global_settings tests
|
||||
// ========================================================================
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_global_settings_upserts(db: Pool<Postgres>) {
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert(
|
||||
"test_op_setting_a".to_string(),
|
||||
serde_json::json!("value_a"),
|
||||
);
|
||||
desired.insert("test_op_setting_b".to_string(), serde_json::json!(42));
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "test_op_setting_a").await,
|
||||
Some(serde_json::json!("value_a"))
|
||||
);
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "test_op_setting_b").await,
|
||||
Some(serde_json::json!(42))
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_global_settings_updates_existing(db: Pool<Postgres>) {
|
||||
// Pre-populate a setting
|
||||
insert_global_setting(&db, "test_op_existing", serde_json::json!("old")).await;
|
||||
|
||||
// Sync with new value
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert("test_op_existing".to_string(), serde_json::json!("new"));
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "test_op_existing").await,
|
||||
Some(serde_json::json!("new"))
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_global_settings_deletes_absent(db: Pool<Postgres>) {
|
||||
// Pre-populate settings
|
||||
insert_global_setting(&db, "test_op_keep", serde_json::json!("keep")).await;
|
||||
insert_global_setting(&db, "test_op_remove", serde_json::json!("remove")).await;
|
||||
|
||||
// Sync with only one of them
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert("test_op_keep".to_string(), serde_json::json!("keep"));
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
assert!(get_global_setting(&db, "test_op_keep").await.is_some());
|
||||
assert!(
|
||||
get_global_setting(&db, "test_op_remove").await.is_none(),
|
||||
"Setting absent from desired should be deleted"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_global_settings_protects_ducklake(db: Pool<Postgres>) {
|
||||
// Pre-populate a protected setting
|
||||
insert_global_setting(
|
||||
&db,
|
||||
"ducklake_settings",
|
||||
serde_json::json!({"protected": true}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Sync with empty desired — protected key should survive
|
||||
let desired = BTreeMap::new();
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
assert!(
|
||||
get_global_setting(&db, "ducklake_settings").await.is_some(),
|
||||
"Protected setting ducklake_settings should not be deleted"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_global_settings_protects_all_protected_keys(db: Pool<Postgres>) {
|
||||
let protected_keys = [
|
||||
"ducklake_user_pg_pwd",
|
||||
"ducklake_settings",
|
||||
"custom_instance_pg_databases",
|
||||
];
|
||||
|
||||
for key in &protected_keys {
|
||||
insert_global_setting(&db, key, serde_json::json!("protected_value")).await;
|
||||
}
|
||||
|
||||
// Sync with empty desired
|
||||
let desired = BTreeMap::new();
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
for key in &protected_keys {
|
||||
assert!(
|
||||
get_global_setting(&db, key).await.is_some(),
|
||||
"Protected key {key} should not be deleted"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_global_settings_empty_desired(db: Pool<Postgres>) {
|
||||
// Pre-populate a non-protected setting
|
||||
insert_global_setting(&db, "test_op_ephemeral", serde_json::json!("gone")).await;
|
||||
|
||||
let desired = BTreeMap::new();
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
assert!(
|
||||
get_global_setting(&db, "test_op_ephemeral").await.is_none(),
|
||||
"Non-protected settings should be deleted when desired is empty"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_global_settings_complex_json_values(db: Pool<Postgres>) {
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert(
|
||||
"test_op_complex".to_string(),
|
||||
serde_json::json!({
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"tls": true,
|
||||
"nested": {"array": [1, 2, 3]}
|
||||
}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
let stored = get_global_setting(&db, "test_op_complex")
|
||||
.await
|
||||
.expect("Setting should exist");
|
||||
assert_eq!(stored["host"], "smtp.example.com");
|
||||
assert_eq!(stored["port"], 587);
|
||||
assert_eq!(stored["nested"]["array"][1], 2);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// sync_worker_configs tests
|
||||
// ========================================================================
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_worker_configs_upserts_with_prefix(db: Pool<Postgres>) {
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert(
|
||||
"test_group".to_string(),
|
||||
serde_json::json!({"dedicated_worker": false}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
let config = get_config(&db, "worker__test_group")
|
||||
.await
|
||||
.expect("Config should exist with worker__ prefix");
|
||||
assert_eq!(config["dedicated_worker"], false);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_worker_configs_updates_existing(db: Pool<Postgres>) {
|
||||
// Pre-populate
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__test_wc_existing",
|
||||
serde_json::json!({"old": true}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert(
|
||||
"test_wc_existing".to_string(),
|
||||
serde_json::json!({"new": true}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
let config = get_config(&db, "worker__test_wc_existing")
|
||||
.await
|
||||
.expect("Config should exist");
|
||||
assert_eq!(config["new"], true);
|
||||
assert!(config.get("old").is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_worker_configs_deletes_absent(db: Pool<Postgres>) {
|
||||
// Pre-populate two worker configs
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__test_wc_keep",
|
||||
serde_json::json!({"keep": true}),
|
||||
)
|
||||
.await;
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__test_wc_remove",
|
||||
serde_json::json!({"remove": true}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Sync with only one
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert(
|
||||
"test_wc_keep".to_string(),
|
||||
serde_json::json!({"keep": true}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
assert!(get_config(&db, "worker__test_wc_keep").await.is_some());
|
||||
assert!(
|
||||
get_config(&db, "worker__test_wc_remove").await.is_none(),
|
||||
"Worker config absent from desired should be deleted"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_worker_configs_does_not_touch_non_worker_configs(db: Pool<Postgres>) {
|
||||
// Insert a non-worker config (no worker__ prefix)
|
||||
insert_config(&db, "server_config", serde_json::json!({"important": true})).await;
|
||||
|
||||
// Sync with empty worker configs
|
||||
let desired = BTreeMap::new();
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
assert!(
|
||||
get_config(&db, "server_config").await.is_some(),
|
||||
"Non-worker configs should not be touched"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_worker_configs_multiple_groups(db: Pool<Postgres>) {
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert(
|
||||
"default".to_string(),
|
||||
serde_json::json!({"init_bash": "echo default"}),
|
||||
);
|
||||
desired.insert(
|
||||
"gpu".to_string(),
|
||||
serde_json::json!({"dedicated_worker": true}),
|
||||
);
|
||||
desired.insert(
|
||||
"native".to_string(),
|
||||
serde_json::json!({"init_bash": "echo native"}),
|
||||
);
|
||||
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
assert!(get_config(&db, "worker__default").await.is_some());
|
||||
assert!(get_config(&db, "worker__gpu").await.is_some());
|
||||
assert!(get_config(&db, "worker__native").await.is_some());
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_sync_worker_configs_empty_desired(db: Pool<Postgres>) {
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__test_wc_gone",
|
||||
serde_json::json!({"ephemeral": true}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let desired = BTreeMap::new();
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &desired)
|
||||
.await
|
||||
.expect("sync should succeed");
|
||||
|
||||
assert!(
|
||||
get_config(&db, "worker__test_wc_gone").await.is_none(),
|
||||
"Worker config should be deleted when desired is empty"
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// End-to-end: both syncs together
|
||||
// ========================================================================
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_full_declarative_sync(db: Pool<Postgres>) {
|
||||
// Pre-populate some existing state
|
||||
insert_global_setting(&db, "test_op_stale_setting", serde_json::json!("stale")).await;
|
||||
insert_config(
|
||||
&db,
|
||||
"worker__test_stale_group",
|
||||
serde_json::json!({"stale": true}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Define desired state
|
||||
let mut global_settings = BTreeMap::new();
|
||||
global_settings.insert(
|
||||
"test_op_base_url".to_string(),
|
||||
serde_json::json!("https://windmill.example.com"),
|
||||
);
|
||||
global_settings.insert(
|
||||
"test_op_license_key".to_string(),
|
||||
serde_json::json!("my-license"),
|
||||
);
|
||||
|
||||
let mut worker_configs = BTreeMap::new();
|
||||
worker_configs.insert(
|
||||
"default".to_string(),
|
||||
serde_json::json!({"init_bash": "echo hello"}),
|
||||
);
|
||||
|
||||
// Sync both
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &global_settings)
|
||||
.await
|
||||
.expect("global sync should succeed");
|
||||
windmill_operator::db_sync::sync_worker_configs(&db, &worker_configs)
|
||||
.await
|
||||
.expect("worker sync should succeed");
|
||||
|
||||
// Verify desired state is present
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "test_op_base_url").await,
|
||||
Some(serde_json::json!("https://windmill.example.com"))
|
||||
);
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "test_op_license_key").await,
|
||||
Some(serde_json::json!("my-license"))
|
||||
);
|
||||
assert!(get_config(&db, "worker__default").await.is_some());
|
||||
|
||||
// Verify stale state is removed
|
||||
assert!(
|
||||
get_global_setting(&db, "test_op_stale_setting")
|
||||
.await
|
||||
.is_none(),
|
||||
"Stale global setting should be removed"
|
||||
);
|
||||
assert!(
|
||||
get_config(&db, "worker__test_stale_group").await.is_none(),
|
||||
"Stale worker config should be removed"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_idempotent_sync(db: Pool<Postgres>) {
|
||||
let mut desired = BTreeMap::new();
|
||||
desired.insert("test_op_idempotent".to_string(), serde_json::json!("value"));
|
||||
|
||||
// Run sync twice — should be idempotent
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("first sync should succeed");
|
||||
windmill_operator::db_sync::sync_global_settings(&db, &desired)
|
||||
.await
|
||||
.expect("second sync should succeed");
|
||||
|
||||
assert_eq!(
|
||||
get_global_setting(&db, "test_op_idempotent").await,
|
||||
Some(serde_json::json!("value"))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
mod common;
|
||||
use crate::common::*;
|
||||
use windmill_test_utils::*;
|
||||
use sqlx::postgres::Postgres;
|
||||
use sqlx::Pool;
|
||||
use windmill_common::scripts::ScriptLang;
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio_stream::StreamExt;
|
||||
use windmill_api_client::types::NewScript;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
mod relock_skip {
|
||||
use super::*;
|
||||
|
||||
fn quick_ns(
|
||||
content: &str,
|
||||
language: windmill_api_client::types::ScriptLang,
|
||||
path: &str,
|
||||
lock: Option<String>,
|
||||
parent_hash: Option<String>,
|
||||
) -> NewScript {
|
||||
NewScript {
|
||||
content: content.into(),
|
||||
language,
|
||||
lock,
|
||||
parent_hash,
|
||||
path: path.into(),
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
cache_ttl: None,
|
||||
dedicated_worker: None,
|
||||
description: "".to_string(),
|
||||
draft_only: None,
|
||||
envs: vec![],
|
||||
is_template: None,
|
||||
kind: None,
|
||||
summary: "".to_string(),
|
||||
tag: None,
|
||||
schema: std::collections::HashMap::new(),
|
||||
ws_error_handler_muted: Some(false),
|
||||
priority: None,
|
||||
delete_after_use: None,
|
||||
timeout: None,
|
||||
restart_unless_cancelled: None,
|
||||
deployment_message: None,
|
||||
concurrency_key: None,
|
||||
visible_to_runner_only: None,
|
||||
no_main_func: None,
|
||||
codebase: None,
|
||||
has_preprocessor: None,
|
||||
on_behalf_of_email: None,
|
||||
assets: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
async fn init(db: Pool<Postgres>) -> (windmill_api_client::Client, u16, ApiServer) {
|
||||
init_client(db).await
|
||||
}
|
||||
|
||||
/// Counts occurrences of a pattern in job logs for all jobs created after a given time
|
||||
async fn count_pattern_in_job_logs(
|
||||
db: &Pool<Postgres>,
|
||||
pattern: &str,
|
||||
after: chrono::DateTime<chrono::Utc>,
|
||||
) -> i64 {
|
||||
let logs = sqlx::query_scalar!(
|
||||
"SELECT logs FROM job_logs WHERE created_at > $1",
|
||||
after
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
logs.iter()
|
||||
.filter_map(|l| l.as_ref())
|
||||
.map(|l| l.matches(pattern).count() as i64)
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Waits for exactly N jobs to complete. Returns the timestamp before waiting.
|
||||
async fn wait_for_jobs(
|
||||
completed: &mut (impl futures::Stream<Item = uuid::Uuid> + Unpin),
|
||||
count: usize,
|
||||
) -> chrono::DateTime<chrono::Utc> {
|
||||
let before = chrono::Utc::now();
|
||||
for _ in 0..count {
|
||||
completed.next().await;
|
||||
}
|
||||
before
|
||||
}
|
||||
|
||||
/// Waits for at least N jobs to complete, then drains any additional jobs
|
||||
/// that complete within a short timeout. Returns the timestamp before waiting.
|
||||
async fn wait_for_jobs_ge(
|
||||
completed: &mut (impl futures::Stream<Item = uuid::Uuid> + Unpin),
|
||||
min_count: usize,
|
||||
) -> chrono::DateTime<chrono::Utc> {
|
||||
let before = chrono::Utc::now();
|
||||
for _ in 0..min_count {
|
||||
completed.next().await;
|
||||
}
|
||||
// Drain any additional jobs that complete within 5 seconds
|
||||
loop {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(1), completed.next()).await {
|
||||
Ok(Some(_)) => continue,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
before
|
||||
}
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[sqlx::test(fixtures("base", "dependency_map"))]
|
||||
async fn relock_skip_on_script_redeployment(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
std::env::set_var("DEPENDENCY_JOB_DEBOUNCE_DELAY", "0");
|
||||
let (client, port, _s) = init(db.clone()).await;
|
||||
let mut completed = listen_for_completed_jobs(&db).await;
|
||||
|
||||
in_test_worker(&db, async {
|
||||
// Step 1: Redeploy leaf_1 - first time, no hashes exist, all should relock
|
||||
let before = chrono::Utc::now();
|
||||
client
|
||||
.create_script(
|
||||
"test-workspace",
|
||||
&quick_ns(
|
||||
"
|
||||
def main():
|
||||
return 'leaf1'
|
||||
",
|
||||
windmill_api_client::types::ScriptLang::Python3,
|
||||
"f/rel/leaf_1",
|
||||
None,
|
||||
Some("0000000000051658".into()),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// leaf_1(1) + branch(1) + root_script(2: leaf_1+branch) + root_flow(2) + root_app(2) = 8 jobs
|
||||
wait_for_jobs(&mut completed, 8).await;
|
||||
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await;
|
||||
assert_eq!(skipping_count, 0, "First deployment should not skip");
|
||||
assert!(relocking_count > 0, "First deployment should have relocking jobs");
|
||||
|
||||
// Step 2: Redeploy leaf_2 - first time for leaf_2, should relock
|
||||
let before = chrono::Utc::now();
|
||||
client
|
||||
.create_script(
|
||||
"test-workspace",
|
||||
&quick_ns(
|
||||
"
|
||||
def main():
|
||||
return 'leaf2'
|
||||
",
|
||||
windmill_api_client::types::ScriptLang::Python3,
|
||||
"f/rel/leaf_2",
|
||||
None,
|
||||
Some("0000000000051659".into()),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// leaf_2(1) + root_script(1) + root_flow(1) + root_app(1) = 4 jobs (no cascade, branch doesn't depend on leaf_2)
|
||||
wait_for_jobs(&mut completed, 4).await;
|
||||
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await;
|
||||
assert_eq!(skipping_count, 0, "leaf_2 first deployment should not skip");
|
||||
assert!(relocking_count > 0, "leaf_2 first deployment should have relocking jobs");
|
||||
|
||||
// Step 3: Redeploy leaf_2 with trivial change (comment) - lock stays same, should SKIP
|
||||
// Get current parent hash for leaf_2
|
||||
let leaf2_hash = sqlx::query_scalar!(
|
||||
"SELECT hash FROM script WHERE path = 'f/rel/leaf_2' AND workspace_id = 'test-workspace' AND archived = false ORDER BY created_at DESC LIMIT 1"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let before = chrono::Utc::now();
|
||||
client
|
||||
.create_script(
|
||||
"test-workspace",
|
||||
&quick_ns(
|
||||
"
|
||||
# comment to change hash but not lock
|
||||
def main():
|
||||
return 'leaf2'
|
||||
",
|
||||
windmill_api_client::types::ScriptLang::Python3,
|
||||
"f/rel/leaf_2",
|
||||
None,
|
||||
Some(format!("{:016X}", leaf2_hash)),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Same as leaf_2 first deployment: 4 jobs
|
||||
wait_for_jobs(&mut completed, 4).await;
|
||||
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
assert!(skipping_count > 0, "Trivial change (comment only) should skip - lock unchanged");
|
||||
|
||||
// Step 4: Redeploy leaf_2 with actual dependency change (add tiny via comment) - should NOT skip
|
||||
let leaf2_hash = sqlx::query_scalar!(
|
||||
"SELECT hash FROM script WHERE path = 'f/rel/leaf_2' AND workspace_id = 'test-workspace' AND archived = false ORDER BY created_at DESC LIMIT 1"
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let before = chrono::Utc::now();
|
||||
client
|
||||
.create_script(
|
||||
"test-workspace",
|
||||
&quick_ns(
|
||||
"
|
||||
# requirements:
|
||||
# tiny
|
||||
|
||||
def main():
|
||||
return 'leaf2 with tiny'
|
||||
",
|
||||
windmill_api_client::types::ScriptLang::Python3,
|
||||
"f/rel/leaf_2",
|
||||
None,
|
||||
Some(format!("{:016X}", leaf2_hash)),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Same as leaf_2 first deployment: 4 jobs
|
||||
wait_for_jobs(&mut completed, 4).await;
|
||||
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
assert_eq!(skipping_count, 0, "Changed dependencies should not skip");
|
||||
}, port).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[sqlx::test(fixtures("base", "dependency_map"))]
|
||||
async fn relock_skip_on_workspace_deps_redeployment(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
use windmill_common::scripts::ScriptLang;
|
||||
use windmill_dep_map::workspace_dependencies::NewWorkspaceDependencies;
|
||||
|
||||
std::env::set_var("DEPENDENCY_JOB_DEBOUNCE_DELAY", "0");
|
||||
std::env::set_var("EXISTS_CACHE_TIMEOUT_MS", "0");
|
||||
|
||||
let (_client, port, _s) = init(db.clone()).await;
|
||||
let mut completed = listen_for_completed_jobs(&db).await;
|
||||
|
||||
// Step 1: Redeploy default (unnamed) workspace deps - first time, should relock
|
||||
let before = chrono::Utc::now();
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: "".into(),
|
||||
name: None, // Default/unnamed
|
||||
description: None,
|
||||
}
|
||||
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
in_test_worker(&db, wait_for_jobs_ge(&mut completed, 10), port).await;
|
||||
|
||||
// Note: within a cascade, the same script may be triggered multiple times.
|
||||
// After the first trigger relocks and stores the hash, subsequent triggers skip.
|
||||
// We allow up to 3 skips from cascade re-triggers.
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await;
|
||||
assert!(skipping_count <= 3, "First deployment should have at most 3 skips from cascade");
|
||||
assert!(relocking_count >= 3, "First deployment should have at least 3 relocking jobs");
|
||||
|
||||
// Step 2: Redeploy default workspace deps again - should SKIP
|
||||
let before = chrono::Utc::now();
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: "".into(),
|
||||
name: None,
|
||||
description: None,
|
||||
}
|
||||
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
in_test_worker(&db, wait_for_jobs_ge(&mut completed, 10), port).await;
|
||||
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
assert!(skipping_count >= 3, "Second deployment of same content should skip at least 3 times");
|
||||
|
||||
// Step 3: Redeploy default workspace deps with different content - should NOT skip
|
||||
let before = chrono::Utc::now();
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: "tiny".into(),
|
||||
name: None,
|
||||
description: None,
|
||||
}
|
||||
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
in_test_worker(&db, wait_for_jobs_ge(&mut completed, 10), port).await;
|
||||
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await;
|
||||
assert!(skipping_count <= 4, "Changed content should have at most 3 skips from cascade");
|
||||
assert!(relocking_count >= 3, "Changed content should trigger at least 3 relocking jobs");
|
||||
|
||||
// Step 4: Deploy named workspace deps first time - should relock (no hash exists yet)
|
||||
// Named deps trigger exactly 3 independent objects with no cascade
|
||||
let before = chrono::Utc::now();
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: "".into(),
|
||||
name: Some("test".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
in_test_worker(&db, wait_for_jobs(&mut completed, 3), port).await;
|
||||
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await;
|
||||
assert_eq!(skipping_count, 0, "Named workspace deps first deployment should not skip");
|
||||
assert!(relocking_count > 0, "Named workspace deps first deployment should relock");
|
||||
|
||||
// Step 5: Deploy named workspace deps again with no change - should SKIP
|
||||
let before = chrono::Utc::now();
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: "".into(),
|
||||
name: Some("test".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
in_test_worker(&db, wait_for_jobs(&mut completed, 3), port).await;
|
||||
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await;
|
||||
|
||||
assert!(skipping_count > 0, "Named workspace deps second deployment should skip");
|
||||
assert_eq!(relocking_count, 0, "Named workspace deps second deployment should not relock");
|
||||
|
||||
// Step 6: Deploy named workspace deps with small change - should NOT skip
|
||||
let before = chrono::Utc::now();
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: "tiny".into(),
|
||||
name: Some("test".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
in_test_worker(&db, wait_for_jobs(&mut completed, 3), port).await;
|
||||
|
||||
let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await;
|
||||
let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await;
|
||||
assert_eq!(skipping_count, 0, "Named workspace deps with change should not skip");
|
||||
assert!(relocking_count > 0, "Named workspace deps with change should relock");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
mod retry {
|
||||
use crate::common::*;
|
||||
use windmill_test_utils::*;
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
/// Test that the workspace success handler cache works correctly with 60s TTL
|
||||
#[cfg(feature = "deno_core")]
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
mod common;
|
||||
|
||||
mod suspend_resume {
|
||||
#[cfg(feature = "deno_core")]
|
||||
use serde_json::json;
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
use crate::common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
use futures::{Stream, StreamExt};
|
||||
|
||||
@@ -25,8 +25,7 @@ use windmill_common::{
|
||||
jobs::{JobPayload, RawCode},
|
||||
scripts::ScriptLang,
|
||||
};
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use futures::StreamExt;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
mod common;
|
||||
mod workspace_dependencies {
|
||||
|
||||
use crate::common::in_test_worker;
|
||||
use crate::common::init_client;
|
||||
use crate::common::listen_for_completed_jobs;
|
||||
use windmill_test_utils::in_test_worker;
|
||||
use windmill_test_utils::init_client;
|
||||
use windmill_test_utils::listen_for_completed_jobs;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio_stream::StreamExt;
|
||||
use windmill_common::scripts::ScriptLang;
|
||||
@@ -122,14 +121,6 @@ mod workspace_dependencies {
|
||||
);
|
||||
|
||||
// Verify built-in fixtures exist
|
||||
// Check that the setup_app exists
|
||||
let app_exists =
|
||||
sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM app WHERE path = 'g/all/setup_app')")
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(app_exists.unwrap(), "Expected g/all/setup_app to exist");
|
||||
|
||||
// Check that hub_sync script exists and is a Bun script
|
||||
let hub_sync_lang = sqlx::query_scalar!(
|
||||
r#"SELECT language AS "language: ScriptLang" FROM script WHERE path = 'u/admin/hub_sync'"#
|
||||
|
||||
@@ -12,9 +12,9 @@ if [[ "$(uname)" == "Darwin" ]]; then
|
||||
|
||||
# Run cargo sqlx prepare with deno_core_mac
|
||||
echo "Running cargo sqlx prepare with deno_core_mac..."
|
||||
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,private,deno_core_mac
|
||||
cargo sqlx prepare --workspace -- --workspace --all-targets --features all_sqlx_features,private,deno_core_mac,deno_core,enterprise,mcp
|
||||
else
|
||||
# Run cargo sqlx prepare
|
||||
echo "Running cargo sqlx prepare..."
|
||||
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,ee
|
||||
cargo sqlx prepare --workspace -- --workspace --all-targets --features all_sqlx_features,ee,deno_core,private,enterprise,mcp
|
||||
fi
|
||||
|
||||
@@ -23,7 +23,7 @@ use windmill_common::{
|
||||
DB,
|
||||
};
|
||||
|
||||
use windmill_api_auth::{ApiAuthed, require_devops_role};
|
||||
use windmill_api_auth::{require_devops_role, ApiAuthed};
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
@@ -128,12 +128,16 @@ async fn update_config(
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if name.starts_with("worker__") {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Worker groups configurable from UI available only in the enterprise version"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let config = if name.starts_with("worker__") {
|
||||
// In CE, only allow setting worker_tags, cache_clear, and init_bash
|
||||
serde_json::json!({
|
||||
"worker_tags": config.get("worker_tags"),
|
||||
"cache_clear": config.get("cache_clear"),
|
||||
"init_bash": config.get("init_bash")
|
||||
})
|
||||
} else {
|
||||
config
|
||||
};
|
||||
|
||||
if name.starts_with("worker__") {
|
||||
let periodic_script_bash = config
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "windmill-api-flows"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "windmill_api_flows"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
enterprise = ["windmill-common/enterprise"]
|
||||
private = ["windmill-common/private"]
|
||||
|
||||
[dependencies]
|
||||
windmill-common = { workspace = true, default-features = false }
|
||||
windmill-api-auth.workspace = true
|
||||
windmill-queue.workspace = true
|
||||
windmill-audit.workspace = true
|
||||
windmill-git-sync.workspace = true
|
||||
windmill-dep-map.workspace = true
|
||||
|
||||
axum.workspace = true
|
||||
hyper.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sql-builder.workspace = true
|
||||
sqlx.workspace = true
|
||||
tracing.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2022
|
||||
* This file and its contents are licensed under the AGPLv3 License.
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
pub mod flows;
|
||||
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "windmill-api-integration-tests"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "windmill_api_integration_tests"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
private = ["windmill-test-utils/private", "dep:aws-config", "dep:aws-credential-types", "dep:aws-sdk-sqs"]
|
||||
enterprise = ["windmill-test-utils/enterprise", "dep:base64"]
|
||||
deno_core = ["windmill-test-utils/deno_core"]
|
||||
mcp = []
|
||||
|
||||
[dependencies]
|
||||
windmill-test-utils.workspace = true
|
||||
windmill-api-client.workspace = true
|
||||
windmill-common = { workspace = true, default-features = false }
|
||||
sqlx.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
reqwest.workspace = true
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
uuid.workspace = true
|
||||
rand.workspace = true
|
||||
rumqttc.workspace = true
|
||||
rdkafka.workspace = true
|
||||
async-nats.workspace = true
|
||||
aws-config = { workspace = true, optional = true }
|
||||
aws-credential-types = { workspace = true, optional = true }
|
||||
aws-sdk-sqs = { workspace = true, optional = true }
|
||||
base64 = { workspace = true, optional = true }
|
||||
@@ -0,0 +1,2 @@
|
||||
// This crate exists only to host integration tests.
|
||||
// See tests/ directory for the actual test files.
|
||||
@@ -1,8 +1,7 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
fn app_url(port: u16, endpoint: &str, path: &str) -> String {
|
||||
format!("http://localhost:{port}/api/w/test-workspace/apps/{endpoint}/{path}")
|
||||
@@ -40,7 +39,7 @@ fn new_app(path: &str, summary: &str) -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_app_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
@@ -1,8 +1,7 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
fn client() -> reqwest::Client {
|
||||
reqwest::Client::new()
|
||||
@@ -12,7 +11,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
builder.header("Authorization", "Bearer SECRET_TOKEN")
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_draft_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
@@ -0,0 +1,146 @@
|
||||
-- used for backend automated testing
|
||||
-- https://docs.rs/sqlx/latest/sqlx/attr.test.html
|
||||
|
||||
INSERT INTO workspace
|
||||
(id, name, owner)
|
||||
VALUES ('test-workspace', 'test-workspace', 'test-user');
|
||||
|
||||
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
|
||||
('test-workspace', 'test@windmill.dev', 'test-user', true, 'Admin');
|
||||
|
||||
INSERT INTO workspace_key(workspace_id, kind, key) VALUES
|
||||
('test-workspace', 'cloud', 'test-key');
|
||||
|
||||
|
||||
INSERT INTO workspace_settings (workspace_id) VALUES
|
||||
('test-workspace');
|
||||
|
||||
INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES
|
||||
('test-workspace', 'all', 'All users', '{}');
|
||||
|
||||
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username)
|
||||
VALUES ('test@windmill.dev', 'not-a-real-hash', 'password', true, true, 'Test User', 'test-user');
|
||||
|
||||
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
|
||||
VALUES ('test2@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 2');
|
||||
|
||||
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
|
||||
VALUES ('test3@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 3');
|
||||
|
||||
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
|
||||
('test-workspace', 'test2@windmill.dev', 'test-user-2', false, 'User');
|
||||
|
||||
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
|
||||
('test-workspace', 'test3@windmill.dev', 'test-user-3', false, 'User');
|
||||
|
||||
insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN', 'test@windmill.dev', 'test token', true);
|
||||
insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_2', 'test2@windmill.dev', 'test token 2', false);
|
||||
insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_3', 'test3@windmill.dev', 'test token 3', false);
|
||||
|
||||
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_admin;
|
||||
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_user;
|
||||
|
||||
CREATE FUNCTION "notify_insert_on_completed_job" ()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('completed', NEW.id::text);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE PLPGSQL;
|
||||
|
||||
CREATE TRIGGER "notify_insert_on_completed_job"
|
||||
AFTER INSERT ON "v2_job_completed"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION "notify_insert_on_completed_job" ();
|
||||
|
||||
|
||||
CREATE FUNCTION "notify_queue" ()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('queued', NEW.id::text);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE PLPGSQL;
|
||||
|
||||
CREATE TRIGGER "notify_queue_after_insert"
|
||||
AFTER INSERT ON "v2_job_queue"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION "notify_queue" ();
|
||||
|
||||
CREATE TRIGGER "notify_queue_after_flow_status_update"
|
||||
AFTER UPDATE ON "v2_job_status"
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.flow_status IS DISTINCT FROM OLD.flow_status)
|
||||
EXECUTE FUNCTION "notify_queue" ();
|
||||
|
||||
-- Apply phase 4:
|
||||
DROP FUNCTION IF EXISTS v2_job_after_update CASCADE;
|
||||
DROP FUNCTION IF EXISTS v2_job_completed_before_insert CASCADE;
|
||||
DROP FUNCTION IF EXISTS v2_job_completed_before_update CASCADE;
|
||||
DROP FUNCTION IF EXISTS v2_job_queue_after_insert CASCADE;
|
||||
DROP FUNCTION IF EXISTS v2_job_queue_before_insert CASCADE;
|
||||
DROP FUNCTION IF EXISTS v2_job_queue_before_update CASCADE;
|
||||
DROP FUNCTION IF EXISTS v2_job_runtime_before_insert CASCADE;
|
||||
DROP FUNCTION IF EXISTS v2_job_runtime_before_update CASCADE;
|
||||
DROP FUNCTION IF EXISTS v2_job_status_before_insert CASCADE;
|
||||
DROP FUNCTION IF EXISTS v2_job_status_before_update CASCADE;
|
||||
|
||||
DROP VIEW IF EXISTS completed_job, completed_job_view, job, queue, queue_view CASCADE;
|
||||
|
||||
ALTER TABLE v2_job_queue
|
||||
DROP COLUMN IF EXISTS __parent_job CASCADE,
|
||||
DROP COLUMN IF EXISTS __created_by CASCADE,
|
||||
DROP COLUMN IF EXISTS __script_hash CASCADE,
|
||||
DROP COLUMN IF EXISTS __script_path CASCADE,
|
||||
DROP COLUMN IF EXISTS __args CASCADE,
|
||||
DROP COLUMN IF EXISTS __logs CASCADE,
|
||||
DROP COLUMN IF EXISTS __raw_code CASCADE,
|
||||
DROP COLUMN IF EXISTS __canceled CASCADE,
|
||||
DROP COLUMN IF EXISTS __last_ping CASCADE,
|
||||
DROP COLUMN IF EXISTS __job_kind CASCADE,
|
||||
DROP COLUMN IF EXISTS __env_id CASCADE,
|
||||
DROP COLUMN IF EXISTS __schedule_path CASCADE,
|
||||
DROP COLUMN IF EXISTS __permissioned_as CASCADE,
|
||||
DROP COLUMN IF EXISTS __flow_status CASCADE,
|
||||
DROP COLUMN IF EXISTS __raw_flow CASCADE,
|
||||
DROP COLUMN IF EXISTS __is_flow_step CASCADE,
|
||||
DROP COLUMN IF EXISTS __language CASCADE,
|
||||
DROP COLUMN IF EXISTS __same_worker CASCADE,
|
||||
DROP COLUMN IF EXISTS __raw_lock CASCADE,
|
||||
DROP COLUMN IF EXISTS __pre_run_error CASCADE,
|
||||
DROP COLUMN IF EXISTS __email CASCADE,
|
||||
DROP COLUMN IF EXISTS __visible_to_owner CASCADE,
|
||||
DROP COLUMN IF EXISTS __mem_peak CASCADE,
|
||||
DROP COLUMN IF EXISTS __root_job CASCADE,
|
||||
DROP COLUMN IF EXISTS __leaf_jobs CASCADE,
|
||||
DROP COLUMN IF EXISTS __concurrent_limit CASCADE,
|
||||
DROP COLUMN IF EXISTS __concurrency_time_window_s CASCADE,
|
||||
DROP COLUMN IF EXISTS __timeout CASCADE,
|
||||
DROP COLUMN IF EXISTS __flow_step_id CASCADE,
|
||||
DROP COLUMN IF EXISTS __cache_ttl CASCADE;
|
||||
|
||||
LOCK TABLE v2_job_queue IN ACCESS EXCLUSIVE MODE;
|
||||
ALTER TABLE v2_job_completed
|
||||
DROP COLUMN IF EXISTS __parent_job CASCADE,
|
||||
DROP COLUMN IF EXISTS __created_by CASCADE,
|
||||
DROP COLUMN IF EXISTS __created_at CASCADE,
|
||||
DROP COLUMN IF EXISTS __success CASCADE,
|
||||
DROP COLUMN IF EXISTS __script_hash CASCADE,
|
||||
DROP COLUMN IF EXISTS __script_path CASCADE,
|
||||
DROP COLUMN IF EXISTS __args CASCADE,
|
||||
DROP COLUMN IF EXISTS __logs CASCADE,
|
||||
DROP COLUMN IF EXISTS __raw_code CASCADE,
|
||||
DROP COLUMN IF EXISTS __canceled CASCADE,
|
||||
DROP COLUMN IF EXISTS __job_kind CASCADE,
|
||||
DROP COLUMN IF EXISTS __env_id CASCADE,
|
||||
DROP COLUMN IF EXISTS __schedule_path CASCADE,
|
||||
DROP COLUMN IF EXISTS __permissioned_as CASCADE,
|
||||
DROP COLUMN IF EXISTS __raw_flow CASCADE,
|
||||
DROP COLUMN IF EXISTS __is_flow_step CASCADE,
|
||||
DROP COLUMN IF EXISTS __language CASCADE,
|
||||
DROP COLUMN IF EXISTS __is_skipped CASCADE,
|
||||
DROP COLUMN IF EXISTS __raw_lock CASCADE,
|
||||
DROP COLUMN IF EXISTS __email CASCADE,
|
||||
DROP COLUMN IF EXISTS __visible_to_owner CASCADE,
|
||||
DROP COLUMN IF EXISTS __tag CASCADE,
|
||||
DROP COLUMN IF EXISTS __priority CASCADE;
|
||||
@@ -0,0 +1,238 @@
|
||||
-- Permissions test fixture
|
||||
-- This sets up a comprehensive permission testing scenario
|
||||
|
||||
-- ============================================
|
||||
-- USERS
|
||||
-- ============================================
|
||||
|
||||
-- Admin user (may already exist from base.sql, use ON CONFLICT)
|
||||
INSERT INTO usr (workspace_id, email, username, is_admin, role)
|
||||
VALUES ('test-workspace', 'admin@windmill.dev', 'admin', true, 'Admin')
|
||||
ON CONFLICT (workspace_id, username) DO NOTHING;
|
||||
|
||||
-- Regular users (non-admin)
|
||||
INSERT INTO usr (workspace_id, email, username, is_admin, role)
|
||||
VALUES
|
||||
('test-workspace', 'alice@windmill.dev', 'alice', false, 'Developer'),
|
||||
('test-workspace', 'bob@windmill.dev', 'bob', false, 'Developer'),
|
||||
('test-workspace', 'charlie@windmill.dev', 'charlie', false, 'Developer');
|
||||
|
||||
-- Operator user (can execute but cannot create/update scripts, flows, apps)
|
||||
INSERT INTO usr (workspace_id, email, username, is_admin, operator, role)
|
||||
VALUES
|
||||
('test-workspace', 'operator@windmill.dev', 'operator', false, true, 'Operator');
|
||||
|
||||
-- Add users to password table (use ON CONFLICT since admin may exist from base.sql)
|
||||
INSERT INTO password (email, password_hash, login_type, super_admin, verified, name)
|
||||
VALUES
|
||||
('admin@windmill.dev', 'dummy_hash', 'password', false, true, 'Admin User'),
|
||||
('alice@windmill.dev', 'dummy_hash', 'password', false, true, 'Alice'),
|
||||
('bob@windmill.dev', 'dummy_hash', 'password', false, true, 'Bob'),
|
||||
('charlie@windmill.dev', 'dummy_hash', 'password', false, true, 'Charlie'),
|
||||
('operator@windmill.dev', 'dummy_hash', 'password', false, true, 'Operator')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- TOKENS for authentication
|
||||
-- ============================================
|
||||
|
||||
-- Tokens associated with emails (workspace-scoped)
|
||||
-- The auth system will look up the user by email in the usr table
|
||||
-- Note: tokens must be at least 10 characters (TOKEN_PREFIX_LEN)
|
||||
INSERT INTO token (token, email, label, super_admin, owner, workspace_id)
|
||||
VALUES
|
||||
('ADMIN_TOKEN_TEST', 'admin@windmill.dev', 'Admin token', false, 'u/admin', 'test-workspace'),
|
||||
('ALICE_TOKEN_TEST', 'alice@windmill.dev', 'Alice token', false, 'u/alice', 'test-workspace'),
|
||||
('BOB_TOKEN_TEST12', 'bob@windmill.dev', 'Bob token', false, 'u/bob', 'test-workspace'),
|
||||
('CHARLIE_TOKEN_01', 'charlie@windmill.dev', 'Charlie token', false, 'u/charlie', 'test-workspace'),
|
||||
('OPERATOR_TOKEN_1', 'operator@windmill.dev', 'Operator token', false, 'u/operator', 'test-workspace');
|
||||
|
||||
-- ============================================
|
||||
-- GROUPS
|
||||
-- ============================================
|
||||
|
||||
-- 'developers' group - Charlie is a member
|
||||
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
|
||||
VALUES ('test-workspace', 'developers', 'Developer group', '{}');
|
||||
|
||||
-- 'editors' group - Bob is a member (has write access to some folders)
|
||||
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
|
||||
VALUES ('test-workspace', 'editors', 'Editor group', '{}');
|
||||
|
||||
-- Group memberships
|
||||
INSERT INTO usr_to_group (workspace_id, group_, usr)
|
||||
VALUES
|
||||
('test-workspace', 'developers', 'charlie'),
|
||||
('test-workspace', 'editors', 'bob');
|
||||
|
||||
-- ============================================
|
||||
-- FOLDERS
|
||||
-- ============================================
|
||||
|
||||
-- 'shared' folder:
|
||||
-- - Alice has read-only access (extra_perms: u/alice -> false)
|
||||
-- - Bob has write access (extra_perms: u/bob -> true)
|
||||
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'shared', 'Shared Folder', '{"u/admin"}',
|
||||
'{"u/alice": false, "u/bob": true}', 'admin');
|
||||
|
||||
-- 'team' folder:
|
||||
-- - 'developers' group has read-only access
|
||||
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'team', 'Team Folder', '{"u/admin"}',
|
||||
'{"g/developers": false}', 'admin');
|
||||
|
||||
-- 'editable' folder:
|
||||
-- - 'editors' group has write access
|
||||
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'editable', 'Editable Folder', '{"u/admin"}',
|
||||
'{"g/editors": true}', 'admin');
|
||||
|
||||
-- 'admin_only' folder:
|
||||
-- - Only admin has access (but we'll add item-level perms for specific items)
|
||||
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'admin_only', 'Admin Only Folder', '{"u/admin"}',
|
||||
'{}', 'admin');
|
||||
|
||||
-- 'alice_owned' folder:
|
||||
-- - Alice is an owner (owners must also be in extra_perms for get_folders_for_user to find them)
|
||||
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'alice_owned', 'Alice Owned Folder', '{"u/alice"}',
|
||||
'{"u/alice": true}', 'alice');
|
||||
|
||||
-- ============================================
|
||||
-- SCRIPTS
|
||||
-- ============================================
|
||||
|
||||
-- Alice's personal script
|
||||
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
|
||||
VALUES ('test-workspace', 1001, 'u/alice/my_script',
|
||||
'export function main() { return "alice script"; }',
|
||||
'deno', 'script', 'alice', '{}', 'Alice script', '', '', '{}');
|
||||
|
||||
-- Bob's personal script
|
||||
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
|
||||
VALUES ('test-workspace', 1002, 'u/bob/my_script',
|
||||
'export function main() { return "bob script"; }',
|
||||
'deno', 'script', 'bob', '{}', 'Bob script', '', '', '{}');
|
||||
|
||||
-- Script in shared folder (accessible by Alice read-only, Bob write)
|
||||
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
|
||||
VALUES ('test-workspace', 1003, 'f/shared/public_script',
|
||||
'export function main() { return "public"; }',
|
||||
'deno', 'script', 'admin', '{}', 'Public script', '', '', '{}');
|
||||
|
||||
-- Script in team folder (accessible by developers group)
|
||||
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
|
||||
VALUES ('test-workspace', 1004, 'f/team/team_script',
|
||||
'export function main() { return "team"; }',
|
||||
'deno', 'script', 'admin', '{}', 'Team script', '', '', '{}');
|
||||
|
||||
-- Script in admin_only folder but with explicit share to Alice
|
||||
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
|
||||
VALUES ('test-workspace', 1005, 'f/admin_only/shared_with_alice',
|
||||
'export function main() { return "shared"; }',
|
||||
'deno', 'script', 'admin', '{}', 'Shared with Alice', '', '',
|
||||
'{"u/alice": false}');
|
||||
|
||||
-- Script in alice_owned folder
|
||||
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
|
||||
VALUES ('test-workspace', 1006, 'f/alice_owned/owner_script',
|
||||
'export function main() { return "owner"; }',
|
||||
'deno', 'script', 'alice', '{}', 'Owner script', '', '', '{}');
|
||||
|
||||
-- Alice's script with extra_perms sharing to Bob (read-only)
|
||||
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
|
||||
VALUES ('test-workspace', 1007, 'u/alice/extra_shared_script',
|
||||
'export function main() { return "extra shared"; }',
|
||||
'deno', 'script', 'alice', '{}', 'Extra shared script', '', '',
|
||||
'{"u/bob": false}');
|
||||
|
||||
-- ============================================
|
||||
-- FLOWS (with flow_versions)
|
||||
-- ============================================
|
||||
|
||||
-- Create flows first (without versions)
|
||||
INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema, extra_perms)
|
||||
VALUES
|
||||
('test-workspace', 'u/alice/my_flow', 'Alice flow', '', '{"modules": []}', 'alice', NOW(), '{}', '{}'),
|
||||
('test-workspace', 'u/bob/my_flow', 'Bob flow', '', '{"modules": []}', 'bob', NOW(), '{}', '{}'),
|
||||
('test-workspace', 'f/shared/shared_flow', 'Shared flow', '', '{"modules": []}', 'admin', NOW(), '{}', '{}');
|
||||
|
||||
-- Create flow versions
|
||||
INSERT INTO flow_version (id, workspace_id, path, value, schema, created_by, created_at)
|
||||
VALUES
|
||||
(1001, 'test-workspace', 'u/alice/my_flow', '{"modules": []}', '{}', 'alice', NOW()),
|
||||
(1002, 'test-workspace', 'u/bob/my_flow', '{"modules": []}', '{}', 'bob', NOW()),
|
||||
(1003, 'test-workspace', 'f/shared/shared_flow', '{"modules": []}', '{}', 'admin', NOW());
|
||||
|
||||
-- Update flows with version references
|
||||
UPDATE flow SET versions = ARRAY[1001::bigint] WHERE path = 'u/alice/my_flow' AND workspace_id = 'test-workspace';
|
||||
UPDATE flow SET versions = ARRAY[1002::bigint] WHERE path = 'u/bob/my_flow' AND workspace_id = 'test-workspace';
|
||||
UPDATE flow SET versions = ARRAY[1003::bigint] WHERE path = 'f/shared/shared_flow' AND workspace_id = 'test-workspace';
|
||||
|
||||
-- ============================================
|
||||
-- RESOURCES
|
||||
-- ============================================
|
||||
|
||||
-- Alice's personal resource
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/alice/my_resource', '{"key": "alice_value"}',
|
||||
'Alice resource', 'object', '{}', 'alice');
|
||||
|
||||
-- Bob's personal resource
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/bob/my_resource', '{"key": "bob_value"}',
|
||||
'Bob resource', 'object', '{}', 'bob');
|
||||
|
||||
-- ============================================
|
||||
-- VARIABLES
|
||||
-- ============================================
|
||||
|
||||
-- Alice's personal variable (non-secret for testing permissions, not encryption)
|
||||
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
|
||||
VALUES ('test-workspace', 'u/alice/my_variable', 'alice_value', false,
|
||||
'Alice variable', '{}');
|
||||
|
||||
-- Bob's personal variable (non-secret for testing permissions, not encryption)
|
||||
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
|
||||
VALUES ('test-workspace', 'u/bob/my_variable', 'bob_value', false,
|
||||
'Bob variable', '{}');
|
||||
|
||||
-- ============================================
|
||||
-- SCHEDULES
|
||||
-- ============================================
|
||||
|
||||
-- Alice's personal schedule
|
||||
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
|
||||
VALUES ('test-workspace', 'u/alice/my_schedule', 'alice', NOW(), '0 * * * *', false,
|
||||
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}');
|
||||
|
||||
-- Bob's personal schedule
|
||||
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
|
||||
VALUES ('test-workspace', 'u/bob/my_schedule', 'bob', NOW(), '0 * * * *', false,
|
||||
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}');
|
||||
|
||||
-- ============================================
|
||||
-- APPS (with app_versions)
|
||||
-- ============================================
|
||||
|
||||
-- Alice's personal app
|
||||
INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms)
|
||||
VALUES (2001, 'test-workspace', 'u/alice/my_app', 'Alice app', '{}',
|
||||
'{"on_behalf_of": "u/alice", "on_behalf_of_email": "alice@windmill.dev", "execution_mode": "viewer"}', '{}');
|
||||
|
||||
-- Shared folder app
|
||||
INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms)
|
||||
VALUES (2002, 'test-workspace', 'f/shared/shared_app', 'Shared app', '{}',
|
||||
'{"on_behalf_of": "u/admin", "on_behalf_of_email": "admin@windmill.dev", "execution_mode": "viewer"}', '{}');
|
||||
|
||||
-- Create app versions
|
||||
INSERT INTO app_version (id, app_id, value, created_by, created_at)
|
||||
VALUES
|
||||
(2001, 2001, '{"grid": []}', 'alice', NOW()),
|
||||
(2002, 2002, '{"grid": []}', 'admin', NOW());
|
||||
|
||||
-- Update apps with version references
|
||||
UPDATE app SET versions = ARRAY[2001::bigint] WHERE id = 2001;
|
||||
UPDATE app SET versions = ARRAY[2002::bigint] WHERE id = 2002;
|
||||
@@ -0,0 +1,69 @@
|
||||
-- Combined fixture for resource endpoint tests
|
||||
|
||||
-- === resource type test data ===
|
||||
|
||||
INSERT INTO resource_type (workspace_id, name, schema, description, created_by)
|
||||
VALUES ('test-workspace', 'test_db', '{"type": "object", "properties": {"host": {"type": "string"}}}',
|
||||
'Test DB type', 'test-user');
|
||||
|
||||
-- === get_value_interpolated test data ===
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/simple_resource', '{"host": "localhost", "port": 5432}',
|
||||
'Simple resource', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
|
||||
VALUES ('test-workspace', 'u/test-user/db_password', 'hunter2', false, 'DB password', '{}');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/resource_with_var', '{"host": "localhost", "password": "$var:u/test-user/db_password"}',
|
||||
'Resource with var ref', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/db_credentials', '{"user": "admin", "password": "secret123"}',
|
||||
'DB credentials', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/resource_with_res', '{"host": "localhost", "credentials": "$res:u/test-user/db_credentials"}',
|
||||
'Resource with res ref', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/resource_mixed', '{"host": "localhost", "password": "$var:u/test-user/db_password", "credentials": "$res:u/test-user/db_credentials"}',
|
||||
'Resource with mixed refs', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/null_resource', null,
|
||||
'Null resource', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
|
||||
VALUES ('test-workspace', 'u/test-user/api_key', 'sk-abc123', false, 'API key', '{}');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/inner_resource', '{"key": "$var:u/test-user/api_key"}',
|
||||
'Inner resource', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/chained_resource', '{"service": "myapi", "auth": "$res:u/test-user/inner_resource"}',
|
||||
'Chained resource', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/resource_with_array', '{"hosts": ["host1", "host2"], "port": 5432}',
|
||||
'Resource with array', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/scalar_var_resource', '"$var:u/test-user/db_password"',
|
||||
'Scalar var ref', 'string', '{}', 'test-user');
|
||||
|
||||
-- === mcp_tools test data ===
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/mcp_valid', '{"name": "test-mcp", "url": "http://127.0.0.1:19999/mcp"}',
|
||||
'Valid MCP resource (unreachable)', 'mcp_server', '{}', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/mcp_invalid_format', '{"host": "localhost", "port": 5432}',
|
||||
'Not an MCP resource', 'object', '{}', 'test-user');
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
|
||||
VALUES ('test-workspace', 'u/test-user/mcp_null', null,
|
||||
'Null MCP resource', 'mcp_server', '{}', 'test-user');
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Fixture for variable endpoint tests
|
||||
|
||||
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
|
||||
VALUES ('test-workspace', 'u/test-user/plain_var', 'hello world', false, 'A plain variable', '{}');
|
||||
|
||||
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
|
||||
VALUES ('test-workspace', 'u/test-user/secret_var', 'supersecret', true, 'A secret variable', '{}');
|
||||
|
||||
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
|
||||
VALUES ('test-workspace', 'u/test-user/another_var', 'foobar', false, 'Another variable', '{}');
|
||||
@@ -1,8 +1,7 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
fn flow_url(port: u16, endpoint: &str, path: &str) -> String {
|
||||
format!("http://localhost:{port}/api/w/test-workspace/flows/{endpoint}/{path}")
|
||||
@@ -40,7 +39,7 @@ fn new_flow(path: &str, summary: &str) -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_flow_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
@@ -1,8 +1,7 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
fn folder_url(port: u16, endpoint: &str, name: &str) -> String {
|
||||
format!("http://localhost:{port}/api/w/test-workspace/folders/{endpoint}/{name}")
|
||||
@@ -16,7 +15,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
builder.header("Authorization", "Bearer SECRET_TOKEN")
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_folder_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
@@ -1,8 +1,7 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
fn group_url(port: u16, endpoint: &str, name: &str) -> String {
|
||||
format!("http://localhost:{port}/api/w/test-workspace/groups/{endpoint}/{name}")
|
||||
@@ -16,7 +15,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
builder.header("Authorization", "Bearer SECRET_TOKEN")
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_group_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
+11
-6
@@ -1,10 +1,13 @@
|
||||
#[cfg(feature = "deno_core")]
|
||||
use serde_json::json;
|
||||
#[cfg(feature = "deno_core")]
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
#[cfg(feature = "deno_core")]
|
||||
use windmill_test_utils::*;
|
||||
|
||||
/// Helper to create a client authenticated as a specific user
|
||||
#[cfg(feature = "deno_core")]
|
||||
async fn create_client_for_user(_port: u16, token: &str) -> reqwest::Client {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
@@ -18,12 +21,14 @@ async fn create_client_for_user(_port: u16, token: &str) -> reqwest::Client {
|
||||
}
|
||||
|
||||
/// Test helper to check if a GET request succeeds
|
||||
#[cfg(feature = "deno_core")]
|
||||
async fn can_read(client: &reqwest::Client, url: &str) -> bool {
|
||||
let resp = client.get(url).send().await.unwrap();
|
||||
resp.status().is_success()
|
||||
}
|
||||
|
||||
/// Test helper to check if a POST request succeeds (for write operations)
|
||||
#[cfg(feature = "deno_core")]
|
||||
async fn can_write(client: &reqwest::Client, url: &str, body: serde_json::Value) -> bool {
|
||||
let resp = client.post(url).json(&body).send().await.unwrap();
|
||||
let status = resp.status();
|
||||
@@ -44,7 +49,7 @@ async fn can_write(client: &reqwest::Client, url: &str, body: serde_json::Value)
|
||||
/// `cargo test --features deno_core permissions -- --ignored`
|
||||
#[ignore]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "permissions_test"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))]
|
||||
async fn test_permissions_exhaustive(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
@@ -477,7 +482,7 @@ async fn test_permissions_exhaustive(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
/// Additional test for verifying group permission inheritance
|
||||
#[ignore]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "permissions_test"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))]
|
||||
async fn test_group_permission_inheritance(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
@@ -547,7 +552,7 @@ async fn test_group_permission_inheritance(db: Pool<Postgres>) -> anyhow::Result
|
||||
/// Test that permissions work correctly for all item types
|
||||
#[ignore]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "permissions_test"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))]
|
||||
async fn test_all_item_types_permissions(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
@@ -588,7 +593,7 @@ async fn test_all_item_types_permissions(db: Pool<Postgres>) -> anyhow::Result<(
|
||||
/// Operators have limited permissions - they can execute but cannot manage resources
|
||||
#[ignore]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "permissions_test"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))]
|
||||
async fn test_operator_cannot_create_update(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
+2
-3
@@ -7,8 +7,7 @@ use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::workspaces::invalidate_protection_rules_cache;
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
fn client() -> reqwest::Client {
|
||||
reqwest::Client::new()
|
||||
@@ -51,7 +50,7 @@ fn new_flow(path: &str, summary: &str) -> serde_json::Value {
|
||||
|
||||
/// Comprehensive test for protection rules functionality.
|
||||
/// Tests all essential cases in a single test to avoid cache interference.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_protection_rules(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
invalidate_protection_rules_cache("test-workspace");
|
||||
+3
-4
@@ -1,8 +1,7 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
fn resource_url(port: u16, endpoint: &str, path: &str) -> String {
|
||||
format!("http://localhost:{port}/api/w/test-workspace/resources/{endpoint}/{path}")
|
||||
@@ -23,7 +22,7 @@ async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base", "resources_test"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base", "resources_test"))]
|
||||
async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
@@ -400,7 +399,7 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
#[sqlx::test(fixtures("base", "resources_test"))]
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base", "resources_test"))]
|
||||
async fn test_mcp_tools(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user