dockerfile_parser/
stage.rsuse std::fmt;
use std::ops::Index;
use crate::dockerfile_parser::{Dockerfile, Instruction};
use crate::image::ImageRef;
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum StageParent<'a> {
Image(&'a ImageRef),
Stage(usize),
Scratch
}
impl<'a> fmt::Display for StageParent<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StageParent::Image(image) => image.fmt(f),
StageParent::Stage(index) => index.fmt(f),
StageParent::Scratch => write!(f, "scratch")
}
}
}
#[derive(Debug, Eq)]
pub struct Stage<'a> {
pub index: usize,
pub name: Option<String>,
pub instructions: Vec<&'a Instruction>,
pub parent: StageParent<'a>,
pub root: StageParent<'a>
}
impl<'a> Ord for Stage<'a> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.index.cmp(&other.index)
}
}
impl<'a> PartialOrd for Stage<'a> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(&other))
}
}
impl<'a> PartialEq for Stage<'a> {
fn eq(&self, other: &Self) -> bool {
self.index == other.index
}
}
impl<'a> Stage<'a> {
pub fn arg_index(&self, name: &str) -> Option<usize> {
self.instructions
.iter()
.enumerate()
.find_map(|(i, ins)| match ins {
Instruction::Arg(a) => if a.name.content == name { Some(i) } else { None },
_ => None
})
}
}
#[derive(Debug)]
pub struct Stages<'a> {
pub stages: Vec<Stage<'a>>
}
impl<'a> Stages<'a> {
pub fn new(dockerfile: &'a Dockerfile) -> Stages<'a> {
let mut stages = Stages { stages: vec![] };
let mut next_stage_index = 0;
for ins in &dockerfile.instructions {
if let Instruction::From(from) = ins {
let image_name = from.image.as_ref().to_ascii_lowercase();
let parent = if image_name == "scratch" {
StageParent::Scratch
} else if let Some(stage) = stages.get_by_name(&image_name) {
StageParent::Stage(stage.index)
} else {
StageParent::Image(&from.image_parsed)
};
let root = if let StageParent::Stage(parent_stage) = parent {
stages.stages[parent_stage].root.clone()
} else {
parent.clone()
};
stages.stages.push(Stage {
index: next_stage_index,
name: from.alias.as_ref().map(|a| a.as_ref().to_ascii_lowercase()),
instructions: vec![ins],
parent,
root
});
next_stage_index += 1;
} else if !stages.stages.is_empty() {
let len = stages.stages.len();
if let Some(stage) = stages.stages.get_mut(len - 1) {
stage.instructions.push(ins);
}
}
}
stages
}
pub fn get_by_name(&'a self, name: &str) -> Option<&'a Stage<'a>> {
self.stages.iter().find(|s| s.name == Some(name.to_ascii_lowercase()))
}
pub fn get(&'a self, s: &str) -> Option<&'a Stage<'a>> {
match s.parse::<usize>() {
Ok(index) => self.stages.get(index),
Err(_) => self.get_by_name(s)
}
}
pub fn iter(&self) -> std::slice::Iter<'_, Stage<'a>> {
self.stages.iter()
}
}
impl<'a> Index<usize> for Stages<'a> {
type Output = Stage<'a>;
fn index(&self, index: usize) -> &Self::Output {
&self.stages[index]
}
}
impl<'a> IntoIterator for Stages<'a> {
type Item = Stage<'a>;
type IntoIter = std::vec::IntoIter<Stage<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.stages.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn test_stages() {
let dockerfile = Dockerfile::parse(indoc!(r#"
FROM alpine:3.12
FROM ubuntu:18.04 as build
RUN echo "hello world"
FROM build as build2
COPY /foo /bar
COPY /bar /baz
FROM build as build3
"#)).unwrap();
let stages = Stages::new(&dockerfile);
assert_eq!(stages.stages.len(), 4);
assert_eq!(stages[1], Stage {
index: 1,
name: Some("build".into()),
instructions: vec![&dockerfile.instructions[1], &dockerfile.instructions[2]],
parent: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
root: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
});
assert_eq!(stages[2], Stage {
index: 2,
name: Some("build2".into()),
instructions: dockerfile.instructions[3..5].iter().collect(),
parent: StageParent::Stage(1),
root: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
});
assert_eq!(stages[3], Stage {
index: 3,
name: Some("build3".into()),
instructions: vec![&dockerfile.instructions[6]],
parent: StageParent::Stage(2),
root: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
});
}
#[test]
fn test_stages_get() {
let dockerfile = Dockerfile::parse(indoc!(r#"
FROM alpine:3.12
FROM ubuntu:18.04 as build
FROM build as build2
"#)).unwrap();
let stages = Stages::new(&dockerfile);
assert_eq!(stages.get("0").unwrap().index, 0);
assert_eq!(stages.get("1"), stages.get("build"));
assert_eq!(stages.get("2"), stages.get("build2"));
}
}