From 7c97fac74665fbbc5db530d75a852d78ed5bd3dd Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 6 Nov 2022 23:48:18 +0100 Subject: [PATCH] feat: support bash as 4th language (#865) * bash support * bash backend working * frontend part * bash backend working --- backend/Cargo.lock | 15 +++ backend/Cargo.toml | 1 + .../migrations/20221106081530_bash.down.sql | 1 + backend/migrations/20221106081530_bash.up.sql | 2 + .../parsers/windmill-parser-bash/Cargo.toml | 18 +++ .../parsers/windmill-parser-bash/src/lib.rs | 84 ++++++++++++++ backend/tests/worker.rs | 24 ++++ backend/windmill-api/Cargo.toml | 1 + backend/windmill-api/openapi.yaml | 33 +++++- backend/windmill-api/src/scripts.rs | 7 ++ backend/windmill-api/src/workspaces.rs | 1 + backend/windmill-common/src/scripts.rs | 2 + backend/windmill-worker/Cargo.toml | 1 + .../nsjail/run.bash.config.proto | 105 ++++++++++++++++++ backend/windmill-worker/src/worker.rs | 101 ++++++++++++++++- frontend/.config/sway/config | 0 frontend/src/lib/components/Editor.svelte | 6 +- frontend/src/lib/components/EditorBar.svelte | 17 ++- .../src/lib/components/HighlightCode.svelte | 7 +- .../src/lib/components/ScriptBuilder.svelte | 16 +-- .../src/lib/components/ScriptPicker.svelte | 2 +- .../lib/components/SuperadminSettings.svelte | 11 +- .../src/lib/components/TestJobLoader.svelte | 2 +- .../src/lib/components/UserSettings.svelte | 13 ++- .../flows/content/FlowModuleScript.svelte | 2 +- .../components/scriptEditor/LogPanel.svelte | 8 +- frontend/src/lib/editorUtils.ts | 2 + frontend/src/lib/infer.ts | 6 +- frontend/src/lib/script_helpers.ts | 13 ++- frontend/src/lib/utils.ts | 6 +- frontend/src/routes/__layout@root.svelte | 8 +- .../src/routes/flows/get/[...path].svelte | 2 +- frontend/src/routes/scripts.svelte | 2 +- .../src/routes/scripts/get/[...hash].svelte | 2 +- .../src/routes/user/workspaces@user.svelte | 4 +- 35 files changed, 475 insertions(+), 50 deletions(-) create mode 100644 backend/migrations/20221106081530_bash.down.sql create mode 100644 backend/migrations/20221106081530_bash.up.sql create mode 100644 backend/parsers/windmill-parser-bash/Cargo.toml create mode 100644 backend/parsers/windmill-parser-bash/src/lib.rs create mode 100644 backend/windmill-worker/nsjail/run.bash.config.proto create mode 100644 frontend/.config/sway/config diff --git a/backend/Cargo.lock b/backend/Cargo.lock index abc4021f47..966d171c01 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -4086,6 +4086,7 @@ dependencies = [ "windmill-audit", "windmill-common", "windmill-parser", + "windmill-parser-bash", "windmill-parser-go", "windmill-parser-py", "windmill-parser-ts", @@ -4152,6 +4153,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "windmill-parser-bash" +version = "1.44.0" +dependencies = [ + "anyhow", + "itertools", + "phf 0.11.1", + "regex", + "unicode-general-category", + "windmill-common", + "windmill-parser", +] + [[package]] name = "windmill-parser-go" version = "1.44.0" @@ -4240,6 +4254,7 @@ dependencies = [ "windmill-audit", "windmill-common", "windmill-parser", + "windmill-parser-bash", "windmill-parser-go", "windmill-parser-py", "windmill-parser-ts", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 0b87d30439..114cfdb927 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -59,6 +59,7 @@ windmill-parser = { path = "./parsers/windmill-parser" } windmill-parser-ts = { path = "./parsers/windmill-parser-ts" } windmill-parser-py = { path = "./parsers/windmill-parser-py" } windmill-parser-go = { path = "./parsers/windmill-parser-go" } +windmill-parser-bash = { path = "./parsers/windmill-parser-bash" } axum = { version = "^0", features = ["headers"] } headers = "^0" hyper = { version = "^0", features = ["full"] } diff --git a/backend/migrations/20221106081530_bash.down.sql b/backend/migrations/20221106081530_bash.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20221106081530_bash.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20221106081530_bash.up.sql b/backend/migrations/20221106081530_bash.up.sql new file mode 100644 index 0000000000..922cf28104 --- /dev/null +++ b/backend/migrations/20221106081530_bash.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TYPE SCRIPT_LANG ADD VALUE 'bash'; \ No newline at end of file diff --git a/backend/parsers/windmill-parser-bash/Cargo.toml b/backend/parsers/windmill-parser-bash/Cargo.toml new file mode 100644 index 0000000000..2915529014 --- /dev/null +++ b/backend/parsers/windmill-parser-bash/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "windmill-parser-bash" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[lib] +name = "windmill_parser_bash" +path = "./src/lib.rs" + +[dependencies] +windmill-parser.workspace = true +windmill-common.workspace = true +phf.workspace = true +unicode-general-category.workspace = true +itertools.workspace = true +anyhow.workspace = true +regex.workspace = true \ No newline at end of file diff --git a/backend/parsers/windmill-parser-bash/src/lib.rs b/backend/parsers/windmill-parser-bash/src/lib.rs new file mode 100644 index 0000000000..fdd5f26fe6 --- /dev/null +++ b/backend/parsers/windmill-parser-bash/src/lib.rs @@ -0,0 +1,84 @@ +#![allow(non_snake_case)] // TODO: switch to parse_* function naming + +use regex::Regex; + +use std::collections::HashMap; +use windmill_parser::{Arg, MainArgSignature, Typ}; + +pub fn parse_bash_sig(code: &str) -> windmill_common::error::Result { + let parsed = parse_file(&code)?; + if let Some(x) = parsed { + let args = x; + Ok(MainArgSignature { star_args: false, star_kwargs: false, args }) + } else { + Err(windmill_common::error::Error::BadRequest( + "Error parsing bash script".to_string(), + )) + } +} + +fn parse_file(code: &str) -> anyhow::Result>> { + let mut hm = HashMap::new(); + let re = Regex::new(r#"(?m)^(\w+)="\$(\d+)"$"#).unwrap(); + for cap in re.captures_iter(code) { + hm.insert(cap[2].parse::()?, cap[1].to_string()); + } + let mut args = vec![]; + for i in 1..20 { + if hm.contains_key(&i) { + args.push(Arg { + name: hm[&i].clone(), + typ: Typ::Str(None), + default: None, + otyp: None, + has_default: false, + }); + } else { + break; + } + } + Ok(Some(args)) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_parse_bash_sig() -> anyhow::Result<()> { + let code = r#" +token="$1" +image="$2" +digest="${3:-latest}" +foo="$4" + +"#; + //println!("{}", serde_json::to_string()?); + assert_eq!( + parse_bash_sig(code)?, + MainArgSignature { + star_args: false, + star_kwargs: false, + args: vec![ + Arg { + otyp: None, + name: "token".to_string(), + typ: Typ::Str(None), + default: None, + has_default: false + }, + Arg { + otyp: None, + name: "image".to_string(), + typ: Typ::Str(None), + default: None, + has_default: false + } + ] + } + ); + + Ok(()) + } +} diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 672a896671..706f84bff8 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -1515,6 +1515,30 @@ func main(derp string) (string, error) { assert_eq!(result, serde_json::json!("hello world")); } +#[sqlx::test(fixtures("base"))] +async fn test_bash_job(db: Pool) { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await; + let port = server.addr.port(); + + let content = r#" +msg="$1" +echo "hello $msg" +"# + .to_owned(); + + let job = RunJob::from(JobPayload::Code(RawCode { + content, + path: None, + language: ScriptLang::Bash, + })) + .arg("msg", json!("world")) + .run_until_complete(&db, port) + .await; + + assert_eq!(job.result, Some(json!("hello world"))); +} + #[sqlx::test(fixtures("base"))] async fn test_python_job(db: Pool) { initialize_tracing().await; diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index b5a7d64b29..2e4c7aceaa 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -28,6 +28,7 @@ windmill-parser.workspace = true windmill-parser-ts.workspace = true windmill-parser-go.workspace = true windmill-parser-py.workspace = true +windmill-parser-bash.workspace = true tokio.workspace = true anyhow.workspace = true argon2.workspace = true diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 59843f55e0..0b1f3fcbb3 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1671,7 +1671,7 @@ paths: schema: {} language: type: string - enum: [deno, python3, go] + enum: [deno, python3, go, bash] summary: type: string required: @@ -1794,7 +1794,7 @@ paths: type: string language: type: string - enum: [python3, deno, go] + enum: [python3, deno, go, bash] kind: type: string enum: [script, failure, trigger, command, approval] @@ -1854,6 +1854,27 @@ paths: schema: $ref: "#/components/schemas/MainArgSignature" + /scripts/bash/tojsonschema: + post: + summary: inspect bash code to infer jsonschema of arguments + operationId: bashToJsonschema + tags: + - script + requestBody: + description: bash code with the main function + required: true + content: + application/json: + schema: + type: string + responses: + "200": + description: parsed args + content: + application/json: + schema: + $ref: "#/components/schemas/MainArgSignature" + /scripts/go/tojsonschema: post: summary: inspect go code to infer jsonschema of arguments @@ -3585,7 +3606,7 @@ components: type: string language: type: string - enum: [python3, deno, go] + enum: [python3, deno, go, bash] kind: type: string enum: [script, failure, trigger, command, approval] @@ -3677,7 +3698,7 @@ components: type: boolean language: type: string - enum: [python3, deno, go] + enum: [python3, deno, go, bash] required: - id - running @@ -3755,7 +3776,7 @@ components: type: boolean language: type: string - enum: [python3, deno, go] + enum: [python3, deno, go, bash] is_skipped: type: boolean required: @@ -4126,7 +4147,7 @@ components: $ref: "#/components/schemas/ScriptArgs" language: type: string - enum: [python3, deno, go] + enum: [python3, deno, go, bash] required: - content diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index a7e7a78a02..23ed04b577 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -50,6 +50,7 @@ pub fn global_service() -> Router { ) .route("/deno/tojsonschema", post(parse_deno_code_to_jsonschema)) .route("/go/tojsonschema", post(parse_go_code_to_jsonschema)) + .route("/bash/tojsonschema", post(parse_bash_code_to_jsonschema)) .route("/hub/list", get(list_hub_scripts)) .route("/hub/get/*path", get(get_hub_script_by_path)) .route("/hub/get_full/*path", get(get_full_hub_script_by_path)) @@ -661,3 +662,9 @@ async fn parse_go_code_to_jsonschema( ) -> JsonResult { windmill_parser_go::parse_go_sig(&code).map(Json) } + +async fn parse_bash_code_to_jsonschema( + Json(code): Json, +) -> JsonResult { + windmill_parser_bash::parse_bash_sig(&code).map(Json) +} diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 687c3e5f2b..5b9066f37a 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -562,6 +562,7 @@ async fn tarball_workspace( ScriptLang::Python3 => "py", ScriptLang::Deno => "ts", ScriptLang::Go => "go", + ScriptLang::Bash => "sh", }; write_to_archive( script.content, diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index 74a0cb6f2d..f631e95676 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -28,6 +28,7 @@ pub enum ScriptLang { Deno, Python3, Go, + Bash, } impl ScriptLang { @@ -36,6 +37,7 @@ impl ScriptLang { ScriptLang::Deno => "deno", ScriptLang::Python3 => "python3", ScriptLang::Go => "go", + ScriptLang::Bash => "bash", } } } diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index 1f18f5ae85..303111a741 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -23,6 +23,7 @@ windmill-parser.workspace = true windmill-parser-ts.workspace = true windmill-parser-go.workspace = true windmill-parser-py.workspace = true +windmill-parser-bash.workspace = true sqlx.workspace = true uuid.workspace = true tracing.workspace = true diff --git a/backend/windmill-worker/nsjail/run.bash.config.proto b/backend/windmill-worker/nsjail/run.bash.config.proto new file mode 100644 index 0000000000..8dc062cf0c --- /dev/null +++ b/backend/windmill-worker/nsjail/run.bash.config.proto @@ -0,0 +1,105 @@ +name: "bash run script" + +mode: ONCE +hostname: "bash" +log_level: ERROR +time_limit: 300 + +rlimit_as: 2048 +rlimit_cpu: 1000 +rlimit_fsize: 1024 +rlimit_nofile: 64 + +cwd: "/tmp" + +clone_newnet: false +clone_newuser: {CLONE_NEWUSER} + +keep_caps: false +keep_env: true + +mount { + src: "/bin" + dst: "/bin" + is_bind: true +} + +mount { + src: "/lib" + dst: "/lib" + is_bind: true +} + + +mount { + src: "/lib64" + dst: "/lib64" + is_bind: true +} + + +mount { + src: "/usr" + dst: "/usr" + is_bind: true +} + +mount { + src: "/dev/null" + dst: "/dev/null" + is_bind: true + rw: true +} + +mount { + dst: "/tmp" + fstype: "tmpfs" + rw: true + options: "size=500000000" +} + +mount { + src: "{JOB_DIR}/main.sh" + dst: "/tmp/main.sh" + is_bind: true + mandatory: false +} + +mount { + src: "/etc/ssl" + dst: "/etc/ssl" + is_bind: true +} + +mount { + src: "/etc/pki" + dst: "/etc/pki" + is_bind: true + mandatory: false +} + +mount { + src: "/etc/resolv.conf" + dst: "/etc/resolv.conf" + is_bind: true +} + +mount { + src: "/dev/random" + dst: "/dev/random" + is_bind: true +} + +iface_no_lo: true + +mount { + src: "{CACHE_DIR}" + dst: "/tmp/.cache/go" + is_bind: true + rw: true + mandatory: false +} + +{SHARED_MOUNT} + + diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 562a45f046..7c5c7900c6 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -139,8 +139,8 @@ const DEFAULT_HEAVY_DEPS: [&str; 18] = [ const INCLUDE_DEPS_PY_SH_CONTENT: &str = include_str!("../nsjail/download_deps.py.sh"); const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto"); const NSJAIL_CONFIG_RUN_PYTHON3_CONTENT: &str = include_str!("../nsjail/run.python3.config.proto"); - const NSJAIL_CONFIG_RUN_GO_CONTENT: &str = include_str!("../nsjail/run.go.config.proto"); +const NSJAIL_CONFIG_RUN_BASH_CONTENT: &str = include_str!("../nsjail/run.bash.config.proto"); const NSJAIL_CONFIG_RUN_DENO_CONTENT: &str = include_str!("../nsjail/run.deno.config.proto"); const MAX_LOG_SIZE: u32 = 200000; @@ -904,6 +904,21 @@ mount {{ ) .await } + Some(ScriptLang::Bash) => { + handle_bash_job( + worker_config, + envs, + logs, + job, + db, + token, + &inner_content, + timeout, + job_dir, + &shared_mount, + ) + .await + } }; tracing::info!( worker_name = %worker_name, @@ -1100,6 +1115,90 @@ func Run(req Req) (interface{{}}, error){{ read_result(job_dir).await } +#[tracing::instrument(level = "trace", skip_all)] +async fn handle_bash_job( + WorkerConfig { base_internal_url, disable_nuser, disable_nsjail, base_url, .. }: &WorkerConfig, + Envs { nsjail_path, path_env, home_env, .. }: &Envs, + logs: &mut String, + job: &QueuedJob, + db: &sqlx::Pool, + token: String, + content: &str, + timeout: i32, + job_dir: &str, + shared_mount: &str, +) -> Result { + logs.push_str("\n\n--- BASH CODE EXECUTION ---\n"); + set_logs(logs, job.id, db).await; + write_file(job_dir, "main.sh", content).await?; + + let mut reserved_variables = get_reserved_variables(job, &token, &base_url, db).await?; + reserved_variables.insert("RUST_LOG".to_string(), "info".to_string()); + + let hm = match job.args { + Some(Value::Object(ref hm)) => hm.clone(), + _ => serde_json::Map::new(), + }; + let args_owned = windmill_parser_bash::parse_bash_sig(&content)? + .args + .iter() + .map(|arg| { + hm.get(&arg.name) + .and_then(|v| match v { + Value::String(s) => Some(s.clone()), + _ => serde_json::to_string(v).ok(), + }) + .unwrap_or_else(String::new) + }) + .collect::>(); + let args = args_owned.iter().map(|s| &s[..]).collect::>(); + + let child = if !disable_nsjail { + let _ = write_file( + job_dir, + "run.config.proto", + &NSJAIL_CONFIG_RUN_BASH_CONTENT + .replace("{JOB_DIR}", job_dir) + .replace("{CLONE_NEWUSER}", &(!disable_nuser).to_string()) + .replace("{SHARED_MOUNT}", shared_mount), + ) + .await?; + let mut cmd_args = vec!["--config", "run.config.proto", "--", "/bin/sh", "main.sh"]; + cmd_args.extend(args); + Command::new(nsjail_path) + .current_dir(job_dir) + .env_clear() + .envs(reserved_variables) + .env("PATH", path_env) + .env("BASE_INTERNAL_URL", base_internal_url) + .args(cmd_args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()? + } else { + let mut cmd_args = vec!["main.sh"]; + cmd_args.extend(&args); + Command::new("/bin/sh") + .current_dir(job_dir) + .env_clear() + .envs(reserved_variables) + .env("PATH", path_env) + .env("BASE_INTERNAL_URL", base_internal_url) + .env("HOME", home_env) + .args(cmd_args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()? + }; + handle_child(&job.id, db, logs, timeout, child).await?; + //for now bash jobs have an empty result object + Ok(serde_json::json!(logs + .lines() + .last() + .map(|x| x.to_string()) + .unwrap_or_else(String::new))) +} + fn capitalize(s: &str) -> String { let mut c = s.chars(); match c.next() { diff --git a/frontend/.config/sway/config b/frontend/.config/sway/config new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index 368f59f2df..819d3bb681 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -33,7 +33,7 @@ import getMessageServiceOverride from 'vscode/service-override/messages' import { StandaloneServices } from 'vscode/services' import { - DENO_INIT_CODE, + BASH_INIT_CODE, DENO_INIT_CODE_CLEAR, GO_INIT_CODE, PYTHON_INIT_CODE_CLEAR @@ -57,7 +57,7 @@ let divEl: HTMLDivElement | null = null let editor: monaco.editor.IStandaloneCodeEditor - export let lang: 'typescript' | 'python' | 'go' + export let lang: 'typescript' | 'python' | 'go' | 'shell' export let code: string = '' export let hash: string = randomHash() export let cmdEnterAction: (() => void) | undefined = undefined @@ -147,6 +147,8 @@ setCode(PYTHON_INIT_CODE_CLEAR) } else if (lang == 'go') { setCode(GO_INIT_CODE) + } else if (lang == 'shell') { + setCode(BASH_INIT_CODE) } } } diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index d6c99a6aaa..95d3ae960d 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -25,10 +25,8 @@ import HighlightCode from './HighlightCode.svelte' import DrawerContent from './common/drawer/DrawerContent.svelte' import { Drawer } from './common' - import Icon from 'svelte-awesome' - import Popover from './Popover.svelte' - export let lang: 'python3' | 'deno' | 'go' + export let lang: 'python3' | 'deno' | 'go' | 'bash' export let editor: Editor export let websocketAlive: { pyright: boolean; black: boolean; deno: boolean; go: boolean } export let iconOnly: boolean = false @@ -42,7 +40,7 @@ let resourceEditor: ResourceEditor let codeViewer: Drawer - let codeLang: 'python3' | 'deno' | 'go' = 'deno' + let codeLang: 'python3' | 'deno' | 'go' | 'bash' = 'deno' let codeContent: string = '' function addEditorActions() { @@ -109,6 +107,13 @@ editor.insertAtBeginning('import os\n') } editor.insertAtCursor(`os.environ.get("${name}")`) + } else if (lang == 'go') { + if (!editor.getCode().includes('"os"')) { + editor.insertAtLine('import "os"\n', 2) + } + editor.insertAtCursor(`os.Getenv("${name}")`) + } else if (lang == 'bash') { + editor.insertAtCursor(`$${name}`) } sendUserToast(`${name} inserted at cursor`) }} @@ -137,6 +142,8 @@ editor.insertAtLine('import wmill "github.com/windmill-labs/windmill-go-client"\n\n', 3) } editor.insertAtCursor(`v, _ := wmill.GetVariable("${path}")`) + } else if (lang == 'bash') { + sendUserToast('Not supported yet', true) } sendUserToast(`${name} inserted at cursor`) }} @@ -181,6 +188,8 @@ editor.insertAtLine('import wmill "github.com/windmill-labs/windmill-go-client"\n\n', 3) } editor.insertAtCursor(`r, _ := wmill.GetResource("${path}")`) + } else if (lang == 'bash') { + sendUserToast('Not supported yet', true) } sendUserToast(`${path} inserted at cursor`) }} diff --git a/frontend/src/lib/components/HighlightCode.svelte b/frontend/src/lib/components/HighlightCode.svelte index 9245c6b18c..f39ec77e67 100644 --- a/frontend/src/lib/components/HighlightCode.svelte +++ b/frontend/src/lib/components/HighlightCode.svelte @@ -3,9 +3,10 @@ import python from 'svelte-highlight/languages/python' import typescript from 'svelte-highlight/languages/typescript' import go from 'svelte-highlight/languages/go' + import shell from 'svelte-highlight/languages/shell' export let code: string = '' - export let language: 'python3' | 'deno' | 'go' | undefined + export let language: 'python3' | 'deno' | 'go' | 'bash' | undefined function getLang(lang: string | undefined) { switch (lang) { @@ -15,8 +16,10 @@ return typescript case 'go': return go + case 'bash': + return shell default: - return python + return typescript } } diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 2bde7cecf0..03dee280e7 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -41,7 +41,7 @@ } function initContent( - language: 'deno' | 'python3' | 'go', + language: 'deno' | 'python3' | 'go' | 'bash', kind: Script.kind, template: 'pgsql' | 'script' ) { @@ -193,7 +193,8 @@ options={[ ['Typescript (Deno)', 'deno'], ['Python 3.10', 'python3'], - ['Go', 'go'] + ['Go', 'go'], + ['Bash', 'bash'] ]} on:change={(e) => initContent(e.detail, script.kind, template)} bind:value={script.language} @@ -275,11 +276,12 @@ {/if} {/if} - - +
+ +
{:else if step === 2} diff --git a/frontend/src/lib/components/ScriptPicker.svelte b/frontend/src/lib/components/ScriptPicker.svelte index 4681b4935f..d175401af0 100644 --- a/frontend/src/lib/components/ScriptPicker.svelte +++ b/frontend/src/lib/components/ScriptPicker.svelte @@ -21,7 +21,7 @@ let itemPicker: ItemPicker let drawerViewer: Drawer let code: string = '' - let lang: 'deno' | 'python3' | 'go' | undefined + let lang: 'deno' | 'python3' | 'go' | 'bash' | undefined let options: [[string, any]] = [['Script', 'script']] allowHub && options.unshift(['Hub', 'hub']) diff --git a/frontend/src/lib/components/SuperadminSettings.svelte b/frontend/src/lib/components/SuperadminSettings.svelte index f80caa11cb..e9508eeaf6 100644 --- a/frontend/src/lib/components/SuperadminSettings.svelte +++ b/frontend/src/lib/components/SuperadminSettings.svelte @@ -10,9 +10,17 @@ import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte' let drawer: Drawer + + export function openDrawer() { + loadVersion() + listUsers() + drawer?.openDrawer() + } + export function toggleDrawer() { drawer?.toggleDrawer() } + let version: string | undefined let users: GlobalUserInfo[] = [] let filteredUsers: GlobalUserInfo[] | undefined @@ -37,9 +45,6 @@ users = await UserService.listUsersAsSuperAdmin({ perPage: 100000 }) fuse?.setCollection(users) } - - loadVersion() - listUsers() diff --git a/frontend/src/lib/components/TestJobLoader.svelte b/frontend/src/lib/components/TestJobLoader.svelte index f992f02a43..d62b9a157a 100644 --- a/frontend/src/lib/components/TestJobLoader.svelte +++ b/frontend/src/lib/components/TestJobLoader.svelte @@ -20,7 +20,7 @@ export async function runPreview( path: string | undefined, code: string, - lang: 'deno' | 'go' | 'python3', + lang: 'deno' | 'go' | 'python3' | 'bash', args: Record ): Promise { try { diff --git a/frontend/src/lib/components/UserSettings.svelte b/frontend/src/lib/components/UserSettings.svelte index d8c6908d4a..de1d978b31 100644 --- a/frontend/src/lib/components/UserSettings.svelte +++ b/frontend/src/lib/components/UserSettings.svelte @@ -23,12 +23,15 @@ import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte' let drawer: Drawer + + export function openDrawer() { + loadVersion() + loadLoginType() + listTokens() + drawer?.openDrawer() + } + export function toggleDrawer() { - if (!drawer || !drawer?.isOpen()) { - loadVersion() - loadLoginType() - listTokens() - } drawer?.toggleDrawer() } diff --git a/frontend/src/lib/components/flows/content/FlowModuleScript.svelte b/frontend/src/lib/components/flows/content/FlowModuleScript.svelte index 83a5a3d65e..e4543498db 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleScript.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleScript.svelte @@ -5,7 +5,7 @@ export let path: string let code: string - let language: 'deno' | 'python3' | 'go' + let language: 'deno' | 'python3' | 'go' | 'bash' async function loadCode(path: string) { const script = await getScriptByPath(path!) diff --git a/frontend/src/lib/components/scriptEditor/LogPanel.svelte b/frontend/src/lib/components/scriptEditor/LogPanel.svelte index 39ba29e6b2..a34dacd8c4 100644 --- a/frontend/src/lib/components/scriptEditor/LogPanel.svelte +++ b/frontend/src/lib/components/scriptEditor/LogPanel.svelte @@ -16,7 +16,7 @@ import DrawerContent from '../common/drawer/DrawerContent.svelte' import HighlightCode from '../HighlightCode.svelte' import LogViewer from '../LogViewer.svelte' - import { Pane, Splitpanes } from 'svelte-splitpanes' + import { Pane } from 'svelte-splitpanes' import SplitPanesWrapper from '../splitPanes/SplitPanesWrapper.svelte' export let path: string | undefined @@ -59,7 +59,7 @@ class="overflow-x-auto break-words relative h-full m-2 text-xs bg-white shadow-inner p-2"> {drawerContent?.content} - {:else if drawerContent?.mode === 'deno' || drawerContent?.mode === 'python3' || drawerContent?.mode === 'go'} + {:else if drawerContent?.mode === 'deno' || drawerContent?.mode === 'python3' || drawerContent?.mode === 'go' || drawerContent?.mode === 'bash'} {/if} @@ -82,8 +82,8 @@ {#if previewJob != undefined && 'result' in previewJob && previewJob.result != undefined} -
-							
+						

 						
{:else}
diff --git a/frontend/src/lib/editorUtils.ts b/frontend/src/lib/editorUtils.ts index 6653c05bd6..35f903fd6f 100644 --- a/frontend/src/lib/editorUtils.ts +++ b/frontend/src/lib/editorUtils.ts @@ -43,6 +43,8 @@ export function langToExt(lang: string): string { return 'py' case 'go': return 'go' + case 'bash': + return 'sh' default: return 'unknown' } diff --git a/frontend/src/lib/infer.ts b/frontend/src/lib/infer.ts index 8200c48ce4..c0e0f357d4 100644 --- a/frontend/src/lib/infer.ts +++ b/frontend/src/lib/infer.ts @@ -6,7 +6,7 @@ const loadSchemaLastRun = writable<[string | undefined, MainArgSignature | undef export async function inferArgs( - language: 'python3' | 'deno' | 'go', + language: 'python3' | 'deno' | 'go' | 'bash', code: string, schema: Schema ): Promise { @@ -28,6 +28,10 @@ export async function inferArgs( inferedSchema = await ScriptService.goToJsonschema({ requestBody: code }) + } else if (language == 'bash') { + inferedSchema = await ScriptService.bashToJsonschema({ + requestBody: code + }) } else { return } diff --git a/frontend/src/lib/script_helpers.ts b/frontend/src/lib/script_helpers.ts index b0fc0dd587..ec3e63c3dd 100644 --- a/frontend/src/lib/script_helpers.ts +++ b/frontend/src/lib/script_helpers.ts @@ -143,6 +143,15 @@ export async function main( return query.rows; }` +export const BASH_INIT_CODE = ` +# arguments of the form X="$I" are parsed as parameters X of type string +msg="$1" + +# the last line of the stdout is the return value +echo "Hello $msg" +` + + export const DENO_INIT_CODE_TRIGGER = `import * as wmill from "https://deno.land/x/windmill@v${__pkg__.version}/mod.ts" export async function main() { @@ -181,7 +190,7 @@ export function isInitialCode(content: string): boolean { return false } -export function initialCode(language: 'deno' | 'python3' | 'go', kind: Script.kind, subkind: 'pgsql' | 'flow' | 'script' | undefined): string { +export function initialCode(language: 'deno' | 'python3' | 'go' | 'bash', kind: Script.kind, subkind: 'pgsql' | 'flow' | 'script' | undefined): string { if (language === 'deno') { if (kind === 'trigger') { return DENO_INIT_CODE_TRIGGER @@ -210,6 +219,8 @@ export function initialCode(language: 'deno' | 'python3' | 'go', kind: Script.ki } else { return PYTHON_INIT_CODE } + } else if (language == 'bash') { + return BASH_INIT_CODE } else { if (kind === 'failure') { return GO_FAILURE_MODULE_CODE diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 2fbdd484f8..ec4466e0fc 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -490,7 +490,7 @@ export function scriptPathToHref(path: string): string { export async function getScriptByPath(path: string): Promise<{ content: string - language: 'deno' | 'python3' | 'go', + language: 'deno' | 'python3' | 'go' | 'bash', schema: any }> { if (path.startsWith('hub/')) { @@ -594,11 +594,13 @@ export function classNames(...classes: Array): string { return classes.filter(Boolean).join(' ') } -export function scriptLangToEditorLang(lang: Script.language): 'typescript' | 'python' | 'go' { +export function scriptLangToEditorLang(lang: Script.language): 'typescript' | 'python' | 'go' | 'shell' { if (lang == 'deno') { return 'typescript' } else if (lang == 'python3') { return 'python' + } else if (lang == 'bash') { + return 'shell' } else { return lang } diff --git a/frontend/src/routes/__layout@root.svelte b/frontend/src/routes/__layout@root.svelte index 763246cc94..92b9782fdc 100644 --- a/frontend/src/routes/__layout@root.svelte +++ b/frontend/src/routes/__layout@root.svelte @@ -111,8 +111,8 @@
userSettings.toggleDrawer()} - on:superadmin-settings={() => superadminSettings.toggleDrawer()} + on:user-settings={() => userSettings.openDrawer()} + on:superadmin-settings={() => superadminSettings.openDrawer()} />
@@ -146,8 +146,8 @@
superadminSettings.toggleDrawer()} + on:user-settings={userSettings.openDrawer} + on:superadmin-settings={() => superadminSettings.openDrawer()} {isCollapsed} />
diff --git a/frontend/src/routes/flows/get/[...path].svelte b/frontend/src/routes/flows/get/[...path].svelte index 97745e914b..dbf8bb467a 100644 --- a/frontend/src/routes/flows/get/[...path].svelte +++ b/frontend/src/routes/flows/get/[...path].svelte @@ -246,7 +246,7 @@ > Copy - +
{#if schedule} diff --git a/frontend/src/routes/scripts.svelte b/frontend/src/routes/scripts.svelte index f6bee663c6..efc5761928 100644 --- a/frontend/src/routes/scripts.svelte +++ b/frontend/src/routes/scripts.svelte @@ -69,7 +69,7 @@ let codeViewer: Drawer let codeViewerContent: string = '' - let codeViewerLanguage: 'deno' | 'python3' | 'go' = 'deno' + let codeViewerLanguage: 'deno' | 'python3' | 'go' | 'bash' = 'deno' let codeViewerPath: string = '' $: filteredScripts = diff --git a/frontend/src/routes/scripts/get/[...hash].svelte b/frontend/src/routes/scripts/get/[...hash].svelte index 36ba7fdf81..9e75e3b4da 100644 --- a/frontend/src/routes/scripts/get/[...hash].svelte +++ b/frontend/src/routes/scripts/get/[...hash].svelte @@ -432,7 +432,7 @@ {/each}
- +
{/each} diff --git a/frontend/src/routes/user/workspaces@user.svelte b/frontend/src/routes/user/workspaces@user.svelte index 0c3c303ae8..d5714788bb 100644 --- a/frontend/src/routes/user/workspaces@user.svelte +++ b/frontend/src/routes/user/workspaces@user.svelte @@ -160,11 +160,11 @@ {/each}
{#if $superadmin} - {/if} -