Skip to content

Commit 113ffc6

Browse files
committed
to-disk: use ssh instead of exec
The exec flow is buffered and also doesn't handle things like tty widths etc. We could replicate all of that, but it's just way easier to fork ssh. This feels conceptually less clean in that my preference is actually for systems to be more autonomous, but this way right now is the only way we could sanely get a progress bar for example. Signed-off-by: Colin Walters <[email protected]>
1 parent ca17a93 commit 113ffc6

File tree

2 files changed

+95
-78
lines changed

2 files changed

+95
-78
lines changed

crates/kit/src/run_ephemeral.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -301,23 +301,6 @@ pub fn run(opts: RunEphemeralOpts) -> Result<()> {
301301
return Err(cmd.exec()).context("execve");
302302
}
303303

304-
/// Launch privileged container with QEMU+KVM for ephemeral VM and wait for completion.
305-
/// Unlike `run()`, this function waits for completion instead of using exec(), making it suitable
306-
/// for programmatic use where the caller needs to capture output and exit codes.
307-
pub fn run_synchronous(opts: RunEphemeralOpts) -> Result<()> {
308-
let (mut cmd, temp_dir) = prepare_run_command_with_temp(opts)?;
309-
// Keep temp_dir alive until command completes
310-
311-
// Use the same approach as run_detached but wait for completion instead of detaching
312-
let status = cmd.status().context("Failed to execute podman command")?;
313-
if !status.success() {
314-
return Err(color_eyre::eyre::eyre!("ephemeral run failed {status:?}",));
315-
}
316-
// Explicitly drop temp_dir after successful completion
317-
drop(temp_dir);
318-
Ok(())
319-
}
320-
321304
fn prepare_run_command_with_temp(
322305
opts: RunEphemeralOpts,
323306
) -> Result<(std::process::Command, tempfile::TempDir)> {

crates/kit/src/to_disk.rs

Lines changed: 95 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,17 @@
7373
//! quay.io/centos-bootc/centos-bootc:stream10 output.img
7474
//! ```
7575
76+
use std::io::IsTerminal;
77+
7678
use crate::install_options::InstallOptions;
77-
use crate::run_ephemeral::{run_synchronous as run_ephemeral, CommonVmOpts, RunEphemeralOpts};
78-
use crate::{images, utils};
79+
use crate::run_ephemeral::{run_detached, CommonVmOpts, RunEphemeralOpts};
80+
use crate::run_ephemeral_ssh::wait_for_ssh_ready;
81+
use crate::{images, ssh, utils};
7982
use camino::Utf8PathBuf;
8083
use clap::{Parser, ValueEnum};
81-
use color_eyre::eyre::Context;
84+
use color_eyre::eyre::{eyre, Context};
8285
use color_eyre::Result;
83-
use std::borrow::Cow;
86+
use indoc::indoc;
8487
use tracing::debug;
8588

8689
/// Supported disk image formats
@@ -162,43 +165,51 @@ impl ToDiskOpts {
162165
}
163166
}
164167

