Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ exclude = ["build/tmp", "target", ".venv", "examples/*/.venv"]
resolver = "2"

[workspace.dependencies]
boxlite = { path = "src/boxlite", version = "0.9.7" }
boxlite-shared = { path = "src/shared", version = "0.9.7" }
bubblewrap-sys = { path = "src/deps/bubblewrap-sys", version = "0.9.7" }
e2fsprogs-sys = { path = "src/deps/e2fsprogs-sys", version = "0.9.7" }
libgvproxy-sys = { path = "src/deps/libgvproxy-sys", version = "0.9.7" }
libkrun-sys = { path = "src/deps/libkrun-sys", version = "0.9.7" }
boxlite = { path = "src/boxlite", version = "0.9.8" }
boxlite-shared = { path = "src/shared", version = "0.9.8" }
bubblewrap-sys = { path = "src/deps/bubblewrap-sys", version = "0.9.8" }
e2fsprogs-sys = { path = "src/deps/e2fsprogs-sys", version = "0.9.8" }
libgvproxy-sys = { path = "src/deps/libgvproxy-sys", version = "0.9.8" }
libkrun-sys = { path = "src/deps/libkrun-sys", version = "0.9.8" }

[patch.crates-io]
# Pull in youki PR #3504 (split intermediate/init readiness channels) which
Expand All @@ -33,7 +33,7 @@ libkrun-sys = { path = "src/deps/libkrun-sys", version = "0.9.7" }
libcontainer = { git = "https://github.com/youki-dev/youki", rev = "4b2f0e00a4a11107f3a338c21c17407d2f664ec9" }

[workspace.package]
version = "0.9.7"
version = "0.9.8"
edition = "2024"
authors = ["Dorian Zheng <https://github.com/dorianzheng>"]
license = "Apache-2.0"
Expand Down
2 changes: 2 additions & 0 deletions sdks/node/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@ impl TryFrom<JsBoxOptions> for BoxOptions {
// client that attaches to the main command, which the SDKs cannot
// do until they grow `attach()` (see sdk-run-semantics-api.md).
tty: false,
// Capture stays off until the SDKs can also read the log back.
capture_logs: false,
secrets,
})
}
Expand Down
15 changes: 15 additions & 0 deletions src/boxlite/src/litebox/init/tasks/guest_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ const MIN_PRIVILEGED_CONTAINER_GUEST_VERSION: crate::portal::interfaces::guest::
/// with no `/dev/kvm` while the caller believes nesting was granted.
const MIN_DEVICE_GUEST_VERSION: crate::portal::interfaces::guest::GuestVersion = (0, 9, 8);

/// Oldest guest release that honors `log_capture` on `Container.Init`. Same trap
/// again, and the one the startup barrier cannot cover: an unaware guest never
/// writes `begin`, so the caller would find no log and no failure either.
const MIN_LOG_CAPTURE_GUEST_VERSION: crate::portal::interfaces::guest::GuestVersion = (0, 9, 8);

pub struct GuestInitTask;

struct GuestBootstrapConfig {
Expand Down Expand Up @@ -97,6 +102,11 @@ impl PipelineTask<InitCtx> for GuestInitTask {
} else {
Vec::new()
},
log_capture: ctx
.config
.options
.capture_logs
.then(|| uuid::Uuid::new_v4().to_string()),
advanced: advanced.into(),
},
};
Expand Down Expand Up @@ -157,6 +167,11 @@ async fn run_guest_init(
.require_min_version(MIN_DEVICE_GUEST_VERSION)
.await?;
}
if bootstrap.container.log_capture.is_some() {
guest_interface
.require_min_version(MIN_LOG_CAPTURE_GUEST_VERSION)
.await?;
}
guest_interface.init(bootstrap.guest).await?;
tracing::info!("Guest initialized successfully");

Expand Down
48 changes: 46 additions & 2 deletions src/boxlite/src/portal/interfaces/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use boxlite_shared::{
ContainerAdvancedOptions as ProtoContainerAdvancedOptions,
ContainerCapabilities as ProtoContainerCapabilities, ContainerClient,
ContainerConfig as ProtoContainerConfig, ContainerDevice, ContainerInitErrorKind,
ContainerInitRequest, DiskRootfs, LinuxOptions, MergedRootfs, MountOptions, OverlayRootfs,
RootfsInit, container_init_response,
ContainerInitRequest, DiskRootfs, LinuxOptions, LogCapture, MergedRootfs, MountOptions,
OverlayRootfs, RootfsInit, container_init_response,
};
use tonic::transport::Channel;

