Lines
31.75 %
Functions
11.11 %
Branches
100 %
use std::io::{
BufRead as _,
BufReader,
};
use std::path::PathBuf;
use std::process::Command as ProcessCommand;
use std::thread;
use anyhow::Context as _;
use git2::Repository;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::defaults::default_verbose;
use crate::schema::{
get_output_handler,
is_shell_command,
is_template_command,
ContainerRuntime,
TaskContext,
use crate::{
get_template_command_value,
handle_output,
run_shell_command,
#[derive(Debug, Deserialize, Clone, JsonSchema)]
pub struct ContainerBuildArgs {
/// The image name to build
pub image_name: String,
/// Defines the path to a directory to build the container
pub context: String,
/// The containerfile or dockerfile to use
#[serde(default)]
pub containerfile: Option<String>,
/// The tags to apply to the container image
pub tags: Option<Vec<String>>,
/// Build arguments to pass to the container
pub build_args: Option<Vec<String>>,
/// Labels to apply to the container image
pub labels: Option<Vec<String>>,
/// Generate a Software Bill of Materials (SBOM) for the container image
pub sbom: bool,
/// Do not use cache when building the container
pub no_cache: bool,
/// Always remove intermediate containers
pub force_rm: bool,
/// The container runtime to use
pub runtime: Option<ContainerRuntime>,
}
pub struct ContainerBuild {
/// The command to run in the container
pub container_build: ContainerBuildArgs,
/// Show verbose output
pub verbose: Option<bool>,
#[allow(dead_code)]
impl ContainerBuild {
pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
assert!(!self.container_build.context.is_empty());
let verbose = self.verbose.or(context.verbose).unwrap_or(default_verbose());
let stdout = get_output_handler(verbose);
let stderr = get_output_handler(verbose);
let resolved_context = self.resolved_context(context);
let resolved_containerfile = self.resolved_containerfile(context);
let container_runtime = ContainerRuntime::resolve(
self
.container_build
.runtime
.as_ref()
.or(context.container_runtime.as_ref()),
)?;
let mut cmd = ProcessCommand::new(container_runtime);
cmd.arg("build").stdout(stdout).stderr(stderr);
if self.container_build.sbom {
cmd.arg("--sbom=true");
if self.container_build.no_cache {
cmd.arg("--no-cache=true");
if self.container_build.force_rm {
cmd.arg("--force-rm=true");
if let Some(build_args) = &self.container_build.build_args {
for arg in build_args {
cmd.arg("--build-arg").arg(arg);
if let Some(labels) = &self.container_build.labels {
for label in labels {
let label = self.get_label(context, label.trim())?;
cmd.arg("--label").arg(label);
if let Some(tags) = &self.container_build.tags {
for tag in tags {
let tag = self.get_tag(context, tag.trim())?;
let tag = format!("{}:{}", &self.container_build.image_name, tag);
cmd.arg("-t").arg(tag);
} else {
let tag = format!("{}:latest", &self.container_build.image_name);
if let Some(containerfile) = &resolved_containerfile {
cmd.arg("-f").arg(containerfile);
let dockerfile = resolved_context.join("Dockerfile");
let containerfile = resolved_context.join("Containerfile");
// Check for Dockerfile and Containerfile
if dockerfile.exists() {
cmd.arg("-f").arg(dockerfile);
} else if containerfile.exists() {
anyhow::bail!("Failed to find Dockerfile or Containerfile in context");
cmd.arg(&resolved_context);
let cmd_str = format!("{:?}", cmd);
context.multi.println(cmd_str)?;
// Inject environment variables in both container and command
for (key, value) in context.env_vars.iter() {
cmd.env(key, value);
log::trace!("Running command: {:?}", cmd);
let mut cmd = cmd.spawn()?;
if verbose {
handle_output!(cmd.stdout, context);
handle_output!(cmd.stderr, context);
let status = cmd.wait()?;
if !status.success() {
// Note: container build failures are always fatal and do not honor task-level ignore_errors.
anyhow::bail!("Container build failed");
Ok(())
fn get_tag(&self, context: &TaskContext, tag_in: &str) -> anyhow::Result<String> {
if is_shell_command(tag_in)? {
let mut cmd = context.shell().proc();
let output = run_shell_command!(tag_in, cmd, verbose);
Ok(output)
} else if is_template_command(tag_in)? {
let output = get_template_command_value!(tag_in, context);
Ok(tag_in.to_string())
fn get_label(&self, context: &TaskContext, label_in: &str) -> anyhow::Result<String> {
use chrono::prelude::*;
if let Some((key, value)) = label_in.split_once('=') {
match value {
"MK_NOW" => {
// Create formatted time in +%Y-%m-%dT%H:%M:%S%z format
let now: DateTime<Local> = Local::now();
let now = now.format("%Y-%m-%dT%H:%M:%S%z").to_string();
Ok(format!("{}={}", key, now))
},
"MK_GIT_REVISION" => {
let revision = self
.get_git_revision(context)
.unwrap_or_else(|_| "unknown".to_string());
Ok(format!("{}={}", key, revision))
"MK_GIT_REMOTE_ORIGIN" => {
let remote_url = self
.get_git_remote_origin(context)
Ok(format!("{}={}", key, remote_url))
_ => {
let value = if is_shell_command(value)? {
run_shell_command!(value, cmd, verbose)
} else if is_template_command(value)? {
get_template_command_value!(value, context)
value.to_string()
Ok(format!("{}={}", key, value))
Ok(label_in.to_string())
fn get_git_revision(&self, context: &TaskContext) -> anyhow::Result<String> {
let repo = self.open_git_repository(context)?;
let head = repo.head().context("Failed to get git HEAD reference")?;
let commit = head
.peel_to_commit()
.context("Failed to resolve git HEAD commit")?;
Ok(commit.id().to_string())
fn get_git_remote_origin(&self, context: &TaskContext) -> anyhow::Result<String> {
let remote = repo
.find_remote("origin")
.context("Failed to find git remote origin")?;
let url = remote.url().context("Failed to get git remote URL")?;
Ok(url.to_string())
pub fn resolved_context(&self, context: &TaskContext) -> PathBuf {
context.resolve_from_config(&self.container_build.context)
pub fn resolved_containerfile(&self, context: &TaskContext) -> Option<PathBuf> {
.containerfile
.map(|containerfile| context.resolve_from_config(containerfile))
fn open_git_repository(&self, context: &TaskContext) -> anyhow::Result<Repository> {
Repository::discover(&resolved_context).with_context(|| {
format!(
"Failed to open git repository from build context - {}",
resolved_context.to_string_lossy()
)
})
#[cfg(test)]
mod test {
use anyhow::Ok;
use super::*;
#[test]
fn test_container_build_1() -> anyhow::Result<()> {
let yaml = r#"
container_build:
image_name: my-image
context: .
tags:
- latest
labels:
- "org.opencontainers.image.created=MK_NOW"
- "org.opencontainers.image.revision=MK_GIT_REVISION"
- "org.opencontainers.image.source=MK_GIT_REMOTE_ORIGIN"
sbom: true
no_cache: true
force_rm: true
verbose: false
"#;
let container_build = serde_yaml::from_str::<ContainerBuild>(yaml)?;
assert_eq!(container_build.verbose, Some(false));
assert_eq!(container_build.container_build.image_name, "my-image");
assert_eq!(container_build.container_build.context, ".");
assert_eq!(
container_build.container_build.tags,
Some(vec!["latest".to_string()])
);
container_build.container_build.labels,
Some(vec![
"org.opencontainers.image.created=MK_NOW".to_string(),
"org.opencontainers.image.revision=MK_GIT_REVISION".to_string(),
"org.opencontainers.image.source=MK_GIT_REMOTE_ORIGIN".to_string(),
])
assert!(container_build.container_build.sbom);
assert!(container_build.container_build.no_cache);
assert!(container_build.container_build.force_rm);
fn test_container_build_2() -> anyhow::Result<()> {
assert_eq!(container_build.verbose, None);
assert_eq!(container_build.container_build.tags, None,);
assert_eq!(container_build.container_build.labels, None,);
assert!(!container_build.container_build.sbom);
assert!(!container_build.container_build.no_cache);
assert!(!container_build.container_build.force_rm);
fn test_container_build_3() -> anyhow::Result<()> {
image_name: docker.io/my-image/my-image
context: /hello/world
container_build.container_build.image_name,
"docker.io/my-image/my-image"
assert_eq!(container_build.container_build.context, "/hello/world");