235 lines
8.6 KiB
Rust
235 lines
8.6 KiB
Rust
//! Build + sideload the container images the e2e harness deploys.
|
|
//!
|
|
//! v1: agent only (operator and callout get added when those layers
|
|
//! join the harness). Same pattern as
|
|
//! [`example_fleet_auth_callout::build_and_load_callout_image`]:
|
|
//!
|
|
//! 1. `cargo build --release -p <crate>` on the host (uses the
|
|
//! workspace cargo cache; incremental builds are fast).
|
|
//! 2. Stage the binary into a fresh tempdir.
|
|
//! 3. Single-stage Dockerfile copying the binary into an archlinux
|
|
//! runtime (matched glibc — see the callout Dockerfile rationale).
|
|
//! 4. `podman build`, then `podman save | k3d image import`.
|
|
//!
|
|
//! No content-hashing yet: cargo incremental + podman layer cache
|
|
//! cover the common case, and skipping that hash work keeps the
|
|
//! harness small. `FLEET_E2E_FORCE_REBUILD=1` is reserved as an env
|
|
//! escape hatch for the day we add caching.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Stdio;
|
|
|
|
use anyhow::{Context, Result};
|
|
use k3d_rs::K3d;
|
|
|
|
pub const AGENT_IMAGE_TAG: &str = "localhost/harmony-fleet-agent:e2e";
|
|
pub const OPERATOR_IMAGE_TAG: &str = "localhost/harmony-fleet-operator:e2e";
|
|
|
|
/// Tags of the images this harness build produces. Returned to the
|
|
/// caller so manifests reference the exact same tag we just pushed
|
|
/// to the cluster.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Images {
|
|
pub agent: String,
|
|
pub operator: String,
|
|
}
|
|
|
|
/// Build + sideload every image the harness needs. Idempotent across
|
|
/// runs (the tag is fixed; cargo + podman caches make subsequent
|
|
/// runs near-noop).
|
|
pub async fn build_and_sideload(k3d: &K3d, cluster_name: &str) -> Result<Images> {
|
|
build_and_load_agent_image(k3d, cluster_name).await?;
|
|
build_and_load_operator_image(k3d, cluster_name).await?;
|
|
Ok(Images {
|
|
agent: AGENT_IMAGE_TAG.to_string(),
|
|
operator: OPERATOR_IMAGE_TAG.to_string(),
|
|
})
|
|
}
|
|
|
|
async fn build_and_load_operator_image(k3d: &K3d, cluster_name: &str) -> Result<()> {
|
|
let workspace_root = workspace_root_from_env();
|
|
tracing::info!("cargo build --release -p harmony-fleet-operator");
|
|
let status = tokio::process::Command::new("cargo")
|
|
.args(["build", "--release", "-p", "harmony-fleet-operator"])
|
|
.current_dir(&workspace_root)
|
|
.status()
|
|
.await
|
|
.context("spawning cargo build")?;
|
|
if !status.success() {
|
|
anyhow::bail!("cargo build harmony-fleet-operator failed");
|
|
}
|
|
let ctx = tempfile::tempdir()?;
|
|
let bin_dst = ctx.path().join("target/release");
|
|
std::fs::create_dir_all(&bin_dst)?;
|
|
std::fs::copy(
|
|
workspace_root.join("target/release/harmony-fleet-operator"),
|
|
bin_dst.join("harmony-fleet-operator"),
|
|
)
|
|
.context("staging operator binary into build context")?;
|
|
std::fs::write(
|
|
ctx.path().join("Dockerfile"),
|
|
r#"FROM docker.io/library/archlinux:base
|
|
COPY target/release/harmony-fleet-operator /usr/local/bin/harmony-fleet-operator
|
|
USER 65532:65532
|
|
ENTRYPOINT ["/usr/local/bin/harmony-fleet-operator"]
|
|
"#,
|
|
)?;
|
|
tracing::info!("podman build -t {OPERATOR_IMAGE_TAG}");
|
|
let status = tokio::process::Command::new("podman")
|
|
.args(["build", "-q", "-t", OPERATOR_IMAGE_TAG, "."])
|
|
.current_dir(ctx.path())
|
|
.stderr(Stdio::inherit())
|
|
.status()
|
|
.await
|
|
.context("spawning podman build")?;
|
|
if !status.success() {
|
|
anyhow::bail!("podman build for harmony-fleet-operator failed");
|
|
}
|
|
let tar_path = std::env::temp_dir().join(format!(
|
|
"harmony-fleet-operator-image-{}.tar",
|
|
std::process::id()
|
|
));
|
|
let _ = std::fs::remove_file(&tar_path);
|
|
let status = tokio::process::Command::new("podman")
|
|
.args(["save", "-o", tar_path.to_str().unwrap(), OPERATOR_IMAGE_TAG])
|
|
.status()
|
|
.await
|
|
.context("podman save")?;
|
|
if !status.success() {
|
|
anyhow::bail!("podman save for harmony-fleet-operator failed");
|
|
}
|
|
tracing::info!("k3d image import {OPERATOR_IMAGE_TAG} (cluster={cluster_name})");
|
|
let tar_path_str = tar_path.to_str().unwrap().to_string();
|
|
let cluster_for_blocking = cluster_name.to_string();
|
|
let data_dir = k3d_data_dir();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
K3d::new(data_dir, Some(cluster_for_blocking.clone())).run_k3d_command([
|
|
"image",
|
|
"import",
|
|
tar_path_str.as_str(),
|
|
"-c",
|
|
cluster_for_blocking.as_str(),
|
|
])
|
|
})
|
|
.await
|
|
.context("spawn_blocking k3d image import")?;
|
|
let _ = std::fs::remove_file(&tar_path);
|
|
let output = result.map_err(|e| anyhow::anyhow!("k3d image import failed: {e}"))?;
|
|
if !output.status.success() {
|
|
anyhow::bail!(
|
|
"k3d image import returned status {}: {}",
|
|
output.status,
|
|
String::from_utf8_lossy(&output.stderr),
|
|
);
|
|
}
|
|
let _ = k3d;
|
|
Ok(())
|
|
}
|
|
|
|
async fn build_and_load_agent_image(k3d: &K3d, cluster_name: &str) -> Result<()> {
|
|
let workspace_root = workspace_root_from_env();
|
|
|
|
tracing::info!("cargo build --release -p harmony-fleet-agent");
|
|
let status = tokio::process::Command::new("cargo")
|
|
.args(["build", "--release", "-p", "harmony-fleet-agent"])
|
|
.current_dir(&workspace_root)
|
|
.status()
|
|
.await
|
|
.context("spawning cargo build")?;
|
|
if !status.success() {
|
|
anyhow::bail!("cargo build harmony-fleet-agent failed");
|
|
}
|
|
|
|
let ctx = tempfile::tempdir()?;
|
|
let bin_dst = ctx.path().join("target/release");
|
|
std::fs::create_dir_all(&bin_dst)?;
|
|
std::fs::copy(
|
|
workspace_root.join("target/release/harmony-fleet-agent"),
|
|
bin_dst.join("harmony-fleet-agent"),
|
|
)
|
|
.context("staging agent binary into build context")?;
|
|
|
|
// Single-stage Dockerfile inline (mirrors what the callout build
|
|
// does). archlinux:base for matched glibc against the host build —
|
|
// the canonical multi-stage Dockerfile in the agent crate is for
|
|
// production registries, not the e2e fast path.
|
|
std::fs::write(
|
|
ctx.path().join("Dockerfile"),
|
|
r#"FROM docker.io/library/archlinux:base
|
|
COPY target/release/harmony-fleet-agent /usr/local/bin/harmony-fleet-agent
|
|
USER 65532:65532
|
|
ENTRYPOINT ["/usr/local/bin/harmony-fleet-agent"]
|
|
"#,
|
|
)?;
|
|
|
|
tracing::info!("podman build -t {AGENT_IMAGE_TAG}");
|
|
let status = tokio::process::Command::new("podman")
|
|
.args(["build", "-q", "-t", AGENT_IMAGE_TAG, "."])
|
|
.current_dir(ctx.path())
|
|
.stderr(Stdio::inherit())
|
|
.status()
|
|
.await
|
|
.context("spawning podman build")?;
|
|
if !status.success() {
|
|
anyhow::bail!("podman build for harmony-fleet-agent failed");
|
|
}
|
|
|
|
let tar_path = std::env::temp_dir().join(format!(
|
|
"harmony-fleet-agent-image-{}.tar",
|
|
std::process::id()
|
|
));
|
|
let _ = std::fs::remove_file(&tar_path);
|
|
let status = tokio::process::Command::new("podman")
|
|
.args(["save", "-o", tar_path.to_str().unwrap(), AGENT_IMAGE_TAG])
|
|
.status()
|
|
.await
|
|
.context("podman save")?;
|
|
if !status.success() {
|
|
anyhow::bail!("podman save for harmony-fleet-agent failed");
|
|
}
|
|
|
|
tracing::info!("k3d image import {AGENT_IMAGE_TAG} (cluster={cluster_name})");
|
|
let tar_path_str = tar_path.to_str().unwrap().to_string();
|
|
let cluster_for_blocking = cluster_name.to_string();
|
|
let data_dir = k3d_data_dir();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
K3d::new(data_dir, Some(cluster_for_blocking.clone())).run_k3d_command([
|
|
"image",
|
|
"import",
|
|
tar_path_str.as_str(),
|
|
"-c",
|
|
cluster_for_blocking.as_str(),
|
|
])
|
|
})
|
|
.await
|
|
.context("spawn_blocking k3d image import")?;
|
|
let _ = std::fs::remove_file(&tar_path);
|
|
let output = result.map_err(|e| anyhow::anyhow!("k3d image import failed: {e}"))?;
|
|
if !output.status.success() {
|
|
anyhow::bail!(
|
|
"k3d image import returned status {}: {}",
|
|
output.status,
|
|
String::from_utf8_lossy(&output.stderr),
|
|
);
|
|
}
|
|
// Keep the k3d helper warm in scope so the borrow checker is
|
|
// happy (k3d is referenced by the caller's bring-up; this just
|
|
// ensures we used its cluster name even if the binary path went
|
|
// through the blocking task).
|
|
let _ = k3d;
|
|
Ok(())
|
|
}
|
|
|
|
fn workspace_root_from_env() -> PathBuf {
|
|
let root = std::env::var("CARGO_MANIFEST_DIR")
|
|
.map(|d| PathBuf::from(d).join("..").join(".."))
|
|
.unwrap_or_else(|_| PathBuf::from("."));
|
|
root.canonicalize().unwrap_or(root)
|
|
}
|
|
|
|
fn k3d_data_dir() -> PathBuf {
|
|
directories::BaseDirs::new()
|
|
.map(|dirs| dirs.data_dir().join("harmony").join("k3d"))
|
|
.unwrap_or_else(|| PathBuf::from("/tmp/harmony"))
|
|
}
|