working out the worker/execution interface
This commit is contained in:
@@ -7,6 +7,7 @@ pub mod dependency;
|
||||
pub mod local;
|
||||
pub mod log_writer;
|
||||
pub mod native;
|
||||
pub mod parameter_passing;
|
||||
pub mod python;
|
||||
pub mod python_venv;
|
||||
pub mod shell;
|
||||
@@ -18,6 +19,7 @@ pub use python::PythonRuntime;
|
||||
pub use shell::ShellRuntime;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use attune_common::models::{ParameterDelivery, ParameterFormat};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
@@ -29,6 +31,7 @@ pub use dependency::{
|
||||
DependencySpec, EnvironmentInfo,
|
||||
};
|
||||
pub use log_writer::{BoundedLogResult, BoundedLogWriter};
|
||||
pub use parameter_passing::{ParameterDeliveryConfig, PreparedParameters};
|
||||
pub use python_venv::PythonVenvManager;
|
||||
|
||||
/// Runtime execution result
|
||||
@@ -108,6 +111,14 @@ pub struct ExecutionContext {
|
||||
/// Maximum stderr size in bytes (for log truncation)
|
||||
#[serde(default = "default_max_log_bytes")]
|
||||
pub max_stderr_bytes: usize,
|
||||
|
||||
/// How parameters should be delivered to the action
|
||||
#[serde(default)]
|
||||
pub parameter_delivery: ParameterDelivery,
|
||||
|
||||
/// Format for parameter serialization
|
||||
#[serde(default)]
|
||||
pub parameter_format: ParameterFormat,
|
||||
}
|
||||
|
||||
fn default_max_log_bytes() -> usize {
|
||||
@@ -133,6 +144,8 @@ impl ExecutionContext {
|
||||
runtime_name: None,
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: ParameterDelivery::default(),
|
||||
parameter_format: ParameterFormat::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
//! This runtime is used for Rust binaries and other compiled executables.
|
||||
|
||||
use super::{
|
||||
parameter_passing::{self, ParameterDeliveryConfig},
|
||||
BoundedLogWriter, ExecutionContext, ExecutionResult, Runtime, RuntimeError, RuntimeResult,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::time::Instant;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Native runtime for executing compiled binaries
|
||||
@@ -35,11 +37,11 @@ impl NativeRuntime {
|
||||
/// Execute a native binary with parameters and environment variables
|
||||
async fn execute_binary(
|
||||
&self,
|
||||
binary_path: std::path::PathBuf,
|
||||
parameters: &std::collections::HashMap<String, serde_json::Value>,
|
||||
binary_path: PathBuf,
|
||||
secrets: &std::collections::HashMap<String, String>,
|
||||
env: &std::collections::HashMap<String, String>,
|
||||
exec_timeout: Option<u64>,
|
||||
parameters_stdin: Option<&str>,
|
||||
timeout: Option<u64>,
|
||||
max_stdout_bytes: usize,
|
||||
max_stderr_bytes: usize,
|
||||
) -> RuntimeResult<ExecutionResult> {
|
||||
@@ -76,22 +78,11 @@ impl NativeRuntime {
|
||||
cmd.current_dir(work_dir);
|
||||
}
|
||||
|
||||
// Add environment variables
|
||||
// Add environment variables (including parameter delivery metadata)
|
||||
for (key, value) in env {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
// Add parameters as environment variables with ATTUNE_ACTION_ prefix
|
||||
for (key, value) in parameters {
|
||||
let value_str = match value {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
serde_json::Value::Number(n) => n.to_string(),
|
||||
serde_json::Value::Bool(b) => b.to_string(),
|
||||
_ => serde_json::to_string(value)?,
|
||||
};
|
||||
cmd.env(format!("ATTUNE_ACTION_{}", key.to_uppercase()), value_str);
|
||||
}
|
||||
|
||||
// Configure stdio
|
||||
cmd.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
@@ -102,29 +93,42 @@ impl NativeRuntime {
|
||||
.spawn()
|
||||
.map_err(|e| RuntimeError::ExecutionFailed(format!("Failed to spawn binary: {}", e)))?;
|
||||
|
||||
// Write secrets to stdin - if this fails, the process has already started
|
||||
// so we should continue and capture whatever output we can
|
||||
let stdin_write_error = if !secrets.is_empty() {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
// Write to stdin - parameters (if using stdin delivery) and/or secrets
|
||||
// If this fails, the process has already started, so we continue and capture output
|
||||
let stdin_write_error = if let Some(mut stdin) = child.stdin.take() {
|
||||
let mut error = None;
|
||||
|
||||
// Write parameters first if using stdin delivery
|
||||
if let Some(params_data) = parameters_stdin {
|
||||
if let Err(e) = stdin.write_all(params_data.as_bytes()).await {
|
||||
error = Some(format!("Failed to write parameters to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n---ATTUNE_PARAMS_END---\n").await {
|
||||
error = Some(format!("Failed to write parameter delimiter: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// Write secrets as JSON (always, for backward compatibility)
|
||||
if error.is_none() && !secrets.is_empty() {
|
||||
match serde_json::to_string(secrets) {
|
||||
Ok(secrets_json) => {
|
||||
if let Err(e) = stdin.write_all(secrets_json.as_bytes()).await {
|
||||
Some(format!("Failed to write secrets to stdin: {}", e))
|
||||
} else if let Err(e) = stdin.shutdown().await {
|
||||
Some(format!("Failed to close stdin: {}", e))
|
||||
} else {
|
||||
None
|
||||
error = Some(format!("Failed to write secrets to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n").await {
|
||||
error = Some(format!("Failed to write newline to stdin: {}", e));
|
||||
}
|
||||
}
|
||||
Err(e) => Some(format!("Failed to serialize secrets: {}", e)),
|
||||
Err(e) => error = Some(format!("Failed to serialize secrets: {}", e)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
// Close stdin
|
||||
if let Err(e) = stdin.shutdown().await {
|
||||
if error.is_none() {
|
||||
error = Some(format!("Failed to close stdin: {}", e));
|
||||
}
|
||||
}
|
||||
error
|
||||
} else {
|
||||
if let Some(stdin) = child.stdin.take() {
|
||||
drop(stdin); // Close stdin if no secrets
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
@@ -184,8 +188,8 @@ impl NativeRuntime {
|
||||
let (stdout_writer, stderr_writer) = tokio::join!(stdout_task, stderr_task);
|
||||
|
||||
// Wait for process with timeout
|
||||
let wait_result = if let Some(timeout_secs) = exec_timeout {
|
||||
match timeout(Duration::from_secs(timeout_secs), child.wait()).await {
|
||||
let wait_result = if let Some(timeout_secs) = timeout {
|
||||
match tokio::time::timeout(Duration::from_secs(timeout_secs), child.wait()).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
@@ -317,10 +321,26 @@ impl Runtime for NativeRuntime {
|
||||
|
||||
async fn execute(&self, context: ExecutionContext) -> RuntimeResult<ExecutionResult> {
|
||||
info!(
|
||||
"Executing native action: {} (execution_id: {})",
|
||||
context.action_ref, context.execution_id
|
||||
"Executing native action: {} (execution_id: {}) with parameter delivery: {:?}, format: {:?}",
|
||||
context.action_ref, context.execution_id, context.parameter_delivery, context.parameter_format
|
||||
);
|
||||
|
||||
// Prepare environment and parameters according to delivery method
|
||||
let mut env = context.env.clone();
|
||||
let config = ParameterDeliveryConfig {
|
||||
delivery: context.parameter_delivery,
|
||||
format: context.parameter_format,
|
||||
};
|
||||
|
||||
let prepared_params = parameter_passing::prepare_parameters(
|
||||
&context.parameters,
|
||||
&mut env,
|
||||
config,
|
||||
)?;
|
||||
|
||||
// Get stdin content if parameters are delivered via stdin
|
||||
let parameters_stdin = prepared_params.stdin_content();
|
||||
|
||||
// Get the binary path
|
||||
let binary_path = context.code_path.ok_or_else(|| {
|
||||
RuntimeError::InvalidAction("Native runtime requires code_path to be set".to_string())
|
||||
@@ -328,9 +348,9 @@ impl Runtime for NativeRuntime {
|
||||
|
||||
self.execute_binary(
|
||||
binary_path,
|
||||
&context.parameters,
|
||||
&context.secrets,
|
||||
&context.env,
|
||||
&env,
|
||||
parameters_stdin,
|
||||
context.timeout,
|
||||
context.max_stdout_bytes,
|
||||
context.max_stderr_bytes,
|
||||
|
||||
320
crates/worker/src/runtime/parameter_passing.rs
Normal file
320
crates/worker/src/runtime/parameter_passing.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
//! Parameter Passing Module
|
||||
//!
|
||||
//! Provides utilities for formatting and delivering action parameters
|
||||
//! in different formats (dotenv, JSON, YAML) via different methods
|
||||
//! (environment variables, stdin, temporary files).
|
||||
|
||||
use attune_common::models::{ParameterDelivery, ParameterFormat};
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
use tracing::debug;
|
||||
|
||||
use super::RuntimeError;
|
||||
|
||||
/// Format parameters according to the specified format
|
||||
pub fn format_parameters(
|
||||
parameters: &HashMap<String, JsonValue>,
|
||||
format: ParameterFormat,
|
||||
) -> Result<String, RuntimeError> {
|
||||
match format {
|
||||
ParameterFormat::Dotenv => format_dotenv(parameters),
|
||||
ParameterFormat::Json => format_json(parameters),
|
||||
ParameterFormat::Yaml => format_yaml(parameters),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format parameters as dotenv (key='value')
|
||||
/// Note: Parameter names are preserved as-is (case-sensitive)
|
||||
fn format_dotenv(parameters: &HashMap<String, JsonValue>) -> Result<String, RuntimeError> {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for (key, value) in parameters {
|
||||
let value_str = value_to_string(value);
|
||||
|
||||
// Escape single quotes in value
|
||||
let escaped_value = value_str.replace('\'', "'\\''");
|
||||
|
||||
lines.push(format!("{}='{}'", key, escaped_value));
|
||||
}
|
||||
|
||||
Ok(lines.join("\n"))
|
||||
}
|
||||
|
||||
/// Format parameters as JSON
|
||||
fn format_json(parameters: &HashMap<String, JsonValue>) -> Result<String, RuntimeError> {
|
||||
serde_json::to_string_pretty(parameters).map_err(|e| {
|
||||
RuntimeError::ExecutionFailed(format!(
|
||||
"Failed to serialize parameters to JSON: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Format parameters as YAML
|
||||
fn format_yaml(parameters: &HashMap<String, JsonValue>) -> Result<String, RuntimeError> {
|
||||
serde_yaml_ng::to_string(parameters).map_err(|e| {
|
||||
RuntimeError::ExecutionFailed(format!(
|
||||
"Failed to serialize parameters to YAML: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert JSON value to string representation
|
||||
fn value_to_string(value: &JsonValue) -> String {
|
||||
match value {
|
||||
JsonValue::String(s) => s.clone(),
|
||||
JsonValue::Number(n) => n.to_string(),
|
||||
JsonValue::Bool(b) => b.to_string(),
|
||||
JsonValue::Null => String::new(),
|
||||
_ => serde_json::to_string(value).unwrap_or_else(|_| String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a temporary file with parameters
|
||||
pub fn create_parameter_file(
|
||||
parameters: &HashMap<String, JsonValue>,
|
||||
format: ParameterFormat,
|
||||
) -> Result<NamedTempFile, RuntimeError> {
|
||||
let formatted = format_parameters(parameters, format)?;
|
||||
|
||||
let mut temp_file = NamedTempFile::new()
|
||||
.map_err(|e| RuntimeError::IoError(e))?;
|
||||
|
||||
// Set restrictive permissions (owner read-only)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = temp_file.as_file().metadata()
|
||||
.map_err(|e| RuntimeError::IoError(e))?
|
||||
.permissions();
|
||||
perms.set_mode(0o400); // Read-only for owner
|
||||
temp_file.as_file().set_permissions(perms)
|
||||
.map_err(|e| RuntimeError::IoError(e))?;
|
||||
}
|
||||
|
||||
temp_file
|
||||
.write_all(formatted.as_bytes())
|
||||
.map_err(|e| RuntimeError::IoError(e))?;
|
||||
|
||||
temp_file
|
||||
.flush()
|
||||
.map_err(|e| RuntimeError::IoError(e))?;
|
||||
|
||||
debug!(
|
||||
"Created parameter file at {:?} with format {:?}",
|
||||
temp_file.path(),
|
||||
format
|
||||
);
|
||||
|
||||
Ok(temp_file)
|
||||
}
|
||||
|
||||
/// Parameter delivery configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParameterDeliveryConfig {
|
||||
pub delivery: ParameterDelivery,
|
||||
pub format: ParameterFormat,
|
||||
}
|
||||
|
||||
/// Prepared parameters ready for execution
|
||||
#[derive(Debug)]
|
||||
pub enum PreparedParameters {
|
||||
/// Parameters are in environment variables
|
||||
Environment,
|
||||
/// Parameters will be passed via stdin
|
||||
Stdin(String),
|
||||
/// Parameters are in a temporary file
|
||||
File {
|
||||
path: PathBuf,
|
||||
#[allow(dead_code)]
|
||||
temp_file: NamedTempFile,
|
||||
},
|
||||
}
|
||||
|
||||
impl PreparedParameters {
|
||||
/// Get the file path if this is file-based delivery
|
||||
pub fn file_path(&self) -> Option<&PathBuf> {
|
||||
match self {
|
||||
PreparedParameters::File { path, .. } => Some(path),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the stdin content if this is stdin-based delivery
|
||||
pub fn stdin_content(&self) -> Option<&str> {
|
||||
match self {
|
||||
PreparedParameters::Stdin(content) => Some(content),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare parameters for delivery according to the specified method and format
|
||||
pub fn prepare_parameters(
|
||||
parameters: &HashMap<String, JsonValue>,
|
||||
env: &mut HashMap<String, String>,
|
||||
config: ParameterDeliveryConfig,
|
||||
) -> Result<PreparedParameters, RuntimeError> {
|
||||
match config.delivery {
|
||||
ParameterDelivery::Stdin => {
|
||||
// Format parameters for stdin
|
||||
let formatted = format_parameters(parameters, config.format)?;
|
||||
|
||||
// Add environment variables to indicate delivery method
|
||||
env.insert(
|
||||
"ATTUNE_PARAMETER_DELIVERY".to_string(),
|
||||
"stdin".to_string(),
|
||||
);
|
||||
env.insert(
|
||||
"ATTUNE_PARAMETER_FORMAT".to_string(),
|
||||
config.format.to_string(),
|
||||
);
|
||||
|
||||
Ok(PreparedParameters::Stdin(formatted))
|
||||
}
|
||||
ParameterDelivery::File => {
|
||||
// Create temporary file with parameters
|
||||
let temp_file = create_parameter_file(parameters, config.format)?;
|
||||
let path = temp_file.path().to_path_buf();
|
||||
|
||||
// Add environment variables to indicate delivery method and file location
|
||||
env.insert(
|
||||
"ATTUNE_PARAMETER_DELIVERY".to_string(),
|
||||
"file".to_string(),
|
||||
);
|
||||
env.insert(
|
||||
"ATTUNE_PARAMETER_FORMAT".to_string(),
|
||||
config.format.to_string(),
|
||||
);
|
||||
env.insert(
|
||||
"ATTUNE_PARAMETER_FILE".to_string(),
|
||||
path.to_string_lossy().to_string(),
|
||||
);
|
||||
|
||||
Ok(PreparedParameters::File { path, temp_file })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_format_dotenv() {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("message".to_string(), json!("Hello, World!"));
|
||||
params.insert("count".to_string(), json!(42));
|
||||
params.insert("enabled".to_string(), json!(true));
|
||||
|
||||
let result = format_dotenv(¶ms).unwrap();
|
||||
|
||||
assert!(result.contains("message='Hello, World!'"));
|
||||
assert!(result.contains("count='42'"));
|
||||
assert!(result.contains("enabled='true'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_dotenv_escaping() {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("message".to_string(), json!("It's a test"));
|
||||
|
||||
let result = format_dotenv(¶ms).unwrap();
|
||||
|
||||
assert!(result.contains("message='It'\\''s a test'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_json() {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("message".to_string(), json!("Hello"));
|
||||
params.insert("count".to_string(), json!(42));
|
||||
|
||||
let result = format_json(¶ms).unwrap();
|
||||
let parsed: HashMap<String, JsonValue> = serde_json::from_str(&result).unwrap();
|
||||
|
||||
assert_eq!(parsed.get("message"), Some(&json!("Hello")));
|
||||
assert_eq!(parsed.get("count"), Some(&json!(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_yaml() {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("message".to_string(), json!("Hello"));
|
||||
params.insert("count".to_string(), json!(42));
|
||||
|
||||
let result = format_yaml(¶ms).unwrap();
|
||||
|
||||
assert!(result.contains("message:"));
|
||||
assert!(result.contains("Hello"));
|
||||
assert!(result.contains("count:"));
|
||||
assert!(result.contains("42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[test]
|
||||
fn test_create_parameter_file() {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("key".to_string(), json!("value"));
|
||||
|
||||
let temp_file = create_parameter_file(¶ms, ParameterFormat::Json).unwrap();
|
||||
let content = std::fs::read_to_string(temp_file.path()).unwrap();
|
||||
|
||||
assert!(content.contains("key"));
|
||||
assert!(content.contains("value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepare_parameters_stdin() {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("test".to_string(), json!("value"));
|
||||
|
||||
let mut env = HashMap::new();
|
||||
let config = ParameterDeliveryConfig {
|
||||
delivery: ParameterDelivery::Stdin,
|
||||
format: ParameterFormat::Json,
|
||||
};
|
||||
|
||||
let result = prepare_parameters(¶ms, &mut env, config).unwrap();
|
||||
|
||||
assert!(matches!(result, PreparedParameters::Stdin(_)));
|
||||
assert_eq!(
|
||||
env.get("ATTUNE_PARAMETER_DELIVERY"),
|
||||
Some(&"stdin".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
env.get("ATTUNE_PARAMETER_FORMAT"),
|
||||
Some(&"json".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepare_parameters_file() {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("test".to_string(), json!("value"));
|
||||
|
||||
let mut env = HashMap::new();
|
||||
let config = ParameterDeliveryConfig {
|
||||
delivery: ParameterDelivery::File,
|
||||
format: ParameterFormat::Yaml,
|
||||
};
|
||||
|
||||
let result = prepare_parameters(¶ms, &mut env, config).unwrap();
|
||||
|
||||
assert!(matches!(result, PreparedParameters::File { .. }));
|
||||
assert_eq!(
|
||||
env.get("ATTUNE_PARAMETER_DELIVERY"),
|
||||
Some(&"file".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
env.get("ATTUNE_PARAMETER_FORMAT"),
|
||||
Some(&"yaml".to_string())
|
||||
);
|
||||
assert!(env.contains_key("ATTUNE_PARAMETER_FILE"));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Executes shell scripts and commands using subprocess execution.
|
||||
|
||||
use super::{
|
||||
parameter_passing::{self, ParameterDeliveryConfig},
|
||||
BoundedLogWriter, ExecutionContext, ExecutionResult, Runtime, RuntimeError, RuntimeResult,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
@@ -53,6 +54,7 @@ impl ShellRuntime {
|
||||
&self,
|
||||
mut cmd: Command,
|
||||
secrets: &std::collections::HashMap<String, String>,
|
||||
parameters_stdin: Option<&str>,
|
||||
timeout_secs: Option<u64>,
|
||||
max_stdout_bytes: usize,
|
||||
max_stderr_bytes: usize,
|
||||
@@ -66,22 +68,36 @@ impl ShellRuntime {
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
// Write secrets to stdin - if this fails, the process has already started
|
||||
// so we should continue and capture whatever output we can
|
||||
// Write to stdin - parameters (if using stdin delivery) and/or secrets
|
||||
// If this fails, the process has already started, so we continue and capture output
|
||||
let stdin_write_error = if let Some(mut stdin) = child.stdin.take() {
|
||||
match serde_json::to_string(secrets) {
|
||||
Ok(secrets_json) => {
|
||||
if let Err(e) = stdin.write_all(secrets_json.as_bytes()).await {
|
||||
Some(format!("Failed to write secrets to stdin: {}", e))
|
||||
} else if let Err(e) = stdin.write_all(b"\n").await {
|
||||
Some(format!("Failed to write newline to stdin: {}", e))
|
||||
} else {
|
||||
drop(stdin);
|
||||
None
|
||||
}
|
||||
let mut error = None;
|
||||
|
||||
// Write parameters first if using stdin delivery
|
||||
if let Some(params_data) = parameters_stdin {
|
||||
if let Err(e) = stdin.write_all(params_data.as_bytes()).await {
|
||||
error = Some(format!("Failed to write parameters to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n---ATTUNE_PARAMS_END---\n").await {
|
||||
error = Some(format!("Failed to write parameter delimiter: {}", e));
|
||||
}
|
||||
Err(e) => Some(format!("Failed to serialize secrets: {}", e)),
|
||||
}
|
||||
|
||||
// Write secrets as JSON (always, for backward compatibility)
|
||||
if error.is_none() && !secrets.is_empty() {
|
||||
match serde_json::to_string(secrets) {
|
||||
Ok(secrets_json) => {
|
||||
if let Err(e) = stdin.write_all(secrets_json.as_bytes()).await {
|
||||
error = Some(format!("Failed to write secrets to stdin: {}", e));
|
||||
} else if let Err(e) = stdin.write_all(b"\n").await {
|
||||
error = Some(format!("Failed to write newline to stdin: {}", e));
|
||||
}
|
||||
}
|
||||
Err(e) => error = Some(format!("Failed to serialize secrets: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
drop(stdin);
|
||||
error
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -315,9 +331,10 @@ impl ShellRuntime {
|
||||
/// Execute shell script directly
|
||||
async fn execute_shell_code(
|
||||
&self,
|
||||
script: String,
|
||||
code: String,
|
||||
secrets: &std::collections::HashMap<String, String>,
|
||||
env: &std::collections::HashMap<String, String>,
|
||||
parameters_stdin: Option<&str>,
|
||||
timeout_secs: Option<u64>,
|
||||
max_stdout_bytes: usize,
|
||||
max_stderr_bytes: usize,
|
||||
@@ -329,7 +346,7 @@ impl ShellRuntime {
|
||||
|
||||
// Build command
|
||||
let mut cmd = Command::new(&self.shell_path);
|
||||
cmd.arg("-c").arg(&script);
|
||||
cmd.arg("-c").arg(&code);
|
||||
|
||||
// Add environment variables
|
||||
for (key, value) in env {
|
||||
@@ -339,6 +356,7 @@ impl ShellRuntime {
|
||||
self.execute_with_streaming(
|
||||
cmd,
|
||||
secrets,
|
||||
parameters_stdin,
|
||||
timeout_secs,
|
||||
max_stdout_bytes,
|
||||
max_stderr_bytes,
|
||||
@@ -349,22 +367,23 @@ impl ShellRuntime {
|
||||
/// Execute shell script from file
|
||||
async fn execute_shell_file(
|
||||
&self,
|
||||
code_path: PathBuf,
|
||||
script_path: PathBuf,
|
||||
secrets: &std::collections::HashMap<String, String>,
|
||||
env: &std::collections::HashMap<String, String>,
|
||||
parameters_stdin: Option<&str>,
|
||||
timeout_secs: Option<u64>,
|
||||
max_stdout_bytes: usize,
|
||||
max_stderr_bytes: usize,
|
||||
) -> RuntimeResult<ExecutionResult> {
|
||||
debug!(
|
||||
"Executing shell file: {:?} with {} secrets",
|
||||
code_path,
|
||||
script_path,
|
||||
secrets.len()
|
||||
);
|
||||
|
||||
// Build command
|
||||
let mut cmd = Command::new(&self.shell_path);
|
||||
cmd.arg(&code_path);
|
||||
cmd.arg(&script_path);
|
||||
|
||||
// Add environment variables
|
||||
for (key, value) in env {
|
||||
@@ -374,6 +393,7 @@ impl ShellRuntime {
|
||||
self.execute_with_streaming(
|
||||
cmd,
|
||||
secrets,
|
||||
parameters_stdin,
|
||||
timeout_secs,
|
||||
max_stdout_bytes,
|
||||
max_stderr_bytes,
|
||||
@@ -412,29 +432,49 @@ impl Runtime for ShellRuntime {
|
||||
|
||||
async fn execute(&self, context: ExecutionContext) -> RuntimeResult<ExecutionResult> {
|
||||
info!(
|
||||
"Executing shell action: {} (execution_id: {})",
|
||||
context.action_ref, context.execution_id
|
||||
"Executing shell action: {} (execution_id: {}) with parameter delivery: {:?}, format: {:?}",
|
||||
context.action_ref, context.execution_id, context.parameter_delivery, context.parameter_format
|
||||
);
|
||||
info!(
|
||||
"Action parameters (count: {}): {:?}",
|
||||
context.parameters.len(),
|
||||
context.parameters
|
||||
);
|
||||
|
||||
// Prepare environment and parameters according to delivery method
|
||||
let mut env = context.env.clone();
|
||||
let config = ParameterDeliveryConfig {
|
||||
delivery: context.parameter_delivery,
|
||||
format: context.parameter_format,
|
||||
};
|
||||
|
||||
let prepared_params = parameter_passing::prepare_parameters(
|
||||
&context.parameters,
|
||||
&mut env,
|
||||
config,
|
||||
)?;
|
||||
|
||||
// Get stdin content if parameters are delivered via stdin
|
||||
let parameters_stdin = prepared_params.stdin_content();
|
||||
|
||||
if let Some(stdin_data) = parameters_stdin {
|
||||
info!(
|
||||
"Parameters to be sent via stdin (length: {} bytes):\n{}",
|
||||
stdin_data.len(),
|
||||
stdin_data
|
||||
);
|
||||
} else {
|
||||
info!("No parameters will be sent via stdin");
|
||||
}
|
||||
|
||||
// If code_path is provided, execute the file directly
|
||||
if let Some(code_path) = &context.code_path {
|
||||
// Merge parameters into environment variables with ATTUNE_ACTION_ prefix
|
||||
let mut env = context.env.clone();
|
||||
for (key, value) in &context.parameters {
|
||||
let value_str = match value {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
serde_json::Value::Number(n) => n.to_string(),
|
||||
serde_json::Value::Bool(b) => b.to_string(),
|
||||
_ => serde_json::to_string(value)?,
|
||||
};
|
||||
env.insert(format!("ATTUNE_ACTION_{}", key.to_uppercase()), value_str);
|
||||
}
|
||||
|
||||
return self
|
||||
.execute_shell_file(
|
||||
code_path.clone(),
|
||||
&context.secrets,
|
||||
&env,
|
||||
parameters_stdin,
|
||||
context.timeout,
|
||||
context.max_stdout_bytes,
|
||||
context.max_stderr_bytes,
|
||||
@@ -447,7 +487,8 @@ impl Runtime for ShellRuntime {
|
||||
self.execute_shell_code(
|
||||
script,
|
||||
&context.secrets,
|
||||
&context.env,
|
||||
&env,
|
||||
parameters_stdin,
|
||||
context.timeout,
|
||||
context.max_stdout_bytes,
|
||||
context.max_stderr_bytes,
|
||||
@@ -534,6 +575,8 @@ mod tests {
|
||||
runtime_name: Some("shell".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
parameter_format: attune_common::models::ParameterFormat::default(),
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
@@ -564,6 +607,8 @@ mod tests {
|
||||
runtime_name: Some("shell".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
parameter_format: attune_common::models::ParameterFormat::default(),
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
@@ -589,6 +634,8 @@ mod tests {
|
||||
runtime_name: Some("shell".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
parameter_format: attune_common::models::ParameterFormat::default(),
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
@@ -616,6 +663,8 @@ mod tests {
|
||||
runtime_name: Some("shell".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
parameter_format: attune_common::models::ParameterFormat::default(),
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
@@ -658,6 +707,8 @@ echo "missing=$missing"
|
||||
runtime_name: Some("shell".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
parameter_format: attune_common::models::ParameterFormat::default(),
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user