dockerfile_parser/
stage.rs

1// (C) Copyright 2020 Hewlett Packard Enterprise Development LP
2
3use std::fmt;
4use std::ops::Index;
5
6use crate::dockerfile_parser::{Dockerfile, Instruction};
7use crate::image::ImageRef;
8
9/// The parent image of a Docker build stage
10#[derive(Debug, Eq, PartialEq, Clone)]
11pub enum StageParent<'a> {
12  /// An externally-built image, potentially from a remote registry
13  Image(&'a ImageRef),
14
15  /// An index of a previous stage within the current Dockerfile
16  Stage(usize),
17
18  /// The empty (scratch) parent image
19  Scratch
20}
21
22impl<'a> fmt::Display for StageParent<'a> {
23  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24    match self {
25      StageParent::Image(image) => image.fmt(f),
26      StageParent::Stage(index) => index.fmt(f),
27      StageParent::Scratch => write!(f, "scratch")
28    }
29  }
30}
31
32/// A single stage in a [multi-stage build].
33///
34/// A stage begins with (and includes) a `FROM` instruction and continues until
35/// (but does *not* include) the next `FROM` instruction, if any.
36///
37/// Stages have an index and an optional alias. Later `COPY --from=$index [...]`
38/// instructions may copy files between unnamed build stages. The alias, if
39/// defined in this stage's `FROM` instruction, may be used as well.
40///
41/// Note that instructions in a Dockerfile before the first `FROM` are not
42/// included in the first stage's list of instructions.
43///
44/// [multi-stage build]: https://docs.docker.com/develop/develop-images/multistage-build/
45#[derive(Debug, Eq)]
46pub struct Stage<'a> {
47  /// The stage index.
48  pub index: usize,
49
50  /// The stage's FROM alias, if any.
51  pub name: Option<String>,
52
53  /// An ordered list of instructions in this stage.
54  pub instructions: Vec<&'a Instruction>,
55
56  /// The direct parent of this stage.
57  ///
58  /// If this is the first stage, it will be equal to the root stage.
59  pub parent: StageParent<'a>,
60
61  /// The root image of this stage, either an external reference (possibly from
62  /// a remote registry) or `scratch`.
63  pub root: StageParent<'a>
64}
65
66impl<'a> Ord for Stage<'a> {
67  fn cmp(&self, other: &Self) -> std::cmp::Ordering {
68    self.index.cmp(&other.index)
69  }
70}
71
72impl<'a> PartialOrd for Stage<'a> {
73  fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
74    Some(self.cmp(&other))
75  }
76}
77
78impl<'a> PartialEq for Stage<'a> {
79  fn eq(&self, other: &Self) -> bool {
80    self.index == other.index
81  }
82}
83
84impl<'a> Stage<'a> {
85  /// Finds the index, relative to this stage, of an ARG instruction defining
86  /// the given name. Per the Dockerfile spec, only instructions following the
87  /// ARG definition in a particular stage will have the value in scope, even
88  /// if it was a defined globally or in a previous stage.
89  pub fn arg_index(&self, name: &str) -> Option<usize> {
90    self.instructions
91      .iter()
92      .enumerate()
93      .find_map(|(i, ins)| match ins {
94        Instruction::Arg(a) => if a.name.content == name { Some(i) } else { None },
95        _ => None
96      })
97  }
98}
99
100/// A collection of stages in a [multi-stage build].
101///
102/// # Example
103/// ```
104/// use dockerfile_parser::Dockerfile;
105///
106/// let dockerfile = Dockerfile::parse(r#"
107///   FROM alpine:3.12 as build
108///   RUN echo "hello world" > /foo
109///
110///   FROM ubuntu:18.04
111///   COPY --from=0 /foo /foo
112/// "#).unwrap();
113///
114/// for stage in dockerfile.stages() {
115///   println!("stage #{}, name: {:?}", stage.index, stage.name)
116/// }
117/// ```
118#[derive(Debug)]
119pub struct Stages<'a> {
120  pub stages: Vec<Stage<'a>>
121}
122
123impl<'a> Stages<'a> {
124  pub fn new(dockerfile: &'a Dockerfile) -> Stages<'a> {
125    // note: instructions before the first FROM are not part of any stage and
126    // are not included in the first stage's instruction list
127
128    let mut stages = Stages { stages: vec![] };
129    let mut next_stage_index = 0;
130
131    for ins in &dockerfile.instructions {
132      if let Instruction::From(from) = ins {
133        let image_name = from.image.as_ref().to_ascii_lowercase();
134        let parent = if image_name == "scratch" {
135          StageParent::Scratch
136        } else if let Some(stage) = stages.get_by_name(&image_name) {
137          StageParent::Stage(stage.index)
138        } else {
139          StageParent::Image(&from.image_parsed)
140        };
141
142        let root = if let StageParent::Stage(parent_stage) = parent {
143          stages.stages[parent_stage].root.clone()
144        } else {
145          parent.clone()
146        };
147
148        stages.stages.push(Stage {
149          index: next_stage_index,
150          name: from.alias.as_ref().map(|a| a.as_ref().to_ascii_lowercase()),
151          instructions: vec![ins],
152          parent,
153          root
154        });
155
156        next_stage_index += 1;
157      } else if !stages.stages.is_empty() {
158        let len = stages.stages.len();
159        if let Some(stage) = stages.stages.get_mut(len - 1) {
160          stage.instructions.push(ins);
161        }
162      }
163    }
164
165    stages
166  }
167
168  /// Attempts to fetch a stage by its name (`FROM` alias).
169  pub fn get_by_name(&'a self, name: &str) -> Option<&'a Stage<'a>> {
170    self.stages.iter().find(|s| s.name == Some(name.to_ascii_lowercase()))
171  }
172
173  /// Attempts to fetch a stage by its string representation.
174  ///
175  /// Stages with a valid integer value are retrieved by index, otherwise by
176  /// name.
177  pub fn get(&'a self, s: &str) -> Option<&'a Stage<'a>> {
178    match s.parse::<usize>() {
179      Ok(index) => self.stages.get(index),
180      Err(_) => self.get_by_name(s)
181    }
182  }
183
184  /// Returns an iterator over `stages`, wrapping the underlying `Vec::iter()`.
185  pub fn iter(&self) -> std::slice::Iter<'_, Stage<'a>> {
186    self.stages.iter()
187  }
188}
189
190impl<'a> Index<usize> for Stages<'a> {
191  type Output = Stage<'a>;
192
193  fn index(&self, index: usize) -> &Self::Output {
194    &self.stages[index]
195  }
196}
197
198impl<'a> IntoIterator for Stages<'a> {
199  type Item = Stage<'a>;
200  type IntoIter = std::vec::IntoIter<Stage<'a>>;
201
202  fn into_iter(self) -> Self::IntoIter {
203    self.stages.into_iter()
204  }
205}
206
207#[cfg(test)]
208mod tests {
209  use super::*;
210  use indoc::indoc;
211
212  #[test]
213  fn test_stages() {
214    let dockerfile = Dockerfile::parse(indoc!(r#"
215      FROM alpine:3.12
216
217      FROM ubuntu:18.04 as build
218      RUN echo "hello world"
219
220      FROM build as build2
221      COPY /foo /bar
222      COPY /bar /baz
223
224      FROM build as build3
225    "#)).unwrap();
226
227    let stages = Stages::new(&dockerfile);
228    assert_eq!(stages.stages.len(), 4);
229    assert_eq!(stages[1], Stage {
230      index: 1,
231      name: Some("build".into()),
232      instructions: vec![&dockerfile.instructions[1], &dockerfile.instructions[2]],
233      parent: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
234      root: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
235    });
236
237    assert_eq!(stages[2], Stage {
238      index: 2,
239      name: Some("build2".into()),
240      instructions: dockerfile.instructions[3..5].iter().collect(),
241      parent: StageParent::Stage(1),
242      root: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
243    });
244
245    assert_eq!(stages[3], Stage {
246      index: 3,
247      name: Some("build3".into()),
248      instructions: vec![&dockerfile.instructions[6]],
249      parent: StageParent::Stage(2),
250      root: StageParent::Image(&ImageRef::parse("ubuntu:18.04")),
251    });
252  }
253
254  #[test]
255  fn test_stages_get() {
256    let dockerfile = Dockerfile::parse(indoc!(r#"
257      FROM alpine:3.12
258
259      FROM ubuntu:18.04 as build
260
261      FROM build as build2
262    "#)).unwrap();
263
264    let stages = Stages::new(&dockerfile);
265    assert_eq!(stages.get("0").unwrap().index, 0);
266    assert_eq!(stages.get("1"), stages.get("build"));
267    assert_eq!(stages.get("2"), stages.get("build2"));
268  }
269}