diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index ce242a891a..04f181f4fc 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -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') }} /> diff --git a/frontend/src/lib/script_helpers.test.ts b/frontend/src/lib/script_helpers.test.ts new file mode 100644 index 0000000000..1badf03e11 --- /dev/null +++ b/frontend/src/lib/script_helpers.test.ts @@ -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 +// ` or bare `# docker` select a container; a bare `# sandbox` does not. + +describe('bashRunsInCustomImage', () => { + it('true for `# sandbox ` (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) + }) +}) diff --git a/frontend/src/lib/script_helpers.ts b/frontend/src/lib/script_helpers.ts index d936d60f25..4a85107b70 100644 --- a/frontend/src/lib/script_helpers.ts +++ b/frontend/src/lib/script_helpers.ts @@ -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 ` 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 ` 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)) {