#![cfg(all(unix, feature = "docker", feature = "integration-tests"))]
use foundation_core::valtron::valtron_test;
use foundation_deployment_docker::client::build_context::ContextTar;
use foundation_deployment_docker::client::images::ImageBuildOptions;
use foundation_deployment_docker::client::body::ProgressStream;
use foundation_deployment_docker::generated::shared::BuildInfo;
use foundation_deployment_docker::{DockerClient, DockerError};
#[derive(Debug)]
struct BuildOutcome {
logs: String,
image_id: Option<String>,
}
async fn read_build(mut stream: ProgressStream<BuildInfo>) -> Result<BuildOutcome, DockerError> {
let mut outcome = BuildOutcome { logs: String::new(), image_id: None };
while let Some(item) = stream.next().await {
let item = item?;
if let Some(text) = item.stream {
outcome.logs.push_str(&text);
}
if let Some(id) = item.aux.and_then(|aux| aux.id) {
outcome.image_id = Some(id);
}
}
Ok(outcome)
}
fn client() -> DockerClient {
DockerClient::connect_unix("/var/run/docker.sock")
}
fn tag(name: &str) -> String {
format!("ewe-build-it-{name}:{}", std::process::id())
}
async fn remove_image(c: &DockerClient, t: &str) {
let _ = c.image_delete(t, Some(true), Some(false)).await;
}
#[valtron_test]
async fn builds_an_inline_dockerfile_and_tags_it() {
let c = client();
let t = tag("inline");
remove_image(&c, &t).await;
let context = ContextTar::from_inline_dockerfile(
"Dockerfile",
"FROM alpine:3\nRUN echo built-by-the-native-client > /marker\n",
)
.expect("tar the inline Dockerfile");
let outcome = c
.image_build(
&context,
&ImageBuildOptions {
tag: Some(t.clone()),
..Default::default()
},
)
.await
.expect("build should start");
let outcome = read_build(outcome).await.expect("build should succeed");
assert!(
outcome.logs.contains("FROM alpine:3"),
"the build log should carry the daemon's steps, got: {:?}",
outcome.logs
);
let inspect = c.image_inspect(&t).await.expect("built image should exist");
assert!(inspect.id.is_some(), "built image should report an id");
remove_image(&c, &t).await;
}
#[valtron_test]
async fn context_directory_files_are_uploaded_and_copyable() {
let c = client();
let t = tag("context");
remove_image(&c, &t).await;
let dir = std::env::temp_dir().join(format!("ewe-build-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 context = ContextTar::from_dir_with_dockerfile(
&dir,
"Dockerfile",
"FROM alpine:3\nCOPY payload.txt /payload.txt\nRUN cat /payload.txt\n",
)
.expect("tar the context dir");
let outcome = c
.image_build(
&context,
&ImageBuildOptions {
tag: Some(t.clone()),
..Default::default()
},
)
.await
.expect("build with a context should start");
let outcome = read_build(outcome).await.expect("build with a context should succeed");
assert!(
outcome.logs.contains("file-from-the-context"),
"the context file's content should appear in the build log, got: {:?}",
outcome.logs
);
remove_image(&c, &t).await;
let _ = std::fs::remove_dir_all(&dir);
}
#[valtron_test]
async fn build_args_reach_the_build() {
let c = client();
let t = tag("args");
remove_image(&c, &t).await;
let context = ContextTar::from_inline_dockerfile(
"Dockerfile",
"FROM alpine:3\nARG GREETING=unset\nRUN echo arg-was-$GREETING\n",
)
.expect("tar the inline Dockerfile");
let outcome = c
.image_build(
&context,
&ImageBuildOptions {
tag: Some(t.clone()),
build_args: vec![("GREETING".to_string(), "delivered".to_string())],
no_cache: true,
..Default::default()
},
)
.await
.expect("build should start");
let outcome = read_build(outcome).await.expect("build should succeed");
assert!(
outcome.logs.contains("arg-was-delivered"),
"the build arg should have reached the build, got: {:?}",
outcome.logs
);
remove_image(&c, &t).await;
}
#[valtron_test]
async fn a_failing_build_is_an_error_not_a_success() {
let c = client();
let t = tag("failing");
let context = ContextTar::from_inline_dockerfile(
"Dockerfile",
"FROM alpine:3\nRUN exit 7\n",
)
.expect("tar the inline Dockerfile");
let result = c
.image_build(
&context,
&ImageBuildOptions {
tag: Some(t.clone()),
no_cache: true,
force_rm: true,
..Default::default()
},
)
.await
.expect("the daemon answers 200: a failed build is reported in the stream");
match read_build(result).await {
Err(DockerError::BuildFailed(msg)) => {
assert!(
msg.contains('7') || msg.to_lowercase().contains("non-zero"),
"the failure should name the failing command's exit, got: {msg:?}"
);
}
Err(other) => panic!("expected BuildFailed, got: {other:?}"),
Ok(outcome) => panic!("a failing Dockerfile must not report success: {outcome:?}"),
}
remove_image(&c, &t).await;
}