clusterflux-public/crates/clusterflux-node/src/lib.rs
Clusterflux release dry run 6f52bb46cd Public dry run dryrun-a43e907efd9d
Source commit: a43e907efd9d1561c23fe73499478e881f868355

Public tree identity: sha256:453dea30195485dd8939f575a69b39f4bb7acd84c4df23f8aa29b55d044f2673
2026-07-15 01:54:51 +02:00

1007 lines
36 KiB
Rust

use std::path::PathBuf;
use clusterflux_core::{
Capability, CommandBackendKind, CommandInvocation, CommandPlan, Digest, EnvironmentKind,
GuestRuntimeKind, ProcessId, TaskInstanceId, VfsObject, VfsOverlay, VfsPath,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
mod command_runner;
mod windows_dev;
pub use clusterflux_wasm_runtime::{
WasmDebugControl, WasmTaskError, WasmTaskHost, WasmtimeDebugProbe, WasmtimeTaskRuntime,
};
use command_runner::capture_command_logs;
pub use command_runner::{
authorize_node_command, CapturedCommandLogs, CommandOutput, LocalCommandExecutor,
VirtualThreadCommand, DEFAULT_COMMAND_LOG_LIMIT_BYTES,
};
pub use windows_dev::{WindowsCommandDevBackend, WindowsSandboxStubBackend};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MaterializedEnvironment {
pub name: String,
pub backend: CommandBackendKind,
pub local_reference: String,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum BackendError {
#[error("native command denied: {0}")]
Denied(String),
#[error("environment is required for this backend")]
MissingEnvironment,
#[error("command failed to execute: {0}")]
Command(String),
#[error("task execution cancelled: {0}")]
Cancelled(String),
#[error("artifact staging failed: {0}")]
Artifact(String),
#[error("unsupported environment kind for backend")]
UnsupportedEnvironment,
#[error("node cannot freeze task `{task}` for the current debug epoch")]
DebugFreezeUnsupported { task: TaskInstanceId },
}
pub trait CommandBackend {
fn kind(&self) -> CommandBackendKind;
fn plan(&self, invocation: &CommandInvocation) -> Result<CommandPlan, BackendError>;
}
#[derive(Clone, Debug, Default)]
pub struct LinuxRootlessPodmanBackend;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PodmanCommand {
pub program: String,
pub args: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProcessOutput {
pub status_code: Option<i32>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
pub trait ProcessRunner {
fn run(&mut self, command: &PodmanCommand) -> Result<ProcessOutput, BackendError>;
}
#[derive(Clone, Debug, Default)]
pub struct StdProcessRunner;
impl ProcessRunner for StdProcessRunner {
fn run(&mut self, command: &PodmanCommand) -> Result<ProcessOutput, BackendError> {
let output = std::process::Command::new(&command.program)
.args(&command.args)
.output()
.map_err(|err| BackendError::Command(format!("{err:#}")))?;
Ok(ProcessOutput {
status_code: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PodmanEnvironmentMaterialization {
pub environment: String,
pub image_tag: String,
pub build: PodmanCommand,
pub rootless_user_podman: bool,
pub embeds_full_image_in_bundle: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalSourceCheckout {
pub host_path: PathBuf,
pub snapshot: Digest,
}
#[derive(Clone, Debug)]
pub struct LocalCheckoutTaskRequest<'a> {
pub process: ProcessId,
pub virtual_thread: TaskInstanceId,
pub invocation: &'a CommandInvocation,
pub checkout: LocalSourceCheckout,
pub output_root: PathBuf,
pub stage_stdout_as: Option<VfsPath>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SourceAccessMode {
LocalCheckoutBindMount {
host_path: PathBuf,
container_path: String,
read_only: bool,
snapshot: Digest,
},
NodePreparedSnapshot {
node_path: PathBuf,
container_path: String,
snapshot: Digest,
},
}
impl SourceAccessMode {
pub fn uses_full_repo_tarball(&self) -> bool {
false
}
pub fn coordinator_routed_file_reads(&self) -> bool {
false
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum LinuxTaskState {
Running,
Frozen,
Cancelled,
Completed,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinuxTaskLifecycle {
pub process: ProcessId,
pub virtual_thread: TaskInstanceId,
pub log_stream: String,
pub cancellation_token: String,
pub debug_participant: String,
pub freeze_supported: bool,
pub state: LinuxTaskState,
}
impl LinuxTaskLifecycle {
pub fn new(process: ProcessId, virtual_thread: TaskInstanceId) -> Self {
Self {
log_stream: format!("process/{process}/task/{virtual_thread}/logs"),
cancellation_token: format!("cancel:{process}:{virtual_thread}"),
debug_participant: format!("debug:{process}:{virtual_thread}"),
process,
virtual_thread,
freeze_supported: true,
state: LinuxTaskState::Running,
}
}
pub fn freeze_for_debug_epoch(&mut self) -> Result<(), BackendError> {
if !self.freeze_supported {
return Err(BackendError::DebugFreezeUnsupported {
task: self.virtual_thread.clone(),
});
}
self.state = LinuxTaskState::Frozen;
Ok(())
}
pub fn resume_after_debug_epoch(&mut self) {
if self.state == LinuxTaskState::Frozen {
self.state = LinuxTaskState::Running;
}
}
pub fn cancel(&mut self) {
self.state = LinuxTaskState::Cancelled;
}
pub fn complete(&mut self) {
self.state = LinuxTaskState::Completed;
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinuxCommandRunPlan {
pub process: ProcessId,
pub virtual_thread: TaskInstanceId,
pub image_tag: String,
pub run: PodmanCommand,
pub source_access: SourceAccessMode,
pub output_root: PathBuf,
pub stage_stdout_as: Option<VfsPath>,
pub uses_full_repo_tarball: bool,
pub coordinator_routed_file_reads: bool,
pub lifecycle: LinuxTaskLifecycle,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinuxCommandTaskOutput {
pub virtual_thread: TaskInstanceId,
pub status_code: Option<i32>,
pub stdout: String,
pub stderr: String,
pub stdout_truncated: bool,
pub stderr_truncated: bool,
pub log_backpressured: bool,
pub staged_artifact: Option<VfsObject>,
pub lifecycle: LinuxTaskLifecycle,
}
impl LinuxRootlessPodmanBackend {
pub fn materialize_environment(
&self,
env: &clusterflux_core::EnvironmentResource,
) -> Result<PodmanEnvironmentMaterialization, BackendError> {
match env.kind {
EnvironmentKind::Containerfile | EnvironmentKind::Dockerfile => {}
EnvironmentKind::NixFlake => return Err(BackendError::UnsupportedEnvironment),
}
let image_tag = self.image_tag(env);
Ok(PodmanEnvironmentMaterialization {
environment: env.name.clone(),
image_tag: image_tag.clone(),
build: PodmanCommand {
program: "podman".to_owned(),
args: vec![
"build".to_owned(),
"--pull=missing".to_owned(),
"--tag".to_owned(),
image_tag,
"--file".to_owned(),
env.recipe_path.to_string_lossy().into_owned(),
env.context_path.to_string_lossy().into_owned(),
],
},
rootless_user_podman: true,
embeds_full_image_in_bundle: false,
})
}
pub fn plan_local_checkout_run(
&self,
process: ProcessId,
virtual_thread: TaskInstanceId,
invocation: &CommandInvocation,
checkout: LocalSourceCheckout,
output_root: PathBuf,
stage_stdout_as: Option<VfsPath>,
) -> Result<LinuxCommandRunPlan, BackendError> {
let env = invocation
.env
.as_ref()
.ok_or(BackendError::MissingEnvironment)?;
let materialization = self.materialize_environment(env)?;
let source_access = SourceAccessMode::LocalCheckoutBindMount {
host_path: checkout.host_path.clone(),
container_path: "/workspace".to_owned(),
read_only: true,
snapshot: checkout.snapshot,
};
let lifecycle = LinuxTaskLifecycle::new(process.clone(), virtual_thread.clone());
let network = match invocation.network {
clusterflux_core::CommandNetworkPolicy::Disabled => "none",
clusterflux_core::CommandNetworkPolicy::Enabled => "slirp4netns",
};
let mut args = vec![
"run".to_owned(),
"--rm".to_owned(),
"--network".to_owned(),
network.to_owned(),
"--cpus".to_owned(),
"2".to_owned(),
"--memory".to_owned(),
"2g".to_owned(),
"--pids-limit".to_owned(),
"256".to_owned(),
"--security-opt".to_owned(),
"no-new-privileges".to_owned(),
"--cap-drop".to_owned(),
"all".to_owned(),
"--volume".to_owned(),
format!("{}:/workspace:ro,Z", checkout.host_path.to_string_lossy()),
"--volume".to_owned(),
format!("{}:/clusterflux/output:rw,Z", output_root.to_string_lossy()),
"--env".to_owned(),
"CARGO_TARGET_DIR=/clusterflux/output/target".to_owned(),
"--workdir".to_owned(),
invocation.working_directory.clone(),
];
for (name, value) in &invocation.environment_variables {
args.push("--env".to_owned());
args.push(format!("{name}={value}"));
}
args.push(materialization.image_tag.clone());
args.push(invocation.program.clone());
args.extend(invocation.args.iter().cloned());
Ok(LinuxCommandRunPlan {
process,
virtual_thread,
image_tag: materialization.image_tag,
run: PodmanCommand {
program: "podman".to_owned(),
args,
},
source_access,
output_root,
stage_stdout_as,
uses_full_repo_tarball: false,
coordinator_routed_file_reads: false,
lifecycle,
})
}
pub fn execute_environment_materialization(
&self,
env: &clusterflux_core::EnvironmentResource,
runner: &mut impl ProcessRunner,
) -> Result<MaterializedEnvironment, BackendError> {
let materialization = self.materialize_environment(env)?;
let output = runner.run(&materialization.build)?;
if output.status_code != Some(0) {
return Err(BackendError::Command(format!(
"podman build for environment `{}` failed with status {:?}: {}",
materialization.environment,
output.status_code,
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(MaterializedEnvironment {
name: materialization.environment,
backend: CommandBackendKind::LinuxRootlessPodman,
local_reference: materialization.image_tag,
})
}
pub fn execute_run_plan(
&self,
plan: LinuxCommandRunPlan,
runner: &mut impl ProcessRunner,
overlay: &mut VfsOverlay,
) -> Result<LinuxCommandTaskOutput, BackendError> {
self.execute_run_plan_with_log_limit(plan, runner, overlay, DEFAULT_COMMAND_LOG_LIMIT_BYTES)
}
pub fn execute_run_plan_with_log_limit(
&self,
mut plan: LinuxCommandRunPlan,
runner: &mut impl ProcessRunner,
overlay: &mut VfsOverlay,
max_log_bytes: usize,
) -> Result<LinuxCommandTaskOutput, BackendError> {
let output = runner.run(&plan.run)?;
let logs = capture_command_logs(
&plan.virtual_thread,
&output.stdout,
&output.stderr,
max_log_bytes,
);
let staged_artifact = if output.status_code == Some(0) {
if let Some(path) = plan.stage_stdout_as.take() {
Some(overlay.write(
path,
Digest::sha256(&output.stdout),
output.stdout.len() as u64,
))
} else {
None
}
} else {
None
};
if output.status_code == Some(0) {
plan.lifecycle.complete();
}
Ok(LinuxCommandTaskOutput {
virtual_thread: plan.virtual_thread,
status_code: output.status_code,
stdout: logs.stdout,
stderr: logs.stderr,
stdout_truncated: logs.stdout_truncated,
stderr_truncated: logs.stderr_truncated,
log_backpressured: logs.backpressured,
staged_artifact,
lifecycle: plan.lifecycle,
})
}
pub fn execute_local_checkout_task(
&self,
request: LocalCheckoutTaskRequest<'_>,
runner: &mut impl ProcessRunner,
overlay: &mut VfsOverlay,
) -> Result<LinuxCommandTaskOutput, BackendError> {
let env = request
.invocation
.env
.as_ref()
.ok_or(BackendError::MissingEnvironment)?;
self.execute_environment_materialization(env, runner)?;
let plan = self.plan_local_checkout_run(
request.process,
request.virtual_thread,
request.invocation,
request.checkout,
request.output_root,
request.stage_stdout_as,
)?;
self.execute_run_plan(plan, runner, overlay)
}
fn image_tag(&self, env: &clusterflux_core::EnvironmentResource) -> String {
let name = env
.name
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
ch
} else {
'-'
}
})
.collect::<String>();
let digest = env
.digest
.as_str()
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.' {
ch
} else {
'-'
}
})
.take(24)
.collect::<String>();
format!("clusterflux-env/{name}:{digest}")
}
}
impl CommandBackend for LinuxRootlessPodmanBackend {
fn kind(&self) -> CommandBackendKind {
CommandBackendKind::LinuxRootlessPodman
}
fn plan(&self, invocation: &CommandInvocation) -> Result<CommandPlan, BackendError> {
let env = invocation
.env
.as_ref()
.ok_or(BackendError::MissingEnvironment)?;
match env.kind {
EnvironmentKind::Containerfile | EnvironmentKind::Dockerfile => Ok(CommandPlan {
guest_runtime: GuestRuntimeKind::Wasmtime,
backend: CommandBackendKind::LinuxRootlessPodman,
required_capability: Capability::RootlessPodman,
user_attached_development_execution: false,
}),
EnvironmentKind::NixFlake => Err(BackendError::UnsupportedEnvironment),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::path::PathBuf;
use clusterflux_core::{
DebugRuntimeState, Digest, EnvironmentRequirements, EnvironmentResource, NodeId, ProjectId,
TenantId,
};
use super::*;
#[derive(Default)]
struct RecordingRunner {
commands: Vec<PodmanCommand>,
outputs: VecDeque<ProcessOutput>,
}
impl RecordingRunner {
fn with_outputs(outputs: impl IntoIterator<Item = ProcessOutput>) -> Self {
Self {
commands: Vec::new(),
outputs: outputs.into_iter().collect(),
}
}
}
impl ProcessRunner for RecordingRunner {
fn run(&mut self, command: &PodmanCommand) -> Result<ProcessOutput, BackendError> {
self.commands.push(command.clone());
self.outputs.pop_front().ok_or_else(|| {
BackendError::Command("recording runner has no output queued".to_owned())
})
}
}
fn success_output(stdout: impl Into<Vec<u8>>) -> ProcessOutput {
ProcessOutput {
status_code: Some(0),
stdout: stdout.into(),
stderr: Vec::new(),
}
}
fn container_env() -> EnvironmentResource {
EnvironmentResource {
name: "linux".to_owned(),
kind: EnvironmentKind::Containerfile,
recipe_path: PathBuf::from("envs/linux/Containerfile"),
context_path: PathBuf::from("envs/linux"),
digest: Digest::sha256("recipe"),
requirements: EnvironmentRequirements::linux_container(),
}
}
#[test]
fn linux_backend_plans_rootless_podman_under_wasmtime_virtual_task() {
let invocation = CommandInvocation {
program: "cargo".to_owned(),
args: vec!["build".to_owned()],
working_directory: "/workspace".to_owned(),
environment_variables: Default::default(),
timeout_ms: 60_000,
network: clusterflux_core::CommandNetworkPolicy::Disabled,
env: Some(container_env()),
};
let plan = LinuxRootlessPodmanBackend.plan(&invocation).unwrap();
assert_eq!(plan.guest_runtime, GuestRuntimeKind::Wasmtime);
assert_eq!(plan.backend, CommandBackendKind::LinuxRootlessPodman);
assert_eq!(plan.required_capability, Capability::RootlessPodman);
}
#[test]
fn linux_backend_materializes_containerfile_with_rootless_podman_without_vendored_image() {
let env = container_env();
let materialization = LinuxRootlessPodmanBackend
.materialize_environment(&env)
.unwrap();
assert_eq!(materialization.environment, "linux");
assert!(materialization
.image_tag
.starts_with("clusterflux-env/linux:"));
assert_eq!(materialization.image_tag.matches(':').count(), 1);
assert_eq!(materialization.build.program, "podman");
assert!(materialization.rootless_user_podman);
assert!(!materialization.embeds_full_image_in_bundle);
assert!(materialization.build.args.contains(&"build".to_owned()));
assert!(materialization
.build
.args
.contains(&"--pull=missing".to_owned()));
assert!(materialization
.build
.args
.contains(&"envs/linux/Containerfile".to_owned()));
assert!(materialization
.build
.args
.contains(&"envs/linux".to_owned()));
}
#[test]
fn linux_run_plan_keeps_local_checkout_local_and_avoids_coordinator_file_reads() {
let invocation = CommandInvocation {
program: "cargo".to_owned(),
args: vec!["build".to_owned(), "--release".to_owned()],
working_directory: "/workspace/crate".to_owned(),
environment_variables: std::collections::BTreeMap::from([(
"BUILD_MODE".to_owned(),
"release".to_owned(),
)]),
timeout_ms: 60_000,
network: clusterflux_core::CommandNetworkPolicy::Disabled,
env: Some(container_env()),
};
let plan = LinuxRootlessPodmanBackend
.plan_local_checkout_run(
ProcessId::from("vp"),
TaskInstanceId::from("compile-linux"),
&invocation,
LocalSourceCheckout {
host_path: PathBuf::from("/work/example"),
snapshot: Digest::sha256("checkout"),
},
PathBuf::from("/work/output"),
Some(VfsPath::new("/vfs/artifacts/app").unwrap()),
)
.unwrap();
assert_eq!(plan.run.program, "podman");
assert!(plan.run.args.contains(&"run".to_owned()));
assert!(plan.run.args.contains(&"--network".to_owned()));
assert!(plan.run.args.contains(&"none".to_owned()));
assert!(plan.run.args.contains(&"--cpus".to_owned()));
assert!(plan.run.args.contains(&"--memory".to_owned()));
assert!(plan.run.args.contains(&"--pids-limit".to_owned()));
assert!(plan.run.args.contains(&"no-new-privileges".to_owned()));
assert!(plan.run.args.contains(&"all".to_owned()));
assert!(plan.run.args.contains(&"/workspace/crate".to_owned()));
assert!(plan.run.args.contains(&"BUILD_MODE=release".to_owned()));
assert!(plan
.run
.args
.contains(&"/work/example:/workspace:ro,Z".to_owned()));
assert!(plan.run.args.contains(&"cargo".to_owned()));
assert!(plan.run.args.contains(&"--release".to_owned()));
assert!(!plan.uses_full_repo_tarball);
assert!(!plan.coordinator_routed_file_reads);
assert!(!plan.source_access.uses_full_repo_tarball());
assert!(!plan.source_access.coordinator_routed_file_reads());
assert_eq!(
plan.stage_stdout_as,
Some(VfsPath::new("/vfs/artifacts/app").unwrap())
);
assert_eq!(plan.lifecycle.process, ProcessId::from("vp"));
assert_eq!(
plan.lifecycle.virtual_thread,
TaskInstanceId::from("compile-linux")
);
assert_eq!(plan.lifecycle.state, LinuxTaskState::Running);
match &plan.source_access {
SourceAccessMode::LocalCheckoutBindMount {
host_path,
container_path,
read_only,
..
} => {
assert_eq!(host_path, &PathBuf::from("/work/example"));
assert_eq!(container_path, "/workspace");
assert!(*read_only);
}
SourceAccessMode::NodePreparedSnapshot { .. } => {
panic!("local Linux build should use the node-local checkout")
}
}
}
#[test]
fn linux_backend_executes_podman_build_then_run_and_stages_artifact() {
let invocation = CommandInvocation {
program: "cargo".to_owned(),
args: vec!["build".to_owned()],
working_directory: "/workspace".to_owned(),
environment_variables: Default::default(),
timeout_ms: 60_000,
network: clusterflux_core::CommandNetworkPolicy::Disabled,
env: Some(container_env()),
};
let mut runner =
RecordingRunner::with_outputs([success_output([]), success_output(b"artifact-bytes")]);
let mut overlay =
VfsOverlay::new(TaskInstanceId::from("compile-linux"), NodeId::from("node"));
let output = LinuxRootlessPodmanBackend
.execute_local_checkout_task(
LocalCheckoutTaskRequest {
process: ProcessId::from("vp"),
virtual_thread: TaskInstanceId::from("compile-linux"),
invocation: &invocation,
checkout: LocalSourceCheckout {
host_path: PathBuf::from("/work/demo"),
snapshot: Digest::sha256("checkout"),
},
output_root: PathBuf::from("/work/output"),
stage_stdout_as: Some(VfsPath::new("/vfs/artifacts/app.tar.zst").unwrap()),
},
&mut runner,
&mut overlay,
)
.unwrap();
let manifest = overlay.flush();
assert_eq!(runner.commands.len(), 2);
assert_eq!(runner.commands[0].program, "podman");
assert!(runner.commands[0].args.contains(&"build".to_owned()));
assert_eq!(runner.commands[1].program, "podman");
assert!(runner.commands[1].args.contains(&"run".to_owned()));
assert!(runner.commands[1]
.args
.contains(&"/work/demo:/workspace:ro,Z".to_owned()));
assert_eq!(output.status_code, Some(0));
assert_eq!(output.stdout, "artifact-bytes");
assert_eq!(output.lifecycle.state, LinuxTaskState::Completed);
assert!(output.staged_artifact.is_some());
assert!(manifest
.objects
.contains_key(&VfsPath::new("/vfs/artifacts/app.tar.zst").unwrap()));
assert!(!manifest.large_bytes_uploaded);
}
#[test]
fn linux_backend_caps_logs_without_truncating_staged_artifact_bytes() {
let invocation = CommandInvocation {
program: "cargo".to_owned(),
args: vec!["build".to_owned()],
working_directory: "/workspace".to_owned(),
environment_variables: Default::default(),
timeout_ms: 60_000,
network: clusterflux_core::CommandNetworkPolicy::Disabled,
env: Some(container_env()),
};
let mut runner =
RecordingRunner::with_outputs([success_output(b"abcdef"), success_output(b"unused")]);
let mut overlay =
VfsOverlay::new(TaskInstanceId::from("compile-linux"), NodeId::from("node"));
let plan = LinuxRootlessPodmanBackend
.plan_local_checkout_run(
ProcessId::from("vp"),
TaskInstanceId::from("compile-linux"),
&invocation,
LocalSourceCheckout {
host_path: PathBuf::from("/work/demo"),
snapshot: Digest::sha256("checkout"),
},
PathBuf::from("/work/output"),
Some(VfsPath::new("/vfs/artifacts/app.txt").unwrap()),
)
.unwrap();
let output = LinuxRootlessPodmanBackend
.execute_run_plan_with_log_limit(plan, &mut runner, &mut overlay, 4)
.unwrap();
assert_eq!(output.virtual_thread, TaskInstanceId::from("compile-linux"));
assert_eq!(output.stdout, "abcd");
assert!(output.stdout_truncated);
assert!(output.log_backpressured);
assert_eq!(output.staged_artifact.as_ref().unwrap().size, 6);
}
#[test]
fn failed_podman_materialization_returns_clear_error_before_command_run() {
let invocation = CommandInvocation {
program: "cargo".to_owned(),
args: vec!["build".to_owned()],
working_directory: "/workspace".to_owned(),
environment_variables: Default::default(),
timeout_ms: 60_000,
network: clusterflux_core::CommandNetworkPolicy::Disabled,
env: Some(container_env()),
};
let mut runner = RecordingRunner::with_outputs([ProcessOutput {
status_code: Some(125),
stdout: Vec::new(),
stderr: b"image build failed".to_vec(),
}]);
let mut overlay =
VfsOverlay::new(TaskInstanceId::from("compile-linux"), NodeId::from("node"));
let error = LinuxRootlessPodmanBackend
.execute_local_checkout_task(
LocalCheckoutTaskRequest {
process: ProcessId::from("vp"),
virtual_thread: TaskInstanceId::from("compile-linux"),
invocation: &invocation,
checkout: LocalSourceCheckout {
host_path: PathBuf::from("/work/demo"),
snapshot: Digest::sha256("checkout"),
},
output_root: PathBuf::from("/work/output"),
stage_stdout_as: Some(VfsPath::new("/vfs/artifacts/app.tar.zst").unwrap()),
},
&mut runner,
&mut overlay,
)
.unwrap_err();
assert!(error.to_string().contains("podman build"));
assert!(error.to_string().contains("image build failed"));
assert_eq!(runner.commands.len(), 1);
}
#[test]
fn linux_task_lifecycle_supports_cancel_and_all_stop_freeze_resume() {
let mut lifecycle =
LinuxTaskLifecycle::new(ProcessId::from("vp"), TaskInstanceId::from("compile-linux"));
lifecycle.freeze_for_debug_epoch().unwrap();
assert_eq!(lifecycle.state, LinuxTaskState::Frozen);
lifecycle.resume_after_debug_epoch();
assert_eq!(lifecycle.state, LinuxTaskState::Running);
lifecycle.cancel();
assert_eq!(lifecycle.state, LinuxTaskState::Cancelled);
let mut unsupported = LinuxTaskLifecycle::new(
ProcessId::from("vp"),
TaskInstanceId::from("native-command"),
);
unsupported.freeze_supported = false;
let error = unsupported.freeze_for_debug_epoch().unwrap_err();
assert!(matches!(error, BackendError::DebugFreezeUnsupported { .. }));
}
#[test]
fn windows_backend_is_labeled_user_attached_dev_execution() {
let invocation = CommandInvocation {
program: "cmd".to_owned(),
args: vec!["/C".to_owned(), "build.bat".to_owned()],
working_directory: "/workspace".to_owned(),
environment_variables: Default::default(),
timeout_ms: 60_000,
network: clusterflux_core::CommandNetworkPolicy::Disabled,
env: None,
};
let plan = WindowsCommandDevBackend.plan(&invocation).unwrap();
assert!(plan.user_attached_development_execution);
assert_eq!(plan.required_capability, Capability::WindowsCommandDev);
}
#[test]
fn hosted_control_plane_native_command_is_denied() {
let error = authorize_node_command(true, true).unwrap_err();
assert!(matches!(error, BackendError::Denied(_)));
}
#[test]
fn native_command_is_denied_without_command_capability() {
let error = authorize_node_command(false, false).unwrap_err();
assert!(matches!(error, BackendError::Denied(_)));
assert!(error
.to_string()
.contains("lacks native command capability"));
}
#[test]
fn wasmtime_runtime_runs_named_task_export() {
let runtime = WasmtimeTaskRuntime::new().unwrap();
let wasm = r#"
(module
(func (export "task_add_one") (param i32) (result i32)
local.get 0
i32.const 1
i32.add))
"#;
let result = runtime.run_i32_export(wasm, "task_add_one", 41).unwrap();
assert_eq!(result, 42);
let verified = runtime
.run_i32_export_verified(wasm, &Digest::sha256(wasm), "task_add_one", 41)
.unwrap();
assert_eq!(verified, 42);
}
#[test]
fn wasmtime_runtime_rejects_bundle_digest_mismatch_before_compilation() {
let runtime = WasmtimeTaskRuntime::new().unwrap();
let error = runtime
.run_i32_export_verified(
"not a valid wasm module",
&Digest::sha256("different task bundle bytes"),
"task_add_one",
41,
)
.unwrap_err();
assert!(matches!(error, WasmTaskError::BundleDigestMismatch { .. }));
assert!(!error.to_string().contains("failed to parse"));
}
#[test]
fn wasmtime_runtime_freezes_and_resumes_wasm_debug_participant() {
let runtime = WasmtimeTaskRuntime::new().unwrap();
let probe = runtime
.freeze_resume_i32_export_probe(
r#"
(module
(func (export "task_add_one") (param i32) (result i32)
local.get 0
i32.const 1
i32.add))
"#,
"task_add_one",
41,
)
.unwrap();
assert_eq!(probe.task, TaskInstanceId::from("task_add_one"));
assert_eq!(probe.frozen_state, DebugRuntimeState::Frozen);
assert_eq!(probe.resumed_state, DebugRuntimeState::Running);
assert_eq!(probe.result, 42);
assert!(probe
.stack_frames
.iter()
.any(|frame| frame.contains("task_add_one")));
assert!(probe
.local_values
.iter()
.any(|(name, value)| { name == "wasm_local_0" && value.contains("41") }));
assert!(probe.wasm_pc.is_some());
}
#[cfg(unix)]
#[test]
fn native_command_output_is_associated_with_virtual_thread_and_staged_to_vfs() {
let executor = LocalCommandExecutor {
node: clusterflux_core::NodeId::from("node"),
hosted_control_plane: false,
has_command_capability: true,
};
let mut overlay =
VfsOverlay::new(TaskInstanceId::from("compile-linux"), NodeId::from("node"));
let output = executor
.run(
VirtualThreadCommand {
virtual_thread: TaskInstanceId::from("compile-linux"),
invocation: CommandInvocation {
program: "sh".to_owned(),
args: vec![
"-c".to_owned(),
"printf artifact; printf log >&2".to_owned(),
],
working_directory: "/workspace".to_owned(),
environment_variables: Default::default(),
timeout_ms: 60_000,
network: clusterflux_core::CommandNetworkPolicy::Disabled,
env: None,
},
stage_stdout_as: Some(VfsPath::new("/vfs/artifacts/app.txt").unwrap()),
},
&mut overlay,
)
.unwrap();
let manifest = overlay.flush();
assert_eq!(output.virtual_thread, TaskInstanceId::from("compile-linux"));
assert_eq!(output.stdout, "artifact");
assert_eq!(output.stderr, "log");
assert!(output.staged_artifact.is_some());
assert!(!manifest.large_bytes_uploaded);
assert!(manifest
.objects
.contains_key(&VfsPath::new("/vfs/artifacts/app.txt").unwrap()));
}
#[cfg(unix)]
#[test]
fn local_command_executor_caps_logs_and_reports_backpressure_by_virtual_thread() {
let executor = LocalCommandExecutor {
node: clusterflux_core::NodeId::from("node"),
hosted_control_plane: false,
has_command_capability: true,
};
let mut overlay =
VfsOverlay::new(TaskInstanceId::from("compile-linux"), NodeId::from("node"));
let output = executor
.run_with_log_limit(
VirtualThreadCommand {
virtual_thread: TaskInstanceId::from("compile-linux"),
invocation: CommandInvocation {
program: "sh".to_owned(),
args: vec!["-c".to_owned(), "printf abcdef; printf err >&2".to_owned()],
working_directory: "/workspace".to_owned(),
environment_variables: Default::default(),
timeout_ms: 60_000,
network: clusterflux_core::CommandNetworkPolicy::Disabled,
env: None,
},
stage_stdout_as: None,
},
&mut overlay,
4,
)
.unwrap();
assert_eq!(output.virtual_thread, TaskInstanceId::from("compile-linux"));
assert_eq!(output.stdout, "abcd");
assert_eq!(output.stderr, "");
assert!(output.stdout_truncated);
assert!(output.stderr_truncated);
assert!(output.log_backpressured);
}
#[test]
fn public_node_crate_does_not_require_hosted_private_types() {
let _tenant = TenantId::from("tenant");
let _project = ProjectId::from("project");
let _backend = LinuxRootlessPodmanBackend;
}
}