mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
fix(frontend): curl fallback for +Variable/+Resource in bash sandbox mode (#10235)
* fix(frontend): use curl fallback for +Variable/+Resource in bash sandbox mode When a bash script uses `# sandbox <image>` or `# docker`, the body runs inside a custom container image that does not have the `wmill` CLI installed, so the `wmill variable get` / `wmill resource get` snippets inserted by the +Variable and +Resource pickers fail. Detect `# sandbox`/`# docker` in the editor code and insert a curl-based snippet using the BASE_INTERNAL_URL, WM_TOKEN and WM_WORKSPACE env vars (available in sandbox) instead. Fixes WIN-2215 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): mirror worker grammar and use curl/wget fallback in sandbox mode Address review of the bash sandbox picker fallback: - Extract detection into `bashRunsInCustomImage`, mirroring the worker's BashAnnotations grammar (leading comment lines only; `# sandbox <image>` or bare `# docker`). A bare `# sandbox` is the nsjail-bash modifier that still runs on the worker rootfs where `wmill` is available, so it now correctly keeps the `wmill` snippet. This also fixes the substring false positives (`# sandboxed`, `# docker` in prose/body) and false negatives (`#sandbox <image>`). - The default `# sandbox alpine:latest` image ships busybox `wget`, not `curl`, so the snippet now tries `curl` then falls back to `wget`. - `variables/get_value` returns a JSON-quoted string; strip the outer quotes with `sed` so the sandbox snippet matches the `jq -r .value` output of the non-sandbox branch. Resources return JSON either way. - Add focused unit tests for the detection grammar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getScriptByPath, scriptLangToEditorLang } from '$lib/scripts'
|
||||
import { bashRunsInCustomImage } from '$lib/script_helpers'
|
||||
import Toggle from './Toggle.svelte'
|
||||
|
||||
import {
|
||||
@@ -673,7 +674,17 @@
|
||||
}
|
||||
editor.insertAtCursor(`v, _ := wmill.GetVariable("${path}")`)
|
||||
} else if (lang == 'bash') {
|
||||
editor.insertAtCursor(`wmill variable get ${path} --json | jq -r .value`)
|
||||
if (bashRunsInCustomImage(editor.getCode())) {
|
||||
// Custom image: no wmill CLI. Fall back to curl, then busybox wget
|
||||
// (the default `# sandbox alpine:latest` image ships wget, not curl).
|
||||
// get_value returns a JSON-quoted string, so strip the outer quotes
|
||||
// to match the `jq -r .value` output of the non-sandbox branch.
|
||||
editor.insertAtCursor(
|
||||
`{ curl -sf -H "Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/variables/get_value/${path}" 2>/dev/null || wget -qO- --header="Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/variables/get_value/${path}"; } | sed 's/^"//;s/"$//'`
|
||||
)
|
||||
} else {
|
||||
editor.insertAtCursor(`wmill variable get ${path} --json | jq -r .value`)
|
||||
}
|
||||
} else if (lang == 'powershell') {
|
||||
editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`)
|
||||
editor.arrowDown()
|
||||
@@ -751,7 +762,16 @@ string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri);
|
||||
}
|
||||
editor.insertAtCursor(`r, _ := wmill.GetResource("${path}")`)
|
||||
} else if (lang == 'bash') {
|
||||
editor.insertAtCursor(`wmill resource get ${path} --json | jq .value`)
|
||||
if (bashRunsInCustomImage(editor.getCode())) {
|
||||
// Custom image: no wmill CLI. Fall back to curl, then busybox wget
|
||||
// (the default `# sandbox alpine:latest` image ships wget, not curl).
|
||||
// get_value_interpolated returns JSON, matching the `jq .value` branch.
|
||||
editor.insertAtCursor(
|
||||
`curl -sf -H "Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/resources/get_value_interpolated/${path}" 2>/dev/null || wget -qO- --header="Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/resources/get_value_interpolated/${path}"`
|
||||
)
|
||||
} else {
|
||||
editor.insertAtCursor(`wmill resource get ${path} --json | jq .value`)
|
||||
}
|
||||
} else if (lang == 'powershell') {
|
||||
editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`)
|
||||
editor.arrowDown()
|
||||
@@ -962,7 +982,8 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
// after an unterminated one produces invalid SQL. The separator starts on
|
||||
// its own line so the `;` cannot land inside a trailing line comment.
|
||||
const existing = editor?.getCode() ?? ''
|
||||
const sep = existing.trim() === '' ? '' : endsWithUnterminatedStatement(existing) ? '\n;\n\n' : '\n\n'
|
||||
const sep =
|
||||
existing.trim() === '' ? '' : endsWithUnterminatedStatement(existing) ? '\n;\n\n' : '\n\n'
|
||||
editor?.append(sep + sql + '\n')
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { bashRunsInCustomImage } from './script_helpers'
|
||||
|
||||
// bashRunsInCustomImage decides whether the +Variable/+Resource pickers insert a
|
||||
// curl/wget snippet (custom image, no wmill CLI) or the wmill CLI snippet. It must
|
||||
// stay in sync with the worker's BashAnnotations grammar
|
||||
// (backend/windmill-common/src/worker.rs): leading comment lines only, `# sandbox
|
||||
// <image>` or bare `# docker` select a container; a bare `# sandbox` does not.
|
||||
|
||||
describe('bashRunsInCustomImage', () => {
|
||||
it('true for `# sandbox <image>` (spaced and compact)', () => {
|
||||
expect(bashRunsInCustomImage('# sandbox alpine:latest\necho hi')).toBe(true)
|
||||
expect(bashRunsInCustomImage('#sandbox python:3.12-slim\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('true for a bare `# docker` annotation', () => {
|
||||
expect(bashRunsInCustomImage('# docker\necho hi')).toBe(true)
|
||||
})
|
||||
|
||||
it('true when the sandbox line follows other leading comments (default template)', () => {
|
||||
expect(bashRunsInCustomImage('# shellcheck shell=bash\n# sandbox alpine:latest\necho hi')).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('false for a bare `# sandbox` (nsjail-bash on the worker, wmill available)', () => {
|
||||
expect(bashRunsInCustomImage('# sandbox\necho hi')).toBe(false)
|
||||
})
|
||||
|
||||
it('false for prose comments that merely contain the words', () => {
|
||||
expect(bashRunsInCustomImage('# sandboxed run below\necho hi')).toBe(false)
|
||||
expect(bashRunsInCustomImage('# runs in a docker container\necho hi')).toBe(false)
|
||||
})
|
||||
|
||||
it('false when the annotation is not on a leading comment line', () => {
|
||||
expect(bashRunsInCustomImage('echo hi\n# sandbox alpine')).toBe(false)
|
||||
expect(bashRunsInCustomImage('msg="$1" # docker')).toBe(false)
|
||||
})
|
||||
|
||||
it('false for a plain script with no annotations', () => {
|
||||
expect(bashRunsInCustomImage('# shellcheck shell=bash\necho hi')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1435,6 +1435,30 @@ export const INITIAL_CODE = {
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a bash script body runs inside a custom container image that does not
|
||||
* ship the `wmill` CLI (nor `jq`), namely `# sandbox <image>` or `# docker`. In
|
||||
* that case editor snippets must fall back to a plain HTTP client instead of `wmill`.
|
||||
*
|
||||
* Mirrors the worker's annotation grammar (backend/windmill-common/src/worker.rs,
|
||||
* `BashAnnotations`): only leading comment lines are scanned, stopping at the first
|
||||
* non-comment line. A bare `# sandbox` (no image) is the nsjail-bash modifier that
|
||||
* still runs on the worker rootfs where `wmill` is available, so it is excluded.
|
||||
*/
|
||||
export function bashRunsInCustomImage(code: string): boolean {
|
||||
for (const line of code.split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed === '') continue
|
||||
if (!trimmed.startsWith('#')) break
|
||||
const tokens = trimmed.slice(1).trim().split(/\s+/)
|
||||
// `# sandbox <image>` selects a container; bare `# sandbox` does not.
|
||||
if (tokens[0] === 'sandbox' && tokens[1]) return true
|
||||
// `# docker` (v1 daemon runtime) runs in the referenced image, no wmill.
|
||||
if (tokens[0] === 'docker' && tokens.length === 1) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function isInitialCode(content: string): boolean {
|
||||
for (const lang of Object.values(INITIAL_CODE)) {
|
||||
for (const code of Object.values(lang)) {
|
||||
|
||||
Reference in New Issue
Block a user