refraction-forger/crates/forge-engine/src/phase2/oci.rs
Till Wegmueller 4290439e00
Add comprehensive tests and fix compiler warnings
- Fix false-positive unused_assignments warnings from thiserror/miette
  derive macros in Rust 2024 edition with crate-level #![allow]
- Add 5 tests for tar_layer (empty dir, files, nested dirs, symlinks,
  deterministic digest)
- Add 5 tests for manifest (default options, entrypoint/env, multiple
  layers, config digest verification, no entrypoint)
- Add 6 tests for layout (structure creation, oci-layout content,
  index.json references, layer blobs, config digest, multiple layers)
- Add 11 tests for overlays (file copy, empty file, missing source,
  ensure dir, symlink, remove file, remove dir contents, shadow
  create/update, multiple overlays)
- Add 4 tests for customizations (single user, multiple users, append
  to existing, no users noop)
- Add 3 tests for phase2/oci (layout output, entrypoint/env, empty
  staging)
- Add tempfile dev-dependency to forge-oci for test support

42 tests passing, 0 warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:40:16 +01:00

135 lines
4.1 KiB
Rust

use std::path::Path;
use spec_parser::schema::Target;
use tracing::info;
use crate::error::ForgeError;
/// Build an OCI container image from the staged rootfs.
pub fn build_oci(
target: &Target,
staging_root: &Path,
output_dir: &Path,
) -> Result<(), ForgeError> {
info!("Building OCI container image");
// Create the tar.gz layer from staging
let layer =
forge_oci::tar_layer::create_layer(staging_root).map_err(|e| ForgeError::OciBuild(e.to_string()))?;
info!(
digest = %layer.digest,
size = layer.data.len(),
"Layer created"
);
// Build image options from target spec
let mut options = forge_oci::manifest::ImageOptions::default();
if let Some(ref ep) = target.entrypoint {
options.entrypoint = Some(vec![ep.command.clone()]);
}
if let Some(ref env) = target.environment {
options.env = env
.vars
.iter()
.map(|v| format!("{}={}", v.key, v.value))
.collect();
}
// Build manifest and config
let (config_json, manifest_json) = forge_oci::manifest::build_manifest(&[layer.clone()], &options)
.map_err(|e| ForgeError::OciBuild(e.to_string()))?;
// Write OCI Image Layout
let oci_output = output_dir.join(format!("{}-oci", target.name));
forge_oci::layout::write_oci_layout(&oci_output, &[layer], &config_json, &manifest_json)
.map_err(|e| ForgeError::OciBuild(e.to_string()))?;
info!(path = %oci_output.display(), "OCI Image Layout written");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use spec_parser::schema::{Entrypoint, Environment, EnvVar, TargetKind};
use tempfile::TempDir;
fn make_target(name: &str, entrypoint: Option<Entrypoint>, env: Option<Environment>) -> Target {
Target {
name: name.to_string(),
kind: TargetKind::Oci,
disk_size: None,
bootloader: None,
entrypoint,
environment: env,
pool: None,
}
}
#[test]
fn test_build_oci_produces_layout() {
let staging = TempDir::new().unwrap();
let output = TempDir::new().unwrap();
// Create some files in staging
std::fs::create_dir_all(staging.path().join("etc")).unwrap();
std::fs::write(staging.path().join("etc/hostname"), "test-vm\n").unwrap();
std::fs::write(staging.path().join("etc/motd"), "Welcome\n").unwrap();
let target = make_target("container", None, None);
build_oci(&target, staging.path(), output.path()).unwrap();
let oci_dir = output.path().join("container-oci");
assert!(oci_dir.exists());
assert!(oci_dir.join("oci-layout").exists());
assert!(oci_dir.join("index.json").exists());
assert!(oci_dir.join("blobs/sha256").is_dir());
}
#[test]
fn test_build_oci_with_entrypoint_and_env() {
let staging = TempDir::new().unwrap();
let output = TempDir::new().unwrap();
std::fs::write(staging.path().join("hello.txt"), "hi").unwrap();
let target = make_target(
"app",
Some(Entrypoint {
command: "/bin/sh".to_string(),
}),
Some(Environment {
vars: vec![EnvVar {
key: "PATH".to_string(),
value: "/bin:/usr/bin".to_string(),
}],
}),
);
build_oci(&target, staging.path(), output.path()).unwrap();
let oci_dir = output.path().join("app-oci");
assert!(oci_dir.join("oci-layout").exists());
// Verify index.json is valid
let index: serde_json::Value = serde_json::from_slice(
&std::fs::read(oci_dir.join("index.json")).unwrap(),
)
.unwrap();
assert_eq!(index["schemaVersion"], 2);
}
#[test]
fn test_build_oci_empty_staging() {
let staging = TempDir::new().unwrap();
let output = TempDir::new().unwrap();
let target = make_target("minimal", None, None);
build_oci(&target, staging.path(), output.path()).unwrap();
assert!(output.path().join("minimal-oci/oci-layout").exists());
}
}