Skip to content
Draft
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
1 change: 1 addition & 0 deletions crates/integration-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
60 changes: 60 additions & 0 deletions crates/integration-tests/src/tests/run_ephemeral_test_basic.rs
Original file line number Diff line number Diff line change
@@ -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);
114 changes: 114 additions & 0 deletions crates/kit/src/ephemeral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -39,6 +67,17 @@ pub struct SshOpts {
pub args: Vec<String>,
}

/// 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")]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -216,6 +264,7 @@ impl EphemeralCommands {
Ok(())
}
EphemeralCommands::RmAll { force } => remove_all_ephemeral_containers(force),
EphemeralCommands::TestBasic(opts) => test_basic(opts),
}
}
}
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah let's just re-exec our own process Command::new(proc/self/exe).args().exec()

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)
))
}
}

28 changes: 1 addition & 27 deletions crates/kit/src/run_ephemeral_ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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) };

Expand Down
Loading