Files
windmill/backend/windmill-worker/src/nu_executor.rs
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

364 lines
11 KiB
Rust

use std::{collections::HashMap, process::Stdio};
use itertools::Itertools;
use serde_json::value::RawValue;
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use windmill_common::{
error::Error,
worker::{write_file, Connection},
};
use windmill_parser::Arg;
use windmill_parser_nu::parse_nu_signature;
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
use crate::{
common::{
create_args_and_out_file, get_reserved_variables, read_result, start_child_process,
OccupancyMetrics,
},
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! {
static ref NU_PATH: String = std::env::var("NU_PATH").unwrap_or_else(|_| "/usr/bin/nu".to_string());
// TODO(v1):
// static ref PLUGIN_USE_RE: Regex = Regex::new(r#"(?:plugin use )(?<plugin>.*)"#).unwrap();
}
// TODO: Can be generalized and used for other handlers
#[allow(dead_code)]
pub(crate) struct JobHandlerInput<'a> {
pub base_internal_url: &'a str,
pub canceled_by: &'a mut Option<CanceledBy>,
pub client: &'a AuthedClient,
pub parent_runnable_path: Option<String>,
pub conn: &'a Connection,
pub envs: HashMap<String, String>,
pub inner_content: &'a str,
pub job: &'a MiniPulledJob,
pub job_dir: &'a str,
pub mem_peak: &'a mut i32,
pub occupancy_metrics: &'a mut OccupancyMetrics,
pub requirements_o: Option<&'a String>,
pub shared_mount: &'a str,
pub worker_name: &'a str,
}
pub async fn handle_nu_job<'a>(mut args: JobHandlerInput<'a>) -> Result<Box<RawValue>, Error> {
// TODO(v1):
// --- Handle plugins ---
// let plugins = get_plugins(&mut args).await?;
// TODO(v1):
// --- Handle imports ---
// TODO(v1):
// --- Handle relative ---
// --- Wrap and write to fs ---
{
create_args_and_out_file(&args.client, args.job, args.job_dir, args.conn).await?;
File::create(format!("{}/main.nu", args.job_dir))
.await?
.write_all(&wrap(args.inner_content)?.into_bytes())
.await?;
}
// --- Execute ---
{
run(&mut args).await?;
}
// --- Retrieve results ---
{
read_result(&args.job_dir).await
}
}
// async fn get_plugins<'a>(
// JobHandlerInput {
// occupancy_metrics,
// mem_peak,
// canceled_by,
// worker_name,
// job,
// db,
// inner_content,
// ..
// }: &mut JobHandlerInput<'a>,
// ) -> Result<Vec<&'a str>, Error> {
// let plugins_dir = concatcp!(NU_CACHE_DIR, "/plugins");
// let nu_version = from_utf8_mut(
// Command::new(NU_PATH.as_str())
// .arg("--version")
// .output()
// .await?
// .stdout
// .as_mut_slice(),
// )
// .map_err(|e| windmill_common::error::Error::ExecutionErr(e.to_string()))?
// .to_owned();
// let plugins = parse_plugin_use(inner_content);
// for plugin in &plugins {
// let mut run_cmd = Command::new(CARGO_PATH.as_str());
// // cargo install nu_plugin_query --version (nu --version); plugin add ~/.cargo/bin/nu_plugin_query
// run_cmd
// // TODO: make it work with env_clear
// // .env_clear()
// .args(&[
// "install",
// "--root",
// plugins_dir,
// "--locked",
// &format!("nu_plugin_{plugin}"),
// "--version",
// &nu_version,
// ])
// .stdout(Stdio::piped())
// .stderr(Stdio::piped());
// #[cfg(windows)]
// nsjail_cmd.env("SystemRoot", SYSTEM_ROOT.as_str());
// let child = start_child_process(run_cmd, "cargo").await?;
// // handle_child::handle_child(
// // &job.id,
// // db,
// // mem_peak,
// // canceled_by,
// // child,
// // !*DISABLE_NSJAIL,
// // worker_name,
// // &job.workspace_id,
// // "cargo",
// // job.timeout,
// // false,
// // &mut Some(occupancy_metrics),
// // )
// // .await?;
// }
// Ok(plugins)
// }
// fn parse_plugin_use(inner_content: &str) -> Vec<&str> {
// let mut plugins = vec![];
// // TODO: Ignore plugins with # in the beginning
// for cap in PLUGIN_USE_RE.captures_iter(inner_content).into_iter() {
// if let Some(mat) = cap.name("plugin") {
// plugins.push(mat.as_str());
// }
// }
// plugins
// }
/// Wraps content script
/// that upon execution reads args.json (which are piped and transformed from previous flow step or top level inputs)
/// Also wrapper takes output of program and serializes to result.json (Which windmill will know how to use later)
fn wrap(inner_content: &str) -> Result<String, Error> {
let sig = parse_nu_signature(inner_content)?;
let spread = sig
.args
.clone()
.into_iter()
.map(|Arg { name, typ, has_default, .. }| {
// Apply additional input transformation
let transformation = format!(
"| if $in != null {{ {} }} else {{ $in }}",
match typ {
// JSON converts X.0 to X and nu can't coerce type automatically
windmill_parser::Typ::Datetime => "into datetime",
windmill_parser::Typ::Bytes => "into binary",
windmill_parser::Typ::Float => "into float",
// Ident
_ => "$in",
}
);
let nullguard = if has_default || matches!(typ, windmill_parser::Typ::Unknown) {
"".to_owned()
} else {
format!("| nullguard {name}")
};
format!("\n\t\t\t($parsed_args.{name}? {nullguard} {transformation}) ",)
})
.collect_vec()
.join(" ");
Ok(
r#"
$env.config.table.mode = 'basic'
def nullguard [ name: string ] {
if ($in == null) {
panic $"argument `($name)` of main function can't be null"
}
$in
}
# TODO: Probably needs rework in order for LSP to work
def get_variable [ pat ] {
let addr = $"($env.BASE_INTERNAL_URL)/api/w/($env.WM_WORKSPACE)/variables/get_value/($pat)" ;
http get -H ["Authorization", $"Bearer ($env.WM_TOKEN)"] $addr | return $in
}
def get_resource [ pat ] {
let addr = $"($env.BASE_INTERNAL_URL)/api/w/($env.WM_WORKSPACE)/resources/get_value_interpolated/($pat)" ;
http get -H ["Authorization", $"Bearer ($env.WM_TOKEN)"] $addr | return $in
}
def 'main --wrapped' [] {
let parsed_args = open args.json
(main SPREAD
) | to json | save -f result.json
}
INNER_CONTENT
"#
.replace("INNER_CONTENT", inner_content)
.replace("SPREAD", &spread), // .replace("TRANSFORM", transform)
)
}
async fn run<'a>(
JobHandlerInput {
occupancy_metrics,
mem_peak,
canceled_by,
worker_name,
job,
conn,
job_dir,
shared_mount,
client,
parent_runnable_path,
envs,
base_internal_url,
..
}: &mut JobHandlerInput<'a>,
// plugins: Vec<&'a str>,
) -> Result<(), Error> {
let reserved_variables =
get_reserved_variables(job, &client.token, conn, parent_runnable_path.clone()).await?;
let child = if !cfg!(windows) && !*DISABLE_NSJAIL {
append_logs(
&job.id,
&job.workspace_id,
format!("\n\n--- ISOLATED NU CODE EXECUTION ---\n"),
conn,
)
.await;
write_file(
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_NU_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{NU_PATH}", &NU_PATH)
.replace("{SHARED_MOUNT}", &shared_mount)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
nsjail_cmd
.env_clear()
.current_dir(job_dir)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables)
.envs(PROXY_ENVS.clone())
.args(vec![
"--config",
"run.config.proto",
"--",
NU_PATH.as_str(),
"/tmp/main.nu",
"--wrapped",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
start_child_process(nsjail_cmd, NSJAIL_PATH.as_str()).await?
} else {
append_logs(
&job.id,
&job.workspace_id,
format!("\n\n--- NU CODE EXECUTION ---\n"),
&conn,
)
.await;
// let plugin_registry = format!("{job_dir}/plugin-registry");
// File::create(&plugin_registry).await?;
//
let mut cmd = Command::new(if cfg!(windows) {
"nu"
} else {
NU_PATH.as_str()
});
cmd.env_clear()
.current_dir(job_dir.to_owned())
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables)
.envs(PROXY_ENVS.clone())
.args(&[
"main.nu",
"--wrapped",
// TODO(v1):
// "--plugins",
// &format!(
// "[{}]",
// plugins
// .into_iter()
// .map(|pl| format!("{NU_CACHE_DIR}/plugins/bin/nu_plugin_{pl}"))
// .collect_vec()
// .join(",")
// ),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
cmd.env("SystemRoot", crate::SYSTEM_ROOT.as_str())
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
.env(
"TMP",
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
);
}
start_child_process(cmd, "nu").await?
};
handle_child::handle_child(
&job.id,
conn,
mem_peak,
canceled_by,
child,
!*DISABLE_NSJAIL,
worker_name,
&job.workspace_id,
"nu",
job.timeout,
false,
&mut Some(occupancy_metrics),
None,
)
.await
}
// #[cfg(test)]
// mod test {
// use super::parse_plugin_use;
// #[test]
// fn test_nu_plugin_use() {
// let content = r#"
// plugin use foo
// plugin use bar
// plugin use baz
// plugin use meh
// "#;
// assert_eq!(
// vec!["foo", "bar", "baz", "meh"], //
// parse_plugin_use(content)
// );
// }
// }