mod buildkit;
pub mod config;
mod docker;
mod dockerfile_ssl;
mod method;
mod pack;
mod proxy;
mod railpack;
mod registry;
mod ssl;
pub use method::BuildArgs;
pub(crate) use method::{BuildMethod, BuildOptions};
pub(crate) use railpack::{build_with_buildctl, BuildctlFrontend, RailpackBuildOptions};
pub(crate) use registry::{docker_login, docker_pull, docker_push, docker_tag};
use anyhow::{bail, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};
use buildkit::{check_ssl_cert_and_warn, ensure_managed_buildkit_daemon};
use docker::{build_image_with_dockerfile, DockerBuildOptions};
use method::{requires_buildkit, select_build_method};
use pack::build_image_with_buildpacks;
use railpack::build_image_with_railpacks;
pub(crate) fn env_var_non_empty(key: &str) -> Option<String> {
std::env::var(key)
.ok()
.and_then(|v| if v.is_empty() { None } else { Some(v) })
}
pub(crate) fn resolve_ssl_cert_file() -> Option<std::path::PathBuf> {
let ssl_cert_file = env_var_non_empty("SSL_CERT_FILE")?;
let path = std::path::PathBuf::from(&ssl_cert_file);
if path.exists() {
Some(path)
} else {
warn!(
"SSL_CERT_FILE set to '{}' but file not found",
ssl_cert_file
);
None
}
}
pub(crate) fn parse_bool_env_var(key: &str) -> Option<bool> {
let val = env_var_non_empty(key)?;
match val.to_lowercase().as_str() {
"true" | "1" => Some(true),
"false" | "0" => Some(false),
_ => panic!(
"Invalid boolean value for {}: {:?} (expected true/false/1/0)",
key, val
),
}
}
pub(crate) fn build_image(options: BuildOptions) -> Result<()> {
let container_cli = &options.container_cli;
debug!(
"Using container CLI: {} ({:?})",
container_cli.command(),
container_cli.runtime()
);
info!(
"Building image '{}' from path '{}'",
options.image_tag, options.app_path
);
let app_path = Path::new(&options.app_path);
if !app_path.exists() {
bail!("Path '{}' does not exist", options.app_path);
}
if !app_path.is_dir() {
bail!("Path '{}' is not a directory", options.app_path);
}
let (build_method, dockerfile) = select_build_method(
&options.app_path,
options.backend.as_deref(),
options.dockerfile.as_deref(),
container_cli.command(),
)?;
let managed_buildkit = match options.managed_buildkit {
Some(value) => {
value
}
None => {
env_var_non_empty("BUILDKIT_HOST").is_none()
&& requires_buildkit(&build_method)
&& env_var_non_empty("SSL_CERT_FILE").is_some()
}
};
let buildkit_host = if requires_buildkit(&build_method) && managed_buildkit {
if let Some(existing_host) = env_var_non_empty("BUILDKIT_HOST") {
info!("Using existing BUILDKIT_HOST: {}", existing_host);
Some(existing_host)
} else {
let ssl_cert_path = env_var_non_empty("SSL_CERT_FILE").map(PathBuf::from);
Some(ensure_managed_buildkit_daemon(
ssl_cert_path.as_deref(),
container_cli,
)?)
}
} else {
check_ssl_cert_and_warn(&build_method, managed_buildkit);
None
};
let resolved_build_context = options.build_context.as_ref().map(|ctx| {
let resolved = app_path.join(ctx);
resolved.to_string_lossy().to_string()
});
let resolved_build_contexts: std::collections::HashMap<String, String> = options
.build_contexts
.iter()
.map(|(name, path)| {
let resolved = app_path.join(path);
(name.clone(), resolved.to_string_lossy().to_string())
})
.collect();
match build_method {
BuildMethod::Docker { use_buildx } => {
if options.builder.is_some() {
warn!("--builder flag is ignored when using docker build method");
}
if !options.buildpacks.is_empty() {
warn!("--buildpack flags are ignored when using docker build method");
}
build_image_with_dockerfile(DockerBuildOptions {
app_path: &options.app_path,
dockerfile: dockerfile.as_deref(),
image_tag: &options.image_tag,
container_cli: container_cli.command(),
buildx_supports_push: container_cli.buildx_supports_push(),
use_buildx,
push: options.push,
buildkit_host: buildkit_host.as_deref(),
env: &options.env,
build_context: resolved_build_context.as_deref(),
build_contexts: &resolved_build_contexts,
no_cache: options.no_cache,
})?;
}
BuildMethod::Pack => {
if options.explicit_container_cli {
warn!("--container-cli flag is ignored when using pack build method");
}
if options.managed_buildkit.is_some() {
warn!("--managed-buildkit flag is ignored when using pack build method");
}
build_image_with_buildpacks(
&options.app_path,
&options.image_tag,
options.builder.as_deref(),
&options.buildpacks,
&options.env,
options.no_cache,
)?;
if options.push {
registry::docker_push(container_cli.command(), &options.image_tag)?;
}
}
BuildMethod::Railpack { use_buildctl } => {
if options.builder.is_some() {
warn!("--builder flag is ignored when using railpack build method");
}
if !options.buildpacks.is_empty() {
warn!("--buildpack flags are ignored when using railpack build method");
}
if use_buildctl && options.explicit_container_cli {
warn!("--container-cli flag is ignored when using railpack:buildctl build method");
}
build_image_with_railpacks(RailpackBuildOptions {
app_path: &options.app_path,
image_tag: &options.image_tag,
container_cli: container_cli.command(),
buildx_supports_push: container_cli.buildx_supports_push(),
use_buildctl,
push: options.push,
buildkit_host: buildkit_host.as_deref(),
env: &options.env,
no_cache: options.no_cache,
})?;
}
BuildMethod::Buildctl => {
if options.builder.is_some() {
warn!("--builder flag is ignored when using buildctl build method");
}
if !options.buildpacks.is_empty() {
warn!("--buildpack flags are ignored when using buildctl build method");
}
if options.explicit_container_cli {
warn!("--container-cli flag is ignored when using buildctl build method");
}
let ssl_cert_path = resolve_ssl_cert_file();
let original_dockerfile_path = dockerfile
.as_ref()
.map(|df| Path::new(&options.app_path).join(df))
.unwrap_or_else(|| Path::new(&options.app_path).join("Dockerfile"));
let (_temp_dir, effective_dockerfile) = if ssl_cert_path.is_some() {
if original_dockerfile_path.exists() {
info!("SSL_CERT_FILE detected, preprocessing Dockerfile for bind mounts");
let (temp_dir, processed_path) =
dockerfile_ssl::preprocess_dockerfile_for_ssl(&original_dockerfile_path)?;
(Some(temp_dir), processed_path)
} else {
(None, original_dockerfile_path)
}
} else {
(None, original_dockerfile_path)
};
let mut secrets = proxy::read_and_transform_proxy_vars();
secrets.extend(proxy::parse_env_vars(&options.env)?);
let mut local_contexts = HashMap::new();
let _ssl_cert_context: Option<dockerfile_ssl::SslCertContext> =
if let Some(ref cert_path) = ssl_cert_path {
let context = dockerfile_ssl::SslCertContext::new(cert_path)?;
local_contexts.insert(
dockerfile_ssl::SSL_CERT_BUILD_CONTEXT.to_string(),
context.context_path.to_string_lossy().to_string(),
);
Some(context)
} else {
None
};
build_with_buildctl(
&options.app_path,
&effective_dockerfile,
&options.image_tag,
options.push,
buildkit_host.as_deref(),
&secrets,
&local_contexts,
BuildctlFrontend::Dockerfile,
options.no_cache,
container_cli.command(),
)?;
}
}
info!("✓ Successfully built image '{}'", options.image_tag);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_env_var_non_empty_with_empty_string() {
std::env::set_var("TEST_EMPTY_VAR", "");
assert_eq!(env_var_non_empty("TEST_EMPTY_VAR"), None);
std::env::remove_var("TEST_EMPTY_VAR");
}
#[test]
fn test_env_var_non_empty_with_value() {
std::env::set_var("TEST_VALUE_VAR", "some_value");
assert_eq!(
env_var_non_empty("TEST_VALUE_VAR"),
Some("some_value".to_string())
);
std::env::remove_var("TEST_VALUE_VAR");
}
#[test]
fn test_env_var_non_empty_with_unset() {
std::env::remove_var("TEST_UNSET_VAR");
assert_eq!(env_var_non_empty("TEST_UNSET_VAR"), None);
}
#[test]
fn test_env_var_non_empty_with_whitespace() {
std::env::set_var("TEST_WHITESPACE_VAR", " ");
assert_eq!(
env_var_non_empty("TEST_WHITESPACE_VAR"),
Some(" ".to_string())
);
std::env::remove_var("TEST_WHITESPACE_VAR");
}
}