use std::{
fmt::Write,
hash::{Hash, Hasher},
path::{Path, PathBuf},
};
use bytes::Bytes;
use color_eyre::eyre::{Context, ContextCompat};
use dockerfile_parser_rs::{Dockerfile, Instruction};
use super::{
Link,
types::{
name::TargetName,
target::{build::BuildStmt, copy::CopyStmt, workspace::WorkspaceStmt},
},
};
use crate::scell::{
link::RootNode,
name::SCellId,
types::target::{env::EnvStmt, hang::HangStmt},
};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct SCellImage {
#[serde(flatten)]
inner: SCellImageInner,
#[serde(skip_serializing)]
entry_point: TargetName,
#[serde(skip_serializing)]
blueprint_location: PathBuf,
#[serde(skip_serializing)]
dockerfile: Dockerfile,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
pub struct SCellImageInner {
chain: Vec<Link>,
hang: HangStmt,
}
impl SCellImage {
pub fn entry_point(&self) -> &TargetName {
&self.entry_point
}
pub fn location(&self) -> &Path {
&self.blueprint_location
}
pub fn id(&self) -> color_eyre::Result<SCellId> {
SCellId::new(|hasher| {
self.hash(hasher)?;
Ok(())
})
}
pub fn hash<H: Hasher>(
&self,
hasher: &mut H,
) -> color_eyre::Result<()> {
self.inner.hash(hasher);
self.image_tar_artifact_bytes()?.hash(hasher);
Ok(())
}
pub fn new(
chain: Vec<Link>,
hang: HangStmt,
) -> color_eyre::Result<Self> {
let mut dockerfile_instructions = Vec::new();
let inner = SCellImageInner { chain, hang };
let mut links_iter = inner.chain.iter().rev().peekable();
let mut entry_point = None;
let mut blueprint_location = None;
while let Some(link) = links_iter.next() {
match link {
Link::Root(RootNode::Image(image)) => {
dockerfile_instructions.push(Instruction::From {
platform: None,
image: image.to_string(),
alias: None,
});
},
Link::Root(RootNode::Dockerfile(docker_path)) => {
prepare_dockerfile(&mut dockerfile_instructions, docker_path)?;
},
Link::Node {
build,
copy,
location,
workspace,
env,
name,
} => {
prepare_workspace_stmt(&mut dockerfile_instructions, workspace);
prepare_env_stmt(&mut dockerfile_instructions, env);
prepare_copy_stmt(&mut dockerfile_instructions, copy)?;
prepare_build_stmt(&mut dockerfile_instructions, build);
if links_iter.peek().is_none() {
entry_point = Some(name.clone());
blueprint_location = Some(location.clone());
}
},
}
}
prepare_hang_stmt(&mut dockerfile_instructions, &inner.hang);
let dockerfile = Dockerfile::new(dockerfile_instructions);
Ok(Self {
inner,
entry_point: entry_point.context("'entry_point' cannot be None")?,
blueprint_location: blueprint_location
.context("'blueprint_location' cannot be None")?,
dockerfile,
})
}
fn dump_to_string(&self) -> color_eyre::Result<String> {
let mut dockerfile_str = String::new();
let mut iter = self.dockerfile.instructions.iter().peekable();
while let Some(instruction) = iter.next() {
if iter.peek().is_none() {
if let Instruction::Entrypoint(entrypoint) = instruction
&& let [entrypoint] = entrypoint.as_slice()
{
writeln!(&mut dockerfile_str, "ENTRYPOINT {entrypoint}")?;
} else {
color_eyre::eyre::bail!("Last instruction MUST be only single ETRYPOINT item");
}
} else {
writeln!(&mut dockerfile_str, "{instruction}")?;
}
}
Ok(dockerfile_str)
}
pub fn image_tar_artifact_bytes(&self) -> color_eyre::Result<(Bytes, &str)> {
const DOCKERFILE_NAME: &str = "Dockerfile";
const FILE_MODE: u32 = 0o600;
let mut tar = tar::Builder::new(Vec::new());
for i in &self.dockerfile.instructions {
match i {
Instruction::Copy { sources, .. } | Instruction::Add { sources, .. } => {
for s in sources {
let s = Path::new(s);
color_eyre::eyre::ensure!(
s.is_absolute() && s.exists(),
"Must be an absolute path and exists"
);
let item: PathBuf = s
.components()
.filter(|c| {
!matches!(
c,
std::path::Component::Prefix(_) | std::path::Component::RootDir
)
})
.collect();
if s.is_file() {
let mut f = std::fs::File::open(s)
.context(format!("Cannot open file {}", s.display()))?;
tar.append_file(&item, &mut f)?;
}
if s.is_dir() {
tar.append_dir_all(&item, s)?;
}
}
},
_ => {},
}
}
let dockerfile_str = self.dump_to_string()?;
let mut header = tar::Header::new_gnu();
header.set_path(DOCKERFILE_NAME)?;
header.set_size(dockerfile_str.len() as u64);
header.set_mode(FILE_MODE);
header.set_cksum();
tar.append(&header, dockerfile_str.as_bytes())?;
Ok((tar.into_inner()?.into(), DOCKERFILE_NAME))
}
}
fn prepare_copy_stmt(
dockerfile_instructions: &mut Vec<Instruction>,
copy_stmt: &CopyStmt,
) -> color_eyre::Result<()> {
for e in ©_stmt.0 {
dockerfile_instructions.push(Instruction::Copy {
from: None,
chown: None,
chmod: None,
link: None,
sources: e
.src
.iter()
.map(|s| {
color_eyre::eyre::ensure!(
s.is_absolute(),
"prepare_copy_stmt, path be absolute"
);
Ok(format!("{}", s.display()))
})
.collect::<Result<_, _>>()?,
destination: format!("{}", e.dest.display()),
});
}
Ok(())
}
fn prepare_dockerfile(
dockerfile_instructions: &mut Vec<Instruction>,
dockerfile_p: &Path,
) -> color_eyre::Result<()> {
color_eyre::eyre::ensure!(
dockerfile_p.is_absolute(),
"prepare_dockerfile, path be absolute"
);
let mut dockerfile =
Dockerfile::from(dockerfile_p.to_path_buf()).map_err(|e| color_eyre::eyre::eyre!(e))?;
let dir = dockerfile_p
.parent()
.context("Dockerfile must have a parent directory")?;
for i in &mut dockerfile.instructions {
match i {
Instruction::Copy { sources, .. } | Instruction::Add { sources, .. } => {
for s in sources {
*s = format!("{}", dir.join(&s).display());
}
},
_ => {},
}
}
dockerfile_instructions.extend(dockerfile.instructions);
Ok(())
}
fn prepare_build_stmt(
dockerfile_instructions: &mut Vec<Instruction>,
build_stm: &BuildStmt,
) {
for e in &build_stm.0 {
dockerfile_instructions.push(Instruction::Run {
mount: None,
network: None,
security: None,
command: vec![e.clone()],
heredoc: None,
});
}
}
fn prepare_workspace_stmt(
dockerfile_instructions: &mut Vec<Instruction>,
workspace_stmt: &WorkspaceStmt,
) {
if let Some(workspace) = &workspace_stmt.0 {
dockerfile_instructions.push(Instruction::Workdir {
path: workspace.clone(),
});
}
}
fn prepare_env_stmt(
dockerfile_instructions: &mut Vec<Instruction>,
evn_stmt: &EnvStmt,
) {
if evn_stmt.0.is_empty() {
return;
}
let env_inst = evn_stmt
.0
.iter()
.cloned()
.map(|e| (e.key, e.value))
.collect();
dockerfile_instructions.push(Instruction::Env(env_inst));
}
fn prepare_hang_stmt(
dockerfile_instructions: &mut Vec<Instruction>,
hang_stmt: &HangStmt,
) {
dockerfile_instructions.push(Instruction::Entrypoint(vec![hang_stmt.0.clone()]));
}