use super::{
BoxedExecutor, BuildahError, Image, KubectlExecutor, RemoteExecutor, execute_buildah_command,
set_thread_local_debug, thread_local_debug,
};
use crate::process::CommandResult;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone)]
pub struct BuildahContainer {
name: String,
image: String,
debug: bool,
execution_target: ExecutionTarget,
}
#[derive(Clone)]
enum ExecutionTarget {
Local,
Kubernetes {
namespace: String,
pod_name: String,
container: Option<String>,
},
}
impl BuildahContainer {
pub fn new(name: impl Into<String>, image: impl Into<String>) -> Self {
Self {
name: name.into(),
image: image.into(),
debug: false,
execution_target: ExecutionTarget::Local,
}
}
pub fn debug(mut self, enabled: bool) -> Self {
self.debug = enabled;
self
}
pub fn kubernetes(mut self, namespace: impl Into<String>, pod_name: impl Into<String>) -> Self {
self.execution_target = ExecutionTarget::Kubernetes {
namespace: namespace.into(),
pod_name: pod_name.into(),
container: None,
};
self
}
pub fn container(mut self, container: impl Into<String>) -> Self {
if let ExecutionTarget::Kubernetes {
container: ref mut c,
..
} = self.execution_target
{
*c = Some(container.into());
}
self
}
pub fn build(self) -> Result<Builder, BuildahError> {
match self.execution_target {
ExecutionTarget::Local => {
let mut builder = Builder::new(&self.name, &self.image)?;
builder.set_debug(self.debug);
Ok(builder)
}
ExecutionTarget::Kubernetes {
namespace,
pod_name,
container,
} => {
let mut executor = KubectlExecutor::new(namespace, pod_name).debug(self.debug);
if let Some(c) = container {
executor = executor.container(c);
}
Builder::with_executor(&self.name, &self.image, executor)
}
}
}
}
#[derive(Clone)]
pub struct Builder {
name: String,
container_id: Option<String>,
image: String,
debug: bool,
executor: Option<BoxedExecutor>,
}
impl Builder {
pub fn new(name: &str, image: &str) -> Result<Self, BuildahError> {
if name.trim().is_empty() {
return Err(BuildahError::Other(
"Container name cannot be empty".to_string(),
));
}
if image.trim().is_empty() {
return Err(BuildahError::Other(
"Image name cannot be empty".to_string(),
));
}
let name = name.trim();
let image = image.trim();
let result = execute_buildah_command(&["from", "--name", name, image]);
match result {
Ok(success_result) => {
let container_id = success_result.stdout.trim().to_string();
if container_id.is_empty() {
return Err(BuildahError::Other(
"Buildah returned empty container ID".to_string(),
));
}
Ok(Self {
name: name.to_string(),
container_id: Some(container_id),
image: image.to_string(),
debug: false,
executor: None, })
}
Err(BuildahError::CommandFailed(error_msg)) => {
if error_msg.contains("that name is already in use") {
let container_id = error_msg
.split("already in use by ")
.nth(1)
.and_then(|s| s.split('.').next())
.unwrap_or("")
.trim()
.to_string();
if !container_id.is_empty() {
Ok(Self {
name: name.to_string(),
container_id: Some(container_id),
image: image.to_string(),
debug: false,
executor: None,
})
} else {
Err(BuildahError::Other(
"Failed to extract container ID from existing container error message"
.to_string(),
))
}
} else if error_msg.contains("image not known")
|| error_msg.contains("does not exist")
{
Err(BuildahError::CommandFailed(format!(
"Base image '{}' not found or cannot be accessed: {}",
image, error_msg
)))
} else {
Err(BuildahError::CommandFailed(error_msg))
}
}
Err(e) => {
Err(e)
}
}
}
pub fn with_executor<E: RemoteExecutor + 'static>(
name: &str,
image: &str,
executor: E,
) -> Result<Self, BuildahError> {
let executor: BoxedExecutor = Arc::new(executor);
let result = executor.execute(&["from", "--name", name, image]);
match result {
Ok(success_result) => {
let container_id = success_result.stdout.trim().to_string();
Ok(Self {
name: name.to_string(),
container_id: Some(container_id),
image: image.to_string(),
debug: false,
executor: Some(executor),
})
}
Err(BuildahError::CommandFailed(error_msg)) => {
if error_msg.contains("that name is already in use") {
let container_id = error_msg
.split("already in use by ")
.nth(1)
.and_then(|s| s.split('.').next())
.unwrap_or("")
.trim()
.to_string();
if !container_id.is_empty() {
Ok(Self {
name: name.to_string(),
container_id: Some(container_id),
image: image.to_string(),
debug: false,
executor: Some(executor),
})
} else {
Err(BuildahError::Other(
"Failed to extract container ID from error message".to_string(),
))
}
} else {
Err(BuildahError::CommandFailed(error_msg))
}
}
Err(e) => Err(e),
}
}
fn exec_command(&self, args: &[&str]) -> Result<CommandResult, BuildahError> {
if let Some(executor) = &self.executor {
executor.execute(args)
} else {
let previous_debug = thread_local_debug();
set_thread_local_debug(self.debug);
let result = execute_buildah_command(args);
set_thread_local_debug(previous_debug);
result
}
}
pub fn container_id(&self) -> Option<&String> {
self.container_id.as_ref()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn debug(&self) -> bool {
self.debug
}
pub fn set_debug(&mut self, debug: bool) -> &mut Self {
self.debug = debug;
self
}
pub fn image(&self) -> &str {
&self.image
}
pub fn run(&self, command: &str) -> Result<CommandResult, BuildahError> {
if command.trim().is_empty() {
return Err(BuildahError::Other("Command cannot be empty".to_string()));
}
if let Some(container_id) = &self.container_id {
if container_id.is_empty() {
return Err(BuildahError::Other(
"Container ID is empty or invalid".to_string(),
));
}
self.exec_command(&["run", container_id, "sh", "-c", command])
} else {
Err(BuildahError::Other(
"No container ID available - container may have been removed or not initialized"
.to_string(),
))
}
}
pub fn run_with_isolation(
&self,
command: &str,
isolation: &str,
) -> Result<CommandResult, BuildahError> {
if let Some(container_id) = &self.container_id {
self.exec_command(&[
"run",
"--isolation",
isolation,
container_id,
"sh",
"-c",
command,
])
} else {
Err(BuildahError::Other("No container ID available".to_string()))
}
}
pub fn copy(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
if source.trim().is_empty() {
return Err(BuildahError::Other(
"Source path cannot be empty".to_string(),
));
}
if dest.trim().is_empty() {
return Err(BuildahError::Other(
"Destination path cannot be empty".to_string(),
));
}
if let Some(container_id) = &self.container_id {
if container_id.is_empty() {
return Err(BuildahError::Other(
"Container ID is empty or invalid".to_string(),
));
}
self.exec_command(&["copy", container_id, source, dest])
} else {
Err(BuildahError::Other(
"No container ID available - container may have been removed or not initialized"
.to_string(),
))
}
}
pub fn add(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
if source.trim().is_empty() {
return Err(BuildahError::Other(
"Source path cannot be empty".to_string(),
));
}
if dest.trim().is_empty() {
return Err(BuildahError::Other(
"Destination path cannot be empty".to_string(),
));
}
if let Some(container_id) = &self.container_id {
if container_id.is_empty() {
return Err(BuildahError::Other(
"Container ID is empty or invalid".to_string(),
));
}
self.exec_command(&["add", container_id, source, dest])
} else {
Err(BuildahError::Other(
"No container ID available - container may have been removed or not initialized"
.to_string(),
))
}
}
pub fn commit(&self, image_name: &str) -> Result<CommandResult, BuildahError> {
if image_name.trim().is_empty() {
return Err(BuildahError::Other(
"Image name cannot be empty".to_string(),
));
}
if let Some(container_id) = &self.container_id {
if container_id.is_empty() {
return Err(BuildahError::Other(
"Container ID is empty or invalid".to_string(),
));
}
self.exec_command(&["commit", container_id, image_name])
} else {
Err(BuildahError::Other(
"No container ID available - cannot commit: container may have been removed or not initialized".to_string(),
))
}
}
pub fn remove(&self) -> Result<CommandResult, BuildahError> {
if let Some(container_id) = &self.container_id {
self.exec_command(&["rm", container_id])
} else {
Err(BuildahError::Other("No container ID available".to_string()))
}
}
pub fn reset(&mut self) -> Result<(), BuildahError> {
if let Some(container_id) = &self.container_id {
let result = self.exec_command(&["rm", container_id]);
self.container_id = None;
result.map(|_| ())
} else {
Ok(())
}
}
pub fn push(
&self,
image: &str,
destination: &str,
tls_verify: bool,
) -> Result<CommandResult, BuildahError> {
let mut args = vec!["push"];
if !tls_verify {
args.push("--tls-verify=false");
}
args.push(image);
args.push(destination);
self.exec_command(&args)
}
pub fn pull(&self, image: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
let mut args = vec!["pull"];
if !tls_verify {
args.push("--tls-verify=false");
}
args.push(image);
self.exec_command(&args)
}
pub fn tag(&self, image: &str, new_name: &str) -> Result<CommandResult, BuildahError> {
self.exec_command(&["tag", image, new_name])
}
pub fn list_images(&self) -> Result<Vec<Image>, BuildahError> {
let result = self.exec_command(&["images", "--json"])?;
match serde_json::from_str::<serde_json::Value>(&result.stdout) {
Ok(json) => {
if let serde_json::Value::Array(images_json) = json {
let mut images = Vec::new();
for image_json in images_json {
let id = image_json
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let names = image_json
.get("names")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let size = image_json
.get("size")
.and_then(|v| v.as_str())
.unwrap_or("Unknown")
.to_string();
let created = image_json
.get("created")
.and_then(|v| v.as_str())
.unwrap_or("Unknown")
.to_string();
images.push(Image {
id,
names,
size,
created,
});
}
Ok(images)
} else {
Err(BuildahError::JsonParseError(
"Expected JSON array".to_string(),
))
}
}
Err(e) => Err(BuildahError::JsonParseError(format!(
"Failed to parse image list JSON: {}",
e
))),
}
}
pub fn config(&self, options: HashMap<String, String>) -> Result<CommandResult, BuildahError> {
if let Some(container_id) = &self.container_id {
let mut args_owned: Vec<String> = Vec::new();
args_owned.push("config".to_string());
for (key, value) in options.iter() {
args_owned.push(format!("--{}", key));
args_owned.push(value.clone());
}
args_owned.push(container_id.clone());
let args: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect();
self.exec_command(&args)
} else {
Err(BuildahError::Other("No container ID available".to_string()))
}
}
pub fn set_entrypoint(&self, entrypoint: &str) -> Result<CommandResult, BuildahError> {
if let Some(container_id) = &self.container_id {
self.exec_command(&["config", "--entrypoint", entrypoint, container_id])
} else {
Err(BuildahError::Other("No container ID available".to_string()))
}
}
pub fn set_cmd(&self, cmd: &str) -> Result<CommandResult, BuildahError> {
if let Some(container_id) = &self.container_id {
self.exec_command(&["config", "--cmd", cmd, container_id])
} else {
Err(BuildahError::Other("No container ID available".to_string()))
}
}
pub fn images() -> Result<Vec<Image>, BuildahError> {
let result = execute_buildah_command(&["images", "--json"])?;
match serde_json::from_str::<serde_json::Value>(&result.stdout) {
Ok(json) => {
if let serde_json::Value::Array(images_json) = json {
let mut images = Vec::new();
for image_json in images_json {
let id = match image_json.get("id").and_then(|v| v.as_str()) {
Some(id) => id.to_string(),
None => {
return Err(BuildahError::ConversionError(
"Missing image ID".to_string(),
));
}
};
let names = match image_json.get("names").and_then(|v| v.as_array()) {
Some(names_array) => {
let mut names_vec = Vec::new();
for name_value in names_array {
if let Some(name_str) = name_value.as_str() {
names_vec.push(name_str.to_string());
}
}
names_vec
}
None => Vec::new(), };
let size = match image_json.get("size").and_then(|v| v.as_str()) {
Some(size) => size.to_string(),
None => "Unknown".to_string(), };
let created = match image_json.get("created").and_then(|v| v.as_str()) {
Some(created) => created.to_string(),
None => "Unknown".to_string(), };
images.push(Image {
id,
names,
size,
created,
});
}
Ok(images)
} else {
Err(BuildahError::JsonParseError(
"Expected JSON array".to_string(),
))
}
}
Err(e) => Err(BuildahError::JsonParseError(format!(
"Failed to parse image list JSON: {}",
e
))),
}
}
pub fn image_remove(image: &str) -> Result<CommandResult, BuildahError> {
execute_buildah_command(&["rmi", image])
}
pub fn image_remove_with_debug(
image: &str,
debug: bool,
) -> Result<CommandResult, BuildahError> {
let previous_debug = thread_local_debug();
set_thread_local_debug(debug);
let result = execute_buildah_command(&["rmi", image]);
set_thread_local_debug(previous_debug);
result
}
pub fn image_pull(image: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
let mut args = vec!["pull"];
if !tls_verify {
args.push("--tls-verify=false");
}
args.push(image);
execute_buildah_command(&args)
}
pub fn image_pull_with_debug(
image: &str,
tls_verify: bool,
debug: bool,
) -> Result<CommandResult, BuildahError> {
let previous_debug = thread_local_debug();
set_thread_local_debug(debug);
let mut args = vec!["pull"];
if !tls_verify {
args.push("--tls-verify=false");
}
args.push(image);
let result = execute_buildah_command(&args);
set_thread_local_debug(previous_debug);
result
}
pub fn image_push(
image: &str,
destination: &str,
tls_verify: bool,
) -> Result<CommandResult, BuildahError> {
let mut args = vec!["push"];
if !tls_verify {
args.push("--tls-verify=false");
}
args.push(image);
args.push(destination);
execute_buildah_command(&args)
}
pub fn image_push_with_debug(
image: &str,
destination: &str,
tls_verify: bool,
debug: bool,
) -> Result<CommandResult, BuildahError> {
let previous_debug = thread_local_debug();
set_thread_local_debug(debug);
let mut args = vec!["push"];
if !tls_verify {
args.push("--tls-verify=false");
}
args.push(image);
args.push(destination);
let result = execute_buildah_command(&args);
set_thread_local_debug(previous_debug);
result
}
pub fn image_tag(image: &str, new_name: &str) -> Result<CommandResult, BuildahError> {
execute_buildah_command(&["tag", image, new_name])
}
pub fn image_tag_with_debug(
image: &str,
new_name: &str,
debug: bool,
) -> Result<CommandResult, BuildahError> {
let previous_debug = thread_local_debug();
set_thread_local_debug(debug);
let result = execute_buildah_command(&["tag", image, new_name]);
set_thread_local_debug(previous_debug);
result
}
pub fn image_commit(
container: &str,
image_name: &str,
format: Option<&str>,
squash: bool,
rm: bool,
) -> Result<CommandResult, BuildahError> {
let mut args = vec!["commit"];
if let Some(format_str) = format {
args.push("--format");
args.push(format_str);
}
if squash {
args.push("--squash");
}
if rm {
args.push("--rm");
}
args.push(container);
args.push(image_name);
execute_buildah_command(&args)
}
pub fn image_commit_with_debug(
container: &str,
image_name: &str,
format: Option<&str>,
squash: bool,
rm: bool,
debug: bool,
) -> Result<CommandResult, BuildahError> {
let previous_debug = thread_local_debug();
set_thread_local_debug(debug);
let mut args = vec!["commit"];
if let Some(format_str) = format {
args.push("--format");
args.push(format_str);
}
if squash {
args.push("--squash");
}
if rm {
args.push("--rm");
}
args.push(container);
args.push(image_name);
let result = execute_buildah_command(&args);
set_thread_local_debug(previous_debug);
result
}
pub fn build(
tag: Option<&str>,
context_dir: &str,
file: &str,
isolation: Option<&str>,
) -> Result<CommandResult, BuildahError> {
let mut args = Vec::new();
args.push("build");
if let Some(tag_value) = tag {
args.push("-t");
args.push(tag_value);
}
if let Some(isolation_value) = isolation {
args.push("--isolation");
args.push(isolation_value);
}
args.push("-f");
args.push(file);
args.push(context_dir);
execute_buildah_command(&args)
}
pub fn build_with_debug(
tag: Option<&str>,
context_dir: &str,
file: &str,
isolation: Option<&str>,
debug: bool,
) -> Result<CommandResult, BuildahError> {
let previous_debug = thread_local_debug();
set_thread_local_debug(debug);
let mut args = Vec::new();
args.push("build");
if let Some(tag_value) = tag {
args.push("-t");
args.push(tag_value);
}
if let Some(isolation_value) = isolation {
args.push("--isolation");
args.push(isolation_value);
}
args.push("-f");
args.push(file);
args.push(context_dir);
let result = execute_buildah_command(&args);
set_thread_local_debug(previous_debug);
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_rejects_empty_name() {
let result = Builder::new("", "alpine:latest");
assert!(result.is_err());
match result {
Err(BuildahError::Other(msg)) => {
assert!(msg.contains("name cannot be empty"));
}
_ => panic!("Expected validation error for empty name"),
}
}
#[test]
fn test_builder_rejects_empty_image() {
let result = Builder::new("test-builder", "");
assert!(result.is_err());
match result {
Err(BuildahError::Other(msg)) => {
assert!(msg.contains("Image name cannot be empty"));
}
_ => panic!("Expected validation error for empty image"),
}
}
#[test]
fn test_builder_trims_inputs() {
let name = " test-builder ";
let trimmed = name.trim();
assert_eq!(trimmed, "test-builder");
}
#[test]
fn test_run_rejects_empty_command() {
let builder = Builder {
name: "test".to_string(),
container_id: Some("abc123".to_string()),
image: "alpine:latest".to_string(),
debug: false,
executor: None,
};
let result = builder.run("");
assert!(result.is_err());
match result {
Err(BuildahError::Other(msg)) => {
assert!(msg.contains("Command cannot be empty"));
}
_ => panic!("Expected validation error for empty command"),
}
}
#[test]
fn test_run_fails_without_container() {
let builder = Builder {
name: "test".to_string(),
container_id: None,
image: "alpine:latest".to_string(),
debug: false,
executor: None,
};
let result = builder.run("echo hello");
assert!(result.is_err());
match result {
Err(BuildahError::Other(msg)) => {
assert!(msg.contains("No container ID available"));
}
_ => panic!("Expected error for missing container"),
}
}
#[test]
fn test_copy_validates_paths() {
let builder = Builder {
name: "test".to_string(),
container_id: Some("abc123".to_string()),
image: "alpine:latest".to_string(),
debug: false,
executor: None,
};
let result = builder.copy("", "/dest");
assert!(result.is_err());
match result {
Err(BuildahError::Other(msg)) => {
assert!(msg.contains("Source path cannot be empty"));
}
_ => panic!("Expected validation error for empty source"),
}
let result = builder.copy("/source", "");
assert!(result.is_err());
match result {
Err(BuildahError::Other(msg)) => {
assert!(msg.contains("Destination path cannot be empty"));
}
_ => panic!("Expected validation error for empty destination"),
}
}
#[test]
fn test_add_validates_paths() {
let builder = Builder {
name: "test".to_string(),
container_id: Some("abc123".to_string()),
image: "alpine:latest".to_string(),
debug: false,
executor: None,
};
let result = builder.add("", "/dest");
assert!(result.is_err());
match result {
Err(BuildahError::Other(msg)) => {
assert!(msg.contains("Source path cannot be empty"));
}
_ => panic!("Expected validation error for empty source"),
}
}
#[test]
fn test_commit_validates_image_name() {
let builder = Builder {
name: "test".to_string(),
container_id: Some("abc123".to_string()),
image: "alpine:latest".to_string(),
debug: false,
executor: None,
};
let result = builder.commit("");
assert!(result.is_err());
match result {
Err(BuildahError::Other(msg)) => {
assert!(msg.contains("Image name cannot be empty"));
}
_ => panic!("Expected validation error for empty image name"),
}
}
#[test]
fn test_commit_fails_without_container() {
let builder = Builder {
name: "test".to_string(),
container_id: None,
image: "alpine:latest".to_string(),
debug: false,
executor: None,
};
let result = builder.commit("myimage:latest");
assert!(result.is_err());
match result {
Err(BuildahError::Other(msg)) => {
assert!(msg.contains("No container ID available"));
}
_ => panic!("Expected error for missing container"),
}
}
#[test]
fn test_reset_handles_missing_container() {
let mut builder = Builder {
name: "test".to_string(),
container_id: None,
image: "alpine:latest".to_string(),
debug: false,
executor: None,
};
let result = builder.reset();
assert!(result.is_ok());
}
#[test]
fn test_getters() {
let builder = Builder {
name: "test-builder".to_string(),
container_id: Some("container-123".to_string()),
image: "alpine:latest".to_string(),
debug: true,
executor: None,
};
assert_eq!(builder.name(), "test-builder");
assert_eq!(builder.image(), "alpine:latest");
assert_eq!(builder.debug(), true);
assert_eq!(builder.container_id(), Some(&"container-123".to_string()));
}
#[test]
fn test_set_debug_chaining() {
let mut builder = Builder {
name: "test".to_string(),
container_id: Some("abc123".to_string()),
image: "alpine:latest".to_string(),
debug: false,
executor: None,
};
let result = builder.set_debug(true);
assert_eq!(result.debug(), true);
}
}