Compare commits

...

34 Commits

Author SHA1 Message Date
claude[bot]
22c495e139 feat: refactor EE file structure to use OSS proxy pattern
- Create OSS proxy files for all *_ee.rs files
- Update lib.rs to use OSS modules instead of EE modules directly
- Follow the established pattern from agent_workers_oss.rs

This refactoring allows the codebase to use a consistent proxy pattern where
OSS files act as proxies to the EE implementations, improving code organization
and maintainability.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: diegoimbert <diegoimbert@users.noreply.github.com>
2025-05-28 15:43:22 +00:00
Diego Imbert
ee1c7c300c agent_workers_oss proxy file 2025-05-28 17:38:44 +02:00
Ruben Fiszel
6ffb40be26 chore(main): release 1.493.2 (#5827)
* chore(main): release 1.493.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-05-28 15:51:05 +02:00
Ruben Fiszel
e0f4f83ebf fix: improve monaco editor memory leak 2025-05-28 15:46:55 +02:00
Ruben Fiszel
7b70348b4b fix: improve monaco javascript extra lib refresh 2025-05-28 14:04:36 +02:00
Ruben Fiszel
662674e151 chore(main): release 1.493.1 (#5826)
* chore(main): release 1.493.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-05-28 13:54:45 +02:00
Ruben Fiszel
a2c8ea69a3 fix: improve monaco javascript extra lib refresh 2025-05-28 13:50:42 +02:00
Guilhem
af9bde33fe triggers panel polishing (#5825)
* Allways use custom label for triggers

* Add default path name for new schedule

* Improve warning message

* Add confirmation modal for deleting triggers
2025-05-28 10:19:31 +02:00
Ruben Fiszel
da503dc3c5 chore(main): release 1.493.0 (#5808)
* chore(main): release 1.493.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-05-28 01:53:20 +02:00
Ruben Fiszel
0d459d5d22 fix: avoid monaco memory leak 2025-05-28 01:21:22 +02:00
Ruben Fiszel
feae9b0924 fix: error handler node rendering at top level 2025-05-27 21:04:56 +02:00
Diego Imbert
fdefd4be93 feat: duckdb sql lang support (#5761) 2025-05-27 15:52:57 +02:00
Guilhem
5dcefeff84 Allways render content in the app menu to load runnables (#5815) 2025-05-27 01:17:32 +02:00
Guilhem
5897e7e01b Fix(frontend): auto completion and render of tailwind classes in app editor (#5817)
* fix auto completion and render

* Remove tailwind_full.css links and add tailwindUtils to package.json exports

- Removed `<link rel="stylesheet" href="/tailwind_full.css" />` from AppEditor.svelte and AppPreview.svelte
- Added `"./tailwindUtils"` export to package.json exports section for external consumption
- Added tailwindUtils to typesVersions section for TypeScript support

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2025-05-27 01:09:55 +02:00
centdix
e49cf74967 use ai instead of aider (#5814) 2025-05-26 19:45:09 +02:00
HugoCasa
306f3eabd1 fix: add missing http_trigger_version_seq grants (#5816) 2025-05-26 19:38:01 +02:00
Guilhem
d940b39509 fix triggers reset upon deploy (#5812) 2025-05-26 09:21:46 +02:00
Ruben Fiszel
5b96bccedd feat: add aws oidc support for instance s3 storage (#5810)
* backend

* iterate

* all

* all

* all

* iterate

* revert

* all

* add tracing to get of authed client

* all

* all

* lal

* all

* update

* fix

* push

* all

* all

* revert

* frontend

* fix checks

* avoid deadlock

* safer

* fix

* fix
2025-05-25 14:03:38 +02:00
pyranota
26222539e6 feat(python): inline script metadata (PEP 723) (#5712)
* make resolver

* more updates

* fix build

* fix raw_dependencies job type

* compat with http agent workers

* refactor

* rename

* more refactor

* cleanup

* more tests

* fix s3

* small fixes

* more fixing

* fix endpoint

* nit: update comment

* update ee ref

* update ee ref

* update ee ref

* implement safer `list_available_python_versions`

* add tracing to get of authed client

* internal: Trigger claude when commenting with /aider (#5783)

* add claude instructions files

* call claude too when using aider

* fix

* add draft for linear claude integration

* fix build

* update ee ref

* ignore versions <=3.9

* fix windows build

* correct versions filter

* fix windows build (this time for real)

* inject error to debug CI

* update CI

* undo debug of CI

* fix tests

* remove outdated comment

* update ee repo ref

* Update ee-repo-ref.txt

* Update backend/parsers/windmill-parser-py-imports/src/lib.rs

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Update InstanceSetting.svelte

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: centdix <40307056+centdix@users.noreply.github.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-24 02:13:47 +02:00
centdix
b68f1afa26 fix aider install (#5806) 2025-05-23 19:57:09 +02:00
centdix
3f3b2a0c86 small fix (#5805) 2025-05-23 16:39:28 +02:00
centdix
611e118fb6 internal: Handle discord call to aider flow (#5803)
* handle discord call

* Update .github/workflows/aider-common.yml

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Update .github/workflows/aider-common.yml

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* fix indent

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-23 16:08:31 +02:00
Ruben Fiszel
fc8f878584 chore(main): release 1.492.1 (#5801)
* chore(main): release 1.492.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-05-22 20:26:12 +02:00
Ruben Fiszel
59f6024cbd fix: fix strum compile 2025-05-22 20:17:50 +02:00
Ruben Fiszel
a411e2e9a6 chore(main): release 1.492.0 (#5772)
* chore(main): release 1.492.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-05-22 20:06:11 +02:00
centdix
0b6d5e9dca internal: fix branch issues + rules issues (#5799)
* fix branch issues + rules issues

* fix

* fetch issue from linear if issue comes from linear

* ask claude to check after its changes
2025-05-22 20:01:32 +02:00
wendrul
66a997afc3 feat: shift/ctrl+click/enter to open ctrl+k menu results in new tab (#5800)
* feat: add shift/ctrl+click/enter to open ctrl+k menu results in a new tab

* Correct empty else block
2025-05-22 20:01:07 +02:00
Ruben Fiszel
ee86ab00df nit 2025-05-22 19:59:23 +02:00
Ruben Fiszel
262e73e6d6 check fix 2025-05-22 19:58:00 +02:00
Ruben Fiszel
af74653b7f improve err message 2025-05-22 19:48:26 +02:00
Ruben Fiszel
f5e789336f latest ref 2025-05-22 19:45:01 +02:00
Ruben Fiszel
7c24fbcef2 fix: improve docker mode unexpected exit handling 2025-05-22 19:37:47 +02:00
Ruben Fiszel
3f825ec77f reduce the pip resolution cache duration 2025-05-22 18:47:02 +02:00
Ruben Fiszel
6381cdf7d3 improve service log select 2025-05-22 18:26:46 +02:00
186 changed files with 5336 additions and 1858 deletions

View File

@@ -90,4 +90,5 @@ jobs:
with:
needs_processing: false
base_prompt: ${{ needs.check-and-prepare.outputs.prompt_content }}
rules_files: "CLAUDE.md backend/CLAUDE.md frontend/CLAUDE.md"
secrets: inherit

View File

@@ -33,7 +33,11 @@ on:
description: "Prompt for probe-chat"
required: false
type: string
default: 'I''m giving you a request that needs to be implemented. Your role is ONLY to give me the files that are relevant to the request and nothing else. The request is prepended with the word REQUEST. REQUEST: $FINAL_PROMPT. Give me all the files relevant to this request. Your output MUST be a single json array that can be parsed with programatic json parsing, with the relevant files. Files can be rust or typescript or javascript files. DO NOT INCLUDE ANY OTHER TEXT IN YOUR OUTPUT. ONLY THE JSON ARRAY. Example of output: ["file1.py", "file2.py"]'
default: 'I''m giving you a request that needs to be implemented. Your role is ONLY to give me the files that are relevant to the request and nothing else. The request is prepended with the word REQUEST. Give me all the files relevant to this request. Your output MUST be a single json array that can be parsed with programatic json parsing, with the relevant files. Files can be rust or typescript or javascript files. DO NOT INCLUDE ANY OTHER TEXT IN YOUR OUTPUT. ONLY THE JSON ARRAY. Example of output: ["file1.py", "file2.py"]'
rules_files:
description: "Rules files for Aider"
required: false
type: string
outputs:
files_to_edit:
description: "Files identified by probe-chat for editing"
@@ -67,6 +71,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
steps:
- name: Harden Runner
@@ -114,7 +119,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.11"
- name: Cache Python dependencies
uses: actions/cache@v3
@@ -124,27 +129,18 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Cache Aider installation
id: cache-aider
uses: actions/cache@v3
with:
path: ~/.local/bin/aider
key: ${{ runner.os }}-aider-install-${{ hashFiles('**/requirements.txt', '**/setup.py') }}
restore-keys: |
${{ runner.os }}-aider-install-
- name: Install Aider and Dependencies
run: |
if [ -f ~/.local/bin/aider ] && [ -x ~/.local/bin/aider ]; then
echo "Using cached Aider installation"
export PATH="$HOME/.local/bin:$PATH"
else
echo "Installing Aider..."
python -m pip install aider-install; aider-install
fi
pip install -U google-generativeai
echo "Installing Aider..."
python -m pip install uv
python -m venv ~/uv-env
source ~/uv-env/bin/activate
uv pip install configargparse==1.7
uv pip install aider-chat==0.83.1
uv pip install -U google-generativeai
sudo apt-get update && sudo apt-get install -y jq
echo "$HOME/.local/bin" >> $GITHUB_PATH
echo "VIRTUAL_ENV_PATH=$HOME/uv-env" >> $GITHUB_ENV
- name: Create Prompt for Aider
id: create_prompt
@@ -206,7 +202,7 @@ jobs:
fi
else
echo "No issue title or body given. Using base prompt."
FINAL_PROMPT_CONTENT="$BASE_PROMPT_ENV"
FINAL_PROMPT_CONTENT=$(printf "%s\nINSTRUCTION:\n%s" "$BASE_PROMPT_ENV" "$INSTRUCTION_ENV")
fi
echo "Final prompt: $FINAL_PROMPT_CONTENT"
@@ -219,11 +215,11 @@ jobs:
shell: bash
env:
FINAL_PROMPT: ${{ steps.create_prompt.outputs.final_prompt }}
PROBE_PROMPT: ${{ inputs.probe_prompt }}
run: |
echo "Running probe-chat to find relevant files..."
# escape the final prompt
printf -v MESSAGE_FOR_PROBE 'I'\''m giving you a request that needs to be implemented. Your role is ONLY to give me the files that are relevant to the request and nothing else. The request is prepended with the word REQUEST.\nREQUEST: %s. Give me all the files relevant to this request. Your output MUST be a single json array that can be parsed with programatic json parsing, with the relevant files. Files can be rust or typescript or javascript files. DO NOT INCLUDE ANY OTHER TEXT IN YOUR OUTPUT. ONLY THE JSON ARRAY. Example of output: ["file1.py", "file2.py"]' "$FINAL_PROMPT"
MESSAGE_FOR_PROBE=$(printf "%s\nREQUEST:\n%s" "$PROBE_PROMPT" "$FINAL_PROMPT")
set -o pipefail
PROBE_OUTPUT=$(npx --yes @buger/probe-chat@latest --max-iterations 50 --model-name gemini-2.5-pro-preview-05-06 --message "$MESSAGE_FOR_PROBE") || {
@@ -256,21 +252,63 @@ jobs:
restore-keys: |
${{ runner.os }}-aider-
- name: Prepare branch for Aider
id: prepare_branch
env:
ISSUE_ID: ${{ inputs.issue_id }}
run: |
if [[ "$ISSUE_ID" != "" ]]; then
BRANCH_NAME="aider-fix-issue-${ISSUE_ID}"
# Check if branch exists remotely
if git ls-remote --heads origin $BRANCH_NAME | grep -q $BRANCH_NAME; then
echo "Branch $BRANCH_NAME already exists remotely, fetching it"
git fetch origin $BRANCH_NAME
git checkout $BRANCH_NAME
git pull origin $BRANCH_NAME
else
echo "Creating new branch $BRANCH_NAME"
git checkout -b $BRANCH_NAME
fi
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
else
# We're in a pull_request_review event
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_HEAD_REF="${{ github.event.pull_request.head.ref }}"
echo "Handling pull_request_review for PR #$PR_NUMBER on branch $PR_HEAD_REF"
# Ensure we're on the correct branch
git config pull.rebase true
git fetch origin $PR_HEAD_REF
git checkout $PR_HEAD_REF
git pull origin $PR_HEAD_REF
echo "Using PR branch $PR_HEAD_REF for PR #$PR_NUMBER"
echo "BRANCH_NAME=$PR_HEAD_REF" >> $GITHUB_OUTPUT
fi
- name: Run Aider
id: run_aider
shell: bash
env:
FILES_TO_EDIT: ${{ steps.probe_files.outputs.files_to_edit }}
FINAL_PROMPT: ${{ steps.create_prompt.outputs.final_prompt }}
RULES_FILES: ${{ inputs.rules_files }}
run: |
source $VIRTUAL_ENV_PATH/bin/activate
echo "$FINAL_PROMPT" > .aider_final_prompt.txt
echo "FILES_TO_EDIT: $FILES_TO_EDIT"
RULES=""
if [ -n "$RULES_FILES" ]; then
for rule in $RULES_FILES; do
RULES="$RULES --read $rule"
done
fi
aider \
--read .cursor/rules/rust-best-practices.mdc \
--read .cursor/rules/svelte5-best-practices.mdc \
--read .cursor/rules/windmill-overview.mdc \
$RULES \
$FILES_TO_EDIT \
--model gemini/gemini-2.5-pro-preview-05-06 \
--message-file .aider_final_prompt.txt \
@@ -295,40 +333,31 @@ jobs:
id: commit_and_push
env:
ISSUE_ID: ${{ inputs.issue_id }}
BRANCH_NAME: ${{ steps.prepare_branch.outputs.BRANCH_NAME }}
run: |
if [[ "$ISSUE_ID" != "" ]]; then
BRANCH_NAME="aider-fix-issue-${ISSUE_ID}"
# Check if branch exists remotely
if git ls-remote --heads origin $BRANCH_NAME | grep -q $BRANCH_NAME; then
echo "Branch $BRANCH_NAME already exists remotely, fetching it"
git fetch origin $BRANCH_NAME
git checkout $BRANCH_NAME
git pull origin $BRANCH_NAME
else
echo "Creating new branch $BRANCH_NAME"
git checkout -b $BRANCH_NAME
# Check if there are any uncommitted changes
if [[ -n $(git status --porcelain) ]]; then
echo "Found uncommitted changes, committing them"
git add .
git commit -m "Aider changes"
fi
echo "Created/checked out branch $BRANCH_NAME for issue #${ISSUE_ID}"
git push origin $BRANCH_NAME
echo "Pushed to branch $BRANCH_NAME"
echo "PR_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "CHANGES_APPLIED_MESSAGE=Aider changes pushed to branch $BRANCH_NAME." >> $GITHUB_OUTPUT
# Push changes to the branch
if git push origin $BRANCH_NAME; then
echo "Pushed to branch $BRANCH_NAME"
echo "PR_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "CHANGES_APPLIED_MESSAGE=Aider changes pushed to branch $BRANCH_NAME." >> $GITHUB_OUTPUT
echo "CHANGES_APPLIED=true" >> $GITHUB_OUTPUT
else
echo "::warning::Push to PR branch $BRANCH_NAME failed."
echo "CHANGES_APPLIED_MESSAGE=Aider ran, but failed to push changes to PR branch $BRANCH_NAME." >> $GITHUB_OUTPUT
echo "CHANGES_APPLIED=false" >> $GITHUB_OUTPUT
fi
else
# We're in a pull_request_review event
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_HEAD_REF="${{ github.event.pull_request.head.ref }}"
echo "Handling pull_request_review for PR #$PR_NUMBER on branch $PR_HEAD_REF"
# Ensure we're on the correct branch
git config pull.rebase true
git fetch origin $PR_HEAD_REF
git checkout $PR_HEAD_REF
git pull origin $PR_HEAD_REF
echo "Attempting to push changes to PR branch $PR_HEAD_REF for PR #$PR_NUMBER"
echo "Attempting to push changes to PR branch $PR_HEAD_REF"
if git push origin $PR_HEAD_REF; then
echo "Push to $PR_HEAD_REF successful (or no new changes to push)."
echo "CHANGES_APPLIED_MESSAGE=Aider changes (if any) pushed to PR branch $PR_HEAD_REF." >> $GITHUB_OUTPUT
@@ -349,23 +378,20 @@ jobs:
PR_BRANCH: ${{ steps.commit_and_push.outputs.PR_BRANCH_NAME }}
ISSUE_NUM: ${{ inputs.issue_id }}
ISSUE_TITLE: ${{ inputs.issue_title }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
run: |
# Debug: Check latest commit and branch status
echo "Checking latest commit on branch $PR_BRANCH"
git log -1 --pretty=format:"%h - %an, %ar : %s"
echo "Changes not yet committed:"
git status --porcelain
# Check if there are any changes to commit
if [[ -n $(git status --porcelain) ]]; then
echo "Found uncommitted changes, committing them"
git add .
git commit -m "Aider changes for issue #${ISSUE_NUM}"
git push origin $PR_BRANCH
fi
# Create PR description in a temporary file to avoid command line length limits and ensure it stays under 40k chars
HEADER="This PR was created automatically by Aider to fix issue #${ISSUE_NUM}."
# if event is repository_dispatch, add the issue title to the header
if [ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]; then
if [[ "${{ github.event.client_payload.source }}" == "linear" ]]; then
HEADER="This PR was created automatically by Aider to fix issue #linear:${ISSUE_NUM}."
elif [[ "${{ github.event.client_payload.source }}" == "discord" ]]; then
HEADER="This PR was created automatically by Aider to fix issue #discord:${ISSUE_NUM}."
fi
fi
cat > /tmp/pr-description.md << EOL | head -c 40000
This PR was created automatically by Aider to fix issue #${ISSUE_NUM}.
$HEADER
## Aider Output
\`\`\`
@@ -375,11 +401,16 @@ jobs:
# Create PR using the file for the body content, handle errors gracefully
set +e # Don't exit on error
PR_TITLE="[Aider PR] Fix: ${ISSUE_TITLE}"
if [ -z "$ISSUE_TITLE" ]; then
PR_TITLE="[Aider PR] AI changes after request"
fi
gh pr create \
--title "[Aider PR] Fix: ${ISSUE_TITLE}" \
--title "$PR_TITLE" \
--body-file /tmp/pr-description.md \
--head "$PR_BRANCH" \
--base main
--base main \
--draft
PR_CREATE_EXIT_CODE=$?
set -e # Re-enable exit on error
@@ -437,12 +468,13 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository }}
JOB_STATUS: ${{ job.status }}
PR_CREATED: ${{ steps.create_pr.outputs.PR_CREATED }}
PR_URL: ${{ steps.create_pr.outputs.PR_URL }}
run: |
echo "Commenting on issue/PR #${{ github.event.issue.number }} to let the user know Aider has finished working on the request."
if [[ "$JOB_STATUS" == "success" ]]; then
if [[ "$PR_CREATED" == "true" ]]; then
COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created."
COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created. $PR_URL"
else
COMMENT_BODY="🤖 Aider has finished working on your request, but was unable to create a PR."
fi
@@ -460,12 +492,14 @@ jobs:
JOB_STATUS: ${{ job.status }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
PR_CREATED: ${{ steps.create_pr.outputs.PR_CREATED }}
PR_URL: ${{ steps.create_pr.outputs.PR_URL }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
SOURCE: ${{ github.event.client_payload.source }}
run: |
echo "Commenting on linear issue #${{ github.event.client_payload.issue_id }} to let the user know Aider has finished working on the request."
echo "Notifying user about Aider completion status for $SOURCE request #${{ github.event.client_payload.issue_id }}"
if [[ "$JOB_STATUS" == "success" ]]; then
if [[ "$PR_CREATED" == "true" ]]; then
COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created."
COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created. $PR_URL"
else
COMMENT_BODY="🤖 Aider has finished working on your request, but was unable to create a PR."
fi
@@ -473,8 +507,16 @@ jobs:
COMMENT_BODY="⚠️ Aider encountered issues while working on your request. Please check the workflow logs for details."
fi
curl -X POST \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
"https://api.linear.app/graphql" \
-d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"${COMMENT_BODY}\\\" }) { success } }\"}"
if [[ "$SOURCE" == "discord" ]]; then
curl -X POST \
-H "Authorization: Bot $DISCORD_BOT_TOKEN" \
-H "Content-Type: application/json" \
"https://discord.com/api/v10/channels/${{ github.event.client_payload.channel_id }}/messages" \
-d "{\"content\":\"${COMMENT_BODY}\"}"
else
curl -X POST \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
"https://api.linear.app/graphql" \
-d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"${COMMENT_BODY}\\\" }) { success } }\"}"
fi

View File

@@ -21,18 +21,29 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
steps:
- name: Acknowledge Request
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
run: |
echo "Commenting on Linear issue #${{ github.event.client_payload.issue_id }} to acknowledge the request."
curl -X POST \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
"https://api.linear.app/graphql" \
-d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes.\\\" }) { success } }\"}"
if [[ "${{ github.event.client_payload.source }}" == "linear" ]]; then
echo "Commenting on Linear issue #${{ github.event.client_payload.issue_id }} to acknowledge the request."
curl -X POST \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
"https://api.linear.app/graphql" \
-d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes.\\\" }) { success } }\"}"
elif [[ "${{ github.event.client_payload.source }}" == "discord" ]]; then
echo "Commenting on Discord thread #${{ github.event.client_payload.channel_id }} to acknowledge the request."
curl -X POST \
-H "Authorization: Bot $DISCORD_BOT_TOKEN" \
-H "Content-Type: application/json" \
"https://discord.com/api/v10/channels/${{ github.event.client_payload.channel_id }}/messages" \
-d "{\"content\":\"🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes.\"}"
fi
- name: Determine inputs for Aider
id: determine_inputs
@@ -65,4 +76,5 @@ jobs:
issue_body: ${{ needs.check-and-prepare.outputs.issue_body }}
instruction: ${{ needs.check-and-prepare.outputs.instruction }}
issue_id: ${{ github.event.client_payload.issue_id }}
rules_files: "CLAUDE.md backend/CLAUDE.md frontend/CLAUDE.md"
secrets: inherit

View File

@@ -72,6 +72,7 @@ jobs:
COMMENT_BODY: ${{ github.event.comment.body }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
GITHUB_REPOSITORY: ${{ github.repository }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
run: |
echo "Determining inputs for Aider..."
ISSUE_TITLE_VAL=""
@@ -91,12 +92,25 @@ jobs:
if [[ ! -z "$PR_BODY_VAL" ]]; then
REFERENCED_ISSUE=""
if [[ "$PR_BODY_VAL" =~ \#([0-9]+) ]]; then
if [[ "$PR_BODY_VAL" =~ \#linear:([a-f0-9-]+) ]]; then
REFERENCED_ISSUE="${BASH_REMATCH[1]}"
fi
if [[ ! -z "$REFERENCED_ISSUE" ]]; then
echo "Found referenced issue #$REFERENCED_ISSUE in PR description"
echo "Found referenced Linear issue #$REFERENCED_ISSUE in PR description"
LINEAR_ISSUE_JSON=$(curl -s -H "Authorization: $LINEAR_API_KEY" \
"https://api.linear.app/graphql" \
-X POST \
-H "Content-Type: application/json" \
-d "{\"query\":\"query { issue(id: \\\"$REFERENCED_ISSUE\\\") { title description } }\"}")
if [[ $? -eq 0 && ! "$LINEAR_ISSUE_JSON" =~ "error" ]]; then
ISSUE_TITLE_VAL=$(jq -r '.data.issue.title // ""' <<< "$LINEAR_ISSUE_JSON")
ISSUE_BODY_VAL=$(jq -r '.data.issue.description // ""' <<< "$LINEAR_ISSUE_JSON")
echo "Successfully fetched Linear issue details"
else
echo "Error fetching Linear issue details for #$REFERENCED_ISSUE"
fi
elif [[ "$PR_BODY_VAL" =~ \#([0-9]+) ]]; then
REFERENCED_ISSUE="${BASH_REMATCH[1]}"
echo "Found referenced GitHub issue #$REFERENCED_ISSUE in PR description"
ISSUE_DETAILS_JSON=$(gh issue view "$REFERENCED_ISSUE" --json title,body --repo "$GITHUB_REPOSITORY")
if [[ $? -ne 0 ]]; then
@@ -147,4 +161,5 @@ jobs:
issue_body: ${{ needs.check-and-prepare.outputs.issue_body }}
instruction: ${{ needs.check-and-prepare.outputs.comment_content }}
issue_id: ${{ github.event.issue.number }}
rules_files: "CLAUDE.md backend/CLAUDE.md frontend/CLAUDE.md"
secrets: inherit

View File

@@ -45,9 +45,9 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.1.43
- uses: astral-sh/setup-uv@v4
- uses: astral-sh/setup-uv@v6
with:
version: "0.4.18"
version: "0.6.2"
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend

View File

@@ -13,10 +13,10 @@ on:
jobs:
check-membership:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/aider') && !contains(github.event.comment.user.login, '[bot]')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/aider') && !contains(github.event.comment.user.login, '[bot]')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/aider') && !contains(github.event.review.user.login, '[bot]')) ||
(github.event_name == 'issues' && contains(github.event.issue.body, '/aider') && !contains(github.event.issue.user.login, '[bot]'))
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai') && !contains(github.event.review.user.login, '[bot]')) ||
(github.event_name == 'issues' && contains(github.event.issue.body, '/ai') && !contains(github.event.issue.user.login, '[bot]'))
runs-on: ubicloud-standard-2
outputs:
is_member: ${{ steps.check-membership.outputs.is_member }}
@@ -69,6 +69,17 @@ jobs:
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
allowed_tools: "mcp__github__create_pull_request"
custom_instructions: "IMPORTANT: Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a PR from that branch to main, with the title starting with [Claude PR]"
trigger_phrase: "/aider"
allowed_tools: "mcp__github__create_pull_request,Bash(npm run check),Bash(npm install),Bash(cargo check),Bash(curl https://sh.rustup.rs -sSf | sh)"
custom_instructions: "IMPORTANT INSTRUCTIONS:
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a PR from that branch to main, with the title starting with [Claude PR].
- If you made changes to the frontend code, run npm install, then npm run generate-backend-client, then npm run check. You can ignore warnings that are reported by the check script, but fix the errors.
- If you made changes to the backend code, install Rust and then run cargo check. You can ignore warnings that are reported by the check script, but fix the errors.
- DO NOT FORGET TO OPEN A PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
AVAILABLE TOOLS:
- mcp__github__create_pull_request: Create a PR from a branch to main
- Bash(npm run check): Run the check script. You should run this tool after making changes to the frontend code.
- Bash(npm install): Install dependencies. You need this to run npm run check.
- Bash(npm run generate-backend-client): Generate the backend client. You need this to run npm run check.
- Bash(cargo check): Run the cargo check script. You should run this tool after making changes to the backend code.
- Bash(curl https://sh.rustup.rs -sSf | sh): Install Rust. You need this to run cargo check."
trigger_phrase: "/ai"

View File

@@ -29,4 +29,4 @@ jobs:
DISCORD_GUILD_ID: "930051556043276338"
PR_NUMBER: ${{ github.event.pull_request.number }}
secrets:
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_PR_BOT_TOKEN }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}

View File

@@ -84,7 +84,7 @@ jobs:
fi
# 2) get the first message in that thread
messages=$(curl -H "Authorization: Bot $BOT_TOKEN" \
"https://discord.com/api/v10/channels/$thread_id/messages?limit=1")
"https://discord.com/api/v10/channels/$thread_id/messages")
message_id=$(echo "$messages" | jq -r '.[-1].id')
if [ -z "$message_id" ]; then

View File

@@ -1,5 +1,63 @@
# Changelog
## [1.493.2](https://github.com/windmill-labs/windmill/compare/v1.493.1...v1.493.2) (2025-05-28)
### Bug Fixes
* improve monaco editor memory leak ([e0f4f83](https://github.com/windmill-labs/windmill/commit/e0f4f83ebf4416c3bcc24433a7bf606349e1f75a))
* improve monaco javascript extra lib refresh ([7b70348](https://github.com/windmill-labs/windmill/commit/7b70348b4bba3726e3fb26c964219a5a2aa6af55))
## [1.493.1](https://github.com/windmill-labs/windmill/compare/v1.493.0...v1.493.1) (2025-05-28)
### Bug Fixes
* improve monaco javascript extra lib refresh ([a2c8ea6](https://github.com/windmill-labs/windmill/commit/a2c8ea69a3962a350273717cd237d8a96523fd00))
## [1.493.0](https://github.com/windmill-labs/windmill/compare/v1.492.1...v1.493.0) (2025-05-27)
### Features
* add aws oidc support for instance s3 storage ([#5810](https://github.com/windmill-labs/windmill/issues/5810)) ([5b96bcc](https://github.com/windmill-labs/windmill/commit/5b96bccedd6e68fea631580dd49338301ad0305f))
* duckdb sql lang support ([#5761](https://github.com/windmill-labs/windmill/issues/5761)) ([fdefd4b](https://github.com/windmill-labs/windmill/commit/fdefd4be9398b9610a539360353fd61b521732d4))
* **python:** inline script metadata (PEP 723) ([#5712](https://github.com/windmill-labs/windmill/issues/5712)) ([2622253](https://github.com/windmill-labs/windmill/commit/26222539e66bce7e88f86a7e5917e6ca99350865))
### Bug Fixes
* add missing http_trigger_version_seq grants ([#5816](https://github.com/windmill-labs/windmill/issues/5816)) ([306f3ea](https://github.com/windmill-labs/windmill/commit/306f3eabd1c03fa904b0e59438de124a0e680597))
* avoid monaco memory leak ([0d459d5](https://github.com/windmill-labs/windmill/commit/0d459d5d223728270854e37715ecc1663ede9870))
* error handler node rendering at top level ([feae9b0](https://github.com/windmill-labs/windmill/commit/feae9b09240ba306c007013a36d2aefb0b273766))
* **frontend:** auto completion and render of tailwind classes in app editor ([#5817](https://github.com/windmill-labs/windmill/issues/5817)) ([5897e7e](https://github.com/windmill-labs/windmill/commit/5897e7e01b8839425c30c2a97481ef7bb9090661))
## [1.492.1](https://github.com/windmill-labs/windmill/compare/v1.492.0...v1.492.1) (2025-05-22)
### Bug Fixes
* fix strum compile ([59f6024](https://github.com/windmill-labs/windmill/commit/59f6024cbdaface9c9f0ed61c4a415a13b558515))
## [1.492.0](https://github.com/windmill-labs/windmill/compare/v1.491.5...v1.492.0) (2025-05-22)
### Features
* job search pagination + result count ([#5789](https://github.com/windmill-labs/windmill/issues/5789)) ([55ae766](https://github.com/windmill-labs/windmill/commit/55ae76648475ce9ff14b2fa33b2a71b90fbd50a1))
* **python:** add annotation to skip result post-processing ([#5769](https://github.com/windmill-labs/windmill/issues/5769)) ([07c2ff5](https://github.com/windmill-labs/windmill/commit/07c2ff5668f4725a3b9a8a2655248b0945ac251c))
* shift/ctrl+click/enter to open ctrl+k menu results in new tab ([#5800](https://github.com/windmill-labs/windmill/issues/5800)) ([66a997a](https://github.com/windmill-labs/windmill/commit/66a997afc399de2d592c469faf9a5b2cd6433aac))
* triggers git sync ([#5766](https://github.com/windmill-labs/windmill/issues/5766)) ([065a814](https://github.com/windmill-labs/windmill/commit/065a814d35a5749725c2ada1155481abba782684))
### Bug Fixes
* improve app css consistency ([88482c3](https://github.com/windmill-labs/windmill/commit/88482c3bd76ddad16738354f7531d16fa806ad2f))
* improve docker mode unexpected exit handling ([7c24fbc](https://github.com/windmill-labs/windmill/commit/7c24fbcef2ecfe5fc034870c4c65dd80513301a4))
* postgres trigger ssl issue ([#5790](https://github.com/windmill-labs/windmill/issues/5790)) ([b9a776c](https://github.com/windmill-labs/windmill/commit/b9a776c97b3411af18e58cde7a070c4955aaaab4))
* specify using inline type in system prompt for AI ([#5787](https://github.com/windmill-labs/windmill/issues/5787)) ([791296f](https://github.com/windmill-labs/windmill/commit/791296fa41c5bc45c32944db8bc1b66e1515ea82))
* workspace preprocessor improvements ([#5784](https://github.com/windmill-labs/windmill/issues/5784)) ([30edcdf](https://github.com/windmill-labs/windmill/commit/30edcdfe0e950b0ab850942bcbc9b4b5ff4fc00c))
## [1.491.5](https://github.com/windmill-labs/windmill/compare/v1.491.4...v1.491.5) (2025-05-17)

3
backend/.gitignore vendored
View File

@@ -5,4 +5,5 @@ oauth2.json
tracing.folded
heaptrack*
index/
windmill-api/openapi-*.*
windmill-api/openapi-*.*
.duckdb/*

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
"query": "INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('5 mins')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
"describe": {
"columns": [],
"parameters": {
@@ -11,5 +11,5 @@
},
"nullable": []
},
"hash": "4fb3881cdbb4b9e93e28f460a9b3715bdc6a52b76c89f3a3913023b13c4e085c"
"hash": "9a9e4a8779b0bf8a275d029221dfa1465e5d44cd8a7be5879219ffc8cd7ae6b1"
}

742
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.491.5"
version = "1.493.2"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.491.5"
version = "1.493.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -59,7 +59,7 @@ embedding = ["windmill-api/embedding"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet", "dep:object_store"]
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus", "dep:prometheus"]
flow_testing = ["windmill-worker/flow_testing"]
openidconnect = ["windmill-api/openidconnect"]
openidconnect = ["windmill-api/openidconnect", "windmill-common/openidconnect"]
cloud = ["windmill-queue/cloud", "windmill-worker/cloud", "windmill-common/cloud", "windmill-api/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"]
@@ -83,17 +83,18 @@ zip = ["windmill-api/zip"]
static_frontend = ["windmill-api/static_frontend"]
scoped_cache = ["windmill-common/scoped_cache"]
# Languages
python = ["windmill-worker/python"]
python = ["windmill-worker/python", "windmill-api/python"]
rust = ["windmill-worker/rust"]
mysql = ["windmill-worker/mysql"]
oracledb = ["windmill-worker/oracledb"]
duckdb = ["windmill-worker/duckdb"]
mssql = ["windmill-worker/mssql"]
bigquery = ["windmill-worker/bigquery"]
php = ["windmill-worker/php"]
csharp = ["windmill-worker/csharp"]
nu = ["windmill-worker/nu"]
java = ["windmill-worker/java"]
all_languages = [ "python", "deno_core", "rust", "mysql", "oracledb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
all_languages = [ "python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
[patch.crates-io]
@@ -135,10 +136,12 @@ quote.workspace = true
memchr.workspace = true
v8 = { workspace = true, optional = true }
rustls.workspace = true
pep440_rs.workspace = true
systemstat.workspace = true
size.workspace = true
strum.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = { optional = true, workspace = true }
tikv-jemalloc-sys = { optional = true, workspace = true }
@@ -219,6 +222,7 @@ git-version = "^0"
malachite = "=0.4.18"
malachite-bigint = "=0.2.0"
rustpython-parser = "^0"
pep440_rs = "0.7.3"
php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb411dec09450946ef57920b7ffced7f6495d" }
cron = "^0"
mail-send = { version = "0.4.0", features = ["builder"], default-features=false }
@@ -235,6 +239,7 @@ json-pointer = "^0"
itertools = "^0"
regex = "^1"
semver = "^1"
duckdb = { version = "1.2.2", features = ["bundled"] }
v8 = "=130.0.7" # Exact version NOTE: Do not forget to update version and hash in flake.nix
deno_fetch = "0.214.0"
@@ -343,7 +348,7 @@ openidconnect = { version = "4.0.0-rc.1" }
aws-config = "^1"
aws-sdk-sqs = "1.57.0"
aws-sdk-sts = "^1"
aws-smithy-types-convert = { version = "^0", features = ["convert-chrono"] }
crc = "^3"
tar = "^0"
http = "^1"
@@ -390,5 +395,5 @@ tree-sitter-c-sharp = "0.23.0"
tree-sitter-java = "0.23.0"
oracle = { version = "0.6.3", features = ["chrono"] }
rumqttc = { version = "0.24.0", features = ["use-native-tls"]}
strum = "^0"
strum = { version = "0.27", features = ["derive"] }
strum_macros = "^0"

View File

@@ -1 +1 @@
bea87fa885dc041fba83b2491609a4a2cdbbfa6f
32039f675060b5996951708368bdefe14278d5cd

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
GRANT ALL ON SEQUENCE http_trigger_version_seq TO windmill_user;
GRANT ALL ON SEQUENCE http_trigger_version_seq TO windmill_admin;

View File

@@ -27,3 +27,6 @@ anyhow.workspace = true
lazy_static.workspace = true
sqlx.workspace = true
async-recursion.workspace = true
toml.workspace = true
serde.workspace = true
pep440_rs.workspace = true

View File

@@ -11,7 +11,7 @@ mod mapping;
use async_recursion::async_recursion;
use itertools::Itertools;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::{collections::HashMap, str::FromStr};
use mapping::{FULL_IMPORTS_MAP, SHORT_IMPORTS_MAP};
#[cfg(not(target_arch = "wasm32"))]
@@ -25,7 +25,10 @@ use rustpython_parser::{
Parse,
};
use sqlx::{Pool, Postgres};
use windmill_common::{error, worker::PythonAnnotations};
use windmill_common::{
error::{self, to_anyhow},
worker::PythonAnnotations,
};
const DEF_MAIN: &str = "def main(";
@@ -242,8 +245,7 @@ pub async fn parse_python_imports(
w_id: &str,
path: &str,
db: &Pool<Postgres>,
already_visited: &mut Vec<String>,
annotated_pyv_numeric: &mut Option<u32>,
version_specifiers: &mut Vec<pep440_rs::VersionSpecifier>,
) -> error::Result<(Vec<String>, Option<String>)> {
let mut compile_error_hint: Option<String> = None;
let mut imports = parse_python_imports_inner(
@@ -251,9 +253,10 @@ pub async fn parse_python_imports(
w_id,
path,
db,
already_visited,
annotated_pyv_numeric,
&mut annotated_pyv_numeric.and_then(|_| Some(path.to_owned())),
&mut vec![],
version_specifiers,
// &mut version_specifier.and_then(|_| Some(path.to_owned())),
&mut None
)
.await?
.into_values()
@@ -279,6 +282,7 @@ pub async fn parse_python_imports(
.flatten()
.collect::<error::Result<Vec<String>>>()?
.into_iter()
.filter(|x| !x.trim_start().starts_with("--") && !x.trim().is_empty())
.unique()
.collect_vec();
@@ -304,11 +308,34 @@ async fn parse_python_imports_inner(
path: &str,
db: &Pool<Postgres>,
already_visited: &mut Vec<String>,
annotated_pyv_numeric: &mut Option<u32>,
version_specifiers: &mut Vec<pep440_rs::VersionSpecifier>,
path_where_annotated_pyv: &mut Option<String>,
) -> error::Result<HashMap<String, NImportResolved>> {
let PythonAnnotations { py310, py311, py312, py313, .. } = PythonAnnotations::parse(&code);
let mut push_version_specifiers = |perform, unparsed: String| -> error::Result<()> {
if perform {
pep440_rs::VersionSpecifiers::from_str(unparsed.as_str())
.ok()
.map(|vs| version_specifiers.extend(vs.to_vec()));
}
Ok(())
};
push_version_specifiers(py310, "==3.10.*".to_owned())?;
push_version_specifiers(py311, "==3.11.*".to_owned())?;
push_version_specifiers(py312, "==3.12.*".to_owned())?;
push_version_specifiers(py313, "==3.13.*".to_owned())?;
for x in code.lines() {
if x.starts_with("# py:") || x.starts_with("#py:") {
push_version_specifiers(
true,
x.replace('#', "").replace("py:", "").trim().to_owned(),
)?;
} else if !x.starts_with('#') {
break;
}
}
// we pass only if there is none or only one annotation
// Naive:
@@ -323,39 +350,48 @@ async fn parse_python_imports_inner(
// This way we make sure there is no multiple annotations for same script
// and we get detailed span on conflicting versions
let mut check = |is_py_xyz, numeric| -> error::Result<()> {
if is_py_xyz {
if let Some(v) = annotated_pyv_numeric {
if *v != numeric {
return Err(error::Error::from(anyhow::anyhow!(
"Annotated 2 or more different python versions: \n - py{v} at {}\n - py{numeric} at {path}\nIt is possible to use only one.",
path_where_annotated_pyv.clone().unwrap_or("Unknown".to_owned())
)));
}
} else {
*annotated_pyv_numeric = Some(numeric);
}
*path_where_annotated_pyv = Some(path.to_owned());
}
Ok(())
};
#[derive(serde::Serialize, serde::Deserialize)]
struct InlineMetadata {
requires_python: String,
dependencies: Vec<String>,
}
check(py310, 310)?;
check(py311, 311)?;
check(py312, 312)?;
check(py313, 313)?;
let find_requirements = code
.lines()
.find_position(|x| x.starts_with("#requirements:") || x.starts_with("# requirements:"));
if let Some((pos, _)) = find_requirements {
let find_requirements = code.lines().find_position(|x| {
x.starts_with("#requirements:")
|| x.starts_with("# requirements:")
|| x.starts_with("# /// script")
});
if let Some((pos, item)) = find_requirements {
let mut requirements = HashMap::new();
code.lines()
.skip(pos + 1)
.map_while(|x| {
RE.captures(x).and_then(|x| {
x.get(1).map(|m| {
let requirement = m.as_str().to_string();
if item.starts_with("# /// script") {
let mut incorrect = false;
let metadata = code
.lines()
.skip(pos + 1)
.map_while(|x| {
incorrect = !x.starts_with('#');
if incorrect || x.starts_with("# ///") {
None
} else {
x.get(1..)
}
})
.join("\n")
.parse::<toml::Table>()
.map_err(to_anyhow)?;
{
if let Some(v) = metadata.get("requires-python").and_then(|v| v.as_str()) {
push_version_specifiers(true, v.to_owned())?;
}
};
metadata
.get("dependencies")
.and_then(|dependencies| dependencies.as_array())
.inspect(|list| {
for dependency_v in list.into_iter() {
let requirement = dependency_v.as_str().unwrap_or("ERROR").to_owned();
let key = extract_pkg_name(&requirement);
requirements.insert(
key.clone(),
@@ -367,11 +403,31 @@ async fn parse_python_imports_inner(
key,
},
);
}
});
} else {
code.lines()
.skip(pos + 1)
.map_while(|x| {
RE.captures(x).and_then(|x| {
x.get(1).map(|m| {
let requirement = m.as_str().to_string();
let key = extract_pkg_name(&requirement);
requirements.insert(
key.clone(),
NImportResolved::Pin {
pins: vec![ImportPin {
pkg: requirement.clone(),
path: Default::default(),
}],
key,
},
);
})
})
})
})
.collect_vec();
.collect_vec();
}
Ok(requirements)
} else {
let find_extra_requirements = code.lines().find_position(|x| {
@@ -442,7 +498,7 @@ async fn parse_python_imports_inner(
&rpath,
db,
already_visited,
annotated_pyv_numeric,
version_specifiers,
path_where_annotated_pyv,
)
.await?

View File

@@ -18,16 +18,8 @@ def main():
pass
";
let mut already_visited = vec![];
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
&db,
&mut already_visited,
&mut None,
)
.await?;
let (r, ..) =
parse_python_imports(code, "test-workspace", "f/foo/bar", &db, &mut vec![]).await?;
// println!("{}", serde_json::to_string(&r)?);
assert_eq!(
r,
@@ -59,16 +51,8 @@ def main():
pass
";
let mut already_visited = vec![];
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
&db,
&mut already_visited,
&mut None,
)
.await?;
let (r, ..) =
parse_python_imports(code, "test-workspace", "f/foo/bar", &db, &mut vec![]).await?;
println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["burkina=0.4", "nigeria"]);
@@ -89,17 +73,9 @@ def main():
pass
";
let mut already_visited = vec![];
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
&db,
&mut already_visited,
&mut None,
)
.await?;
let (r, ..) =
parse_python_imports(code, "test-workspace", "f/foo/bar", &db, &mut vec![]).await?;
println!("{}", serde_json::to_string(&r)?);
assert_eq!(
r,

View File

@@ -83,6 +83,21 @@ pub fn parse_bigquery_sig(code: &str) -> anyhow::Result<MainArgSignature> {
}
}
pub fn parse_duckdb_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_duckdb_file(&code)?;
if let Some(args) = parsed {
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
}
}
pub fn parse_snowflake_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_snowflake_file(&code)?;
if let Some(x) = parsed {
@@ -212,6 +227,9 @@ lazy_static::lazy_static! {
// -- @name (type) = default
static ref RE_ARG_BIGQUERY: Regex = Regex::new(r#"(?m)^-- @(\w+) \((\w+(?:\[\])?)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
// -- $name (type) = default
static ref RE_ARG_DUCKDB: Regex = Regex::new(r#"(?m)^-- \$(\w+) \((\w+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
static ref RE_ARG_SNOWFLAKE: Regex = Regex::new(r#"(?m)^-- \? (\w+) \((\w+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
@@ -577,6 +595,35 @@ fn parse_bigquery_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
Ok(Some(args))
}
fn parse_duckdb_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut args: Vec<Arg> = vec![];
for cap in RE_ARG_DUCKDB.captures_iter(code) {
let name = cap.get(1).map(|x| x.as_str().to_string()).unwrap();
let typ = cap
.get(2)
.map(|x| x.as_str().to_string().to_lowercase())
.unwrap();
let default = cap.get(3).map(|x| x.as_str().to_string());
let has_default = default.is_some();
let parsed_typ = parse_duckdb_typ(typ.as_str());
let parsed_default = default.and_then(|x| parsed_default(&parsed_typ, x));
args.push(Arg {
name,
typ: parsed_typ,
default: parsed_default,
otyp: Some(typ),
has_default,
oidx: None,
});
}
args.append(&mut parse_sql_sanitized_interpolation(code));
Ok(Some(args))
}
fn parse_snowflake_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut args: Vec<Arg> = vec![];
@@ -729,6 +776,33 @@ pub fn parse_bigquery_typ(typ: &str) -> Typ {
}
}
pub fn parse_duckdb_typ(typ: &str) -> Typ {
if typ.ends_with("[]") {
let base_typ = parse_duckdb_typ(typ.strip_suffix("[]").unwrap());
Typ::List(Box::new(base_typ))
} else {
match typ {
"varchar" | "char" | "bpchar" | "text" | "string" => Typ::Str(None),
"blob" | "bytea" | "binary" | "varbinary" | "bitstring" => Typ::Bytes,
"boolean" | "bool" | "bit" | "logical" => Typ::Bool,
"bigint" | "int8" | "long" | "integer" | "int4" | "int" | "smallint" | "int2"
| "short" | "tinyint" | "int1" | "signed" | "ubigint" | "uhugeint" | "uinteger"
| "usmallint" | "utinyint" => Typ::Int,
"decimal" | "numeric" | "double" | "float8" | "float" | "float4" | "real" => Typ::Float,
"date"
| "time"
| "timestamp with time zone"
| "timestamptz"
| "timestamp"
| "datetime" => Typ::Datetime,
"uuid" | "json" => Typ::Str(None),
"interval" | "hugeint" => Typ::Str(None),
"s3object" => Typ::Resource("S3Object".to_string()),
_ => Typ::Str(None),
}
}
}
pub fn parse_snowflake_typ(typ: &str) -> Typ {
match typ {
"varchar" => Typ::Str(None),

View File

@@ -96,6 +96,12 @@ pub fn parse_oracledb(code: &str) -> String {
wrap_sig(windmill_parser_sql::parse_oracledb_sig(code))
}
#[cfg(feature = "sql-parser")]
#[wasm_bindgen]
pub fn parse_duckdb(code: &str) -> String {
wrap_sig(windmill_parser_sql::parse_duckdb_sig(code))
}
#[cfg(feature = "sql-parser")]
#[wasm_bindgen]
pub fn parse_bigquery(code: &str) -> String {

View File

@@ -69,7 +69,7 @@ use tikv_jemallocator::Jemalloc;
static GLOBAL: Jemalloc = Jemalloc;
#[cfg(feature = "parquet")]
use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING;
use windmill_common::global_settings::OBJECT_STORE_CONFIG_SETTING;
use windmill_worker::{
get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, CSHARP_CACHE_DIR,
@@ -92,7 +92,7 @@ use crate::monitor::{
};
#[cfg(feature = "parquet")]
use crate::monitor::reload_s3_cache_setting;
use windmill_common::s3_helpers::reload_object_store_setting;
const DEFAULT_NUM_WORKERS: usize = 1;
const DEFAULT_PORT: u16 = 8000;
@@ -907,9 +907,9 @@ Windmill Community Edition {GIT_VERSION}
reload_job_default_timeout_setting(&conn).await
},
#[cfg(feature = "parquet")]
OBJECT_STORE_CACHE_CONFIG_SETTING => {
OBJECT_STORE_CONFIG_SETTING => {
if !disable_s3_store {
reload_s3_cache_setting(&db).await
reload_object_store_setting(&db).await;
}
},
SCIM_TOKEN_SETTING => {

View File

@@ -33,8 +33,11 @@ use windmill_common::ee::low_disk_alerts;
#[cfg(feature = "enterprise")]
use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts};
use windmill_common::client::AuthedClient;
#[cfg(feature = "oauth2")]
use windmill_common::global_settings::OAUTH_SETTING;
#[cfg(feature = "parquet")]
use windmill_common::s3_helpers::reload_object_store_setting;
use windmill_common::{
agent_workers::DECODED_AGENT_TOKEN,
auth::create_token_for_owner,
@@ -75,19 +78,13 @@ use windmill_common::{
};
use windmill_queue::{cancel_job, MiniPulledJob, SameWorkerPayload};
use windmill_worker::{
handle_job_error, AuthedClient, JobCompletedSender, SameWorkerSender, BUNFIG_INSTALL_SCOPES,
handle_job_error, JobCompletedSender, SameWorkerSender, BUNFIG_INSTALL_SCOPES,
INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN,
NPM_CONFIG_REGISTRY, NUGET_CONFIG, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL,
};
#[cfg(feature = "parquet")]
use windmill_common::s3_helpers::{
build_object_store_from_settings, build_s3_client_from_settings, S3Settings,
OBJECT_STORE_CACHE_SETTINGS,
};
#[cfg(feature = "parquet")]
use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING;
use windmill_common::s3_helpers::ObjectStoreReload;
#[cfg(feature = "enterprise")]
use crate::ee::verify_license_key;
@@ -241,7 +238,23 @@ pub async fn initial_load(
#[cfg(feature = "parquet")]
if !disable_s3_store {
if let Some(db) = conn.as_sql() {
reload_s3_cache_setting(db).await;
let db2 = db.clone();
match reload_object_store_setting(db).await {
ObjectStoreReload::Later => {
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(10)).await;
match reload_object_store_setting(&db2).await {
ObjectStoreReload::Later => {
tracing::error!("Giving up on loading object store setting");
}
ObjectStoreReload::Never => {
tracing::info!("Object store setting successfully loaded");
}
}
});
}
ObjectStoreReload::Never => (),
}
}
}
@@ -631,7 +644,7 @@ async fn send_log_file_to_object_store(
}
#[cfg(feature = "parquet")]
let s3_client = OBJECT_STORE_CACHE_SETTINGS.read().await.clone();
let s3_client = windmill_common::s3_helpers::get_object_store().await;
#[cfg(feature = "parquet")]
if let Some(s3_client) = s3_client {
let path = std::path::Path::new(TMP_WINDMILL_LOGS_SERVICE)
@@ -917,10 +930,7 @@ async fn delete_log_files_from_disk_and_store(
_s3_prefix: &str,
) {
#[cfg(feature = "parquet")]
let os = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone();
let os = windmill_common::s3_helpers::get_object_store().await;
#[cfg(not(feature = "parquet"))]
let os: Option<()> = None;
@@ -1101,64 +1111,6 @@ pub async fn reload_delete_logs_periodically_setting(conn: &Connection) {
}
}
#[cfg(feature = "parquet")]
pub async fn reload_s3_cache_setting(db: &DB) {
use windmill_common::{
ee::{get_license_plan, LicensePlan},
s3_helpers::ObjectSettings,
};
let s3_config = load_value_from_global_settings(db, OBJECT_STORE_CACHE_CONFIG_SETTING).await;
if let Err(e) = s3_config {
tracing::error!("Error reloading s3 cache config: {:?}", e)
} else {
if let Some(v) = s3_config.unwrap() {
if matches!(get_license_plan().await, LicensePlan::Pro) {
tracing::error!("S3 cache is not available for pro plan");
return;
}
let mut s3_cache_settings = OBJECT_STORE_CACHE_SETTINGS.write().await;
let setting = serde_json::from_value::<ObjectSettings>(v);
if let Err(e) = setting {
tracing::error!("Error parsing s3 cache config: {:?}", e)
} else {
let setting = setting.unwrap();
let bucket = setting.get_bucket().map(|b| b.to_string());
let s3_client = build_object_store_from_settings(setting).await;
if let Err(e) = s3_client {
tracing::error!("Error building s3 client from settings: {:?}", e)
} else {
tracing::info!("Loaded object store {:?}", bucket);
*s3_cache_settings = Some(s3_client.unwrap());
}
}
} else {
let mut s3_cache_settings = OBJECT_STORE_CACHE_SETTINGS.write().await;
if std::env::var("S3_CACHE_BUCKET").is_ok() {
if matches!(get_license_plan().await, LicensePlan::Pro) {
tracing::error!("S3 cache is not available for pro plan");
return;
}
*s3_cache_settings = build_s3_client_from_settings(S3Settings {
bucket: None,
region: None,
access_key: None,
secret_key: None,
endpoint: None,
store_logs: None,
path_style: None,
allow_http: None,
port: None,
})
.await
.ok();
} else {
*s3_cache_settings = None;
}
}
}
}
pub async fn reload_job_default_timeout_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,

20
backend/tests/fixtures/multipython.sql vendored Normal file
View File

@@ -0,0 +1,20 @@
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'# py312
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/multipython/aliases', 2468135790, 'python3', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'# py: >=3.9,!=3.12.2
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/multipython/script1', 2345678901, 'python3', '');

View File

@@ -3970,7 +3970,7 @@ async fn assert_lockfile(
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_requirements_python(db: Pool<Postgres>) {
let content = r#"
# py311
# py: 3.11.11
# requirements:
# tiny==0.1.3
@@ -3988,7 +3988,7 @@ def main():
&db,
content,
ScriptLang::Python3,
vec!["# py311", "tiny==0.1.3"],
vec!["# py: 3.11.11", "tiny==0.1.3"],
)
.await;
}
@@ -3998,7 +3998,7 @@ def main():
async fn test_extra_requirements_python(db: Pool<Postgres>) {
{
let content = r#"
# py311
# py: ==3.11.11
# extra_requirements:
# tiny
@@ -4016,7 +4016,7 @@ def main():
&db,
content,
ScriptLang::Python3,
vec!["# py311", "bottle==0.13.2", "tiny==0.1.2"],
vec!["# py: 3.11.11", "bottle==0.13.2", "tiny==0.1.2"],
)
.await;
}
@@ -4026,7 +4026,7 @@ def main():
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_extra_requirements_python2(db: Pool<Postgres>) {
let content = r#"
# py311
# py: ==3.11.11
# extra_requirements:
# tiny==0.1.3
@@ -4040,7 +4040,7 @@ def main():
&db,
content,
ScriptLang::Python3,
vec!["# py311", "simplejson==3.20.1", "tiny==0.1.3"],
vec!["# py: 3.11.11", "simplejson==3.20.1", "tiny==0.1.3"],
)
.await;
}
@@ -4049,7 +4049,7 @@ def main():
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_pins_python(db: Pool<Postgres>) {
let content = r#"
# py311
# py: ==3.11.11
# extra_requirements:
# tiny==0.1.3
# bottle==0.13.2
@@ -4069,7 +4069,7 @@ def main():
content,
ScriptLang::Python3,
vec![
"# py311",
"# py: 3.11.11",
"bottle==0.13.2",
"microdot==2.2.0",
"simplejson==3.19.3",
@@ -4078,6 +4078,39 @@ def main():
)
.await;
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "multipython"))]
async fn test_multipython_python(db: Pool<Postgres>) {
let content = r#"# py: <=3.12.2, >=3.12.0
import f.multipython.script1
import f.multipython.aliases
"#
.to_string();
assert_lockfile(&db, content, ScriptLang::Python3, vec!["# py: 3.12.1\n"]).await;
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "multipython"))]
async fn test_inline_script_metadata_python(db: Pool<Postgres>) {
let content = r#"# py_select_latest
# /// script
# requires-python = ">3.11,<3.12.3,!=3.12.2"
# dependencies = [
# "tiny==0.1.3",
# ]
# ///
"#
.to_string();
assert_lockfile(
&db,
content,
ScriptLang::Python3,
vec!["# py: 3.12.1", "tiny==0.1.3"],
)
.await;
}
#[sqlx::test(fixtures("base", "result_format"))]
async fn test_result_format(db: Pool<Postgres>) {
let ordered_result_job_id = "1eecb96a-c8b0-4a3d-b1b6-087878c55e41";

View File

@@ -18,7 +18,7 @@ benchmark = []
embedding = ["dep:tinyvector", "dep:hf-hub", "dep:tokenizers", "dep:candle-core", "dep:candle-transformers", "dep:candle-nn"]
parquet = ["dep:datafusion", "dep:object_store", "dep:url", "windmill-common/parquet", "windmill-worker/parquet"]
prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus", "windmill-worker/prometheus"]
openidconnect = ["dep:openidconnect"]
openidconnect = ["dep:openidconnect", "windmill-common/openidconnect"]
tantivy = ["dep:windmill-indexer"]
kafka = ["dep:rdkafka"]
nats = ["dep:async-nats", "dep:nkeys"]
@@ -36,6 +36,7 @@ deno_core = ["dep:deno_core", "dep:deno_error"]
gcp_trigger = ["dep:thiserror", "dep:google-cloud-pubsub", "dep:google-cloud-googleapis", "dep:tonic"]
cloud = ["windmill-common/cloud"]
mcp = ["dep:rmcp"]
python = []
[dependencies]
rmcp = { git = "https://github.com/windmill-labs/rust-sdk", features = ["transport-sse-server"], optional = true }

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.491.5
version: 1.493.2
title: Windmill API
contact:
@@ -11105,6 +11105,23 @@ paths:
items:
$ref: "#/components/schemas/AutoscalingEvent"
/configs/list_available_python_versions:
get:
summary: Get currently available python versions provided by UV.
operationId: listAvailablePythonVersions
tags:
- config
# parameters:
responses:
"200":
description: List of python versions
content:
application/json:
schema:
type: array
items:
type: string
/agent_workers/create_agent_token:
post:
summary: create agent token
@@ -14225,7 +14242,8 @@ components:
ansible,
csharp,
nu,
java
java,
duckdb
# for related places search: ADD_NEW_LANG
]
@@ -16802,7 +16820,6 @@ components:
type: string
required:
- s3
TeamsChannel:
type: object
required:

View File

@@ -0,0 +1,31 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use crate::db::DB;
use axum::Router;
use serde::{Deserialize, Serialize};
pub fn global_service() -> Router {
crate::agent_workers_ee::global_service()
}
pub fn workspaced_service(
db: DB,
_base_internal_url: String,
) -> (
Router,
Vec<tokio::task::JoinHandle<()>>,
Option<windmill_worker::JobCompletedSender>,
) {
crate::agent_workers_ee::workspaced_service(db, _base_internal_url)
}
pub use crate::agent_workers_ee::AgentAuth;
pub use crate::agent_workers_ee::AgentCache;

View File

@@ -0,0 +1,13 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
pub fn global_unauthed_service() -> Router {
crate::apps_ee::global_unauthed_service()
}

View File

@@ -33,6 +33,10 @@ pub fn global_service() -> Router {
"/list_autoscaling_events/:worker_group",
get(list_autoscaling_events),
)
.route(
"/list_available_python_versions",
get(list_available_python_versions),
)
}
#[derive(Serialize, Deserialize, FromRow)]
@@ -205,6 +209,24 @@ async fn list_autoscaling_events(
Ok(Json(events))
}
async fn list_available_python_versions() -> error::JsonResult<Vec<String>> {
#[cfg(not(feature = "python"))]
return Err(error::Error::BadRequest(
"Python listing available only with 'python' feature enabled".to_string(),
));
#[cfg(feature = "python")]
use itertools::Itertools;
#[cfg(feature = "python")]
return Ok(Json(
windmill_worker::PyV::list_available_python_versions()
.await
.iter()
.map(|v| v.to_string())
.collect_vec(),
));
}
#[cfg(feature = "enterprise")]
async fn list_configs(
authed: ApiAuthed,

View File

@@ -0,0 +1,61 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
use crate::db::DB;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use windmill_common::jobs::QueuedJob;
pub fn workspaced_service() -> Router {
crate::gcp_triggers_ee::workspaced_service()
}
pub fn start_consuming_gcp_pubsub_event(
db: DB,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) {
crate::gcp_triggers_ee::start_consuming_gcp_pubsub_event(db, killpill_rx)
}
pub fn gcp_push_route_handler() -> Router {
crate::gcp_triggers_ee::gcp_push_route_handler()
}
pub async fn manage_google_subscription(
path: String,
trigger: GcpTrigger,
operation: String,
w_id: String,
db: DB,
) -> anyhow::Result<()> {
crate::gcp_triggers_ee::manage_google_subscription(path, trigger, operation, w_id, db).await
}
pub async fn process_google_push_request(
workspace_id: String,
trigger_token: String,
message: HashMap<String, serde_json::Value>,
db: DB,
) -> anyhow::Result<Option<QueuedJob>> {
crate::gcp_triggers_ee::process_google_push_request(workspace_id, trigger_token, message, db).await
}
pub async fn validate_jwt_token(
token: String,
audience: String,
) -> anyhow::Result<()> {
crate::gcp_triggers_ee::validate_jwt_token(token, audience).await
}
pub use crate::gcp_triggers_ee::CreateUpdateConfig;
pub use crate::gcp_triggers_ee::DeliveryType;
pub use crate::gcp_triggers_ee::ExistingGcpSubscription;
pub use crate::gcp_triggers_ee::GcpTrigger;
pub use crate::gcp_triggers_ee::PushConfig;
pub use crate::gcp_triggers_ee::SubscriptionMode;

View File

@@ -0,0 +1,17 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
pub fn workspaced_service() -> Router {
crate::git_sync_ee::workspaced_service()
}
pub fn global_service() -> Router {
crate::git_sync_ee::global_service()
}

View File

@@ -0,0 +1,17 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
pub fn workspaced_service() -> Router {
crate::indexer_ee::workspaced_service()
}
pub fn global_service() -> Router {
crate::indexer_ee::global_service()
}

View File

@@ -0,0 +1,69 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
use crate::db::DB;
use serde::{Deserialize, Serialize};
pub fn workspaced_service() -> Router {
crate::job_helpers_ee::workspaced_service()
}
pub async fn get_workspace_s3_resource(
w_id: &str,
path: Option<String>,
db: &DB,
) -> windmill_common::error::Result<Option<String>> {
crate::job_helpers_ee::get_workspace_s3_resource(w_id, path, db).await
}
pub fn get_random_file_name(file_extension: Option<String>) -> String {
crate::job_helpers_ee::get_random_file_name(file_extension)
}
pub async fn get_s3_resource(
s3_resource_opt: Option<String>,
w_id: &str,
db: &DB,
) -> windmill_common::error::Result<Option<windmill_common::s3_helpers::S3Object>> {
crate::job_helpers_ee::get_s3_resource(s3_resource_opt, w_id, db).await
}
pub async fn upload_file_from_req(
req: axum::extract::Request,
storage: Option<String>,
s3_resource_path: Option<String>,
file_key: Option<String>,
resource_id: String,
db: DB,
) -> Result<axum::Json<UploadFileResponse>, windmill_common::error::Error> {
crate::job_helpers_ee::upload_file_from_req(req, storage, s3_resource_path, file_key, resource_id, db).await
}
pub async fn upload_file_internal(
s3_resource_opt: Option<String>,
w_id: &str,
content: bytes::Bytes,
file_key: String,
db: &DB,
) -> windmill_common::error::Result<String> {
crate::job_helpers_ee::upload_file_internal(s3_resource_opt, w_id, content, file_key, db).await
}
pub async fn download_s3_file_internal(
s3_resource_opt: Option<String>,
w_id: &str,
file_key: &str,
db: &DB,
) -> windmill_common::error::Result<bytes::Bytes> {
crate::job_helpers_ee::download_s3_file_internal(s3_resource_opt, w_id, file_key, db).await
}
pub use crate::job_helpers_ee::DownloadFileQuery;
pub use crate::job_helpers_ee::LoadImagePreviewQuery;
pub use crate::job_helpers_ee::UploadFileResponse;

View File

@@ -83,8 +83,6 @@ use windmill_common::{
},
};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
use windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS;
#[cfg(feature = "prometheus")]
use windmill_common::{METRICS_DEBUG_ENABLED, METRICS_ENABLED};
@@ -1058,7 +1056,7 @@ async fn get_logs_from_store(
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
tracing::debug!("Getting logs from store: {file_index:?}");
if let Some(os) = OBJECT_STORE_CACHE_SETTINGS.read().await.clone() {
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
tracing::debug!("object store client present, streaming from there");
let logs = logs.to_string();
@@ -4962,10 +4960,7 @@ async fn run_bundle_preview_script(
uploaded = true;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let object_store = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone();
let object_store = windmill_common::s3_helpers::get_object_store().await;
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
let object_store: Option<()> = None;
@@ -5663,7 +5658,7 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = OBJECT_STORE_CACHE_SETTINGS.read().await.clone() {
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
let file = os
.get(&object_store::path::Path::from(format!("logs/{file_p}")))
.await;

View File

@@ -0,0 +1,25 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
use crate::db::DB;
pub fn workspaced_service() -> Router {
crate::kafka_triggers_ee::workspaced_service()
}
pub fn start_kafka_consumers(
db: DB,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) {
crate::kafka_triggers_ee::start_kafka_consumers(db, killpill_rx)
}
pub use crate::kafka_triggers_ee::KafkaResourceSecurity;
pub use crate::kafka_triggers_ee::KafkaTrigger;
pub use crate::kafka_triggers_ee::KafkaTriggerConfigConnection;

View File

@@ -12,11 +12,11 @@ use crate::ee::ExternalJwks;
#[cfg(feature = "embedding")]
use crate::embeddings::load_embeddings_db;
#[cfg(feature = "oauth2")]
use crate::oauth2_ee::AllClients;
use crate::oauth2_oss::AllClients;
#[cfg(feature = "oauth2")]
use crate::oauth2_ee::SlackVerifier;
use crate::oauth2_oss::SlackVerifier;
#[cfg(feature = "smtp")]
use crate::smtp_server_ee::SmtpServer;
use crate::smtp_server_oss::SmtpServer;
#[cfg(feature = "mcp")]
use crate::mcp::{setup_mcp_server, Runner as McpRunner};
@@ -28,7 +28,7 @@ use crate::{
};
#[cfg(feature = "agent_worker_server")]
use agent_workers_ee::AgentCache;
use agent_workers_oss::AgentCache;
use anyhow::Context;
use argon2::Argon2;
@@ -58,11 +58,11 @@ use windmill_common::db::UserDB;
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{utils::GIT_VERSION, BASE_URL, INSTANCE_NAME};
use crate::scim_ee::has_scim_token;
use crate::scim_oss::has_scim_token;
use windmill_common::error::AppError;
#[cfg(feature = "agent_worker_server")]
mod agent_workers_ee;
mod agent_workers_oss;
mod ai;
mod apps;
pub mod args;
@@ -86,6 +86,7 @@ mod http_trigger_args;
mod http_trigger_auth;
#[cfg(feature = "http_trigger")]
pub mod http_triggers;
mod indexer_oss;
mod indexer_ee;
mod inputs;
mod integration;
@@ -93,48 +94,64 @@ mod integration;
mod postgres_triggers;
mod approvals;
mod apps_oss;
#[cfg(feature = "enterprise")]
mod apps_ee;
mod gcp_triggers_oss;
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
mod gcp_triggers_ee;
mod git_sync_oss;
#[cfg(feature = "enterprise")]
mod git_sync_ee;
mod job_helpers_oss;
#[cfg(feature = "parquet")]
mod job_helpers_ee;
pub mod job_metrics;
pub mod jobs;
mod kafka_triggers_oss;
#[cfg(all(feature = "enterprise", feature = "kafka"))]
mod kafka_triggers_ee;
#[cfg(feature = "mqtt_trigger")]
mod mqtt_triggers;
mod nats_triggers_oss;
#[cfg(all(feature = "enterprise", feature = "nats"))]
mod nats_triggers_ee;
pub mod oauth2_oss;
#[cfg(feature = "oauth2")]
pub mod oauth2_ee;
mod oidc_oss;
mod oidc_ee;
mod raw_apps;
mod resources;
mod saml_oss;
mod saml_ee;
mod schedule;
mod scim_oss;
mod scim_ee;
mod scripts;
mod service_logs;
mod settings;
mod slack_approvals;
mod smtp_server_oss;
#[cfg(feature = "smtp")]
mod smtp_server_ee;
mod sqs_triggers_oss;
#[cfg(all(feature = "enterprise", feature = "sqs_trigger"))]
mod sqs_triggers_ee;
mod teams_approvals_oss;
mod teams_approvals_ee;
mod trigger_helpers;
mod static_assets;
mod stripe_oss;
#[cfg(all(feature = "stripe", feature = "enterprise"))]
mod stripe_ee;
mod teams_oss;
mod teams_ee;
mod tracing_init;
mod triggers;
mod users;
mod users_oss;
mod users_ee;
mod utils;
mod variables;
@@ -143,6 +160,7 @@ pub mod webhook_util;
mod websocket_triggers;
mod workers;
mod workspaces;
mod workspaces_oss;
mod workspaces_ee;
mod workspaces_export;
mod workspaces_extra;
@@ -278,7 +296,7 @@ pub async fn run_server(
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
.allow_origin(Any);
let sp_extension = Arc::new(saml_ee::build_sp_extension().await?);
let sp_extension = Arc::new(saml_oss::build_sp_extension().await?);
if server_mode {
#[cfg(feature = "embedding")]
@@ -317,7 +335,7 @@ pub async fn run_server(
let job_helpers_service = {
#[cfg(feature = "parquet")]
{
job_helpers_ee::workspaced_service()
job_helpers_oss::workspaced_service()
}
#[cfg(not(feature = "parquet"))]
@@ -329,7 +347,7 @@ pub async fn run_server(
let kafka_triggers_service = {
#[cfg(all(feature = "enterprise", feature = "kafka"))]
{
kafka_triggers_ee::workspaced_service()
kafka_triggers_oss::workspaced_service()
}
#[cfg(not(all(feature = "enterprise", feature = "kafka")))]
@@ -341,7 +359,7 @@ pub async fn run_server(
let nats_triggers_service = {
#[cfg(all(feature = "enterprise", feature = "nats"))]
{
nats_triggers_ee::workspaced_service()
nats_triggers_oss::workspaced_service()
}
#[cfg(not(all(feature = "enterprise", feature = "nats")))]
@@ -365,7 +383,7 @@ pub async fn run_server(
let gcp_triggers_service = {
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
{
gcp_triggers_ee::workspaced_service()
gcp_triggers_oss::workspaced_service()
}
#[cfg(not(all(feature = "enterprise", feature = "gcp_trigger")))]
@@ -377,7 +395,7 @@ pub async fn run_server(
let sqs_triggers_service = {
#[cfg(all(feature = "enterprise", feature = "sqs_trigger"))]
{
sqs_triggers_ee::workspaced_service()
sqs_triggers_oss::workspaced_service()
}
#[cfg(not(all(feature = "enterprise", feature = "sqs_trigger")))]
@@ -432,13 +450,13 @@ pub async fn run_server(
#[cfg(all(feature = "enterprise", feature = "kafka"))]
{
let kafka_killpill_rx = killpill_rx.resubscribe();
kafka_triggers_ee::start_kafka_consumers(db.clone(), kafka_killpill_rx);
kafka_triggers_oss::start_kafka_consumers(db.clone(), kafka_killpill_rx);
}
#[cfg(all(feature = "enterprise", feature = "nats"))]
{
let nats_killpill_rx = killpill_rx.resubscribe();
nats_triggers_ee::start_nats_consumers(db.clone(), nats_killpill_rx);
nats_triggers_oss::start_nats_consumers(db.clone(), nats_killpill_rx);
}
#[cfg(feature = "postgres_trigger")]
@@ -456,13 +474,13 @@ pub async fn run_server(
#[cfg(all(feature = "enterprise", feature = "sqs_trigger"))]
{
let sqs_killpill_rx = killpill_rx.resubscribe();
sqs_triggers_ee::start_sqs(db.clone(), sqs_killpill_rx);
sqs_triggers_oss::start_sqs(db.clone(), sqs_killpill_rx);
}
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
{
let gcp_killpill_rx = killpill_rx.resubscribe();
gcp_triggers_ee::start_consuming_gcp_pubsub_event(db.clone(), gcp_killpill_rx);
gcp_triggers_oss::start_consuming_gcp_pubsub_event(db.clone(), gcp_killpill_rx);
}
}
@@ -497,7 +515,7 @@ pub async fn run_server(
#[cfg(feature = "agent_worker_server")]
let (agent_workers_router, agent_workers_bg_processor, agent_workers_killpill_tx) =
if server_mode {
agent_workers_ee::workspaced_service(db.clone(), _base_internal_url.clone())
agent_workers_oss::workspaced_service(db.clone(), _base_internal_url.clone())
} else {
(Router::new(), vec![], None)
};
@@ -535,7 +553,7 @@ pub async fn run_server(
.nest("/oauth", {
#[cfg(feature = "oauth2")]
{
oauth2_ee::workspaced_service()
oauth2_oss::workspaced_service()
}
#[cfg(not(feature = "oauth2"))]
@@ -552,7 +570,7 @@ pub async fn run_server(
)
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc_ee::workspaced_service())
.nest("/oidc", oidc_oss::workspaced_service())
.nest("/http_triggers", http_triggers_service)
.nest("/websocket_triggers", websocket_triggers_service)
.nest("/kafka_triggers", kafka_triggers_service)
@@ -584,17 +602,17 @@ pub async fn run_server(
.nest("/jobs", jobs::global_root_service())
.nest(
"/srch/w/:workspace_id/index",
indexer_ee::workspaced_service(),
indexer_oss::workspaced_service(),
)
.nest("/srch/index", indexer_ee::global_service())
.nest("/oidc", oidc_ee::global_service())
.nest("/srch/index", indexer_oss::global_service())
.nest("/oidc", oidc_oss::global_service())
.nest(
"/saml",
saml_ee::global_service().layer(Extension(Arc::clone(&sp_extension))),
saml_oss::global_service().layer(Extension(Arc::clone(&sp_extension))),
)
.nest(
"/scim",
scim_ee::global_service()
scim_oss::global_service()
.route_layer(axum::middleware::from_fn(has_scim_token)),
)
.nest("/concurrency_groups", concurrency_groups::global_service())
@@ -602,7 +620,7 @@ pub async fn run_server(
.nest("/apps_u", {
#[cfg(feature = "enterprise")]
{
apps_ee::global_unauthed_service()
apps_oss::global_unauthed_service()
}
#[cfg(not(feature = "enterprise"))]
@@ -621,7 +639,7 @@ pub async fn run_server(
.nest("/agent_workers", {
#[cfg(feature = "agent_worker_server")]
{
agent_workers_ee::global_service().layer(Extension(agent_cache.clone()))
agent_workers_oss::global_service().layer(Extension(agent_cache.clone()))
}
#[cfg(not(feature = "agent_worker_server"))]
{
@@ -646,7 +664,7 @@ pub async fn run_server(
.nest("/teams", {
#[cfg(feature = "enterprise")]
{
teams_ee::teams_service()
teams_oss::teams_service()
}
#[cfg(not(feature = "enterprise"))]
@@ -660,12 +678,12 @@ pub async fn run_server(
)
.route(
"/w/:workspace_id/jobs/teams_approval/:job_id",
get(teams_approvals_ee::request_teams_approval),
get(teams_approvals_oss::request_teams_approval),
)
.nest("/w/:workspace_id/github_app", {
#[cfg(feature = "enterprise")]
{
git_sync_ee::workspaced_service()
git_sync_oss::workspaced_service()
}
#[cfg(not(feature = "enterprise"))]
@@ -674,7 +692,7 @@ pub async fn run_server(
.nest("/github_app", {
#[cfg(feature = "enterprise")]
{
git_sync_ee::global_service()
git_sync_oss::global_service()
}
#[cfg(not(feature = "enterprise"))]
@@ -695,7 +713,7 @@ pub async fn run_server(
.nest("/oauth", {
#[cfg(feature = "oauth2")]
{
oauth2_ee::global_service().layer(Extension(Arc::clone(&sp_extension)))
oauth2_oss::global_service().layer(Extension(Arc::clone(&sp_extension)))
}
#[cfg(not(feature = "oauth2"))]
@@ -721,7 +739,7 @@ pub async fn run_server(
{
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
{
gcp_triggers_ee::gcp_push_route_handler()
gcp_triggers_oss::gcp_push_route_handler()
}
#[cfg(not(all(feature = "enterprise", feature = "gcp_trigger")))]
{

View File

@@ -0,0 +1,25 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
use crate::db::DB;
pub fn workspaced_service() -> Router {
crate::nats_triggers_ee::workspaced_service()
}
pub fn start_nats_consumers(
db: DB,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) {
crate::nats_triggers_ee::start_nats_consumers(db, killpill_rx)
}
pub use crate::nats_triggers_ee::NatsResourceAuth;
pub use crate::nats_triggers_ee::NatsTrigger;
pub use crate::nats_triggers_ee::NatsTriggerConfigConnection;

View File

@@ -0,0 +1,52 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use std::collections::HashMap;
use axum::Router;
use sqlx::{Postgres, Transaction};
use crate::db::DB;
use windmill_common::error;
pub fn global_service() -> Router {
crate::oauth2_ee::global_service()
}
pub fn workspaced_service() -> Router {
crate::oauth2_ee::workspaced_service()
}
pub async fn build_oauth_clients(
base_url: &str,
oauths_from_config: Option<HashMap<String, OAuthClient>>,
db: &DB,
) -> anyhow::Result<AllClients> {
crate::oauth2_ee::build_oauth_clients(base_url, oauths_from_config, db).await
}
pub async fn _refresh_token<'c>(
tx: Transaction<'c, Postgres>,
path: &str,
w_id: &str,
id: i32,
db: &DB,
) -> error::Result<String> {
crate::oauth2_ee::_refresh_token(tx, path, w_id, id, db).await
}
pub async fn check_nb_of_user(db: &DB) -> error::Result<()> {
crate::oauth2_ee::check_nb_of_user(db).await
}
// Re-export all public types
pub use crate::oauth2_ee::AllClients;
pub use crate::oauth2_ee::BasicClientsMap;
pub use crate::oauth2_ee::ClientWithScopes;
pub use crate::oauth2_ee::OAuthClient;
pub use crate::oauth2_ee::OAuthConfig;
pub use crate::oauth2_ee::SlackVerifier;
pub use crate::oauth2_ee::TokenResponse;

View File

@@ -0,0 +1,17 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
pub fn global_service() -> Router {
crate::oidc_ee::global_service()
}
pub fn workspaced_service() -> Router {
crate::oidc_ee::workspaced_service()
}

View File

@@ -0,0 +1,23 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
pub async fn build_sp_extension() -> anyhow::Result<ServiceProviderExt> {
crate::saml_ee::build_sp_extension().await
}
pub fn global_service() -> Router {
crate::saml_ee::global_service()
}
pub async fn acs() -> String {
crate::saml_ee::acs().await
}
pub use crate::saml_ee::ServiceProviderExt;

View File

@@ -0,0 +1,22 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::{middleware::Next, response::Response, Router};
use http::Request;
pub fn global_service() -> Router {
crate::scim_ee::global_service()
}
pub async fn ee() -> String {
crate::scim_ee::ee().await
}
pub async fn has_scim_token<B>(request: Request<B>, next: Next) -> Response {
crate::scim_ee::has_scim_token(request, next).await
}

View File

@@ -410,10 +410,7 @@ async fn create_snapshot_script(
uploaded = true;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let object_store = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone();
let object_store = windmill_common::s3_helpers::get_object_store().await;
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
let object_store: Option<()> = None;
@@ -1327,10 +1324,12 @@ async fn raw_script_by_path_internal(
w_id
)
.fetch_one(&db)
.await?;
if exists.unwrap_or(false) {
.await?
.unwrap_or(false);
if exists {
return Err(Error::NotFound(format!(
"Script {path} not visible to {} but exists",
"Script {path} exists but {} does not have permissions to access it",
authed.username
)));
}

View File

@@ -98,10 +98,7 @@ async fn get_log_file(
require_devops_role(&db, &email).await?;
let path = path.to_path();
#[cfg(feature = "parquet")]
let s3_client = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone();
let s3_client = windmill_common::s3_helpers::get_object_store().await;
#[cfg(feature = "parquet")]
if let Some(s3_client) = s3_client {
let path = format!("{}{}", windmill_common::tracing_init::LOGS_SERVICE, path);

View File

@@ -120,12 +120,15 @@ use windmill_common::s3_helpers::build_object_store_from_settings;
#[cfg(feature = "parquet")]
pub async fn test_s3_bucket(
_authed: ApiAuthed,
Extension(db): Extension<DB>,
Json(test_s3_bucket): Json<ObjectSettings>,
) -> error::Result<String> {
use bytes::Bytes;
use futures::StreamExt;
let client = build_object_store_from_settings(test_s3_bucket).await?;
let client = build_object_store_from_settings(test_s3_bucket, Some(&db))
.await?
.store;
let mut list = client.list(Some(&object_store::path::Path::from("".to_string())));
let first_file = list.next().await;

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export the EE implementation
pub use crate::smtp_server_ee::SmtpServer;

View File

@@ -0,0 +1,23 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
use crate::db::DB;
pub fn workspaced_service() -> Router {
crate::sqs_triggers_ee::workspaced_service()
}
pub fn start_sqs(
db: DB,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) {
crate::sqs_triggers_ee::start_sqs(db, killpill_rx)
}
pub use crate::sqs_triggers_ee::SqsTrigger;

View File

@@ -0,0 +1,13 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::Router;
pub fn add_stripe_routes(router: Router) -> Router {
crate::stripe_ee::add_stripe_routes(router)
}

View File

@@ -0,0 +1,14 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::http::StatusCode;
use windmill_common::error::Error;
pub async fn request_teams_approval() -> Result<StatusCode, Error> {
crate::teams_approvals_ee::request_teams_approval().await
}

View File

@@ -0,0 +1,34 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use axum::{http::StatusCode, Router};
use windmill_common::error::Error;
pub async fn edit_teams_command() -> Result<StatusCode, Error> {
crate::teams_ee::edit_teams_command().await
}
pub async fn workspaces_list_available_teams_ids() -> Result<StatusCode, Error> {
crate::teams_ee::workspaces_list_available_teams_ids().await
}
pub async fn connect_teams() -> Result<StatusCode, Error> {
crate::teams_ee::connect_teams().await
}
pub async fn run_teams_message_test_job() -> Result<StatusCode, Error> {
crate::teams_ee::run_teams_message_test_job().await
}
pub async fn workspaces_list_available_teams_channels() -> Result<StatusCode, Error> {
crate::teams_ee::workspaces_list_available_teams_channels().await
}
pub fn teams_service() -> Router {
crate::teams_ee::teams_service()
}

View File

@@ -0,0 +1,65 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use crate::db::{ApiAuthed, DB};
use argon2::Argon2;
use serde::Serialize;
use std::sync::Arc;
use windmill_common::error;
pub async fn create_user<T: Serialize>(
db: DB,
w_id: String,
authed: ApiAuthed,
email: String,
password: String,
super_admin: Option<bool>,
name: Option<String>,
company: Option<String>,
username: String,
invite_authed: ApiAuthed,
is_admin: Option<bool>,
is_operator: Option<bool>,
role: Option<String>,
groups: Option<Vec<String>>,
oidc_only: Option<bool>,
) -> error::Result<(String, T)> {
crate::users_ee::create_user(
db,
w_id,
authed,
email,
password,
super_admin,
name,
company,
username,
invite_authed,
is_admin,
is_operator,
role,
groups,
oidc_only,
)
.await
}
pub async fn set_password(
db: DB,
w_id: String,
authed: ApiAuthed,
username: String,
password: String,
argon2: Arc<Argon2<'_>>,
) -> error::Result<String> {
crate::users_ee::set_password(db, w_id, authed, username, password, argon2).await
}
pub fn send_email_if_possible(subject: &str, content: &str, to: &str) {
crate::users_ee::send_email_if_possible(subject, content, to)
}

View File

@@ -360,6 +360,7 @@ pub(crate) async fn tarball_workspace(
ScriptLang::Bigquery => "bq.sql",
ScriptLang::Snowflake => "sf.sql",
ScriptLang::Mssql => "ms.sql",
ScriptLang::DuckDb => "duckdb.sql",
ScriptLang::Graphql => "gql",
ScriptLang::Nativets => "fetch.ts",
ScriptLang::Bun | ScriptLang::Bunnative => {

View File

@@ -0,0 +1,25 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
use crate::db::{ApiAuthed, DB};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct EditAutoInvite {
pub auto_invite_domain: Option<String>,
pub auto_invite_operator: Option<bool>,
}
pub async fn edit_auto_invite(
authed: ApiAuthed,
db: DB,
w_id: String,
ea: EditAutoInvite,
) -> windmill_common::error::Result<String> {
crate::workspaces_ee::edit_auto_invite(authed, db, w_id, ea).await
}

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::audit_ee::*;

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::autoscaling_ee::*;

View File

@@ -12,14 +12,14 @@ tantivy = []
prometheus = ["dep:prometheus"]
loki = ["dep:tracing-loki"]
benchmark = []
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts", "dep:datafusion"]
parquet = ["dep:object_store", "dep:aws-config", "dep:aws-sdk-sts", "dep:aws-smithy-types-convert", "dep:datafusion"]
aws_auth = ["dep:aws-sdk-sts", "dep:aws-config"]
otel = ["dep:opentelemetry-semantic-conventions", "dep:opentelemetry-otlp", "dep:opentelemetry_sdk",
"dep:opentelemetry", "dep:tracing-opentelemetry", "dep:opentelemetry-appender-tracing", "dep:tonic"]
smtp = ["dep:mail-send"]
scoped_cache = []
cloud = []
openidconnect = ["dep:openidconnect"]
[lib]
name = "windmill_common"
path = "src/lib.rs"
@@ -62,6 +62,7 @@ object_store = { workspace = true, optional = true }
prometheus = { workspace = true, optional = true }
aws-config = { workspace = true, optional = true }
aws-sdk-sts = { workspace = true, optional = true }
aws-smithy-types-convert = { workspace = true, optional = true }
indexmap.workspace = true
bytes.workspace = true
mail-send = { workspace = true, optional = true }
@@ -75,6 +76,7 @@ windmill-parser-ts.workspace = true
windmill-parser-py.workspace = true
jsonwebtoken.workspace = true
backon.workspace = true
openidconnect = { workspace = true, optional = true }
strum.workspace = true
strum_macros.workspace = true

View File

@@ -0,0 +1,244 @@
use anyhow::Context;
use reqwest::{Body, Response};
use serde::de::DeserializeOwned;
use crate::{
error::{self, to_anyhow},
s3_helpers::{DuckdbConnectionSettingsQueryV2, DuckdbConnectionSettingsResponse},
utils::HTTP_CLIENT,
};
#[derive(Clone)]
pub struct AuthedClient {
pub base_internal_url: String,
pub workspace: String,
pub token: String,
pub force_client: Option<reqwest::Client>,
}
impl AuthedClient {
pub async fn get(&self, url: &str, query: Vec<(&str, String)>) -> anyhow::Result<Response> {
self.force_client
.as_ref()
.unwrap_or(&HTTP_CLIENT)
.get(url)
.query(&query)
.header(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
)
.header(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", self.token))?,
)
.send()
.await
.map_err(|e| {
tracing::error!("Error executing get request from authed http client to {url} with query {query:?}: {e}");
anyhow::anyhow!("Error executing get request from authed http client to {url} with query {query:?}: {e}")
})
}
pub async fn get_id_token(&self, audience: &str) -> anyhow::Result<String> {
let url = format!(
"{}/api/w/{}/oidc/token/{}",
self.base_internal_url, self.workspace, audience
);
let response = self.get(&url, vec![]).await?;
match response.status().as_u16() {
200u16 => Ok(response
.json::<String>()
.await
.context("decoding oidc token as json string")?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn get_resource_value<T: DeserializeOwned>(&self, path: &str) -> anyhow::Result<T> {
let url = format!(
"{}/api/w/{}/resources/get_value/{}",
self.base_internal_url, self.workspace, path
);
let response = self.get(&url, vec![]).await?;
match response.status().as_u16() {
200u16 => Ok(response
.json::<T>()
.await
.context("decoding resource value as json")?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn get_variable_value(&self, path: &str) -> anyhow::Result<String> {
let url = format!(
"{}/api/w/{}/variables/get_value/{}",
self.base_internal_url, self.workspace, path
);
let response = self.get(&url, vec![]).await?;
match response.status().as_u16() {
200u16 => Ok(response
.json::<String>()
.await
.context("decoding variable value as json")?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn get_resource_value_interpolated<T: DeserializeOwned>(
&self,
path: &str,
job_id: Option<String>,
) -> anyhow::Result<T> {
let url = format!(
"{}/api/w/{}/resources/get_value_interpolated/{}",
self.base_internal_url, self.workspace, path
);
let mut query = Vec::with_capacity(1usize);
if let Some(v) = &job_id {
query.push(("job_id", v.to_string()));
}
let response = self.get(&url, query).await?;
match response.status().as_u16() {
200u16 => Ok(response
.json::<T>()
.await
.context("decoding interpolated resource value as json")?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn get_completed_job_result<T: DeserializeOwned>(
&self,
path: &str,
json_path: Option<String>,
) -> anyhow::Result<T> {
let url = format!(
"{}/api/w/{}/jobs_u/completed/get_result/{}",
self.base_internal_url, self.workspace, path
);
let query = if let Some(json_path) = json_path {
vec![("json_path", json_path)]
} else {
vec![]
};
let response = self.get(&url, query).await?;
match response.status().as_u16() {
200u16 => Ok(response
.json::<T>()
.await
.context("decoding completed job result as json")?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn get_result_by_id<T: DeserializeOwned>(
&self,
flow_job_id: &str,
node_id: &str,
json_path: Option<String>,
) -> anyhow::Result<T> {
let url = format!(
"{}/api/w/{}/jobs/result_by_id/{}/{}",
self.base_internal_url, self.workspace, flow_job_id, node_id
);
let query = if let Some(json_path) = json_path {
vec![("json_path", json_path)]
} else {
vec![]
};
let response = self.get(&url, query).await?;
match response.status().as_u16() {
200u16 => Ok(response
.json::<T>()
.await
.context("decoding result by id as json")?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn upload_s3_file<S>(
&self,
workspace_id: &str,
object_key: String,
storage: Option<String>,
body: S,
) -> anyhow::Result<()>
where
S: futures::stream::TryStream + Send + 'static,
S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
bytes::Bytes: From<S::Ok>,
{
let mut query = vec![("file_key", object_key)];
if let Some(storage) = storage {
query.push(("storage", storage));
}
let response = self
.force_client
.as_ref()
.unwrap_or(&HTTP_CLIENT)
.post(format!(
"{}/api/w/{}/job_helpers/upload_s3_file",
self.base_internal_url, workspace_id
))
.query(&query)
.header(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
)
.header(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", self.token))
.map_err(|e| anyhow::anyhow!(e.to_string()))?,
)
.body(Body::wrap_stream(body))
.send()
.await
.context(format!("Sent upload_s3_file request",))
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
match response.status().as_u16() {
200u16 => Ok(()),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default()))?,
}
}
pub async fn get_duckdb_connection_settings(
&self,
s3: &DuckdbConnectionSettingsQueryV2,
) -> error::Result<DuckdbConnectionSettingsResponse> {
let url = format!(
"{}/api/w/{}/job_helpers/v2/duckdb_connection_settings",
self.base_internal_url, &self.workspace
);
let response = self
.force_client
.as_ref()
.unwrap_or(&HTTP_CLIENT)
.post(url)
.header(
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_static("application/json"),
)
.header(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
)
.header(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", self.token))
.map_err(|e| error::Error::BadConfig(e.to_string()))?,
)
.body(serde_json::to_string(&s3).map_err(to_anyhow)?)
.send()
.await
.context(format!("Sent get_duckdb_connection_settings request",))
.map_err(error::Error::from)?;
match response.status().as_u16() {
200u16 => Ok(response
.json::<DuckdbConnectionSettingsResponse>()
.await
.context("decoding duckdb_connection_settings response as json")?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default()))?,
}
}
}

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::email_ee::*;

View File

@@ -30,7 +30,7 @@ pub const EXPOSE_METRICS_SETTING: &str = "expose_metrics";
pub const EXPOSE_DEBUG_METRICS_SETTING: &str = "expose_debug_metrics";
pub const KEEP_JOB_DIR_SETTING: &str = "keep_job_dir";
pub const REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING: &str = "require_preexisting_user_for_oauth";
pub const OBJECT_STORE_CACHE_CONFIG_SETTING: &str = "object_store_cache_config";
pub const OBJECT_STORE_CONFIG_SETTING: &str = "object_store_cache_config";
pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation";
pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";

View File

@@ -1,18 +1,34 @@
use std::future::Future;
use crate::s3_helpers::{ObjectStoreResource, StorageResourceType};
use crate::{
error::Error,
s3_helpers::{ObjectStoreResource, StorageResourceType},
};
pub async fn get_s3_resource_internal<'c, F, Fut>(
pub async fn get_s3_resource_internal<'c>(
_resource_type: StorageResourceType,
_s3_resource_value_raw: serde_json::Value,
_gen_token: F,
) -> crate::error::Result<ObjectStoreResource>
where
F: FnOnce(String) -> Fut,
Fut: Future<Output = Result<String, Error>> + Send + 'static,
{
_gen_token: TokenGenerator<'c>,
_db: &crate::DB,
) -> crate::error::Result<ObjectStoreResource> {
todo!()
}
pub enum TokenGenerator<'c> {
AsClient(&'c crate::client::AuthedClient),
AsServerInstance(),
}
impl<'c> TokenGenerator<'c> {
pub async fn gen_token(
&self,
_audience: &str,
_db: Option<&crate::DB>,
) -> anyhow::Result<String> {
todo!()
}
}
#[cfg(feature = "parquet")]
pub(crate) async fn generate_s3_aws_oidc_resource<'c>(
_clone: crate::s3_helpers::S3AwsOidcResource,
_token_generator: TokenGenerator<'c>,
_init_private_key: Option<&sqlx::Pool<sqlx::Postgres>>,
) -> crate::error::Result<ObjectStoreResource> {
todo!()
}

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::job_s3_helpers_ee::*;

View File

@@ -608,11 +608,11 @@ pub async fn get_logs_from_store(
logs: &str,
log_file_index: &Option<Vec<String>>,
) -> Option<impl Stream<Item = Result<Bytes, object_store::Error>>> {
use crate::s3_helpers::OBJECT_STORE_CACHE_SETTINGS;
use crate::s3_helpers::get_object_store;
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
if let Some(os) = OBJECT_STORE_CACHE_SETTINGS.read().await.clone() {
if let Some(os) = get_object_store().await {
let logs = logs.to_string();
let stream = async_stream::stream! {
for file_p in file_index.clone() {

View File

@@ -30,6 +30,7 @@ pub mod auth;
#[cfg(feature = "benchmark")]
pub mod bench;
pub mod cache;
pub mod client;
pub mod db;
pub mod ee;
pub mod email_ee;
@@ -43,6 +44,9 @@ pub mod job_metrics;
#[cfg(feature = "parquet")]
pub mod job_s3_helpers_ee;
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
pub mod oidc_ee;
pub mod jobs;
pub mod jwt;
pub mod more_serde;

View File

@@ -0,0 +1,198 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2023
* 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.
*/
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
use {
crate::db::DB,
crate::{auth::IdToken as WindmillIdToken, error::Result},
anyhow,
openidconnect::{
core::{CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey},
IssuerUrl, JsonWebKeyId,
},
std::process::Command,
};
#[cfg(feature = "openidconnect")]
use openidconnect::AdditionalClaims;
#[cfg(feature = "openidconnect")]
impl AdditionalClaims for JobClaim {}
#[cfg(feature = "openidconnect")]
impl AdditionalClaims for WorkspaceClaim {}
#[cfg(feature = "openidconnect")]
impl AdditionalClaims for InstanceClaim {}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct WorkspaceClaim {
pub workspace: String,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct InstanceClaim {}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct JobClaim {
pub job_id: String,
pub path: Option<String>,
pub flow_path: Option<String>,
pub groups: Vec<String>,
pub username: String,
pub email: String,
pub workspace: String,
}
lazy_static::lazy_static! {
static ref PRIVATE_KEY: RwLock<Option<String>> = RwLock::new(None);
}
pub async fn generate_id_token<T: AdditionalClaims>(
db: Option<&DB>,
claim: T,
audience: &str,
identifier: String,
email: Option<String>,
) -> Result<WindmillIdToken> {
use chrono::{Duration, Utc};
use openidconnect::{
core::{CoreGenderClaim, CoreJweContentEncryptionAlgorithm},
Audience, EndUserEmail, IdToken, IdTokenClaims, StandardClaims, SubjectIdentifier,
};
let private_key = get_private_key(db).await?;
let issue_url = format!("{}/api/oidc/", crate::BASE_URL.read().await.clone());
let issue_time = Utc::now();
let expiration = issue_time + Duration::try_hours(48).unwrap();
let id_token = IdToken::<
T,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
>::new(
IdTokenClaims::<T, CoreGenderClaim>::new(
// Specify the issuer URL for the OpenID Connect Provider.
IssuerUrl::new(issue_url)
.map_err(|e| anyhow::anyhow!("Failed to generate IssueUrl: {}", e))?,
// The audience is usually a single entry with the client ID of the client for whom
// the ID token is intended. This is a required claim.
vec![Audience::new(audience.to_string())],
// The ID token expiration is usually much shorter than that of the access or refresh
// tokens issued to clients.
expiration,
// The issue time is usually the current time.
issue_time,
// Set the standard claims defined by the OpenID Connect Core spec.
StandardClaims::new(
// Stable subject identifiers are recommended in place of e-mail addresses or other
// potentially unstable identifiers. This is the only required claim.
SubjectIdentifier::new(identifier),
)
// Optional: specify the user's e-mail address. This should only be provided if the
// client has been granted the 'profile' or 'email' scopes.
.set_email(email.map(|x| EndUserEmail::new(x)))
// Optional: specify whether the provider has verified the user's e-mail address.
.set_email_verified(Some(true)),
// OpenID Connect Providers may supply custom claims by providing a struct that
// implements the AdditionalClaims trait. This requires manually using the
// generic IdTokenClaims struct rather than the CoreIdTokenClaims type alias,
// however.
claim,
),
// The private key used for signing the ID token. For confidential clients (those able
// to maintain a client secret), a CoreHmacKey can also be used, in conjunction
// with one of the CoreJwsSigningAlgorithm::HmacSha* signing algorithms. When using an
// HMAC-based signing algorithm, the UTF-8 representation of the client secret should
// be used as the HMAC key.
&CoreRsaPrivateSigningKey::from_pem(
&private_key,
Some(JsonWebKeyId::new("windmill".to_string())),
)
.map_err(|e| anyhow::anyhow!("Invalid private key: {}", e))?,
// Uses the RS256 signature algorithm. This crate supports any RS*, PS*, or HS*
// signature algorithm.
CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,
// When returning the ID token alongside an access token (e.g., in the Authorization Code
// flow), it is recommended to pass the access token here to set the `at_hash` claim
// automatically.
None,
// When returning the ID token alongside an authorization code (e.g., in the implicit
// flow), it is recommended to pass the authorization code here to set the `c_hash` claim
// automatically.
None,
)
.map_err(|e| anyhow::anyhow!("Failed to generate token: {}", e))?;
Ok(WindmillIdToken::new(id_token.to_string(), expiration))
}
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
pub async fn get_private_key(db: Option<&DB>) -> anyhow::Result<String> {
if let Some(key) = PRIVATE_KEY.read().await.clone() {
return Ok(key);
} else if let Some(db) = db {
let key = sqlx::query_scalar!(
"SELECT value->>'private_key' FROM global_settings WHERE name = 'rsa_keys'",
)
.fetch_optional(db)
.await?
.flatten();
let key = key.filter(|s| !s.is_empty());
if let Some(key) = key {
return Ok(key);
} else {
let keys = gen_pems(db).await?;
return Ok(keys.private_key);
}
} else {
return Err(anyhow::anyhow!("Private key not found and no db provided"));
}
}
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
#[derive(Debug, Clone, serde::Serialize)]
struct Keys {
private_key: String,
}
#[cfg(all(feature = "enterprise", feature = "openidconnect"))]
async fn gen_pems(db: &DB) -> anyhow::Result<Keys> {
use anyhow::anyhow;
let private_key_cmd = Command::new("openssl")
.arg("genrsa")
.arg("--traditional")
.arg("2048")
.output()
.expect("failed to execute process");
let private_key = String::from_utf8(private_key_cmd.stdout)?;
tracing::debug!("Generated private key: {}", private_key);
if private_key.is_empty() {
return Err(anyhow!("Failed to generate RSA key: key is empty"));
}
let keys = Keys { private_key };
sqlx::query!(
r#"INSERT INTO global_settings (name, value) VALUES ('rsa_keys', $1)"#,
serde_json::to_value(&keys).unwrap()
)
.execute(db)
.await?;
Ok(keys)
}

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::oidc_ee::*;

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::otel_ee::*;

View File

@@ -4,6 +4,7 @@ use crate::error;
use aws_sdk_sts::config::ProvideCredentials;
#[cfg(feature = "parquet")]
use axum::async_trait;
use chrono::{DateTime, Utc};
#[cfg(feature = "parquet")]
use object_store::aws::AwsCredential;
#[cfg(feature = "parquet")]
@@ -17,6 +18,7 @@ use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize};
#[cfg(feature = "parquet")]
use std::sync::{Arc, Mutex};
#[cfg(feature = "parquet")]
use tokio::sync::RwLock;
@@ -46,9 +48,170 @@ use tokio::task;
use windmill_parser_sql::S3ModeFormat;
#[cfg(feature = "parquet")]
lazy_static::lazy_static! {
#[derive(Clone)]
pub struct ExpirableObjectStore {
pub store: Arc<dyn ObjectStore>,
pub refresh: Option<ObjectStoreRefresh>,
}
pub static ref OBJECT_STORE_CACHE_SETTINGS: Arc<RwLock<Option<Arc<dyn ObjectStore>>>> = Arc::new(RwLock::new(None));
#[cfg(feature = "parquet")]
#[derive(Clone)]
pub struct ObjectStoreRefresh {
refresh: Option<DateTime<Utc>>,
settings: ObjectSettings,
}
#[cfg(feature = "parquet")]
impl ObjectStoreRefresh {
pub fn new(settings: ObjectSettings, refresh: Option<DateTime<Utc>>) -> Self {
Self { settings, refresh }
}
fn refresh_needed(&self) -> bool {
if let Some(refresh) = self.refresh {
if refresh < Utc::now() - chrono::Duration::minutes(1) {
return true;
}
}
return false;
}
async fn refresh(&self) -> Option<ExpirableObjectStore> {
return build_object_store_from_settings(self.settings.clone(), None)
.await
.map_err(|e| {
tracing::error!("Error building s3 client from settings: {:?}", e);
e
})
.ok();
}
}
#[cfg(feature = "parquet")]
impl From<Arc<dyn ObjectStore>> for ExpirableObjectStore {
fn from(store: Arc<dyn ObjectStore>) -> Self {
Self { store, refresh: None }
}
}
// #[cfg(feature = "parquet")]
// impl ExpirableObjectStore {
// pub fn new(store: Arc<dyn ObjectStore>, expiration: Option<DateTime<Utc>>) -> Self {
// Self { store, expiration }
// }
// }
#[cfg(feature = "parquet")]
lazy_static::lazy_static! {
pub static ref OBJECT_STORE_SETTINGS: Arc<RwLock<Option<ExpirableObjectStore>>> = Arc::new(RwLock::new(None));
}
#[cfg(feature = "parquet")]
pub async fn get_object_store() -> Option<Arc<dyn ObjectStore>> {
let settings = OBJECT_STORE_SETTINGS.read().await;
if let Some(s) = settings.as_ref() {
match &s.refresh {
Some(refresh) => {
if refresh.refresh_needed() {
let refresh = refresh.clone();
drop(settings);
let new_store = refresh.refresh().await;
if let Some(new_store) = new_store {
let mut s3_cache_settings = OBJECT_STORE_SETTINGS.write().await;
let arc = new_store.store.clone();
*s3_cache_settings = Some(new_store);
return Some(arc);
} else {
return None;
}
} else {
return Some(s.store.clone());
}
}
None => {
return Some(s.store.clone());
}
}
} else {
return None;
}
}
#[cfg(feature = "parquet")]
pub enum ObjectStoreReload {
//if the jwks endpoints are not up yet, we should retry later soon
Later,
Never,
}
#[cfg(feature = "parquet")]
pub async fn reload_object_store_setting(db: &crate::DB) -> ObjectStoreReload {
use crate::{
ee::{get_license_plan, LicensePlan},
global_settings::{load_value_from_global_settings, OBJECT_STORE_CONFIG_SETTING},
s3_helpers::ObjectSettings,
};
let s3_config = load_value_from_global_settings(db, OBJECT_STORE_CONFIG_SETTING).await;
if let Err(e) = s3_config {
tracing::error!("Error reloading s3 cache config: {:?}", e)
} else {
if let Some(v) = s3_config.unwrap() {
if matches!(get_license_plan().await, LicensePlan::Pro) {
tracing::error!("S3 cache is not available for pro plan");
return ObjectStoreReload::Never;
}
let setting = serde_json::from_value::<ObjectSettings>(v);
match setting {
Ok(setting) => {
let is_oidc = matches!(setting, ObjectSettings::AwsOidc(_));
let s3_client = build_object_store_from_settings(setting, Some(db)).await;
match s3_client {
Ok(s3_client) => {
let mut s3_cache_settings = OBJECT_STORE_SETTINGS.write().await;
*s3_cache_settings = Some(s3_client);
}
Err(e) => {
if is_oidc {
tracing::error!("Error building s3 client from oidc settings. It may be due to the jwks endpoints not being up yet, it will be attempted again in 10s to leave time for the server to be ready: {:?}", e);
return ObjectStoreReload::Later;
} else {
tracing::error!("Error building s3 client from settings: {:?}", e);
}
}
}
}
Err(e) => {
tracing::error!("Error parsing s3 cache config: {:?}", e)
}
}
} else {
let mut s3_cache_settings = OBJECT_STORE_SETTINGS.write().await;
if std::env::var("S3_CACHE_BUCKET").is_ok() {
if matches!(get_license_plan().await, LicensePlan::Pro) {
tracing::error!("S3 cache is not available for pro plan");
return ObjectStoreReload::Never;
}
*s3_cache_settings = build_s3_client_from_settings(S3Settings {
bucket: None,
region: None,
access_key: None,
secret_key: None,
endpoint: None,
store_logs: None,
path_style: None,
allow_http: None,
port: None,
})
.await
.ok()
.map(|x| ExpirableObjectStore::from(x))
} else {
*s3_cache_settings = None;
}
}
}
return ObjectStoreReload::Never;
}
#[derive(Serialize, Deserialize, Debug)]
@@ -81,6 +244,15 @@ pub enum ObjectStoreResource {
Azure(AzureBlobResource),
}
impl ObjectStoreResource {
pub fn expiration(&self) -> Option<DateTime<Utc>> {
match self {
ObjectStoreResource::S3(s3_resource) => s3_resource.expiration,
_ => None,
}
}
}
#[derive(Deserialize, Debug)]
pub enum StorageResourceType {
S3,
@@ -104,6 +276,8 @@ pub struct S3Resource {
#[serde(rename = "pathStyle")]
pub path_style: Option<bool>,
pub token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiration: Option<DateTime<Utc>>,
pub port: Option<u16>,
}
@@ -126,7 +300,7 @@ pub struct AzureBlobResource {
pub federated_token_file: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, Hash)]
pub struct S3AwsOidcResource {
#[serde(rename = "bucket")]
pub bucket: String,
@@ -412,6 +586,7 @@ pub enum ObjectStoreSettings {
pub enum ObjectSettings {
S3(S3Settings),
Azure(AzureBlobResource),
AwsOidc(S3AwsOidcResource),
}
impl ObjectSettings {
@@ -419,6 +594,7 @@ impl ObjectSettings {
match self {
ObjectSettings::S3(s3_settings) => s3_settings.bucket.as_ref(),
ObjectSettings::Azure(azure_settings) => Some(&azure_settings.container_name),
ObjectSettings::AwsOidc(s3_aws_oidc_settings) => Some(&s3_aws_oidc_settings.bucket),
}
}
}
@@ -426,12 +602,31 @@ impl ObjectSettings {
#[cfg(feature = "parquet")]
pub async fn build_object_store_from_settings(
settings: ObjectSettings,
) -> error::Result<Arc<dyn ObjectStore>> {
init_private_key: Option<&crate::DB>,
) -> error::Result<ExpirableObjectStore> {
match settings {
ObjectSettings::S3(s3_settings) => build_s3_client_from_settings(s3_settings).await,
ObjectSettings::S3(s3_settings) => build_s3_client_from_settings(s3_settings)
.await
.map(|x| ExpirableObjectStore::from(x)),
ObjectSettings::Azure(azure_settings) => {
let azure_blob_resource = azure_settings;
build_azure_blob_client(&azure_blob_resource)
build_azure_blob_client(&azure_blob_resource).map(|x| ExpirableObjectStore::from(x))
}
ObjectSettings::AwsOidc(ref s3_aws_oidc_settings) => {
let token_generator = crate::job_s3_helpers_ee::TokenGenerator::AsServerInstance();
let res = crate::job_s3_helpers_ee::generate_s3_aws_oidc_resource(
s3_aws_oidc_settings.clone(),
token_generator,
init_private_key,
)
.await?;
build_object_store_client(&res)
.await
.map(|x| ExpirableObjectStore {
store: x,
refresh: Some(ObjectStoreRefresh::new(settings.clone(), res.expiration())),
})
}
}
}
@@ -479,6 +674,7 @@ pub async fn build_s3_client_from_settings(
path_style: settings.path_style,
port: settings.port,
token: None,
expiration: None,
};
build_s3_client(&s3_resource).await
@@ -695,3 +891,20 @@ pub async fn convert_json_line_stream<E: Into<anyhow::Error>>(
Ok(tokio_stream::wrappers::ReceiverStream::new(rx))
}
#[derive(Deserialize, Serialize)]
pub struct DuckdbConnectionSettingsResponse {
pub connection_settings_str: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub azure_container_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s3_bucket: Option<String>,
}
#[derive(Deserialize, Serialize)]
pub struct DuckdbConnectionSettingsQueryV2 {
#[serde(skip_serializing_if = "Option::is_none")]
pub s3_resource_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage: Option<String>,
}

View File

@@ -349,7 +349,7 @@ pub fn should_validate_schema(code: &str, lang: &ScriptLang) -> bool {
let comment = match lang {
Nativets | Bun | Bunnative | Deno | Php | CSharp | Java => "//",
Python3 | Go | Bash | Powershell | Graphql | Ansible | Nu => "#",
Postgresql | Mysql | Bigquery | Snowflake | Mssql | OracleDB => "--",
Postgresql | Mysql | Bigquery | Snowflake | Mssql | OracleDB | DuckDb => "--",
Rust => "//!",
// for related places search: ADD_NEW_LANG
};

View File

@@ -46,13 +46,13 @@ pub enum ScriptLang {
Graphql,
Mssql,
OracleDB,
DuckDb,
Php,
Rust,
Ansible,
CSharp,
Nu,
Java,
// for related places search: ADD_NEW_LANG
Java, // for related places search: ADD_NEW_LANG
}
impl ScriptLang {
@@ -73,6 +73,7 @@ impl ScriptLang {
ScriptLang::Mssql => "mssql",
ScriptLang::Graphql => "graphql",
ScriptLang::OracleDB => "oracledb",
ScriptLang::DuckDb => "duckdb",
ScriptLang::Php => "php",
ScriptLang::Rust => "rust",
ScriptLang::Ansible => "ansible",

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::stats_ee::*;

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::teams_ee::*;

View File

@@ -79,6 +79,7 @@ lazy_static::lazy_static! {
"csharp".to_string(),
"nu".to_string(),
"java".to_string(),
"duckdb".to_string(),
// for related places search: ADD_NEW_LANG
"dependency".to_string(),
"flow".to_string(),
@@ -516,6 +517,7 @@ fn parse_file<T: FromStr>(path: &str) -> Option<T> {
pub struct PythonAnnotations {
pub no_cache: bool,
pub no_postinstall: bool,
pub py_select_latest: bool,
pub skip_result_postprocessing: bool,
pub py310: bool,
pub py311: bool,
@@ -581,11 +583,7 @@ pub async fn load_cache(bin_path: &str, _remote_path: &str, is_dir: bool) -> (bo
(true, format!("loaded from local cache: {}\n", bin_path))
} else {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = crate::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
if let Some(os) = crate::s3_helpers::get_object_store().await {
let started = std::time::Instant::now();
use crate::s3_helpers::attempt_fetch_bytes;
@@ -628,11 +626,7 @@ pub async fn exists_in_cache(bin_path: &str, _remote_path: &str) -> bool {
return true;
} else {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = crate::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
if let Some(os) = crate::s3_helpers::get_object_store().await {
return os
.get(&object_store::path::Path::from(_remote_path))
.await
@@ -650,11 +644,7 @@ pub async fn save_cache(
) -> crate::error::Result<String> {
let mut _cached_to_s3 = false;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = crate::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
if let Some(os) = crate::s3_helpers::get_object_store().await {
use object_store::path::Path;
let file_to_cache = if is_dir {
let tar_path = format!(

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::git_sync_ee::*;

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::completed_runs_ee::*;

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::indexer_ee::*;

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::service_logs_ee::*;

View File

@@ -2123,10 +2123,19 @@ pub struct PulledJob {
pub permissioned_as_folders: Option<Vec<serde_json::Value>>,
}
// NOTE:
// Precomputed by the server
// Used to offload work from agent workers to server
#[derive(Serialize, Deserialize)]
pub enum PrecomputedAgentInfo {
Bun { local: String, remote: String },
Python { py_version: Option<u32>, requirements: Option<String> },
Python {
// V1, not used anymore. Exists for compat.
// TODO: Needs to be removed eventually
py_version: Option<u32>,
py_version_v2: Option<String>,
requirements: Option<String> },
}
#[derive(Serialize, Deserialize)]

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::jobs_ee::*;

View File

@@ -31,6 +31,7 @@ csharp = ["dep:windmill-parser-csharp"]
rust = ["dep:windmill-parser-rust"]
nu = ["dep:windmill-parser-nu"]
java = ["dep:windmill-parser-java"]
duckdb = []
[dependencies]
windmill-queue.workspace = true
@@ -92,6 +93,7 @@ deno_permissions = { workspace = true, optional = true }
deno_io = { workspace = true, optional = true }
deno_error = { workspace = true, optional = true }
async-stream.workspace = true
duckdb.workspace = true
postgres-native-tls.workspace = true
native-tls.workspace = true
@@ -116,6 +118,7 @@ convert_case.workspace = true
yaml-rust.workspace = true
backon.workspace = true
winapi = { workspace = true, optional = true }
pep440_rs.workspace = true
opentelemetry = { workspace = true, optional = true }
bollard = { workspace = true, optional = true }

View File

@@ -30,10 +30,11 @@ use crate::{
start_child_process, transform_json, OccupancyMetrics,
},
handle_child::handle_child,
python_executor::{create_dependencies_dir, handle_python_reqs, uv_pip_compile, PyVersion},
AuthedClient, DISABLE_NSJAIL, DISABLE_NUSER, GIT_PATH, HOME_ENV, NSJAIL_PATH, PATH_ENV,
PROXY_ENVS, PY_INSTALL_DIR, TZ_ENV,
python_executor::{create_dependencies_dir, handle_python_reqs, uv_pip_compile},
PyVAlias, DISABLE_NSJAIL, DISABLE_NUSER, GIT_PATH, HOME_ENV, NSJAIL_PATH, PATH_ENV, PROXY_ENVS,
PY_INSTALL_DIR, TZ_ENV,
};
use windmill_common::client::AuthedClient;
lazy_static::lazy_static! {
static ref ANSIBLE_PLAYBOOK_PATH: String =
@@ -373,7 +374,7 @@ async fn handle_ansible_python_deps(
worker_name,
w_id,
&mut Some(occupancy_metrics),
PyVersion::Py311,
PyVAlias::Py311.into(),
false,
)
.await
@@ -387,10 +388,7 @@ async fn handle_ansible_python_deps(
if requirements.len() > 0 {
let mut venv_path = handle_python_reqs(
requirements
.split("\n")
.filter(|x| !x.starts_with("--"))
.collect(),
crate::python_executor::split_requirements(requirements),
job_id,
w_id,
mem_peak,
@@ -400,7 +398,7 @@ async fn handle_ansible_python_deps(
job_dir,
worker_dir,
&mut Some(occupancy_metrics),
crate::python_executor::PyVersion::Py311,
PyVAlias::default().into(),
)
.await?;
additional_python_paths.append(&mut venv_path);
@@ -1193,7 +1191,7 @@ async fn create_file_resources(
job_dir: &str,
args: Option<&HashMap<String, Box<RawValue>>>,
r: &AnsibleRequirements,
client: &crate::AuthedClient,
client: &AuthedClient,
conn: &Connection,
) -> error::Result<Vec<String>> {
let mut logs = String::new();
@@ -1270,7 +1268,7 @@ async fn create_file_resources(
}
async fn get_resource_or_variable_content(
client: &crate::AuthedClient,
client: &AuthedClient,
path: &ResourceOrVariablePath,
job_id: String,
) -> anyhow::Result<String> {

View File

@@ -43,9 +43,11 @@ use crate::{
OccupancyMetrics,
},
handle_child::handle_child,
AuthedClient, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NSJAIL_PATH, PATH_ENV,
DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NSJAIL_PATH, PATH_ENV,
POWERSHELL_CACHE_DIR, POWERSHELL_PATH, PROXY_ENVS, TZ_ENV,
};
use windmill_common::client::AuthedClient;
#[cfg(windows)]
use crate::SYSTEM_ROOT;
@@ -299,16 +301,28 @@ async fn handle_docker_job(
}
let wait_f = async {
let wait = client
let waited = client
.wait_container::<String>(&container_id, None)
.try_collect::<Vec<_>>()
.await
.map_err(|e| {
.await;
match waited {
Ok(wait) => Ok(wait.first().map(|x| x.status_code)),
Err(bollard::errors::Error::DockerResponseServerError { status_code, message }) => {
append_logs(&job_id, &workspace_id, &format!(": {message}"), conn).await;
Ok(Some(status_code as i64))
}
Err(bollard::errors::Error::DockerContainerWaitError { error, code }) => {
append_logs(&job_id, &workspace_id, &format!("{error}"), conn).await;
Ok(Some(code as i64))
}
Err(e) => {
tracing::error!("Error waiting for container: {:?}", e);
anyhow::anyhow!("Error waiting for container: {:?}", e)
})?;
let waited = wait.first().map(|x| x.status_code);
Ok(waited)
Err(Error::ExecutionErr(format!(
"Error waiting for container: {:?}",
e
)))
}
}
};
let ncontainer_id = container_id.to_string();
@@ -317,7 +331,7 @@ async fn handle_docker_job(
let conn2 = conn.clone();
let worker_name2 = worker_name.to_string();
let (tx, mut rx) = tokio::sync::broadcast::channel::<()>(1);
let workspace_id2 = workspace_id.to_string();
let mut killpill_rx = killpill_rx.resubscribe();
let logs = tokio::spawn(async move {
let client = bollard::Docker::connect_with_unix_defaults().map_err(to_anyhow);
@@ -332,6 +346,13 @@ async fn handle_docker_job(
..Default::default()
}),
);
append_logs(
&job_id,
&workspace_id2,
"\ndocker logs stream started\n",
&conn2,
)
.await;
loop {
tokio::select! {
log = log_stream.next() => {
@@ -441,11 +462,14 @@ async fn handle_docker_job(
let result = result.unwrap();
if result.is_some_and(|x| x > 0) {
return Err(Error::ExecutionErr(format!(
"Docker job completed with unsuccessful exit status: {}",
result.unwrap()
)));
}
return Ok(to_raw_value(&json!(format!(
"Docker exit status: {}",
result
.map(|x| x.to_string())
.unwrap_or_else(|| "none".to_string())
"Docker job completed with success exit status"
))));
}

View File

@@ -4,6 +4,7 @@ use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt};
use reqwest::Client;
use serde_json::{json, value::RawValue, Value};
use windmill_common::client::AuthedClient;
use windmill_common::error::to_anyhow;
use windmill_common::s3_helpers::convert_json_line_stream;
use windmill_common::worker::Connection;
@@ -16,15 +17,12 @@ use windmill_queue::CanceledBy;
use serde::Deserialize;
use crate::common::{build_args_values, resolve_job_timeout};
use crate::common::{
build_http_client, s3_mode_args_to_worker_data, OccupancyMetrics, S3ModeWorkerData,
};
use crate::handle_child::run_future_with_polling_update_job_poller;
use crate::sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args;
use crate::{
common::{build_args_values, resolve_job_timeout},
AuthedClient,
};
use gcp_auth::{AuthenticationManager, CustomServiceAccount};

View File

@@ -20,10 +20,11 @@ use crate::{
read_file_content, read_result, start_child_process, write_file_binary, OccupancyMetrics,
},
handle_child::handle_child,
AuthedClient, BUNFIG_INSTALL_SCOPES, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, BUN_PATH,
DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NODE_BIN_PATH, NODE_PATH, NPM_CONFIG_REGISTRY,
NPM_PATH, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, TZ_ENV,
BUNFIG_INSTALL_SCOPES, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, BUN_PATH, DISABLE_NSJAIL,
DISABLE_NUSER, HOME_ENV, NODE_BIN_PATH, NODE_PATH, NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH,
PATH_ENV, PROXY_ENVS, TZ_ENV,
};
use windmill_common::client::AuthedClient;
#[cfg(windows)]
use crate::SYSTEM_ROOT;
@@ -612,10 +613,7 @@ pub async fn pull_codebase(w_id: &str, id: &str, job_dir: &str) -> Result<()> {
extract_saved_codebase(job_dir, &bun_cache_path, is_tar, &dst, false)?;
} else {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let object_store = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone();
let object_store = windmill_common::s3_helpers::get_object_store().await;
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
let object_store: Option<()> = None;

View File

@@ -44,10 +44,8 @@ use windmill_common::{variables, DB};
use tokio::{io::AsyncWriteExt, process::Child, time::Instant};
use crate::agent_workers::UPDATE_PING_URL;
use crate::{
AuthedClient, DISABLE_NSJAIL, JOB_DEFAULT_TIMEOUT, MAX_RESULT_SIZE, MAX_TIMEOUT_DURATION,
PATH_ENV,
};
use crate::{DISABLE_NSJAIL, JOB_DEFAULT_TIMEOUT, MAX_RESULT_SIZE, MAX_TIMEOUT_DURATION, PATH_ENV};
use windmill_common::client::AuthedClient;
pub async fn build_args_map<'a>(
job: &'a MiniPulledJob,
@@ -782,19 +780,17 @@ async fn get_workspace_s3_resource_path(
}
};
let client2 = client.clone();
let token_fn = |audience: String| async move {
client2
.get_id_token(&audience)
.await
.map_err(|e| windmill_common::error::Error::from(e))
};
let s3_resource_value_raw = client
.get_resource_value::<serde_json::Value>(path.as_str())
.await?;
get_s3_resource_internal(rt, s3_resource_value_raw, token_fn)
.await
.map(Some)
get_s3_resource_internal(
rt,
s3_resource_value_raw,
windmill_common::job_s3_helpers_ee::TokenGenerator::AsClient(client),
db,
)
.await
.map(Some)
}
#[cfg(feature = "parquet")]
@@ -1109,7 +1105,7 @@ pub async fn par_install_language_dependencies<'a>(
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
if windmill_common::s3_helpers::OBJECT_STORE_SETTINGS
.read()
.await
.is_none()
@@ -1264,11 +1260,7 @@ pub async fn par_install_language_dependencies<'a>(
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let s3_pull_future = if is_not_pro {
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
Some(crate::global_cache::pull_from_tar(
os,
path.clone(),
@@ -1449,11 +1441,7 @@ pub async fn par_install_language_dependencies<'a>(
};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
{
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
tokio::spawn(async move {
if let Err(e) = crate::global_cache::build_tar_and_push(
os,
@@ -1541,11 +1529,7 @@ pub async fn par_install_language_dependencies<'a>(
};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
{
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
.read()
.await
.clone()
{
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
let language_name = language_name.to_owned();
tokio::spawn(async move {
if let Err(e) = crate::global_cache::build_tar_and_push(
@@ -1591,7 +1575,7 @@ pub struct S3ModeWorkerData {
}
impl S3ModeWorkerData {
pub async fn upload<S>(&self, stream: S) -> error::Result<()>
pub async fn upload<S>(&self, stream: S) -> anyhow::Result<()>
where
S: futures::stream::TryStream + Send + 'static,
S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,

View File

@@ -36,7 +36,7 @@ use crate::{
};
use crate::common::OccupancyMetrics;
use crate::AuthedClient;
use windmill_common::client::AuthedClient;
#[cfg(windows)]
use crate::SYSTEM_ROOT;

View File

@@ -11,9 +11,11 @@ use crate::{
start_child_process, OccupancyMetrics,
},
handle_child::handle_child,
AuthedClient, DENO_CACHE_DIR, DENO_PATH, DISABLE_NSJAIL, HOME_ENV, NPM_CONFIG_REGISTRY,
DENO_CACHE_DIR, DENO_PATH, DISABLE_NSJAIL, HOME_ENV, NPM_CONFIG_REGISTRY,
PATH_ENV, TZ_ENV,
};
use windmill_common::client::AuthedClient;
use tokio::{fs::File, io::AsyncReadExt, process::Command};
use windmill_common::{error::Result, worker::write_file, BASE_URL};
use windmill_common::{

View File

@@ -0,0 +1,664 @@
use std::collections::HashMap;
use std::env;
use duckdb::types::TimeUnit;
use duckdb::{params_from_iter, Row};
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
use serde_json::value::RawValue;
use serde_json::{json, Value};
use tokio::fs::remove_file;
use tokio::task;
use uuid::Uuid;
use windmill_common::error::{to_anyhow, Error, Result};
use windmill_common::s3_helpers::{
DuckdbConnectionSettingsQueryV2, DuckdbConnectionSettingsResponse, S3Object,
};
use windmill_common::worker::{to_raw_value, Connection};
use windmill_parser_sql::{parse_duckdb_sig, parse_sql_blocks};
use windmill_queue::{CanceledBy, MiniPulledJob};
use crate::common::{build_args_values, OccupancyMetrics};
use crate::handle_child::run_future_with_polling_update_job_poller;
#[cfg(feature = "mysql")]
use crate::mysql_executor::MysqlDatabase;
use crate::pg_executor::PgDatabase;
use crate::sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args;
use windmill_common::client::AuthedClient;
fn do_duckdb_inner(
conn: &duckdb::Connection,
query: &str,
job_args: &HashMap<String, duckdb::types::Value>,
skip_collect: bool,
column_order: &mut Option<Vec<String>>,
) -> Result<Box<RawValue>> {
let mut rows_vec = vec![];
let (query, job_args) = interpolate_named_args(query, &job_args);
let mut stmt = conn
.prepare(&query)
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let mut rows = stmt
.query(params_from_iter(job_args))
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
if skip_collect {
return Ok(to_raw_value(&json!([])));
}
// Statement needs to be stepped at least once or stmt.column_names() will panic
let mut column_names = None;
loop {
let row = rows.next();
match row {
Ok(Some(row)) => {
// Set column names if not already set
let stmt = row.as_ref();
let column_names = match column_names.as_ref() {
Some(column_names) => column_names,
None => {
column_names = Some(stmt.column_names());
column_names.as_ref().unwrap()
}
};
let row = row_to_value(row, &column_names.as_slice())
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
rows_vec.push(row);
}
Ok(None) => break,
Err(e) => {
return Err(Error::ExecutionErr(e.to_string()));
}
}
}
if let (Some(column_order), Some(column_names)) = (column_order.as_mut(), column_names) {
*column_order = column_names.clone();
}
return Ok(to_raw_value(&rows_vec));
}
pub async fn do_duckdb(
job: &MiniPulledJob,
client: &AuthedClient,
query: &str,
conn: &Connection,
mem_peak: &mut i32,
canceled_by: &mut Option<CanceledBy>,
worker_name: &str,
column_order_ref: &mut Option<Vec<String>>,
occupancy_metrics: &mut OccupancyMetrics,
) -> Result<Box<RawValue>> {
let result_f = async {
let sig = parse_duckdb_sig(query)?.args;
let mut job_args = build_args_values(job, client, conn).await?;
let (query, _) = &sanitize_and_interpolate_unsafe_sql_args(query, &sig, &job_args)?;
// Prevent interpolate_named_args from detecting argument identifiers in the signature for
// the first query block
let query = trunc_sig(query);
let (_query_with_transformed_s3_uris, mut used_storages) =
transform_s3_uris(query, client).await?;
let query = _query_with_transformed_s3_uris.as_deref().unwrap_or(query);
let job_args = {
let mut m: HashMap<String, duckdb::types::Value> = HashMap::new();
for sig_arg in sig.into_iter() {
let json_value = job_args
.remove(&sig_arg.name)
.or_else(|| sig_arg.default)
.unwrap_or_else(|| json!(null));
if matches!(&sig_arg.otyp.as_ref().map(String::as_str), Some("s3object")) {
let s3_obj = serde_json::from_value::<S3Object>(json_value).map_err(|e| {
Error::ExecutionErr(format!("Failed to deserialize S3Object: {}", e))
})?;
let duckdb_conn_settings: windmill_common::s3_helpers::DuckdbConnectionSettingsResponse = client
.get_duckdb_connection_settings(&DuckdbConnectionSettingsQueryV2 {
s3_resource_path: None,
storage: s3_obj.storage.clone(),
})
.await?;
let uri = match (
&duckdb_conn_settings.s3_bucket,
&duckdb_conn_settings.azure_container_path,
) {
(Some(s3_bucket), None) => format!("s3://{}/{}", s3_bucket, &s3_obj.s3),
(None, Some(az_container)) => format!("{}/{}", az_container, &s3_obj.s3),
_ => {
return Err(Error::ExecutionErr(
"S3Object must have either s3_bucket or azure_container_path"
.to_string(),
));
}
};
m.insert(sig_arg.name, duckdb::types::Value::Text(uri));
used_storages.insert(s3_obj.storage, duckdb_conn_settings);
} else {
let duckdb_value = json_value_to_duckdb_value(
&json_value,
sig_arg
.otyp
.clone()
.unwrap_or_else(|| "text".to_string())
.as_str(),
client,
)?;
m.insert(sig_arg.name, duckdb_value);
}
}
m
};
let query_block_list = parse_sql_blocks(query);
// Replace windmill resource ATTACH statements with the real instructions
let query_block_list = {
let mut v = vec![];
for query_block in query_block_list.iter() {
match parse_attach_db_resource(query_block) {
Some(parsed) => v.extend(
transform_attach_db_resource_query(&parsed, &job.id, client).await?,
),
None => v.push(query_block.to_string()),
};
}
v
};
// duckdb::Connection is not Send so we do it in a single blocking task
let (result, column_order) = task::spawn_blocking(move || {
let conn = duckdb::Connection::open_in_memory()
.map_err(|e| Error::ConnectingToDatabase(e.to_string()))?;
for (_, DuckdbConnectionSettingsResponse { connection_settings_str, .. }) in
used_storages.into_iter()
{
conn.execute_batch(&connection_settings_str)
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
}
let mut result: Option<Box<RawValue>> = None;
let mut column_order = None;
for (query_block_index, query_block) in query_block_list.iter().enumerate() {
result = Some(
do_duckdb_inner(
&conn,
query_block.as_str(),
&job_args,
query_block_index != query_block_list.len() - 1,
&mut column_order,
)
.map_err(|e| Error::ExecutionErr(e.to_string()))?,
);
}
let result = result.unwrap_or_else(|| to_raw_value(&json!([])));
Ok::<_, Error>((result, column_order))
})
.await
.map_err(to_anyhow)??;
*column_order_ref = column_order;
// BigQuery cleanup
let bq_credentials_path = make_bq_credentials_path(&job.id);
env::remove_var("GOOGLE_APPLICATION_CREDENTIALS");
if matches!(tokio::fs::try_exists(&bq_credentials_path).await, Ok(true)) {
remove_file(&bq_credentials_path).await.map_err(to_anyhow)?;
}
Ok(result)
};
let result = run_future_with_polling_update_job_poller(
job.id,
job.timeout,
conn,
mem_peak,
canceled_by,
result_f,
worker_name,
&job.workspace_id,
&mut Some(occupancy_metrics),
Box::pin(futures::stream::once(async { 0 })),
)
.await?;
Ok(result)
}
fn row_to_value(row: &Row<'_>, column_names: &[String]) -> Result<Box<RawValue>> {
let mut obj = serde_json::Map::new();
for (i, key) in column_names.iter().enumerate() {
let value: duckdb::types::Value =
row.get(i).map_err(|e| Error::ExecutionErr(e.to_string()))?;
let json_value = match value {
duckdb::types::Value::Null => serde_json::Value::Null,
duckdb::types::Value::Boolean(b) => serde_json::Value::Bool(b),
duckdb::types::Value::TinyInt(i) => serde_json::Value::Number(i.into()),
duckdb::types::Value::SmallInt(i) => serde_json::Value::Number(i.into()),
duckdb::types::Value::Int(i) => serde_json::Value::Number(i.into()),
duckdb::types::Value::BigInt(i) => serde_json::Value::Number(i.into()),
duckdb::types::Value::HugeInt(i) => serde_json::Value::String(i.to_string()),
duckdb::types::Value::UTinyInt(u) => serde_json::Value::Number(u.into()),
duckdb::types::Value::USmallInt(u) => serde_json::Value::Number(u.into()),
duckdb::types::Value::UInt(u) => serde_json::Value::Number(u.into()),
duckdb::types::Value::UBigInt(u) => serde_json::Value::Number(u.into()),
duckdb::types::Value::Float(f) => serde_json::Value::Number(
serde_json::Number::from_f64(f as f64)
.ok_or_else(|| Error::ExecutionErr("Could not convert to f64".to_string()))?,
),
duckdb::types::Value::Double(f) => serde_json::Value::Number(
serde_json::Number::from_f64(f)
.ok_or_else(|| Error::ExecutionErr("Could not convert to f64".to_string()))?,
),
duckdb::types::Value::Decimal(d) => serde_json::Value::String(d.to_string()),
duckdb::types::Value::Timestamp(_, ts) => serde_json::Value::String(ts.to_string()),
duckdb::types::Value::Text(s) => serde_json::Value::String(s),
duckdb::types::Value::Blob(b) => serde_json::Value::Array(
b.into_iter()
.map(|byte| serde_json::Value::Number(byte.into()))
.collect(),
),
duckdb::types::Value::Date32(d) => serde_json::Value::Number(d.into()),
duckdb::types::Value::Time64(_, t) => serde_json::Value::String(t.to_string()),
duckdb::types::Value::Interval { months, days, nanos } => serde_json::json!({
"months": months,
"days": days,
"nanos": nanos
}),
duckdb::types::Value::List(values) => serde_json::Value::Array(
values
.into_iter()
.map(|v| serde_json::Value::String(format!("{:?}", v)))
.collect(),
),
duckdb::types::Value::Enum(e) => serde_json::Value::String(e),
duckdb::types::Value::Struct(fields) => serde_json::Value::Object(
fields
.iter()
.map(|(k, v)| (k.clone(), serde_json::Value::String(format!("{:?}", v))))
.collect(),
),
duckdb::types::Value::Array(values) => serde_json::Value::Array(
values
.into_iter()
.map(|v| serde_json::Value::String(format!("{:?}", v)))
.collect(),
),
duckdb::types::Value::Map(map) => serde_json::Value::Object(
map.iter()
.map(|(k, v)| {
(
format!("{:?}", k),
serde_json::Value::String(format!("{:?}", v)),
)
})
.collect(),
),
duckdb::types::Value::Union(value) => {
serde_json::Value::String(format!("{:?}", *value))
}
};
obj.insert(key.clone(), json_value);
}
serde_json::value::to_raw_value(&obj).map_err(|e| e.into())
}
fn json_value_to_duckdb_value(
json_value: &serde_json::Value,
arg_type: &str,
client: &AuthedClient,
) -> Result<duckdb::types::Value> {
let arg_type = arg_type.to_lowercase();
let duckdb_value = match json_value {
serde_json::Value::Null => duckdb::types::Value::Null,
serde_json::Value::Bool(b) => duckdb::types::Value::Boolean(*b),
serde_json::Value::String(s)
if matches!(
arg_type.as_str(),
"timestamp" | "timestamptz" | "timestamp with time zone" | "datetime"
) =>
{
string_to_duckdb_timestamp(&s)?
}
serde_json::Value::String(s) if arg_type.as_str() == "date" => string_to_duckdb_date(&s)?,
serde_json::Value::String(s) if arg_type.as_str() == "time" => string_to_duckdb_time(&s)?,
serde_json::Value::String(s) => duckdb::types::Value::Text(s.clone()),
serde_json::Value::Number(n) if n.is_i64() => {
let v = n.as_i64().unwrap();
match arg_type.as_str() {
"tinyint" | "int1" => duckdb::types::Value::TinyInt(v as i8),
"smallint" | "int2" | "short" => duckdb::types::Value::SmallInt(v as i16),
"integer" | "int4" | "int" | "signed" => duckdb::types::Value::Int(v as i32),
"bigint" | "int8" | "long" => duckdb::types::Value::BigInt(v),
"hugeint" => duckdb::types::Value::HugeInt(v as i128),
"float" | "float4" | "real" => duckdb::types::Value::Float(v as f32),
"double" | "float8" => duckdb::types::Value::Double(v as f64),
_ => duckdb::types::Value::BigInt(v), // default fallback
}
}
serde_json::Value::Number(n) if n.is_u64() => {
let v = n.as_u64().unwrap();
match arg_type.as_str() {
"utinyint" => duckdb::types::Value::UTinyInt(v as u8),
"usmallint" => duckdb::types::Value::USmallInt(v as u16),
"uinteger" => duckdb::types::Value::UInt(v as u32),
"ubigint" | "uhugeint" => duckdb::types::Value::UBigInt(v),
_ => duckdb::types::Value::UBigInt(v), // default fallback
}
}
serde_json::Value::Number(n) if n.is_f64() => {
let v = n.as_f64().unwrap();
match arg_type.as_str() {
"float" | "float4" | "real" => duckdb::types::Value::Float(v as f32),
"double" | "float8" => duckdb::types::Value::Double(v),
"decimal" | "numeric" => {
duckdb::types::Value::Decimal(Decimal::from_f64(v).ok_or_else(|| {
Error::ExecutionErr("Could not convert f64 to Decimal".to_string())
})?)
}
_ => duckdb::types::Value::Double(v), // default fallback
}
}
serde_json::Value::Array(arr) => duckdb::types::Value::Array(
arr.iter()
.map(|val| json_value_to_duckdb_value(val, arg_type.as_str(), client))
.collect::<Result<Vec<_>>>()?,
),
serde_json::Value::Object(map) => duckdb::types::Value::Struct(
map.iter()
.map(|(k, v)| {
Ok::<_, Error>((
k.clone(),
json_value_to_duckdb_value(v, arg_type.as_str(), client)?,
))
})
.collect::<Result<Vec<_>>>()?
.into(),
),
value @ _ => {
return Err(Error::ExecutionErr(format!(
"Unsupported type in query: {:?} and signature {arg_type:?}",
value
)))
}
};
Ok(duckdb_value)
}
fn string_to_duckdb_timestamp(s: &str) -> Result<duckdb::types::Value> {
let ts = chrono::DateTime::parse_from_rfc3339(s)
.map_err(|e: chrono::ParseError| Error::ExecutionErr(e.to_string()))?;
Ok(duckdb::types::Value::Timestamp(
TimeUnit::Millisecond,
ts.timestamp_millis(),
))
}
fn string_to_duckdb_date(s: &str) -> Result<duckdb::types::Value> {
use chrono::Datelike;
let date = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap();
Ok(duckdb::types::Value::Date32(date.num_days_from_ce()))
}
fn string_to_duckdb_time(s: &str) -> Result<duckdb::types::Value> {
use chrono::Timelike;
let time = chrono::NaiveTime::parse_from_str(s, "%H:%M:%S").unwrap();
Ok(duckdb::types::Value::Time64(
TimeUnit::Microsecond,
time.num_seconds_from_midnight() as i64,
))
}
struct ParsedAttachDbResource<'a> {
resource_path: &'a str,
name: &'a str,
db_type: &'a str,
extra_args: Option<&'a str>,
}
fn parse_attach_db_resource<'a>(query: &'a str) -> Option<ParsedAttachDbResource<'a>> {
lazy_static::lazy_static! {
static ref RE: regex::Regex = regex::Regex::new(r"ATTACH '\$res:([^']+)' AS (\S+) \(TYPE (\w+)(.*)\)").unwrap();
}
for cap in RE.captures_iter(query) {
if let (Some(resource_path), Some(name), Some(db_type)) =
(cap.get(1), cap.get(2), cap.get(3))
{
let extra_args = cap.get(4).map(|m| query[m.start()..m.end()].trim());
return Some(ParsedAttachDbResource {
resource_path: query[resource_path.start()..resource_path.end()].trim(),
name: query[name.start()..name.end()].trim(),
db_type: query[db_type.start()..db_type.end()].trim(),
extra_args,
});
}
}
None
}
async fn transform_attach_db_resource_query(
parsed: &ParsedAttachDbResource<'_>,
job_id: &Uuid,
client: &AuthedClient,
) -> Result<Vec<String>> {
match parsed.db_type.to_lowercase().as_str() {
"postgres" => {
let resource: PgDatabase = client
.get_resource_value_interpolated(parsed.resource_path, Some(job_id.to_string()))
.await?;
let attach_str = format!(
"ATTACH 'dbname={} {} host={} {} {}' AS {} (TYPE postgres{});",
resource.dbname,
resource
.user
.map(|u| format!("user={}", u))
.unwrap_or_default(),
resource.host,
resource
.password
.map(|p| format!("password={}", p))
.unwrap_or_default(),
resource
.port
.map(|p| format!("port={}", p))
.unwrap_or_default(),
parsed.name,
parsed.extra_args.unwrap_or("")
);
Ok(vec![
"INSTALL postgres;".to_string(),
"LOAD postgres;".to_string(),
attach_str,
])
}
"mysql" => {
#[cfg(not(feature = "mysql"))]
return Err(Error::ExecutionErr(
"MySQL feature is not enabled".to_string(),
));
#[cfg(feature = "mysql")]
{
let resource: MysqlDatabase = client
.get_resource_value_interpolated(parsed.resource_path, Some(job_id.to_string()))
.await?;
let attach_str = format!(
"ATTACH 'database={} host={} ssl_mode={} {} {} {}' AS {} (TYPE mysql{});",
resource.database,
resource.host,
resource
.ssl
.map(|ssl| if ssl { "required" } else { "disabled" })
.unwrap_or("preferred"),
resource
.password
.map(|p| format!("password={}", p))
.unwrap_or_default(),
resource
.port
.map(|p| format!("port={}", p))
.unwrap_or_default(),
resource
.user
.map(|u| format!("user={}", u))
.unwrap_or_default(),
parsed.name,
parsed.extra_args.unwrap_or("")
);
Ok(vec![
"INSTALL mysql;".to_string(),
"LOAD mysql;".to_string(),
attach_str,
])
}
}
"bigquery" => {
let resource: Value = client
.get_resource_value_interpolated(parsed.resource_path, Some(job_id.to_string()))
.await?;
// duckdb's bigquery extension requires a json file as credentials
let bq_credentials_path = make_bq_credentials_path(job_id);
env::set_var("GOOGLE_APPLICATION_CREDENTIALS", &bq_credentials_path);
tokio::fs::write(&bq_credentials_path, resource.to_string())
.await
.map_err(|e| {
Error::ExecutionErr(format!(
"Failed to write BigQuery credentials to {}: {}",
&bq_credentials_path, e
))
})?;
let project_id: String = serde_json::from_value(
resource
.get("project_id")
.ok_or_else(|| {
Error::ExecutionErr("BigQuery resource must contain project_id".to_string())
})?
.to_owned(),
)
.map_err(|_e| Error::ExecutionErr("failed project_id deserialize".to_string()))?;
let attach_str = format!(
"ATTACH 'project={}' as {} (TYPE bigquery{});",
project_id,
parsed.name,
parsed.extra_args.unwrap_or("")
)
.to_string();
Ok(vec![
"INSTALL bigquery FROM community;".to_string(),
"LOAD bigquery;".to_string(),
attach_str,
])
}
_ => Err(Error::ExecutionErr(format!(
"Unsupported db type in DuckDB ATTACH: {}",
parsed.db_type
))),
}
}
// Returns the transformed query and the set of storages used
async fn transform_s3_uris(
query: &str,
client: &AuthedClient,
) -> Result<(
Option<String>,
HashMap<Option<String>, DuckdbConnectionSettingsResponse>,
)> {
let mut transformed_query = None;
lazy_static::lazy_static! {
static ref RE: regex::Regex = regex::Regex::new(r"'s3://([^'/]*)/([^']+)'").unwrap();
}
let mut used_storages = HashMap::new();
for cap in RE.captures_iter(query) {
if let (storage, Some(s3_path)) = (cap.get(1), cap.get(2)) {
let s3_path = s3_path.as_str();
let storage = match storage.map(|m| m.as_str()) {
Some("") | None => None,
Some(s) => Some(s.to_string()),
};
let original_str_lit =
format!("'s3://{}/{}'", storage.as_deref().unwrap_or(""), s3_path);
let duckdb_conn_settings = client
.get_duckdb_connection_settings(&DuckdbConnectionSettingsQueryV2 {
s3_resource_path: None,
storage: storage.clone(),
})
.await?;
let url = match &duckdb_conn_settings {
DuckdbConnectionSettingsResponse { s3_bucket: Some(bucket), .. } => {
format!("'s3://{bucket}/{s3_path}'")
}
DuckdbConnectionSettingsResponse { azure_container_path: Some(base), .. } => {
format!("'{base}/{s3_path}'")
}
_ => {
return Err(Error::ExecutionErr(
"DuckDB connection settings response must have either s3_bucket or azure_container_path".to_string(),
))?;
}
};
transformed_query = Some(
transformed_query
.unwrap_or(query.to_string())
.replace(&original_str_lit, &url),
);
used_storages.insert(storage, duckdb_conn_settings);
}
}
Ok((transformed_query, used_storages))
}
// BigQuery extension requires a json file as credentials
// The file path is set as an env var by do_duckdb
// It is created by transform_attach_db_resource_query (when bigquery is detected)
// and deleted by do_duckdb after the query is executed
fn make_bq_credentials_path(job_id: &Uuid) -> String {
format!("/tmp/service-account-credentials-{}.json", job_id)
}
// duckdb-rs does not support named parameters,
// and it raises an error when passing unused arguments. We cannot prepare batch statements
// but only single SQL statements so it doesn't work when all arguments are not used by
// every single statement.
fn interpolate_named_args<'a>(
query: &str,
args: &'a HashMap<String, duckdb::types::Value>,
) -> (String, Vec<&'a duckdb::types::Value>) {
let mut query = query.to_string();
let mut values = vec![];
for (arg_name, arg_value) in args {
let pat = format!("${}", arg_name);
if !query.contains(&pat) {
continue;
}
values.push(arg_value);
query = query.replace(&pat, &format!("${}", values.len()));
}
(query, values)
}
fn trunc_sig(query: &str) -> &str {
let idx = query.rfind("-- $").unwrap_or(query.len());
// find next \n starting from idx and return everything after it
let idx = query[idx..].find('\n').map(|i| i + idx).unwrap_or(0);
&query[idx..]
}

View File

@@ -22,6 +22,7 @@ pub async fn build_tar_and_push(
platform_agnostic: bool,
) -> error::Result<()> {
use object_store::path::Path;
use tokio::fs::create_dir_all;
use crate::TAR_PYBASE_CACHE_DIR;
@@ -36,7 +37,9 @@ pub async fn build_tar_and_push(
};
let prefix = &format!("{TAR_PYBASE_CACHE_DIR}/{}", lang);
let tar_path = format!("{prefix}/{folder_name}_tar.tar",);
let tar_path = format!("{prefix}/{folder_name}_tar.tar");
create_dir_all(prefix).await?;
let tar_file = std::fs::File::create(&tar_path)?;
let mut tar = tar::Builder::new(tar_file);

View File

@@ -19,9 +19,10 @@ use crate::{
start_child_process, OccupancyMetrics,
},
handle_child::handle_child,
AuthedClient, DISABLE_NSJAIL, DISABLE_NUSER, GOPRIVATE, GOPROXY, GO_BIN_CACHE_DIR,
GO_CACHE_DIR, HOME_ENV, NSJAIL_PATH, PATH_ENV, TZ_ENV,
DISABLE_NSJAIL, DISABLE_NUSER, GOPRIVATE, GOPROXY, GO_BIN_CACHE_DIR, GO_CACHE_DIR, HOME_ENV,
NSJAIL_PATH, PATH_ENV, TZ_ENV,
};
use windmill_common::client::AuthedClient;
const GO_REQ_SPLITTER: &str = "//go.sum\n";
const NSJAIL_CONFIG_RUN_GO_CONTENT: &str = include_str!("../nsjail/run.go.config.proto");
@@ -473,7 +474,7 @@ pub async fn install_go_dependencies(
if non_dep_job {
if let Some(db) = conn.as_sql() {
sqlx::query!(
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('5 mins')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
hash,
req_content
)

View File

@@ -12,7 +12,8 @@ use serde::Deserialize;
use crate::common::{build_http_client, resolve_job_timeout, OccupancyMetrics};
use crate::handle_child::run_future_with_polling_update_job_poller;
use crate::{common::build_args_map, AuthedClient};
use crate::common::build_args_map;
use windmill_common::client::AuthedClient;
#[derive(Deserialize)]
struct GraphqlApi {

View File

@@ -24,9 +24,11 @@ use crate::{
create_args_and_out_file, get_reserved_variables, par_install_language_dependencies,
read_result, start_child_process, OccupancyMetrics, RequiredDependency,
},
handle_child, AuthedClient, COURSIER_CACHE_DIR, DISABLE_NSJAIL, DISABLE_NUSER, JAVA_CACHE_DIR,
handle_child, COURSIER_CACHE_DIR, DISABLE_NSJAIL, DISABLE_NUSER, JAVA_CACHE_DIR,
JAVA_REPOSITORY_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN, NSJAIL_PATH, PATH_ENV, PROXY_ENVS,
};
use windmill_common::client::AuthedClient;
lazy_static::lazy_static! {
static ref JAVA_CONCURRENT_DOWNLOADS: usize = std::env::var("JAVA_CONCURRENT_DOWNLOADS").ok().map(|flag| flag.parse().unwrap_or(20)).unwrap_or(20);
static ref JAVA_PATH: String = std::env::var("JAVA_PATH").unwrap_or_else(|_| "/usr/bin/java".to_string());
@@ -243,7 +245,7 @@ pub async fn resolve<'a>(
if let Connection::Sql(db) = conn {
sqlx::query!(
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('5 mins')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
req_hash,
lock.clone(),
)

View File

@@ -0,0 +1,10 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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.
*/
// Re-export all items from the EE module
pub use crate::job_logger_ee::*;

View File

@@ -48,7 +48,8 @@ use windmill_common::worker::{write_file, TMP_DIR};
use windmill_common::flow_status::JobResult;
use windmill_queue::CanceledBy;
use crate::{common::OccupancyMetrics, AuthedClient};
use crate::common::OccupancyMetrics;
use windmill_common::client::AuthedClient;
#[cfg(feature = "deno_core")]
use crate::{common::unsafe_raw, handle_child::run_future_with_polling_update_job_poller};

View File

@@ -20,6 +20,8 @@ mod csharp_executor;
#[cfg(feature = "enterprise")]
mod dedicated_worker;
mod deno_executor;
#[cfg(feature = "duckdb")]
mod duckdb_executor;
mod global_cache;
mod go_executor;
mod graphql_executor;
@@ -39,6 +41,8 @@ mod pg_executor;
mod php_executor;
#[cfg(feature = "python")]
mod python_executor;
#[cfg(feature = "python")]
mod python_versions;
pub mod result_processor;
#[cfg(feature = "rust")]
mod rust_executor;
@@ -60,3 +64,6 @@ pub use bun_executor::{
prebundle_bun_script, prepare_job_dir,
};
pub use deno_executor::generate_deno_lock;
#[cfg(feature = "python")]
pub use python_versions::{PyV, PyVAlias};

View File

@@ -22,7 +22,7 @@ use windmill_queue::{append_logs, CanceledBy};
use crate::common::{build_args_values, s3_mode_args_to_worker_data, OccupancyMetrics};
use crate::handle_child::run_future_with_polling_update_job_poller;
use crate::sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args;
use crate::AuthedClient;
use windmill_common::client::AuthedClient;
use serde::Deserializer;

View File

@@ -13,6 +13,7 @@ use serde_json::{json, value::RawValue, Value};
use std::str::FromStr;
use tokio::sync::Mutex;
use windmill_common::{
client::AuthedClient,
error::{to_anyhow, Error},
s3_helpers::convert_json_line_stream,
worker::{to_raw_value, Connection},
@@ -28,17 +29,16 @@ use crate::{
common::{build_args_values, s3_mode_args_to_worker_data, OccupancyMetrics, S3ModeWorkerData},
handle_child::run_future_with_polling_update_job_poller,
sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args,
AuthedClient,
};
#[derive(Deserialize)]
struct MysqlDatabase {
host: String,
user: Option<String>,
password: Option<String>,
port: Option<u16>,
database: String,
ssl: Option<bool>,
pub struct MysqlDatabase {
pub host: String,
pub user: Option<String>,
pub password: Option<String>,
pub port: Option<u16>,
pub database: String,
pub ssl: Option<bool>,
}
fn do_mysql_inner<'a>(

View File

@@ -16,8 +16,10 @@ use crate::{
create_args_and_out_file, get_reserved_variables, read_result, start_child_process,
OccupancyMetrics,
},
handle_child, AuthedClient, DISABLE_NSJAIL, DISABLE_NUSER, NSJAIL_PATH, PATH_ENV, PROXY_ENVS,
handle_child, DISABLE_NSJAIL, DISABLE_NUSER, NSJAIL_PATH, PATH_ENV, PROXY_ENVS,
};
use windmill_common::client::AuthedClient;
const NSJAIL_CONFIG_RUN_NU_CONTENT: &str = include_str!("../nsjail/run.nu.config.proto");
lazy_static::lazy_static! {

View File

@@ -27,9 +27,9 @@ use crate::{
OccupancyMetrics, S3ModeWorkerData,
},
handle_child::run_future_with_polling_update_job_poller,
sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args,
AuthedClient,
sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args
};
use windmill_common::client::AuthedClient;
#[derive(Deserialize)]
struct OracleDatabase {

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