chore(baker/prebake): Add comprehensive documentation to prebake module

- Document prebake.rs, bootstrap.rs, and hook.rs with module-level and
  function-level docs
- Rename parse_prebake_config to parse for conciseness
- Change bootstrap() return type from ExecutionResult to ()
- Add dry-run support to bootstrap stage
- Improve test robustness with Docker availability check and unique
  container names
This commit is contained in:
Catty Steve
2026-04-01 10:54:51 +08:00
parent 15aaa6f310
commit b02407a02d
4 changed files with 447 additions and 185 deletions
+66 -5
View File
@@ -1,3 +1,25 @@
//! Prebake workflow orchestration module.
//!
//! This module provides the main prebake workflow execution logic, orchestrating
//! the various stages of build environment preparation.
//!
//! # Stages
//!
//! The prebake workflow executes the following stages in order:
//!
//! 1. **Bootstrap**: Sets up the build environment (user, workspace, privileges)
//! 2. **EarlyHook**: Executes user-defined commands before dependency installation
//! 3. **DepsSystem**: Installs system-level package dependencies
//! 4. **DepsUser**: Installs user-level dependencies
//! 5. **LateHook**: Executes user-defined commands after dependency installation
//! 6. **Ready**: Signals completion of the prebake workflow
//!
//! # Usage
//!
//! ```ignore
//! let result = prebake("/path/to/prebake.yml", &cli, None).await;
//! ```
pub mod config;
pub mod env;
pub mod security;
@@ -12,14 +34,53 @@ use crate::{
pub use config::PrebakeConfig;
pub use security::prebake_drop_privilege;
pub fn parse_prebake_config(yaml_content: &str) -> Result<PrebakeConfig, serde_yaml::Error> {
/// Parses a YAML configuration string into a `PrebakeConfig`.
///
/// # Arguments
///
/// * `yaml_content` - A string containing YAML configuration
///
/// # Returns
///
/// Returns `Ok(PrebakeConfig)` on successful parsing, or `Err(serde_yaml::Error)`
/// if the YAML is invalid.
pub fn parse(yaml_content: &str) -> Result<PrebakeConfig, serde_yaml::Error> {
serde_yaml::from_str(yaml_content)
}
pub fn serialize_prebake_config(config: &PrebakeConfig) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(config)
}
// pub fn serialize_prebake_config(config: &PrebakeConfig) -> Result<String, serde_yaml::Error> {
// serde_yaml::to_string(config)
// }
/// Executes the complete prebake workflow.
///
/// This function orchestrates the entire prebake process, including:
/// - Configuration loading and validation
/// - Privilege dropping based on security settings
/// - Sequential execution of all prebake stages
///
/// # Arguments
///
/// * `config_path` - Path to the prebake.yml configuration file
/// * `cli` - CLI arguments containing pipeline, build_id, username, and dry_run flag
/// * `stage` - Optional specific stage to start from (defaults to Bootstrap)
///
/// # Stage Execution
///
/// The function executes stages sequentially based on the `stage` parameter:
/// - If `stage` is `None`, starts from Bootstrap
/// - If `stage` is `Some`, starts from the specified stage
///
/// Each stage may drop privileges before execution according to the security configuration.
///
/// # Returns
///
/// Returns `Ok(())` on successful completion of all stages, or `anyhow::Result<()>` if:
/// - Configuration file cannot be read
/// - YAML parsing fails
/// - Configuration validation fails
/// - Any stage execution fails
pub async fn prebake(
config_path: &str,
cli: &Cli,
@@ -27,7 +88,7 @@ pub async fn prebake(
) -> anyhow::Result<()> {
// Initialize
let config_content = std::fs::read_to_string(config_path)?;
let config = parse_prebake_config(config_content.as_str()).map_err(|e| {
let config = parse(config_content.as_str()).map_err(|e| {
log::error!("Failed to parse prebake.yml: {}", e);
PrebakeError::YamlParseError(e)
})?;
+99 -5
View File
@@ -1,5 +1,15 @@
//! Bootstrap stage implementation for the prebake workflow.
//!
//! This module handles the initial setup of the build environment, including:
//! - User creation for the build environment
//! - Workspace directory setup
//! - Privilege executor configuration (sudo/doas)
//!
//! The bootstrap script is generated dynamically based on the prebake configuration
//! and the target operating system, supporting multiple package managers.
use crate::ExecutionContext;
use crate::engine::{Engine, EventSender, ExecutionResult, pm};
use crate::engine::{Engine, EventSender, pm};
use crate::error::BootstrapError;
use crate::prebake::config::Bootstrap;
use crate::prebake::config::PrebakeConfig;
@@ -8,12 +18,35 @@ use std::io::Write;
use std::path::PathBuf;
use which::which;
/// Executes the bootstrap stage of the prebake workflow.
///
/// This function generates a bootstrap script based on the provided configuration,
/// writes it to `/bootstrap.sh`, and then executes it within the build environment.
///
/// # Arguments
///
/// * `config` - The prebake configuration containing bootstrap, dependencies, and security settings
/// * `osinfo` - Operating system information for platform-specific configuration
/// * `ctx` - Execution context containing pipeline parameters and dry-run flag
/// * `event_tx` - Event sender for reporting stage progress (currently unused)
///
/// # Returns
///
/// Returns `Ok(())` on successful bootstrap execution, or a `BootstrapError` if:
/// - The bootstrap script generation fails
/// - The generated script does not exist at `/bootstrap.sh`
/// - Script execution fails
///
/// # Dry Run Mode
///
/// When `ctx.dry_run` is `true`, the function generates the bootstrap script
/// and logs it without executing it, allowing inspection of the generated script.
pub async fn bootstrap(
config: &PrebakeConfig,
osinfo: &os_info::Info,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> Result<ExecutionResult, BootstrapError> {
) -> Result<(), BootstrapError> {
let engine = Engine::new();
let bootstrap_path = PathBuf::from("/bootstrap.sh");
@@ -21,9 +54,15 @@ pub async fn bootstrap(
if bootstrap_config.custom.is_none() {
let script = generate_bootstrap(&bootstrap_config, &config, &osinfo)?;
if ctx.dry_run {
log::info!("[DRY_RUN] Generated bootstrap script:\n{}", script);
return Ok(());
}
log::debug!("Generated bootstrap script:\n{}", script);
let mut file = File::create(&bootstrap_path)?;
file.write_all(script.as_bytes())?;
} else {
// Requires PIP(Pipeline Implicit Parameters) to be implemented
unimplemented!("Not supporting custom bootstrap.sh at this time");
}
@@ -37,9 +76,39 @@ pub async fn bootstrap(
engine
.execute_script(&bootstrap_path, ctx, &event_tx)
.await
.map_err(|e| BootstrapError::ExecutionError(e))
.map_err(|e| BootstrapError::ExecutionError(e))?;
Ok(())
}
/// Generates a bootstrap script based on configuration and target OS.
///
/// The generated script performs the following setup tasks:
///
/// 1. **User Creation**: Creates a dedicated user for the build environment
/// using `useradd`, `adduser`, or `busybox useradd` depending on availability
///
/// 2. **Workspace Setup**: Creates the workspace directory and optionally
/// symlinks it to `/workspace` for convenient access
///
/// 3. **Privilege Executor**: Configures `sudo` or `doas` to allow the build user
/// to execute package manager commands without a password
///
/// # Arguments
///
/// * `bootstrap_config` - Bootstrap-specific configuration (user, workspace, sudoers)
/// * `config` - Full prebake configuration for dependency and package manager detection
/// * `osinfo` - Operating system information for package manager detection
///
/// # Returns
///
/// Returns the generated bootstrap script as a `String`, or a `BootstrapError` if:
/// - No suitable user creation binary is found
/// - Package manager configuration is invalid
///
/// # Script Format
///
/// The generated script uses `#!/bin/sh` and includes `set -e` for error handling.
/// It outputs "Bootstrap complete!" upon successful completion.
fn generate_bootstrap(
bootstrap_config: &Bootstrap,
config: &PrebakeConfig,
@@ -54,6 +123,7 @@ fn generate_bootstrap(
// Create user
let user = &bootstrap_config.user;
if user == "root" {
// Requires PIP(Pipeline Implicit Parameters) to be implemented
unimplemented!("Permission check is not implemented, failing...");
} else {
script.push_str("# User creation\n");
@@ -97,15 +167,17 @@ fn generate_bootstrap(
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
let content = format!("{} ALL=(ALL:ALL) NOPASSWD {}", user, allowed_commands);
script.push_str(&format!(
"echo '{}' | tee /etc/sudoers.d/workshop\n",
"echo '{}' | tee /etc/sudoers.d/workshop > /dev/null\n",
content
));
script.push_str("chmod 440 /etc/sudoers.d/workshop\n");
}
} else if which("doas").is_ok() {
if bootstrap_config.doas.is_some() {
// Requires PIP(Pipeline Implicit Parameters) to be implemented
unimplemented!("Permission check is not implemented, failing...")
}
// We do not know how to write doas.conf lol
unimplemented!("doas is not implemented");
}
@@ -113,6 +185,28 @@ fn generate_bootstrap(
Ok(script)
}
/// Detects and selects an appropriate package manager based on configuration and OS.
///
/// This function first attempts to detect the OS's native package manager, then
/// checks if it's configured in the prebake.yml. If the detected package manager
/// is not configured, it falls back to the first configured package manager.
///
/// # Detection Priority
///
/// 1. **Detected Package Manager**: Uses `pm::detect()` to find the OS-native package manager
/// (e.g., `apt` on Debian/Ubuntu, `pacman` on Arch Linux)
/// 2. **Configured Package Manager**: If detected manager is not in config, uses the
/// first configured package manager from the dependencies section
///
/// # Arguments
///
/// * `config` - Prebake configuration containing dependencies.system configuration
/// * `osinfo` - Operating system information for package manager detection
///
/// # Returns
///
/// Returns `Some(Box<dyn PackageManager>)` if a suitable package manager is found,
/// or `None` if no package manager is configured or detectable.
fn get_package_manager(config: &PrebakeConfig, osinfo: &os_info::Info) -> Option<Box<dyn pm::PackageManager>> {
config
.dependencies
@@ -141,7 +235,7 @@ fn get_package_manager(config: &PrebakeConfig, osinfo: &os_info::Info) -> Option
}
}
log::error!("No valid package manager found in configuration");
log::warn!("No valid package manager found in configuration");
None
})
}
+50
View File
@@ -1,9 +1,59 @@
//! Hook execution stage implementation for the prebake workflow.
//!
//! This module handles the execution of user-defined hook commands at various stages
//! of the prebake workflow. Hooks are custom commands that can be executed before
//! or after dependency installation.
//!
//! # Hook Types
//!
//! - **Early Hooks**: Executed before dependency installation
//! - **Late Hooks**: Executed after dependency installation
//!
//! Each hook command supports custom working directories, environment variables,
//! and timeout configurations.
use crate::engine::types::DeltaExecutionContext;
use crate::ExecutionContext;
use crate::engine::{Engine, EventSender, ExecutionError};
use crate::types::command::CustomCommand;
use std::path::PathBuf;
/// Executes a sequence of hook commands.
///
/// This function processes a list of hook commands, executing each one in sequence
/// with its own working directory, environment variables, and timeout settings.
///
/// # Arguments
///
/// * `hook` - Optional vector of hook commands to execute. If `None` or empty,
/// the function returns immediately with `Ok(())`.
/// * `ctx` - Base execution context containing pipeline parameters and settings
/// * `event_tx` - Event sender for reporting hook execution progress
///
/// # Execution Behavior
///
/// 1. Hooks are executed sequentially in the order they appear in the configuration
/// 2. Each hook can specify:
/// - A custom working directory (inherited from parent if not specified)
/// - Custom environment variables (merged with parent environment)
/// - A custom timeout (defaults to the hook's configured value)
/// 3. If any hook fails, execution stops and an error is returned
///
/// # Returns
///
/// Returns `Ok(())` if all hooks execute successfully, or `Err(ExecutionError)`
/// if any hook command fails. The error includes the hook index and failure details.
///
/// # Example
///
/// ```yaml
/// hooks:
/// early:
/// - command: "echo 'Starting dependency installation'"
/// name: "announce-start"
/// working_dir: "/workspace"
/// timeout: 30
/// ```
pub async fn hook(
hook: Option<Vec<CustomCommand>>,
ctx: &ExecutionContext,
+232 -175
View File
@@ -22,6 +22,17 @@ import docker
import pytest
def docker_available():
"""Check if Docker daemon is available."""
try:
client = docker.from_env()
client.ping()
client.close()
return True
except Exception:
return False
class DockerContainer:
"""Context manager for Docker container lifecycle."""
@@ -117,20 +128,19 @@ class DockerContainer:
self.container.put_archive(os.path.dirname(container_path), tar_stream)
class TestPrebakeIntegration:
"""Integration tests for workshop-baker prebake workflow."""
class TestBootstrapUnit:
"""Unit tests for bootstrap stage - tests bootstrap.sh generation."""
IMAGE = "archlinux:latest"
CONTAINER_NAME = "workshop-baker-integration-test"
PROJECT_ROOT = Path(__file__).parent.parent.parent
BINARY_NAME = "workshop-executor"
EXAMPLES_DIR = PROJECT_ROOT / "examples"
PREBAKE_YML = EXAMPLES_DIR / "prebake.yml"
@pytest.fixture(scope="class")
def container_with_binary(self):
"""Provide a running archlinux container with pre-built binary copied."""
# Determine binary path (prefer release)
if not docker_available():
pytest.skip("Docker daemon is not available")
release_binary = self.PROJECT_ROOT / "target" / "release" / self.BINARY_NAME
debug_binary = self.PROJECT_ROOT / "target" / "debug" / self.BINARY_NAME
@@ -143,7 +153,6 @@ class TestPrebakeIntegration:
f"Binary {self.BINARY_NAME} not found. Run 'cargo build --release' first."
)
# Check if image exists locally
client = docker.from_env()
try:
client.images.get(self.IMAGE)
@@ -162,29 +171,25 @@ class TestPrebakeIntegration:
"volumes": {
str(self.PROJECT_ROOT): {"bind": "/workshop-baker", "mode": "rw"},
},
"working_dir": "/workshop-baker",
}
container = DockerContainer(
self.IMAGE,
self.CONTAINER_NAME,
f"workshop-baker-unit-{int(time.time())}",
**container_kwargs,
)
with container:
# Copy pre-built binary to container
container.copy_file(binary_path, f"/usr/local/bin/{self.BINARY_NAME}")
# Make binary executable
exit_code, stdout, stderr = container.exec_run(
f"chmod +x /usr/local/bin/{self.BINARY_NAME}"
)
if exit_code != 0:
pytest.fail(f"Failed to make binary executable: {stderr}")
# Create a minimal bootstrap.sh (required by prebake.yml)
bootstrap_sh = container.host_dir / "bootstrap.sh"
bootstrap_sh.write_text("#!/bin/bash\nexit 0\n")
bootstrap_sh.write_text("#!/bin/bash\necho 'Bootstrap executed'\nexit 0\n")
container.copy_file(bootstrap_sh, "/bootstrap.sh")
exit_code, stdout, stderr = container.exec_run("chmod +x /bootstrap.sh")
@@ -193,88 +198,118 @@ class TestPrebakeIntegration:
yield container
def test_binary_help(self, container_with_binary: DockerContainer):
"""Verify binary runs and shows help."""
cmd = f"/usr/local/bin/{self.BINARY_NAME} --help"
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
assert exit_code == 0, f"Binary help failed: {stderr}"
def test_prebake_subcommand_exists(self, container_with_binary: DockerContainer):
"""Verify prebake subcommand is recognized."""
cmd = f"/usr/local/bin/{self.BINARY_NAME} --help"
exit_code, stdout, stderr = container_with_binary.exec_run(
cmd, workdir="/workshop-baker"
)
combined = stdout + stderr
assert "prebake" in combined.lower() or exit_code == 0
def test_bootstrap_script_runs(self, container_with_binary: DockerContainer):
"""Test that bootstrap.sh can be executed."""
exit_code, stdout, stderr = container_with_binary.exec_run(
"/bootstrap.sh", workdir="/workshop-baker"
)
assert exit_code == 0, f"Bootstrap script failed: {stderr}"
assert "Bootstrap executed" in stdout
class TestPrebakeStages:
"""Integration tests for each prebake stage."""
IMAGE = "archlinux:latest"
PROJECT_ROOT = Path(__file__).parent.parent.parent
BINARY_NAME = "workshop-executor"
EXAMPLES_DIR = PROJECT_ROOT / "examples"
PREBAKE_YML = EXAMPLES_DIR / "prebake.yml"
@pytest.fixture(scope="class")
def container(self):
"""Provide a running archlinux container (without binary copy)."""
def container_with_binary(self):
"""Provide a running archlinux container with pre-built binary copied."""
if not docker_available():
pytest.skip("Docker daemon is not available")
release_binary = self.PROJECT_ROOT / "target" / "release" / self.BINARY_NAME
debug_binary = self.PROJECT_ROOT / "target" / "debug" / self.BINARY_NAME
if release_binary.exists():
binary_path = release_binary
elif debug_binary.exists():
binary_path = debug_binary
else:
pytest.skip(
f"Binary {self.BINARY_NAME} not found. Run 'cargo build --release' first."
)
client = docker.from_env()
try:
client.images.get(self.IMAGE)
except docker.errors.ImageNotFound:
try:
print(f"Pulling image {self.IMAGE}...")
client.images.pull(self.IMAGE)
except docker.errors.DockerException as e:
pytest.skip(f"Could not pull image {self.IMAGE}: {e}")
except Exception as e:
pytest.skip(f"Could not access Docker: {e}")
finally:
client.close()
container_kwargs = {
"volumes": {
str(self.PROJECT_ROOT): {"bind": "/workshop-baker", "mode": "rw"},
},
"working_dir": "/workshop-baker",
}
container = DockerContainer(
self.IMAGE,
None,
f"workshop-baker-stages-{int(time.time())}",
**container_kwargs,
)
with container:
container.copy_file(binary_path, f"/usr/local/bin/{self.BINARY_NAME}")
exit_code, stdout, stderr = container.exec_run(
f"chmod +x /usr/local/bin/{self.BINARY_NAME}"
)
if exit_code != 0:
pytest.fail(f"Failed to make binary executable: {stderr}")
bootstrap_sh = container.host_dir / "bootstrap.sh"
bootstrap_sh.write_text(
"#!/bin/bash\n"
"set -e\n"
"echo 'Bootstrap stage: Creating user'\n"
"if command -v useradd &> /dev/null; then\n"
" useradd -m -s /bin/nologin vulcan 2>/dev/null || true\n"
"elif command -v adduser &> /dev/null; then\n"
" adduser -D vulcan 2>/dev/null || true\n"
"fi\n"
"echo 'Bootstrap stage: Setting up workspace'\n"
"mkdir -p /workspace\n"
"ln -sf /workspace /workspace_link 2>/dev/null || true\n"
"echo 'Bootstrap complete!'\n"
"exit 0\n"
)
container.copy_file(bootstrap_sh, "/bootstrap.sh")
exit_code, stdout, stderr = container.exec_run("chmod +x /bootstrap.sh")
if exit_code != 0:
pytest.fail(f"Failed to set bootstrap.sh executable: {stderr}")
yield container
@staticmethod
def _get_setup_script() -> str:
"""Return shell script to setup container environment (for non-binary tests)."""
return """
set -e
# Update package database
pacman -Sy --noconfirm
# Install base development tools
pacman -S --noconfirm \
base-devel \
git \
curl \
sudo \
python
echo "=== Container Setup Complete ==="
"""
def test_container_connectivity(self, container: DockerContainer):
"""Test that container can reach external networks."""
exit_code, stdout, stderr = container.exec_run("ping -c 1 8.8.8.8")
assert exit_code == 0, f"Container network connectivity failed: {stderr}"
def test_binary_exists(self):
"""Verify pre-built binary exists."""
release_binary = self.PROJECT_ROOT / "target" / "release" / self.BINARY_NAME
debug_binary = self.PROJECT_ROOT / "target" / "debug" / self.BINARY_NAME
assert release_binary.exists() or debug_binary.exists(), (
f"Binary {self.BINARY_NAME} not found. Run 'cargo build --release' first."
)
def test_binary_runs_in_container(self, container_with_binary: DockerContainer):
"""Test that the pre-built binary can execute in the container."""
cmd = f"/usr/local/bin/{self.BINARY_NAME} --help"
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
print(f"Binary help output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
# Binary should at least run and show help or version
assert exit_code == 0, f"Binary failed to execute: {stderr}"
assert (
"usage" in stdout.lower()
or "help" in stdout.lower()
or "version" in stdout.lower()
), f"Binary help output unexpected: {stdout}"
def test_prebake_help_command(self, container_with_binary: DockerContainer):
"""Test that the CLI help works."""
cmd = f"/usr/local/bin/{self.BINARY_NAME} --help"
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
assert exit_code == 0, f"Help command failed: {stderr}"
assert (
"Honey Biscuit Workshop" in stdout
or "Usage" in stdout
or "help" in stdout.lower()
)
def test_prebake_config_parsing(self, container_with_binary: DockerContainer):
"""Test that prebake.yml can be parsed correctly."""
"""Test that prebake.yml can be parsed without errors."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
@@ -287,15 +322,15 @@ echo "=== Container Setup Complete ==="
cmd, workdir="/workshop-baker"
)
print(f"Config parsing output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
print(f"Config parsing:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
# dry-run should succeed even if actual execution would fail
assert exit_code == 0 or "ERROR" not in stderr.upper(), (
combined = stdout + stderr
assert "error" not in combined.lower() or exit_code == 0, (
f"Config parsing failed: {stderr}"
)
def test_prebake_bootstrap_stage(self, container_with_binary: DockerContainer):
"""Test bootstrap stage execution."""
def test_bootstrap_stage_executes(self, container_with_binary: DockerContainer):
"""Test bootstrap stage runs successfully."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
@@ -307,13 +342,14 @@ echo "=== Container Setup Complete ==="
cmd, workdir="/workshop-baker"
)
print(f"Bootstrap test output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
print(f"Bootstrap stage:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
combined_output = stdout + stderr
assert exit_code == 0, f"Bootstrap stage failed: {stderr}"
assert exit_code == 0, (
f"Bootstrap stage failed with exit code {exit_code}: {stderr}"
)
def test_prebake_hooks_execution(self, container_with_binary: DockerContainer):
"""Test that hook commands are properly executed."""
def test_early_hook_executes(self, container_with_binary: DockerContainer):
"""Test early hook runs successfully."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
@@ -325,13 +361,12 @@ echo "=== Container Setup Complete ==="
cmd, workdir="/workshop-baker"
)
combined_output = stdout + stderr
print(f"Hooks test output:\n{combined_output}")
print(f"Early hook:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
assert exit_code == 0, f"Hooks execution failed: {stderr}"
assert exit_code == 0, f"Early hook failed with exit code {exit_code}: {stderr}"
def test_prebake_dependencies_config(self, container_with_binary: DockerContainer):
"""Test that dependencies section is properly processed."""
def test_late_hook_executes(self, container_with_binary: DockerContainer):
"""Test late hook runs successfully."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
@@ -343,12 +378,12 @@ echo "=== Container Setup Complete ==="
cmd, workdir="/workshop-baker"
)
print(f"Dependencies test output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
print(f"Late hook:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
assert exit_code == 0, f"Dependencies processing failed: {stderr}"
assert exit_code == 0, f"Late hook failed with exit code {exit_code}: {stderr}"
def test_prebake_environment_config(self, container_with_binary: DockerContainer):
"""Test that environment section is properly processed."""
def test_full_prebake_pipeline(self, container_with_binary: DockerContainer):
"""Test complete prebake pipeline from start to finish."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
@@ -360,12 +395,103 @@ echo "=== Container Setup Complete ==="
cmd, workdir="/workshop-baker"
)
print(f"Environment test output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
print(f"Full pipeline:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
assert exit_code == 0, f"Environment processing failed: {stderr}"
assert exit_code == 0, (
f"Full prebake pipeline failed with exit code {exit_code}: {stderr}"
)
def test_prebake_cache_config(self, container_with_binary: DockerContainer):
"""Test that cache configuration is properly processed."""
class TestPrebakeIntegration:
"""End-to-end integration tests for prebake workflow."""
IMAGE = "archlinux:latest"
PROJECT_ROOT = Path(__file__).parent.parent.parent
BINARY_NAME = "workshop-executor"
@pytest.fixture(scope="class")
def container_with_binary(self):
"""Provide a running archlinux container with pre-built binary copied."""
if not docker_available():
pytest.skip("Docker daemon is not available")
release_binary = self.PROJECT_ROOT / "target" / "release" / self.BINARY_NAME
debug_binary = self.PROJECT_ROOT / "target" / "debug" / self.BINARY_NAME
if release_binary.exists():
binary_path = release_binary
elif debug_binary.exists():
binary_path = debug_binary
else:
pytest.skip(
f"Binary {self.BINARY_NAME} not found. Run 'cargo build --release' first."
)
client = docker.from_env()
try:
client.images.get(self.IMAGE)
except docker.errors.ImageNotFound:
try:
print(f"Pulling image {self.IMAGE}...")
client.images.pull(self.IMAGE)
except docker.errors.DockerException as e:
pytest.skip(f"Could not pull image {self.IMAGE}: {e}")
except Exception as e:
pytest.skip(f"Could not access Docker: {e}")
finally:
client.close()
container_kwargs = {
"volumes": {
str(self.PROJECT_ROOT): {"bind": "/workshop-baker", "mode": "rw"},
},
}
container = DockerContainer(
self.IMAGE,
None,
**container_kwargs,
)
with container:
container.copy_file(binary_path, f"/usr/local/bin/{self.BINARY_NAME}")
exit_code, stdout, stderr = container.exec_run(
f"chmod +x /usr/local/bin/{self.BINARY_NAME}"
)
if exit_code != 0:
pytest.fail(f"Failed to make binary executable: {stderr}")
bootstrap_sh = container.host_dir / "bootstrap.sh"
bootstrap_sh.write_text(
"#!/bin/bash\nset -e\necho 'Bootstrap executed successfully'\nexit 0\n"
)
container.copy_file(bootstrap_sh, "/bootstrap.sh")
exit_code, stdout, stderr = container.exec_run("chmod +x /bootstrap.sh")
if exit_code != 0:
pytest.fail(f"Failed to set bootstrap.sh executable: {stderr}")
yield container
def test_container_network_connectivity(
self, container_with_binary: DockerContainer
):
"""Test that container has network connectivity."""
exit_code, stdout, stderr = container_with_binary.exec_run("ping -c 1 8.8.8.8")
assert exit_code == 0, f"Container network connectivity failed: {stderr}"
def test_binary_executable_in_container(
self, container_with_binary: DockerContainer
):
"""Test that the pre-built binary is executable in the container."""
cmd = f"/usr/local/bin/{self.BINARY_NAME} --version"
exit_code, stdout, stderr = container_with_binary.exec_run(cmd)
print(f"Binary version:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
assert exit_code == 0, f"Binary execution failed: {stderr}"
def test_prebake_command_in_container(self, container_with_binary: DockerContainer):
"""Test that prebake command works in the container."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
@@ -377,81 +503,12 @@ echo "=== Container Setup Complete ==="
cmd, workdir="/workshop-baker"
)
print(f"Cache config test output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
print(f"Prebake command:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
assert exit_code == 0, f"Cache configuration failed: {stderr}"
def test_prebake_security_config(self, container_with_binary: DockerContainer):
"""Test that security configuration is properly processed."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
"--pipeline test-pipeline "
"--build-id test-build-001 "
"prebake examples/prebake.yml"
combined = stdout + stderr
assert exit_code == 0 or "bootstrap" in combined.lower(), (
f"Prebake command failed with exit code {exit_code}: {stderr}"
)
exit_code, stdout, stderr = container_with_binary.exec_run(
cmd, workdir="/workshop-baker"
)
print(f"Security config test output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
assert exit_code == 0, f"Security configuration failed: {stderr}"
def test_prebake_full_run(self, container_with_binary: DockerContainer):
"""Test complete prebake workflow in actual run mode."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
"--pipeline test-pipeline "
"--build-id test-build-001 "
"prebake examples/prebake.yml"
)
exit_code, stdout, stderr = container_with_binary.exec_run(
cmd, workdir="/workshop-baker"
)
print(f"Full run output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
combined_output = stdout + stderr
assert "prebake" in combined_output.lower() or exit_code == 0, (
f"Prebake command failed to execute properly. Exit code: {exit_code}"
)
def test_prebake_metadata(self, container_with_binary: DockerContainer):
"""Test that metadata is properly processed."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
"--pipeline test-pipeline "
"--build-id test-build-001 "
"prebake examples/prebake.yml"
)
exit_code, stdout, stderr = container_with_binary.exec_run(
cmd, workdir="/workshop-baker"
)
print(f"Metadata test output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
assert exit_code == 0, f"Metadata processing failed: {stderr}"
def test_prebake_late_hooks(self, container_with_binary: DockerContainer):
"""Test that late hooks are properly executed."""
cmd = (
f"/usr/local/bin/{self.BINARY_NAME} "
"--username test-user "
"--pipeline test-pipeline "
"--build-id test-build-001 "
"prebake examples/prebake.yml"
)
exit_code, stdout, stderr = container_with_binary.exec_run(
cmd, workdir="/workshop-baker"
)
print(f"Late hooks test output:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}")
assert exit_code == 0, f"Late hooks execution failed: {stderr}"
class TestPrebakeConfigValidation: