Show progress while downloading sandbox images

This commit is contained in:
ms2k 2026-07-29 00:48:24 +09:00
commit aaee63a6e3
3 changed files with 197 additions and 12 deletions

View file

@ -22,6 +22,7 @@ harness = false
[dependencies]
ed25519-dalek.workspace = true
getrandom.workspace = true
indicatif.workspace = true
lazaret-control-client = { version = "=0.0.0", path = "../lazaret-control-client" }
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
lazaret-domain-state = { version = "=0.0.0", path = "../lazaret-domain-state" }

View file

@ -9,6 +9,7 @@ use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use indicatif::{ProgressBar, ProgressStyle};
use lazaret_core::configuration::{
ConfigurationLoadRequest, ResolvedDomainConfiguration, load_trusted_configuration,
};
@ -17,6 +18,7 @@ use lazaret_core::identity::{
};
use lazaret_core::records::{Domain, Session, SessionPhase};
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_prepared_lower::OciDownloadProgress;
use lazaret_runtime_supervisor::sandbox::{
DOMAIN_OPEN_FILE_LIMIT, RUNSC_EXECUTABLE_NAME, default_sandbox_layout,
};
@ -34,6 +36,45 @@ use crate::waiter::BootstrapWaiterFace;
pub use lazaret_core::protocol::PRIVATE_CLIENT_BOOTSTRAP_ROLE;
const MAX_CONCURRENT_CONNECTIONS: usize = 32;
#[derive(Default)]
struct ImageDownloadProgress {
bar: Option<ProgressBar>,
}
impl ImageDownloadProgress {
fn update(&mut self, progress: OciDownloadProgress) {
match progress {
OciDownloadProgress::Started { total_bytes: 0 } => {}
OciDownloadProgress::Started { total_bytes } => {
let style = ProgressStyle::with_template(
"Lazaret is downloading sandbox image [{wide_bar}] {bytes}/{total_bytes} ({percent}%)",
)
.expect("static progress template must be valid")
.progress_chars("=>-");
self.bar = Some(ProgressBar::new(total_bytes).with_style(style));
}
OciDownloadProgress::Advanced { downloaded_bytes } => {
if let Some(bar) = &self.bar {
bar.set_position(downloaded_bytes);
}
}
OciDownloadProgress::Finished => self.clear(),
}
}
fn clear(&mut self) {
if let Some(bar) = self.bar.take() {
bar.finish_and_clear();
}
}
}
impl Drop for ImageDownloadProgress {
fn drop(&mut self) {
self.clear();
}
}
#[derive(Debug)]
pub struct PrivateClientBootstrapError(String);
@ -140,6 +181,7 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
.enable_all()
.build()
.map_err(failure)?;
let mut image_progress = ImageDownloadProgress::default();
let mut selection = runtime
.block_on(select_runtime(
&configuration,
@ -149,6 +191,7 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
sandbox_init: &installed.join("lazaret-sandbox-init"),
runsc: &installed.join(RUNSC_EXECUTABLE_NAME),
},
|progress| image_progress.update(progress),
))
.map_err(failure)
.inspect_err(|_| signal(b'E'))?;
@ -552,6 +595,18 @@ mod tests {
use super::*;
#[test]
fn image_download_progress_accepts_the_exact_event_lifecycle() {
let mut progress = ImageDownloadProgress::default();
progress.update(OciDownloadProgress::Started { total_bytes: 1_024 });
assert!(progress.bar.is_some());
progress.update(OciDownloadProgress::Advanced {
downloaded_bytes: 1_024,
});
progress.update(OciDownloadProgress::Finished);
assert!(progress.bar.is_none());
}
#[test]
fn host_coordinator_plans_a_bounded_raise_from_a_low_soft_nofile_limit() {
assert_eq!(

View file

@ -16,8 +16,8 @@ use lazaret_linux::filesystem::SharedFileLock;
use lazaret_linux::platform::{PlatformObservation, check_platform};
use lazaret_linux::user_namespace::UserNamespaceMap;
use lazaret_prepared_lower::{
OciRegistryTransport, Platform, PreparedDestination, PreparedDestinationKind,
PreparedLowerError, PreparedLowerStore, RegistryCredentials,
OciDownloadProgress, OciRegistryTransport, Platform, PreparedDestination,
PreparedDestinationKind, PreparedLowerError, PreparedLowerStore, RegistryCredentials,
};
use lazaret_runtime_supervisor::sandbox::{
CREDENTIALS_DESTINATION, DEV_DESTINATION, ESCALATION_CLIENT_DESTINATION,
@ -150,6 +150,7 @@ pub async fn select_runtime(
configuration: &ResolvedDomainConfiguration,
store: Arc<HostCoordinatorDomainStore>,
product_paths: InstalledProductPaths<'_>,
progress: impl FnMut(OciDownloadProgress),
) -> Result<RuntimeSelection, RuntimeSelectionError> {
let kernel_release = std::fs::read_to_string("/proc/sys/kernel/osrelease").map_err(failure)?;
select_runtime_for_platform(
@ -161,6 +162,7 @@ pub async fn select_runtime(
architecture: std::env::consts::ARCH,
kernel_release: kernel_release.trim(),
},
progress,
)
.await
}
@ -170,6 +172,7 @@ async fn select_runtime_for_platform(
store: Arc<HostCoordinatorDomainStore>,
product_paths: InstalledProductPaths<'_>,
observation: &PlatformObservation<'_>,
progress: impl FnMut(OciDownloadProgress),
) -> Result<RuntimeSelection, RuntimeSelectionError> {
check_platform(observation).map_err(failure)?;
let user_namespace_map = UserNamespaceMap::resolve_current().map_err(failure)?;
@ -186,12 +189,13 @@ async fn select_runtime_for_platform(
PreparedLowerStore::under_storage_root(configuration.storage_root(), catalog.clone())?;
recover_before_cold_start(configuration, &store, &catalog, &prepared).map_err(failure)?;
let image = OciRegistryTransport
.pull_confined(
.pull_confined_with_progress(
&configuration.fingerprint_record().image,
&RegistryCredentials::Anonymous,
&Platform::linux("amd64"),
configuration.storage_root(),
random_id()?,
progress,
)
.await
.map_err(RuntimeSelectionError::PreparedLower)?;
@ -493,6 +497,7 @@ mod tests {
runsc: Path::new("/missing-runsc"),
},
&observation,
|_| {},
));
let mut context = Context::from_waker(Waker::noop());
let Poll::Ready(result) = future.as_mut().poll(&mut context) else {