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
355 .iter()
356 .any(|s| s.starts_with("http://") || s.starts_with("https://"))
357 }
358
359 pub fn has_archive(&self) -> bool {
361 const ARCHIVE_EXTENSIONS: &[&str] = &[
362 ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz", ".zip", ".gz",
363 ".bz2", ".xz", ".Z", ".lz", ".lzma",
364 ];
365 self.sources
366 .iter()
367 .any(|s| ARCHIVE_EXTENSIONS.iter().any(|ext| s.ends_with(ext)))
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Default)]
373pub struct AddFlags {
374 pub chown: Option<String>,
376 pub chmod: Option<String>,
378 pub link: bool,
380 pub checksum: Option<String>,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Hash)]
386pub struct Port {
387 pub number: u16,
389 pub protocol: PortProtocol,
391}
392
393impl Port {
394 pub fn tcp(number: u16) -> Self {
396 Self {
397 number,
398 protocol: PortProtocol::Tcp,
399 }
400 }
401
402 pub fn udp(number: u16) -> Self {
404 Self {
405 number,
406 protocol: PortProtocol::Udp,
407 }
408 }
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
413pub enum PortProtocol {
414 #[default]
415 Tcp,
416 Udp,
417}
418
419#[derive(Debug, Clone, PartialEq)]
421pub enum Arguments {
422 Text(String),
424 List(Vec<String>),
426}
427
428impl Arguments {
429 pub fn is_shell_form(&self) -> bool {
431 matches!(self, Self::Text(_))
432 }
433
434 pub fn is_exec_form(&self) -> bool {
436 matches!(self, Self::List(_))
437 }
438
439 pub fn as_text(&self) -> Option<&str> {
441 match self {
442 Self::Text(s) => Some(s),
443 _ => None,
444 }
445 }
446
447 pub fn as_list(&self) -> Option<&[String]> {
449 match self {
450 Self::List(v) => Some(v),
451 _ => None,
452 }
453 }
454
455 pub fn to_string_lossy(&self) -> String {
457 match self {
458 Self::Text(s) => s.clone(),
459 Self::List(v) => v.join(" "),
460 }
461 }
462}
463
464#[derive(Debug, Clone, PartialEq)]
466pub enum HealthCheck {
467 None,
469 Cmd {
471 cmd: Arguments,
473 interval: Option<String>,
475 timeout: Option<String>,
477 start_period: Option<String>,
479 retries: Option<u32>,
481 },
482}
483
484impl HealthCheck {
485 pub fn cmd(cmd: Arguments) -> Self {
487 Self::Cmd {
488 cmd,
489 interval: None,
490 timeout: None,
491 start_period: None,
492 retries: None,
493 }
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500
501 #[test]
502 fn test_base_image() {
503 let img = BaseImage::new("ubuntu");
504 assert!(!img.is_scratch());
505 assert!(!img.is_variable());
506 assert!(!img.has_version());
507
508 let scratch = BaseImage::new("scratch");
509 assert!(scratch.is_scratch());
510
511 let var = BaseImage::new("${BASE_IMAGE}");
512 assert!(var.is_variable());
513
514 let tagged = BaseImage {
515 tag: Some("20.04".to_string()),
516 ..BaseImage::new("ubuntu")
517 };
518 assert!(tagged.has_version());
519 }
520
521 #[test]
522 fn test_image() {
523 let img = Image::new("ubuntu");
524 assert_eq!(img.full_name(), "ubuntu");
525
526 let img_with_reg = Image::with_registry("gcr.io", "my-project/my-image");
527 assert_eq!(img_with_reg.full_name(), "gcr.io/my-project/my-image");
528 }
529
530 #[test]
531 fn test_arguments() {
532 let shell = Arguments::Text("apt-get update".to_string());
533 assert!(shell.is_shell_form());
534 assert_eq!(shell.as_text(), Some("apt-get update"));
535
536 let exec = Arguments::List(vec!["apt-get".to_string(), "update".to_string()]);
537 assert!(exec.is_exec_form());
538 assert_eq!(
539 exec.as_list(),
540 Some(&["apt-get".to_string(), "update".to_string()][..])
541 );
542 }
543
544 #[test]
545 fn test_add_args() {
546 let add = AddArgs::new(vec!["app.tar.gz".to_string()], "/app");
547 assert!(add.has_archive());
548 assert!(!add.has_url());
549
550 let add_url = AddArgs::new(vec!["https://example.com/file.txt".to_string()], "/app");
551 assert!(add_url.has_url());
552 assert!(!add_url.has_archive());
553 }
554}