use foundation_core::valtron::initialize_pool;
use foundation_deployment_platform::docker::{
ContainerConfig, ContainerHandle, DockerClient, DockerError, DockerFileConfig,
};
fn docker_available() -> bool {
std::path::Path::new("/var/run/docker.sock").exists()
}
fn tag(name: &str) -> String {
format!("ewe-f04-{name}:{}", std::process::id())
}
async fn remove_image(t: &str) {
if let Ok(c) = DockerClient::connect_with_defaults() {
let _ = c.image_delete(t, Some(true), Some(false)).await;
}
}
#[test]
#[ignore = "requires Docker daemon running"]
fn builds_an_inline_dockerfile_into_a_runnable_image() {
if !docker_available() {
eprintln!("SKIP: Docker not available");
return;
}
let _pool = initialize_pool(54, Some(4));
foundation_core::valtron::block_on_future(async {
let t = tag("inline");
remove_image(&t).await;
let built = DockerFileConfig::new(
"FROM alpine:3\nRUN echo built-natively > /marker\nCMD [\"cat\", \"/marker\"]\n",
)
.tag(&t)
.build_once()
.await
.expect("inline build should succeed");
assert_eq!(built.image_tag, t);
assert!(!built.sha256.is_empty(), "a built image should report a digest");
assert!(!built.was_cached, "the first build is not a cache hit");
let handle = ContainerHandle::start_async(
ContainerConfig::new(&t).stop_timeout_secs(2),
)
.await
.expect("the built image should run");
let logs = handle.wait_for_exit_async().await.expect("logs");
assert!(
logs.contains("built-natively"),
"the built image should carry what the Dockerfile made, got: {logs:?}"
);
drop(handle);
let again = DockerFileConfig::new("FROM alpine:3\nRUN echo ignored > /marker\n")
.tag(&t)
.build_once()
.await
.expect("second build_once should succeed");
assert!(again.was_cached, "an existing tag must not be rebuilt");
assert_eq!(again.sha256, built.sha256, "the cached digest is the built one");
remove_image(&t).await;
});
}
#[test]
#[ignore = "requires Docker daemon running"]
fn build_context_files_are_uploaded_and_build_args_apply() {
if !docker_available() {
eprintln!("SKIP: Docker not available");
return;
}
let _pool = initialize_pool(54, Some(4));
let dir = std::env::temp_dir().join(format!("ewe-f04-ctx-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create context dir");
std::fs::write(dir.join("payload.txt"), b"file-from-the-context").expect("write context file");
let ctx = dir.clone();
foundation_core::valtron::block_on_future(async move {
let t = tag("context");
remove_image(&t).await;
DockerFileConfig::new(
"FROM alpine:3\n\
ARG GREETING=unset\n\
COPY payload.txt /payload.txt\n\
RUN echo arg-was-$GREETING > /arg.txt\n\
CMD [\"sh\", \"-c\", \"cat /payload.txt; echo; cat /arg.txt\"]\n",
)
.context(&ctx)
.arg("GREETING", "delivered")
.tag(&t)
.build_once()
.await
.expect("context build should succeed");
let handle = ContainerHandle::start_async(
ContainerConfig::new(&t).stop_timeout_secs(2),
)
.await
.expect("the built image should run");
let logs = handle.wait_for_exit_async().await.expect("logs");
assert!(
logs.contains("file-from-the-context"),
"the context file must have been uploaded and COPYed, got: {logs:?}"
);
assert!(
logs.contains("arg-was-delivered"),
"the build arg must have reached the build, got: {logs:?}"
);
drop(handle);
remove_image(&t).await;
});
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "requires Docker daemon running"]
fn a_failing_dockerfile_is_reported_as_an_error() {
if !docker_available() {
eprintln!("SKIP: Docker not available");
return;
}
let _pool = initialize_pool(54, Some(4));
foundation_core::valtron::block_on_future(async {
let t = tag("failing");
let result = DockerFileConfig::new("FROM alpine:3\nRUN exit 7\n")
.tag(&t)
.build_once()
.await;
match result {
Err(trace) => {
let msg = format!("{trace}");
assert!(
msg.contains("Image build failed"),
"a failing build should surface as ImageBuild, got: {msg}"
);
}
Ok(built) => panic!("a failing Dockerfile must not report success: {built:?}"),
}
});
}
#[test]
fn an_untagged_build_is_rejected_up_front() {
let _pool = initialize_pool(54, Some(4));
foundation_core::valtron::block_on_future(async {
let result = DockerFileConfig::new("FROM alpine:3\n").build_once().await;
match result {
Err(trace) => {
let msg = format!("{trace}");
assert!(
msg.contains("tag is required"),
"the error should say what is missing, got: {msg}"
);
}
Ok(built) => panic!("an untagged build must not succeed: {built:?}"),
}
});
}
#[cfg(feature = "buildkit")]
mod buildkit_backend {
use super::*;
use foundation_deployment_platform::docker::BuildBackend;
fn buildkitd_addr() -> Option<String> {
let addr = std::env::var("EWE_BUILDKITD_ADDR")
.unwrap_or_else(|_| "127.0.0.1:13434".to_string());
match std::net::TcpStream::connect(&addr) {
Ok(_) => Some(addr),
Err(_) => {
eprintln!("SKIP: no buildkitd at {addr} (set EWE_BUILDKITD_ADDR)");
None
}
}
}
#[test]
#[ignore = "requires a running buildkitd"]
fn builds_an_inline_dockerfile_through_buildkitd() {
let Some(addr) = buildkitd_addr() else { return };
let _pool = initialize_pool(54, Some(4));
foundation_core::valtron::block_on_future(async move {
let t = tag("buildkit");
let built = DockerFileConfig::new(
"FROM alpine:3\nRUN echo built-by-buildkit > /marker\n",
)
.tag(&t)
.backend(BuildBackend::BuildKit(addr))
.build_once()
.await
.expect("buildkit solve should succeed");
assert_eq!(built.image_tag, t);
assert!(
built.sha256.starts_with("sha256:"),
"buildkitd should report the built image digest, got: {:?}",
built.sha256
);
});
}
#[test]
#[ignore = "requires a running buildkitd"]
fn a_failing_dockerfile_fails_the_solve() {
let Some(addr) = buildkitd_addr() else { return };
let _pool = initialize_pool(54, Some(4));
foundation_core::valtron::block_on_future(async move {
let result = DockerFileConfig::new("FROM alpine:3\nRUN exit 7\n")
.tag(tag("buildkit-fail"))
.backend(BuildBackend::BuildKit(addr))
.build_once()
.await;
match result {
Err(trace) => {
let msg = format!("{trace}");
assert!(
msg.contains("Image build failed"),
"a failing solve should surface as ImageBuild, got: {msg}"
);
}
Ok(built) => panic!("a failing Dockerfile must not report success: {built:?}"),
}
});
}
}