1use std::collections::HashSet;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct InstructionPos {
11 pub instruction: Instruction,
13 pub line_number: u32,
15 pub source_text: String,
17}
18
19impl InstructionPos {
20 pub fn new(instruction: Instruction, line_number: u32, source_text: String) -> Self {
22 Self {
23 instruction,
24 line_number,
25 source_text,
26 }
27 }
28}
29
30#[derive(Debug, Clone, PartialEq)]
32pub enum Instruction {
33 From(BaseImage),
35 Run(RunArgs),
37 Copy(CopyArgs, CopyFlags),
39 Add(AddArgs, AddFlags),
41 Env(Vec<(String, String)>),
43 Label(Vec<(String, String)>),
45 Expose(Vec<Port>),
47 Arg(String, Option<String>),
49 Entrypoint(Arguments),
51 Cmd(Arguments),
53 Shell(Arguments),
55 User(String),
57 Workdir(String),
59 Volume(String),
61 Maintainer(String),
63 Healthcheck(HealthCheck),
65 OnBuild(Box<Instruction>),
67 Stopsignal(String),
69 Comment(String),
71}
72
73impl Instruction {
74 pub fn is_from(&self) -> bool {
76 matches!(self, Self::From(_))
77 }
78
79 pub fn is_run(&self) -> bool {
81 matches!(self, Self::Run(_))
82 }
83
84 pub fn is_copy(&self) -> bool {
86 matches!(self, Self::Copy(_, _))
87 }
88
89 pub fn is_onbuild(&self) -> bool {
91 matches!(self, Self::OnBuild(_))
92 }
93
94 pub fn unwrap_onbuild(&self) -> Option<&Instruction> {
96 match self {
97 Self::OnBuild(inner) => Some(inner.as_ref()),
98 _ => None,
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq)]
105pub struct BaseImage {
106 pub image: Image,
108 pub tag: Option<String>,
110 pub digest: Option<String>,
112 pub alias: Option<ImageAlias>,
114 pub platform: Option<String>,
116}
117
118impl BaseImage {
119 pub fn new(name: impl Into<String>) -> Self {
121 Self {
122 image: Image::new(name),
123 tag: None,
124 digest: None,
125 alias: None,
126 platform: None,
127 }
128 }
129
130 pub fn is_variable(&self) -> bool {
132 self.image.name.starts_with('$')
133 }
134
135 pub fn is_scratch(&self) -> bool {
137 self.image.name.eq_ignore_ascii_case("scratch")
138 }
139
140 pub fn has_version(&self) -> bool {
142 self.tag.is_some() || self.digest.is_some()
143 }
144}
145
146#[derive(Debug, Clone, PartialEq)]
148pub struct Image {
149 pub registry: Option<String>,
151 pub name: String,
153}
154
155impl Image {
156 pub fn new(name: impl Into<String>) -> Self {
158 Self {
159 registry: None,
160 name: name.into(),
161 }
162 }
163
164 pub fn with_registry(registry: impl Into<String>, name: impl Into<String>) -> Self {
166 Self {
167 registry: Some(registry.into()),
168 name: name.into(),
169 }
170 }
171
172 pub fn full_name(&self) -> String {
174 match &self.registry {
175 Some(reg) => format!("{}/{}", reg, self.name),
176 None => self.name.clone(),
177 }
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Hash)]
183pub struct ImageAlias(pub String);
184
185impl ImageAlias {
186 pub fn new(name: impl Into<String>) -> Self {
188 Self(name.into())
189 }
190
191 pub fn as_str(&self) -> &str {
193 &self.0
194 }
195}
196
197#[derive(Debug, Clone, PartialEq)]
199pub struct RunArgs {
200 pub arguments: Arguments,
202 pub flags: RunFlags,
204}
205
206impl RunArgs {
207 pub fn shell(cmd: impl Into<String>) -> Self {
209 Self {
210 arguments: Arguments::Text(cmd.into()),
211 flags: RunFlags::default(),
212 }
213 }
214
215 pub fn exec(args: Vec<String>) -> Self {
217 Self {
218 arguments: Arguments::List(args),
219 flags: RunFlags::default(),
220 }
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Default)]
226pub struct RunFlags {
227 pub mount: HashSet<RunMount>,
229 pub network: Option<String>,
231 pub security: Option<String>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Hash)]
237pub enum RunMount {
238 Bind(BindOpts),
240 Cache(CacheOpts),
242 Tmpfs(TmpOpts),
244 Secret(SecretOpts),
246 Ssh(SshOpts),
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
252pub struct BindOpts {
253 pub target: Option<String>,
254 pub source: Option<String>,
255 pub from: Option<String>,
256 pub read_only: bool,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
261pub struct CacheOpts {
262 pub target: Option<String>,
263 pub id: Option<String>,
264 pub sharing: Option<String>,
265 pub from: Option<String>,
266 pub source: Option<String>,
267 pub mode: Option<String>,
268 pub uid: Option<u32>,
269 pub gid: Option<u32>,
270 pub read_only: bool,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
275pub struct TmpOpts {
276 pub target: Option<String>,
277 pub size: Option<String>,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
282pub struct SecretOpts {
283 pub id: Option<String>,
284 pub target: Option<String>,
285 pub required: bool,
286 pub mode: Option<String>,
287 pub uid: Option<u32>,
288 pub gid: Option<u32>,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
293pub struct SshOpts {
294 pub id: Option<String>,
295 pub target: Option<String>,
296 pub required: bool,
297 pub mode: Option<String>,
298 pub uid: Option<u32>,
299 pub gid: Option<u32>,
300}
301
302#[derive(Debug, Clone, PartialEq)]
304pub struct CopyArgs {
305 pub sources: Vec<String>,
307 pub dest: String,
309}
310
311impl CopyArgs {
312 pub fn new(sources: Vec<String>, dest: impl Into<String>) -> Self {
314 Self {
315 sources,
316 dest: dest.into(),
317 }
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Default)]
323pub struct CopyFlags {
324 pub from: Option<String>,
326 pub chown: Option<String>,
328 pub chmod: Option<String>,
330 pub link: bool,
332}
333
334#[derive(Debug, Clone, PartialEq)]
336pub struct AddArgs {
337 pub sources: Vec<String>,
339 pub dest: String,
341}
342
343impl AddArgs {
344 pub fn new(sources: Vec<String>, dest: impl Into<String>) -> Self {
346 Self {
347 sources,
348 dest: dest.into(),
349 }
350 }
351
352 pub fn has_url(&self) -> bool {
354 self.sources.iter().any(|s| s.starts_with("http://") || s.starts_with("https://"))
355 }
356
357 pub fn has_archive(&self) -> bool {
359 const ARCHIVE_EXTENSIONS: &[&str] = &[
360 ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz",
361 ".zip", ".gz", ".bz2", ".xz", ".Z", ".lz", ".lzma",
362 ];
363 self.sources.iter().any(|s| {
364 ARCHIVE_EXTENSIONS.iter().any(|ext| s.ends_with(ext))
365 })
366 }
367}
368
369#[derive(Debug, Clone, PartialEq, Default)]
371pub struct AddFlags {
372 pub chown: Option<String>,
374 pub chmod: Option<String>,
376 pub link: bool,
378 pub checksum: Option<String>,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Hash)]
384pub struct Port {
385 pub number: u16,
387 pub protocol: PortProtocol,
389}
390
391impl Port {
392 pub fn tcp(number: u16) -> Self {
394 Self {
395 number,
396 protocol: PortProtocol::Tcp,
397 }
398 }
399
400 pub fn udp(number: u16) -> Self {
402 Self {
403 number,
404 protocol: PortProtocol::Udp,
405 }
406 }
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
411pub enum PortProtocol {
412 #[default]
413 Tcp,
414 Udp,
415}
416
417#[derive(Debug, Clone, PartialEq)]
419pub enum Arguments {
420 Text(String),
422 List(Vec<String>),
424}
425
426impl Arguments {
427 pub fn is_shell_form(&self) -> bool {
429 matches!(self, Self::Text(_))
430 }
431
432 pub fn is_exec_form(&self) -> bool {
434 matches!(self, Self::List(_))
435 }
436
437 pub fn as_text(&self) -> Option<&str> {
439 match self {
440 Self::Text(s) => Some(s),
441 _ => None,
442 }
443 }
444
445 pub fn as_list(&self) -> Option<&[String]> {
447 match self {
448 Self::List(v) => Some(v),
449 _ => None,
450 }
451 }
452
453 pub fn to_string_lossy(&self) -> String {
455 match self {
456 Self::Text(s) => s.clone(),
457 Self::List(v) => v.join(" "),
458 }
459 }
460}
461
462#[derive(Debug, Clone, PartialEq)]
464pub enum HealthCheck {
465 None,
467 Cmd {
469 cmd: Arguments,
471 interval: Option<String>,
473 timeout: Option<String>,
475 start_period: Option<String>,
477 retries: Option<u32>,
479 },
480}
481
482impl HealthCheck {
483 pub fn cmd(cmd: Arguments) -> Self {
485 Self::Cmd {
486 cmd,
487 interval: None,
488 timeout: None,
489 start_period: None,
490 retries: None,
491 }
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn test_base_image() {
501 let img = BaseImage::new("ubuntu");
502 assert!(!img.is_scratch());
503 assert!(!img.is_variable());
504 assert!(!img.has_version());
505
506 let scratch = BaseImage::new("scratch");
507 assert!(scratch.is_scratch());
508
509 let var = BaseImage::new("${BASE_IMAGE}");
510 assert!(var.is_variable());
511
512 let tagged = BaseImage {
513 tag: Some("20.04".to_string()),
514 ..BaseImage::new("ubuntu")
515 };
516 assert!(tagged.has_version());
517 }
518
519 #[test]
520 fn test_image() {
521 let img = Image::new("ubuntu");
522 assert_eq!(img.full_name(), "ubuntu");
523
524 let img_with_reg = Image::with_registry("gcr.io", "my-project/my-image");
525 assert_eq!(img_with_reg.full_name(), "gcr.io/my-project/my-image");
526 }
527
528 #[test]
529 fn test_arguments() {
530 let shell = Arguments::Text("apt-get update".to_string());
531 assert!(shell.is_shell_form());
532 assert_eq!(shell.as_text(), Some("apt-get update"));
533
534 let exec = Arguments::List(vec!["apt-get".to_string(), "update".to_string()]);
535 assert!(exec.is_exec_form());
536 assert_eq!(exec.as_list(), Some(&["apt-get".to_string(), "update".to_string()][..]));
537 }
538
539 #[test]
540 fn test_add_args() {
541 let add = AddArgs::new(vec!["app.tar.gz".to_string()], "/app");
542 assert!(add.has_archive());
543 assert!(!add.has_url());
544
545 let add_url = AddArgs::new(vec!["https://example.com/file.txt".to_string()], "/app");
546 assert!(add_url.has_url());
547 assert!(!add_url.has_archive());
548 }
549}