use std::time::Duration;
use crate::instruction::{
Instruction, ADD, ARG, CMD, COPY, ENTRYPOINT, ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, ONBUILD,
RUN, SHELL, STOPSIGNAL, USER, VOLUME, WORKDIR,
};
use anyhow::{anyhow, Result};
use dockerfile_builder_macros::InstructionBuilder;
use url::Url;
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = FROM,
value_method = value,
)]
pub struct FromBuilder {
pub image: String,
pub name: Option<String>,
pub tag: Option<String>,
pub digest: Option<String>,
pub platform: Option<String>,
}
impl FromBuilder {
fn value(&self) -> Result<String> {
if self.tag.is_some() && self.digest.is_some() {
return Err(anyhow!("Dockerfile image can only have tag OR digest"));
}
let tag_or_digest = if let Some(t) = &self.tag {
Some(format!(":{}", t))
} else {
self.digest.as_ref().map(|d| format!("@{}", d))
};
Ok(format!(
"{}{}{}{}",
self.platform
.as_ref()
.map(|s| format!("--platform={} ", s))
.unwrap_or_default(),
&self.image,
tag_or_digest
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
self.name
.as_ref()
.map(|s| format!(" AS {}", s))
.unwrap_or_default(),
))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = ENV,
value_method = value,
)]
pub struct EnvBuilder {
pub key: String,
pub value: String,
}
impl EnvBuilder {
fn value(&self) -> Result<String> {
Ok(format!("{}=\"{}\"", self.key, self.value))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = RUN,
value_method = value,
)]
pub struct RunBuilder {
#[instruction_builder(each = command)]
pub commands: Vec<String>,
}
impl RunBuilder {
fn value(&self) -> Result<String> {
Ok(self.commands.join(" && "))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = RUN,
value_method = value,
)]
pub struct RunExecBuilder {
pub executable: String,
#[instruction_builder(each = param)]
pub params: Option<Vec<String>>,
}
impl RunExecBuilder {
fn value(&self) -> Result<String> {
let params = match self.params.clone() {
Some(param_vec) => {
if param_vec.is_empty() {
String::new()
} else {
format!(r#", "{}""#, param_vec.join(r#"", ""#))
}
}
None => String::new(),
};
Ok(format!(r#"["{}"{}]"#, self.executable, params))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = CMD,
value_method = value,
)]
pub struct CmdBuilder {
pub command: String,
#[instruction_builder(each = param)]
pub params: Option<Vec<String>>,
}
impl CmdBuilder {
fn value(&self) -> Result<String> {
let params = match self.params.clone() {
Some(param_vec) => {
if param_vec.is_empty() {
String::new()
} else {
format!(r#" {}"#, param_vec.join(" "))
}
}
None => String::new(),
};
Ok(format!("{}{}", self.command, params))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = CMD,
value_method = value,
)]
pub struct CmdExecBuilder {
pub executable: Option<String>,
#[instruction_builder(each = param)]
pub params: Option<Vec<String>>,
}
impl CmdExecBuilder {
fn value(&self) -> Result<String> {
if self.executable.is_none() && self.params.is_none() {
return Err(anyhow!("CMD cannot be empty"));
}
let params = match self.params.clone() {
Some(param_vec) => {
if self.executable.is_none() && param_vec.is_empty() {
return Err(anyhow!("CMD cannot be empty"));
} else if param_vec.is_empty() {
String::new()
} else if self.executable.is_none() {
format!(r#""{}""#, param_vec.join(r#"", ""#))
} else {
format!(r#", "{}""#, param_vec.join(r#"", ""#))
}
}
None => String::new(),
};
Ok(format!(
r#"[{}{}]"#,
self.executable
.as_ref()
.map(|e| format!(r#""{}""#, e))
.unwrap_or_default(),
params,
))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = LABEL,
value_method = value,
)]
pub struct LabelBuilder {
pub key: String,
pub value: String,
}
impl LabelBuilder {
fn value(&self) -> Result<String> {
Ok(format!("{}=\"{}\"", self.key, self.value))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = EXPOSE,
value_method = value,
)]
pub struct ExposeBuilder {
pub port: u16,
pub protocol: Option<PortProtocol>,
}
#[derive(Clone, Debug)]
pub enum PortProtocol {
Tcp,
Udp,
}
impl ToString for PortProtocol {
fn to_string(&self) -> String {
match self {
Self::Tcp => "tcp",
Self::Udp => "udp",
}
.to_string()
}
}
impl ExposeBuilder {
fn value(&self) -> Result<String> {
Ok(format!(
"{}{}",
self.port,
self.protocol
.as_ref()
.map(|p| format!("/{}", p.to_string()))
.unwrap_or_default()
))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = ADD,
value_method = value,
)]
pub struct AddBuilder {
#[instruction_builder(each = src)]
pub sources: Vec<String>,
pub dest: String,
pub chown: Option<String>,
pub chmod: Option<u16>,
}
impl AddBuilder {
fn value(&self) -> Result<String> {
Ok(format!(
"{}{}{} {}",
self.chown
.as_ref()
.map(|c| format!("--chown={} ", c))
.unwrap_or_default(),
self.chmod
.as_ref()
.map(|c| format!("--chmod={} ", c))
.unwrap_or_default(),
self.sources.join(" "),
self.dest,
))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = ADD,
value_method = value,
)]
pub struct AddHttpBuilder {
pub src: String,
pub dest: String,
pub checksum: Option<String>,
}
impl AddHttpBuilder {
fn value(&self) -> Result<String> {
if let Err(e) = Url::parse(&self.src) {
return Err(anyhow!("{e}"));
}
Ok(format!(
"{}{} {}",
self.checksum
.as_ref()
.map(|c| format!("--checksum={} ", c))
.unwrap_or_default(),
self.src,
self.dest,
))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = ADD,
value_method = value,
)]
pub struct AddGitBuilder {
pub git_ref: String,
pub dir: String,
pub keep_git_dir: Option<bool>,
}
impl AddGitBuilder {
fn value(&self) -> Result<String> {
if let Err(e) = Url::parse(&self.git_ref) {
return Err(anyhow!("{e}"));
}
Ok(format!(
"{}{} {}",
self.keep_git_dir
.as_ref()
.map(|c| format!("--keep-git-dir={} ", c))
.unwrap_or_default(),
self.git_ref,
self.dir,
))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = COPY,
value_method = value,
)]
pub struct CopyBuilder {
#[instruction_builder(each = src)]
pub sources: Vec<String>,
pub dest: String,
pub chown: Option<String>,
pub chmod: Option<u16>,
pub from: Option<String>,
pub link: Option<bool>,
}
impl CopyBuilder {
fn value(&self) -> Result<String> {
Ok(format!(
"{}{}{}{}{} {}",
self.chown
.as_ref()
.map(|c| format!("--chown={} ", c))
.unwrap_or_default(),
self.chmod
.as_ref()
.map(|c| format!("--chmod={} ", c))
.unwrap_or_default(),
self.link
.as_ref()
.map(|c| match c {
true => "--link ".to_string(),
false => "".to_string(),
})
.unwrap_or_default(),
self.from
.as_ref()
.map(|c| format!("--from={} ", c))
.unwrap_or_default(),
self.sources.join(" "),
self.dest,
))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = ENTRYPOINT,
value_method = value,
)]
pub struct EntrypointBuilder {
pub command: String,
#[instruction_builder(each = param)]
pub params: Option<Vec<String>>,
}
impl EntrypointBuilder {
fn value(&self) -> Result<String> {
let params = match self.params.clone() {
Some(param_vec) => {
if param_vec.is_empty() {
String::new()
} else {
format!(r#" {}"#, param_vec.join(r#" "#))
}
}
None => String::new(),
};
Ok(format!("{}{}", self.command, params))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = ENTRYPOINT,
value_method = value,
)]
pub struct EntrypointExecBuilder {
pub executable: String,
#[instruction_builder(each = param)]
pub params: Option<Vec<String>>,
}
impl EntrypointExecBuilder {
fn value(&self) -> Result<String> {
let params = match self.params.clone() {
Some(param_vec) => {
if param_vec.is_empty() {
String::new()
} else {
format!(r#", "{}""#, param_vec.join(r#"", ""#))
}
}
None => String::new(),
};
Ok(format!(r#"["{}"{}]"#, self.executable, params))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = VOLUME,
value_method = value,
)]
pub struct VolumeBuilder {
#[instruction_builder(each = path)]
pub paths: Vec<String>,
}
impl VolumeBuilder {
fn value(&self) -> Result<String> {
Ok(self.paths.join(" "))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = USER,
value_method = value,
)]
pub struct UserBuilder {
pub user: String,
pub group: Option<String>,
}
impl UserBuilder {
fn value(&self) -> Result<String> {
Ok(format!(
"{}{}",
self.user,
self.group
.as_ref()
.map(|g| format!(":{}", g))
.unwrap_or_default()
))
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = WORKDIR,
value_method = value,
)]
pub struct WorkdirBuilder {
pub path: String,
}
impl WorkdirBuilder {
fn value(&self) -> Result<String> {
Ok(self.path.to_string())
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = ARG,
value_method = value,
)]
pub struct ArgBuilder {
pub name: String,
pub value: Option<String>,
}
impl ArgBuilder {
fn value(&self) -> Result<String> {
let value = match &self.value {
Some(value) => format!("{}=\"{}\"", self.name, value),
None => self.name.to_string(),
};
Ok(value)
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = ONBUILD,
value_method = value,
)]
pub struct OnbuildBuilder {
pub instruction: Instruction,
}
impl OnbuildBuilder {
fn value(&self) -> Result<String> {
match &self.instruction {
Instruction::ONBUILD(_) => Err(anyhow!(
"Chaining ONBUILD instructions using ONBUILD ONBUILD isn’t allowed"
)),
Instruction::FROM(_) => Err(anyhow!(
"ONBUILD instruction may not trigger FROM instruction"
)),
ins => Ok(ins.to_string()),
}
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = STOPSIGNAL,
value_method = value,
)]
pub struct StopsignalBuilder {
pub signal: String,
}
impl StopsignalBuilder {
fn value(&self) -> Result<String> {
Ok(self.signal.to_string())
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = HEALTHCHECK,
value_method = value,
)]
pub struct HealthcheckBuilder {
pub cmd: Option<CMD>,
pub interval: Option<Duration>,
pub timeout: Option<Duration>,
pub start_period: Option<Duration>,
pub start_interval: Option<Duration>,
pub retries: Option<i32>,
}
impl HealthcheckBuilder {
fn value(&self) -> Result<String> {
match self.cmd.is_some() {
true => Ok(format!(
"{}{}{}{}{}{}",
self.interval
.as_ref()
.map(|i| format!("--interal={} ", i.as_secs()))
.unwrap_or_default(),
self.timeout
.as_ref()
.map(|t| format!("--timeout={} ", t.as_secs()))
.unwrap_or_default(),
self.start_period
.as_ref()
.map(|s| format!("--start-period={} ", s.as_secs()))
.unwrap_or_default(),
self.start_interval
.as_ref()
.map(|s| format!("--start-interval={} ", s.as_secs()))
.unwrap_or_default(),
self.retries
.as_ref()
.map(|r| format!("--retries={} ", r))
.unwrap_or_default(),
self.cmd.clone().unwrap(),
)),
false => Ok("NONE".to_string()),
}
}
pub fn set_none() -> Result<String> {
Ok("HEALTHCHECK NONE".to_string())
}
}
#[derive(Debug, InstructionBuilder)]
#[instruction_builder(
instruction_name = SHELL,
value_method = value,
)]
pub struct ShellBuilder {
pub executable: String,
#[instruction_builder(each = param)]
pub params: Option<Vec<String>>,
}
impl ShellBuilder {
fn value(&self) -> Result<String> {
let params = match self.params.clone() {
Some(param_vec) => {
if param_vec.is_empty() {
String::new()
} else {
format!(r#", "{}""#, param_vec.join(r#"", ""#))
}
}
None => String::new(),
};
Ok(format!(r#"["{}"{}]"#, self.executable, params))
}
}