node running, runtime version awareness
This commit is contained in:
@@ -36,6 +36,7 @@ impl LocalRuntime {
|
||||
},
|
||||
environment: None,
|
||||
dependencies: None,
|
||||
env_vars: std::collections::HashMap::new(),
|
||||
};
|
||||
|
||||
Self {
|
||||
@@ -168,6 +169,9 @@ mod tests {
|
||||
code: Some("#!/bin/bash\necho 'hello from shell'".to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
@@ -197,6 +201,9 @@ mod tests {
|
||||
code: Some("some code".to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("unknown".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
|
||||
@@ -9,6 +9,18 @@
|
||||
//! as separate Rust types. Instead, the `ProcessRuntime` handles all
|
||||
//! languages by using the interpreter, environment, and dependency
|
||||
//! configuration stored in the database.
|
||||
//!
|
||||
//! ## Runtime Version Selection
|
||||
//!
|
||||
//! When an action declares a `runtime_version_constraint` (e.g., `">=3.12"`),
|
||||
//! the executor resolves the best matching `RuntimeVersion` from the database
|
||||
//! and passes its `execution_config` through `ExecutionContext::runtime_config_override`.
|
||||
//! The `ProcessRuntime` uses this override instead of its built-in config,
|
||||
//! enabling version-specific interpreter binaries, environment commands, etc.
|
||||
//!
|
||||
//! The environment directory is also overridden to include the version suffix
|
||||
//! (e.g., `python-3.12` instead of `python`) so that different versions
|
||||
//! maintain isolated environments.
|
||||
|
||||
pub mod dependency;
|
||||
pub mod local;
|
||||
@@ -26,6 +38,7 @@ pub use process::ProcessRuntime;
|
||||
pub use shell::ShellRuntime;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use attune_common::models::runtime::RuntimeExecutionConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
@@ -112,6 +125,24 @@ pub struct ExecutionContext {
|
||||
/// Runtime name (python, shell, etc.) - used to select the correct runtime
|
||||
pub runtime_name: Option<String>,
|
||||
|
||||
/// Optional override of the runtime's execution config, set when a specific
|
||||
/// runtime version has been selected (e.g., Python 3.12 vs the parent
|
||||
/// "Python" runtime). When present, `ProcessRuntime` uses this config
|
||||
/// instead of its built-in one for interpreter resolution, environment
|
||||
/// setup, and dependency management.
|
||||
#[serde(skip)]
|
||||
pub runtime_config_override: Option<RuntimeExecutionConfig>,
|
||||
|
||||
/// Optional override of the environment directory suffix. When a specific
|
||||
/// runtime version is selected, the env dir includes the version
|
||||
/// (e.g., `python-3.12` instead of `python`) for per-version isolation.
|
||||
/// Format: just the directory name, not the full path.
|
||||
pub runtime_env_dir_suffix: Option<String>,
|
||||
|
||||
/// The selected runtime version string for logging/diagnostics
|
||||
/// (e.g., "3.12.1"). `None` means the parent runtime config is used as-is.
|
||||
pub selected_runtime_version: Option<String>,
|
||||
|
||||
/// Maximum stdout size in bytes (for log truncation)
|
||||
#[serde(default = "default_max_log_bytes")]
|
||||
pub max_stdout_bytes: usize,
|
||||
@@ -154,6 +185,9 @@ impl ExecutionContext {
|
||||
code,
|
||||
code_path: None,
|
||||
runtime_name: None,
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
|
||||
@@ -94,6 +94,7 @@ impl ProcessRuntime {
|
||||
}
|
||||
|
||||
/// Get the interpreter path, checking for an external pack environment first.
|
||||
#[cfg(test)]
|
||||
fn resolve_interpreter(&self, pack_dir: &Path, env_dir: Option<&Path>) -> PathBuf {
|
||||
self.config.resolve_interpreter_with_env(pack_dir, env_dir)
|
||||
}
|
||||
@@ -472,24 +473,52 @@ impl Runtime for ProcessRuntime {
|
||||
}
|
||||
|
||||
async fn execute(&self, context: ExecutionContext) -> RuntimeResult<ExecutionResult> {
|
||||
info!(
|
||||
"Executing action '{}' (execution_id: {}) with runtime '{}', \
|
||||
parameter delivery: {:?}, format: {:?}, output format: {:?}",
|
||||
context.action_ref,
|
||||
context.execution_id,
|
||||
self.runtime_name,
|
||||
context.parameter_delivery,
|
||||
context.parameter_format,
|
||||
context.output_format,
|
||||
);
|
||||
// Determine the effective execution config: use the version-specific
|
||||
// override if the executor resolved a specific runtime version for this
|
||||
// action, otherwise fall back to this ProcessRuntime's built-in config.
|
||||
let effective_config: &RuntimeExecutionConfig = context
|
||||
.runtime_config_override
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config);
|
||||
|
||||
if let Some(ref ver) = context.selected_runtime_version {
|
||||
info!(
|
||||
"Executing action '{}' (execution_id: {}) with runtime '{}' version {}, \
|
||||
parameter delivery: {:?}, format: {:?}, output format: {:?}",
|
||||
context.action_ref,
|
||||
context.execution_id,
|
||||
self.runtime_name,
|
||||
ver,
|
||||
context.parameter_delivery,
|
||||
context.parameter_format,
|
||||
context.output_format,
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"Executing action '{}' (execution_id: {}) with runtime '{}', \
|
||||
parameter delivery: {:?}, format: {:?}, output format: {:?}",
|
||||
context.action_ref,
|
||||
context.execution_id,
|
||||
self.runtime_name,
|
||||
context.parameter_delivery,
|
||||
context.parameter_format,
|
||||
context.output_format,
|
||||
);
|
||||
}
|
||||
|
||||
let pack_ref = self.extract_pack_ref(&context.action_ref);
|
||||
let pack_dir = self.packs_base_dir.join(pack_ref);
|
||||
|
||||
// Compute external env_dir for this pack/runtime combination.
|
||||
// Pattern: {runtime_envs_dir}/{pack_ref}/{runtime_name}
|
||||
let env_dir = self.env_dir_for_pack(pack_ref);
|
||||
let env_dir_opt = if self.config.environment.is_some() {
|
||||
// When a specific runtime version is selected, the env dir includes a
|
||||
// version suffix (e.g., "python-3.12") for per-version isolation.
|
||||
// Pattern: {runtime_envs_dir}/{pack_ref}/{runtime_name[-version]}
|
||||
let env_dir = if let Some(ref suffix) = context.runtime_env_dir_suffix {
|
||||
self.runtime_envs_dir.join(pack_ref).join(suffix)
|
||||
} else {
|
||||
self.env_dir_for_pack(pack_ref)
|
||||
};
|
||||
let env_dir_opt = if effective_config.environment.is_some() {
|
||||
Some(env_dir.as_path())
|
||||
} else {
|
||||
None
|
||||
@@ -499,7 +528,7 @@ impl Runtime for ProcessRuntime {
|
||||
// (scanning all registered packs) or via pack.registered MQ events when a
|
||||
// new pack is installed. We only log a warning here if the expected
|
||||
// environment directory is missing so operators can investigate.
|
||||
if self.config.environment.is_some() && pack_dir.exists() && !env_dir.exists() {
|
||||
if effective_config.environment.is_some() && pack_dir.exists() && !env_dir.exists() {
|
||||
warn!(
|
||||
"Runtime environment for pack '{}' not found at {}. \
|
||||
The environment should have been created at startup or on pack registration. \
|
||||
@@ -512,8 +541,8 @@ impl Runtime for ProcessRuntime {
|
||||
// If the environment directory exists but contains a broken interpreter
|
||||
// (e.g. broken symlinks from a venv created in a different container),
|
||||
// attempt to recreate it before resolving the interpreter.
|
||||
if self.config.environment.is_some() && env_dir.exists() && pack_dir.exists() {
|
||||
if let Some(ref env_cfg) = self.config.environment {
|
||||
if effective_config.environment.is_some() && env_dir.exists() && pack_dir.exists() {
|
||||
if let Some(ref env_cfg) = effective_config.environment {
|
||||
if let Some(ref interp_template) = env_cfg.interpreter_path {
|
||||
let mut vars = std::collections::HashMap::new();
|
||||
vars.insert("env_dir", env_dir.to_string_lossy().to_string());
|
||||
@@ -550,8 +579,18 @@ impl Runtime for ProcessRuntime {
|
||||
e,
|
||||
);
|
||||
} else {
|
||||
// Recreate the environment
|
||||
match self.setup_pack_environment(&pack_dir, &env_dir).await {
|
||||
// Recreate the environment using a temporary ProcessRuntime
|
||||
// with the effective (possibly version-specific) config.
|
||||
let setup_runtime = ProcessRuntime::new(
|
||||
self.runtime_name.clone(),
|
||||
effective_config.clone(),
|
||||
self.packs_base_dir.clone(),
|
||||
self.runtime_envs_dir.clone(),
|
||||
);
|
||||
match setup_runtime
|
||||
.setup_pack_environment(&pack_dir, &env_dir)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
info!(
|
||||
"Successfully recreated environment for pack '{}' at {}",
|
||||
@@ -575,18 +614,37 @@ impl Runtime for ProcessRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
let interpreter = self.resolve_interpreter(&pack_dir, env_dir_opt);
|
||||
let interpreter = effective_config.resolve_interpreter_with_env(&pack_dir, env_dir_opt);
|
||||
|
||||
info!(
|
||||
"Resolved interpreter: {} (env_dir: {}, env_exists: {}, pack_dir: {})",
|
||||
"Resolved interpreter: {} (env_dir: {}, env_exists: {}, pack_dir: {}, version: {})",
|
||||
interpreter.display(),
|
||||
env_dir.display(),
|
||||
env_dir.exists(),
|
||||
pack_dir.display(),
|
||||
context
|
||||
.selected_runtime_version
|
||||
.as_deref()
|
||||
.unwrap_or("default"),
|
||||
);
|
||||
|
||||
// Prepare environment and parameters according to delivery method
|
||||
let mut env = context.env.clone();
|
||||
|
||||
// Inject runtime-specific environment variables from execution_config.
|
||||
// These are template-based (e.g., NODE_PATH={env_dir}/node_modules) and
|
||||
// resolved against the current pack/env directories.
|
||||
if !effective_config.env_vars.is_empty() {
|
||||
let vars = effective_config.build_template_vars_with_env(&pack_dir, env_dir_opt);
|
||||
for (key, value_template) in &effective_config.env_vars {
|
||||
let resolved = RuntimeExecutionConfig::resolve_template(value_template, &vars);
|
||||
debug!(
|
||||
"Setting runtime env var: {}={} (template: {})",
|
||||
key, resolved, value_template
|
||||
);
|
||||
env.insert(key.clone(), resolved);
|
||||
}
|
||||
}
|
||||
let param_config = ParameterDeliveryConfig {
|
||||
delivery: context.parameter_delivery,
|
||||
format: context.parameter_format,
|
||||
@@ -614,7 +672,7 @@ impl Runtime for ProcessRuntime {
|
||||
debug!("Executing file: {}", code_path.display());
|
||||
process_executor::build_action_command(
|
||||
&interpreter,
|
||||
&self.config.interpreter.args,
|
||||
&effective_config.interpreter.args,
|
||||
code_path,
|
||||
working_dir,
|
||||
&env,
|
||||
@@ -635,7 +693,7 @@ impl Runtime for ProcessRuntime {
|
||||
debug!("Executing action file: {}", action_file.display());
|
||||
process_executor::build_action_command(
|
||||
&interpreter,
|
||||
&self.config.interpreter.args,
|
||||
&effective_config.interpreter.args,
|
||||
&action_file,
|
||||
working_dir,
|
||||
&env,
|
||||
@@ -781,6 +839,7 @@ mod tests {
|
||||
},
|
||||
environment: None,
|
||||
dependencies: None,
|
||||
env_vars: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,6 +872,7 @@ mod tests {
|
||||
"{manifest_path}".to_string(),
|
||||
],
|
||||
}),
|
||||
env_vars: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,6 +897,9 @@ mod tests {
|
||||
code: None,
|
||||
code_path: None,
|
||||
runtime_name: Some("python".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 1024,
|
||||
max_stderr_bytes: 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
@@ -868,6 +931,9 @@ mod tests {
|
||||
code: None,
|
||||
code_path: Some(PathBuf::from("/tmp/packs/mypack/actions/hello.py")),
|
||||
runtime_name: None,
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 1024,
|
||||
max_stderr_bytes: 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
@@ -899,6 +965,9 @@ mod tests {
|
||||
code: None,
|
||||
code_path: Some(PathBuf::from("/tmp/packs/mypack/actions/hello.sh")),
|
||||
runtime_name: None,
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 1024,
|
||||
max_stderr_bytes: 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
@@ -986,6 +1055,9 @@ mod tests {
|
||||
code: None,
|
||||
code_path: Some(script_path),
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 1024 * 1024,
|
||||
max_stderr_bytes: 1024 * 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
@@ -1018,6 +1090,7 @@ mod tests {
|
||||
},
|
||||
environment: None,
|
||||
dependencies: None,
|
||||
env_vars: HashMap::new(),
|
||||
};
|
||||
|
||||
let runtime = ProcessRuntime::new(
|
||||
@@ -1039,6 +1112,9 @@ mod tests {
|
||||
code: None,
|
||||
code_path: Some(script_path),
|
||||
runtime_name: Some("python".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 1024 * 1024,
|
||||
max_stderr_bytes: 1024 * 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
@@ -1074,6 +1150,9 @@ mod tests {
|
||||
code: Some("echo 'inline shell code'".to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 1024 * 1024,
|
||||
max_stderr_bytes: 1024 * 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
@@ -1121,6 +1200,9 @@ mod tests {
|
||||
code: None,
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 1024 * 1024,
|
||||
max_stderr_bytes: 1024 * 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
@@ -1226,6 +1308,9 @@ mod tests {
|
||||
code: None,
|
||||
code_path: Some(script_path),
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 1024 * 1024,
|
||||
max_stderr_bytes: 1024 * 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
|
||||
@@ -665,6 +665,9 @@ def run(x, y):
|
||||
),
|
||||
code_path: None,
|
||||
runtime_name: Some("python".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -701,6 +704,9 @@ def run():
|
||||
),
|
||||
code_path: None,
|
||||
runtime_name: Some("python".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -737,6 +743,9 @@ def run():
|
||||
),
|
||||
code_path: None,
|
||||
runtime_name: Some("python".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -786,6 +795,9 @@ def run():
|
||||
),
|
||||
code_path: None,
|
||||
runtime_name: Some("python".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
|
||||
@@ -615,6 +615,9 @@ mod tests {
|
||||
code: Some("echo 'Hello, World!'".to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -648,6 +651,9 @@ mod tests {
|
||||
code: Some("echo \"Hello, $name!\"".to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -676,6 +682,9 @@ mod tests {
|
||||
code: Some("sleep 10".to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -706,6 +715,9 @@ mod tests {
|
||||
code: Some("exit 1".to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -751,6 +763,9 @@ echo "missing=$missing"
|
||||
),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -791,6 +806,9 @@ echo '{"id": 3, "name": "Charlie"}'
|
||||
),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -854,6 +872,9 @@ printf '{"status_code":200,"body":"hello","json":{\n "args": {\n "hello": "w
|
||||
),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
@@ -906,6 +927,9 @@ echo '{"result": "success", "count": 42}'
|
||||
),
|
||||
code_path: None,
|
||||
runtime_name: Some("shell".to_string()),
|
||||
runtime_config_override: None,
|
||||
runtime_env_dir_suffix: None,
|
||||
selected_runtime_version: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
|
||||
Reference in New Issue
Block a user