diff --git a/crates/integration-tests/src/main.rs b/crates/integration-tests/src/main.rs index 296f1ce2a..9c90dc152 100644 --- a/crates/integration-tests/src/main.rs +++ b/crates/integration-tests/src/main.rs @@ -22,6 +22,7 @@ mod tests { pub mod run_ephemeral; pub mod run_ephemeral_ignition; pub mod run_ephemeral_ssh; + pub mod run_ephemeral_test_basic; pub mod to_disk; pub mod varlink; } diff --git a/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs b/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs new file mode 100644 index 000000000..dd9d2a60b --- /dev/null +++ b/crates/integration-tests/src/tests/run_ephemeral_test_basic.rs @@ -0,0 +1,60 @@ +//! Integration tests for ephemeral test-basic command +//! +//! ⚠️ **CRITICAL INTEGRATION TEST POLICY** ⚠️ +//! +//! INTEGRATION TESTS MUST NEVER "warn and continue" ON FAILURES! +//! +//! If something is not working: +//! - Use `todo!("reason why this doesn't work yet")` +//! - Use `panic!("clear error message")` +//! - Use `assert!()` and `unwrap()` to fail hard +//! +//! NEVER use patterns like: +//! - "Note: test failed - likely due to..." +//! - "This is acceptable in CI/testing environments" +//! - Warning and continuing on failures + +use integration_tests::{integration_test, parameterized_integration_test}; +use itest::TestResult; +use xshell::cmd; + +use crate::{get_bck_command, get_test_image, shell}; + +/// Test the basic smoke test command +/// +/// This test verifies that `bcvk ephemeral test-basic` successfully boots +/// a bootc container image and verifies systemd reaches a healthy state. +fn test_ephemeral_test_basic() -> TestResult { + println!("Running test: bcvk ephemeral test-basic"); + + let sh = shell()?; + let bcvk = get_bck_command()?; + let image = get_test_image(); + + println!("Testing with image: {}", image); + + // Run the test-basic command + // This should boot the VM, check systemd health, and clean up + cmd!(sh, "{bcvk} ephemeral test-basic {image}").run()?; + + println!("Test passed: bcvk ephemeral test-basic"); + Ok(()) +} +integration_test!(test_ephemeral_test_basic); + +/// Parameterized test that runs test-basic against all configured test images +/// +/// Uses BCVK_ALL_IMAGES environment variable to get the list of images to test. +fn test_ephemeral_test_basic_parameterized(image: &str) -> TestResult { + println!("Running parameterized test: bcvk ephemeral test-basic (image: {})", image); + + let sh = shell()?; + let bcvk = get_bck_command()?; + + // Run the test-basic command + cmd!(sh, "{bcvk} ephemeral test-basic {image}").run()?; + + println!("Parameterized test passed for image: {}", image); + Ok(()) +} +parameterized_integration_test!(test_ephemeral_test_basic_parameterized); diff --git a/crates/kit/src/ephemeral.rs b/crates/kit/src/ephemeral.rs index 0c4745cb3..7f4f1c33f 100644 --- a/crates/kit/src/ephemeral.rs +++ b/crates/kit/src/ephemeral.rs @@ -19,6 +19,34 @@ use crate::ssh; /// Label used to identify bcvk ephemeral containers const EPHEMERAL_LABEL: &str = "bcvk.ephemeral=1"; +/// RAII guard for ephemeral container cleanup +/// Ensures container is removed when dropped, even on error paths +pub(crate) struct ContainerCleanup { + container_id: String, +} + +impl ContainerCleanup { + pub(crate) fn new(container_id: String) -> Self { + Self { container_id } + } +} + +impl Drop for ContainerCleanup { + fn drop(&mut self) { + use std::process::Stdio; + tracing::debug!("Cleaning up ephemeral container {}", self.container_id); + let result = Command::new("podman") + .args(["rm", "-f", "--", &self.container_id]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + if let Err(e) = result { + tracing::warn!("Failed to remove container {}: {}", self.container_id, e); + } + } +} + /// SSH connection options for accessing running VMs. /// /// Provides secure shell access to VMs running within containers, @@ -39,6 +67,17 @@ pub struct SshOpts { pub args: Vec, } +/// Options for the test-basic subcommand +#[derive(clap::Parser, Debug)] +pub struct TestBasicOpts { + /// Container image to test + #[clap(help = "Container image to run basic smoke test on")] + pub image: String, + + #[clap(flatten)] + pub common: crate::run_ephemeral::CommonVmOpts, +} + /// Container list entry for ephemeral VMs #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] @@ -155,6 +194,15 @@ pub enum EphemeralCommands { #[clap(short, long)] force: bool, }, + + /// Run basic smoke test on a bootc container image + /// + /// Boots an ephemeral VM from the specified container image and verifies + /// that systemd reaches a healthy state via `systemctl is-system-running`. + /// This provides a quick sanity check to validate that a bootc container + /// image can successfully boot and reach a working state. + #[clap(name = "test-basic")] + TestBasic(TestBasicOpts), } impl EphemeralCommands { @@ -216,6 +264,7 @@ impl EphemeralCommands { Ok(()) } EphemeralCommands::RmAll { force } => remove_all_ephemeral_containers(force), + EphemeralCommands::TestBasic(opts) => test_basic(opts), } } } @@ -335,3 +384,68 @@ fn remove_all_ephemeral_containers(force: bool) -> Result<()> { Ok(()) } + +/// Run basic smoke test on a bootc container image +/// +/// Boots an ephemeral VM and verifies systemd reaches a healthy state +fn test_basic(opts: TestBasicOpts) -> Result<()> { + use crate::run_ephemeral::{run_detached, CommonPodmanOptions, RunEphemeralOpts}; + use std::process::Stdio; + + println!("Running basic smoke test on {}", opts.image); + + // Build ephemeral VM options + let ephemeral_opts = RunEphemeralOpts { + image: opts.image.clone(), + common: opts.common, + podman: CommonPodmanOptions { + detach: true, + ..Default::default() + }, + debug_entrypoint: None, + bind_mounts: Vec::new(), + mount_disk_files: Vec::new(), + }; + + // Start the ephemeral VM + let container_id = run_detached(ephemeral_opts)?; + println!("Started ephemeral VM: {}", container_id); + + // Ensure cleanup on any exit path + let _cleanup = ContainerCleanup::new(container_id.clone()); + + // Wait for SSH to be ready + let progress_bar = crate::boot_progress::create_boot_progress_bar(); + let (duration, progress_bar) = run_ephemeral_ssh::wait_for_ssh_ready(&container_id, None, progress_bar)?; + progress_bar.finish_and_clear(); + println!("VM ready after {:.1}s", duration.as_secs_f64()); + + // Run systemctl status to check system health + println!("Checking system health..."); + let status = Command::new("podman") + .args([ + "exec", + "--", + &container_id, + "/var/lib/bcvk/entrypoint", + "ssh-exec", + "systemctl", + "status", + "--no-pager", + ]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run systemctl status")?; + + if status.success() { + println!("✓ System health check passed"); + Ok(()) + } else { + Err(eyre!( + "System health check failed: systemctl status exited with code {}", + status.code().unwrap_or(-1) + )) + } +} + diff --git a/crates/kit/src/run_ephemeral_ssh.rs b/crates/kit/src/run_ephemeral_ssh.rs index 07c594d41..ece6597ab 100644 --- a/crates/kit/src/run_ephemeral_ssh.rs +++ b/crates/kit/src/run_ephemeral_ssh.rs @@ -6,6 +6,7 @@ use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use tracing::debug; +use crate::ephemeral::ContainerCleanup; use crate::run_ephemeral::{run_detached, RunEphemeralOpts}; use crate::ssh; use crate::supervisor_status::{SupervisorState, SupervisorStatus}; @@ -94,33 +95,6 @@ fn show_container_logs(container_name: &str) { } } -/// RAII guard for ephemeral container cleanup -/// Ensures container is removed when dropped, even on error paths -struct ContainerCleanup { - container_id: String, -} - -impl ContainerCleanup { - fn new(container_id: String) -> Self { - Self { container_id } - } -} - -impl Drop for ContainerCleanup { - fn drop(&mut self) { - debug!("Cleaning up ephemeral container {}", self.container_id); - let result = Command::new("podman") - .args(["rm", "-f", "--", &self.container_id]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - - if let Err(e) = result { - tracing::warn!("Failed to remove container {}: {}", self.container_id, e); - } - } -} - /// Timeout waiting for connection pub(crate) const SSH_TIMEOUT: std::time::Duration = const { Duration::from_secs(240) };