165-
/// Generate the complete bootc installation command
166-
fn generate_bootc_install_command(&self) -> Vec<String> {
168+
/// Generate the complete bootc installation command arguments for SSH execution
169+
fn generate_bootc_install_command(&self) -> Result<Vec<String>> {
167170
let source_imgref = format!("containers-storage:{}", self.source_image);
168171

169-
let bootc_install = [
170-
"env",
171-
// This is the magic trick to pull the storage from the host
172-
"STORAGE_OPTS=additionalimagestore=/run/virtiofs-mnt-hoststorage/",
173-
"bootc",
174-
"install",
175-
"to-disk",
176-
// Default to being a generic image here, if someone cares they can override this
177-
"--generic-image",
178-
// The default in newer versions, but support older ones too
179-
"--skip-fetch-check",
180-
"--source-imgref",
181-
]
182-
.into_iter()
183-
.map(Cow::Borrowed)
184-
.chain(std::iter::once(source_imgref.into()))
185-
.chain(self.install.to_bootc_args().into_iter().map(Cow::Owned))
186-
.chain(std::iter::once(Cow::Borrowed(
187-
"/dev/disk/by-id/virtio-output",
188-
)))
189-
.fold(String::new(), |mut acc, elt| {
190-
if !acc.is_empty() {
191-
acc.push(' ');
192-
}
193-
acc.push_str(&*elt);
194-
acc
195-
});
196-
// TODO: make /var a tmpfs by default (actually make run-ephemeral more like a readonly bootc)
197-
vec![
198-
"mount -t tmpfs tmpfs /var/lib/containers".to_owned(),
199-
"mount -t tmpfs tmpfs /var/tmp".to_owned(),
200-
bootc_install,
201-
]
172+
// Quote each bootc argument individually to prevent shell injection
173+
let mut quoted_bootc_args = Vec::new();
174+
for arg in self.install.to_bootc_args() {
175+
let quoted = shlex::try_quote(&arg)
176+
.map_err(|e| eyre!("Failed to quote bootc argument '{}': {}", arg, e))?;
177+
quoted_bootc_args.push(quoted.to_string());
178+
}
179+
let bootc_args = quoted_bootc_args.join(" ");
180+
181+
// Quote the source image reference to prevent shell injection
182+
let quoted_source_imgref = shlex::try_quote(&source_imgref)
183+
.map_err(|e| eyre!("Failed to quote source imgref '{}': {}", source_imgref, e))?
184+
.to_string();
185+
186+
// Create the complete script by substituting variables directly
187+
let script = indoc! {r#"
188+
set -euo pipefail
189+
190+
echo "Setting up temporary filesystems..."
191+
mount -t tmpfs tmpfs /var/lib/containers
192+
mount -t tmpfs tmpfs /var/tmp
193+
194+
echo "Starting bootc installation..."
195+
echo "Source image: {SOURCE_IMGREF}"
196+
echo "Additional args: {BOOTC_ARGS}"
197+
198+
# Execute bootc installation
199+
env STORAGE_OPTS=additionalimagestore=/run/virtiofs-mnt-hoststorage/ \
200+
bootc install to-disk \
201+
--generic-image \
202+
--skip-fetch-check \
203+
--source-imgref {SOURCE_IMGREF} \
204+
{BOOTC_ARGS} \
205+
/dev/disk/by-id/virtio-output
206+
207+
echo "Installation completed successfully!"
208+
"#}
209+
.replace("{SOURCE_IMGREF}", &quoted_source_imgref)
210+
.replace("{BOOTC_ARGS}", &bootc_args);
211+
212+
Ok(vec!["/bin/bash".to_string(), "-c".to_string(), script])
202213
}
203214

204215
/// Calculate the optimal target disk size based on the source image or explicit size
@@ -230,7 +241,7 @@ impl ToDiskOpts {
230241
}
231242
}
232243

233-
/// Execute a bootc installation using an ephemeral VM
244+
/// Execute a bootc installation using an ephemeral VM with SSH
234245
///
235246
/// Main entry point for the bootc installation process. See module-level documentation
236247
/// for details on the installation workflow and architecture.
@@ -290,10 +301,14 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
290301

291302
// Phase 3: Installation command generation
292303
// Generate complete script including storage setup and bootc install
293-
let bootc_install_command = opts.generate_bootc_install_command();
304+
let bootc_install_command = opts.generate_bootc_install_command()?;
294305

295306
// Phase 4: Ephemeral VM configuration
296-
let common_opts = opts.common.clone();
307+
let mut common_opts = opts.common.clone();
308+
// Enable SSH key generation for SSH-based installation
309+
common_opts.ssh_keygen = true;
310+
311+
let tty = std::io::stdout().is_terminal();
297312

298313
// Configure VM for installation:
299314
// - Use source image as installer environment
@@ -304,7 +319,9 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
304319
image: opts.get_installer_image().to_string(),
305320
common: common_opts,
306321
podman: crate::run_ephemeral::CommonPodmanOptions {
307-
rm: true, // Clean up container after installation
322+
rm: true, // Clean up container after installation
323+
detach: true, // Run in detached mode for SSH approach
324+
tty,
308325
label: opts.label,
309326
..Default::default()
310327
},
@@ -324,24 +341,41 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
324341
)], // Attach target disk
325342
};
326343

327-
// Phase 5: Final VM configuration and execution
328-
let mut final_opts = ephemeral_opts;
329-
// Set the installation script to execute in the VM
330-
final_opts.common.execute = bootc_install_command;
331-
332-
// Ensure clean shutdown after installation completes
333-
final_opts
334-
.common
335-
.kernel_args
336-
.push("systemd.default_target=poweroff.target".to_string());
337-
338-
// Phase 6: Launch VM and execute installation
339-
// The ephemeral VM will:
340-
// 1. Boot using the bootc image
341-
// 2. Mount host storage and target disk
342-
// 3. Execute the installation script
343-
// 4. Shut down automatically after completion
344-
match run_ephemeral(final_opts) {
344+
// Phase 5: SSH-based VM configuration and execution
345+
// Launch VM in detached mode with SSH enabled
346+
debug!("Starting ephemeral VM with SSH...");
347+
let container_id = run_detached(ephemeral_opts)?;
348+
debug!("Ephemeral VM started with container ID: {}", container_id);
349+
350+
// Use the SSH approach for better TTY forwarding and output buffering
351+
let result = (|| -> Result<()> {
352+
// Wait for SSH to be ready
353+
let progress_bar = crate::boot_progress::create_boot_progress_bar();
354+
let progress_bar = wait_for_ssh_ready(
355+
&container_id,
356+
std::time::Duration::from_secs(60),
357+
progress_bar,
358+
)?;
359+
progress_bar.finish_and_clear();
360+
361+
// Connect via SSH and execute the installation command
362+
debug!(
363+
"Executing installation via SSH: {:?}",
364+
bootc_install_command
365+
);
366+
ssh::connect_via_container(&container_id, bootc_install_command)?;
367+
368+
Ok(())
369+
})();
370+
371+
// Cleanup: stop and remove the container
372+
debug!("Cleaning up ephemeral container...");
373+
let _ = std::process::Command::new("podman")
374+
.args(["rm", "-f", &container_id])
375+
.output();
376+
377+
// Handle the result - remove disk file on failure
378+
match result {
345379
Ok(()) => Ok(()),
346380
Err(e) => {
347381
let _ = std::fs::remove_file(&opts.target_disk);

0 commit comments

Comments
 (0)