Expand Down Expand Up @@ -90,6 +90,8 @@ pub struct ContainerInitConfig {
pub tty: bool,
/// Guest device nodes to reproduce inside the OCI workload.
pub devices: Vec<ContainerDevice>,
/// Run id when durable output capture is enabled; `None` disables capture.
pub log_capture: Option<String>,
pub advanced: ContainerAdvancedConfig,
}

Expand Down Expand Up @@ -136,6 +138,7 @@ impl ContainerInterface {
ca_certs,
tty,
devices,
log_capture,
advanced,
} = config;

Expand Down Expand Up @@ -191,6 +194,7 @@ impl ContainerInterface {
rootfs = ?rootfs,
mounts_count = proto_mounts.len(),
device_count = devices.len(),
capture_logs = log_capture.is_some(),
"Container configuration"
);

Expand All @@ -205,6 +209,7 @@ impl ContainerInterface {
// it sent, instead of both sides separately hard-coding it.
execution_id: container_id.clone(),
devices,
log_capture: log_capture.map(|run_id| LogCapture { run_id }),
};

let response = self
Expand Down Expand Up @@ -468,6 +473,7 @@ mod tests {
destination: "/dev/kvm".to_string(),
file_mode: Some(0o666),
}],
log_capture: None,
advanced: ContainerAdvancedConfig {
capabilities: crate::runtime::advanced_options::ContainerCapabilities {
add: vec!["ALL".into()],
Expand Down Expand Up @@ -519,5 +525,43 @@ mod tests {
assert_eq!(mount.source, "/sys");
assert_eq!(mount.destination, "/sys");
assert!(!mount.options.contains(&"rro".to_string()));
assert!(
request.log_capture.is_none(),
"capture must stay off unless asked for"
);
}

/// The run id is what tells one run's records from another's in a log file
/// that outlives the VM, so it has to survive the crossing verbatim.
#[tokio::test]
async fn container_init_forwards_the_capture_run_id() {
let seen = Arc::new(Mutex::new(None));
let mut iface = interface_recording(StartReply::Success, Arc::clone(&seen)).await;
let run_id = "b3f1c0a4-7d2e-4a91-8c55-0e6f2ab41d90";

iface
.init(ContainerInitConfig {
container_id: "container-1".to_string(),
image: crate::images::ContainerImageConfig::default(),
rootfs: ContainerRootfsInitConfig::Merged,
mounts: Vec::new(),
ca_certs: Vec::new(),
tty: false,
devices: Vec::new(),
log_capture: Some(run_id.to_string()),
advanced: ContainerAdvancedConfig {
capabilities: Default::default(),
linux: Default::default(),
mount: Default::default(),
},
})
.await
.unwrap();

let request = seen.lock().unwrap().take().expect("guest saw Init");
assert_eq!(
request.log_capture.expect("capture requested").run_id,
run_id
);
}
}
33 changes: 33 additions & 0 deletions src/boxlite/src/rest/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ impl RestRuntime {
}

fn validate_remote_box_options(options: &BoxOptions) -> BoxliteResult<()> {
// The create request carries no capture field yet, so a server would accept
// this and run the workload with capture off. Refusing here keeps the
// promise the option makes: a caller learns capture is unavailable before
// their workload runs, not after it has already produced the output.
if options.capture_logs {
return Err(BoxliteError::Unsupported(
"capture_logs is local-only for now: remote runtimes cannot yet carry it, \
and a silently uncaptured run is worse than a refused one."
.to_string(),
));
}

if options.ports.is_empty() {
return Ok(());
}
Expand Down Expand Up @@ -504,6 +516,27 @@ mod tests {
assert!(error.to_string().contains("local runtime"));
}

/// The request has no capture field, so accepting this would hand back a box
/// that runs the workload with capture off and reports success — the failure
/// the startup barrier exists to make impossible, one layer up.
#[tokio::test]
async fn create_rejects_capture_logs_in_rest_mode() {
let options = BoxliteRestOptions::new("http://localhost:1");
let runtime = RestRuntime::new(&options).expect("failed to create REST runtime");
let box_options = BoxOptions {
capture_logs: true,
..Default::default()
};

let error = RuntimeBackend::create(&runtime, box_options, None)
.await
.err()
.expect("REST capture_logs must be rejected before network I/O");

assert!(matches!(error, BoxliteError::Unsupported(_)));
assert!(error.to_string().contains("capture_logs"), "{error}");
}

#[tokio::test]
async fn get_or_create_rejects_custom_kernel_for_rest_runtime() {
let temp = tempfile::tempdir().unwrap();
Expand Down
Loading
Loading