1use std::collections::{HashMap, HashSet};
2
3use crate::dofigen_struct::*;
4
5const WARN_MESSAGE_FROM_CONTEXT: &str =
6 "Prefer to use fromImage and fromBuilder instead of fromContext";
7const WARN_MESSAGE_FROM_CONTEXT_UNLESS: &str =
8 "(unless it's really from a build context: https://docs.docker.com/reference/cli/docker/buildx/build/#build-context)";
9
10#[derive(Debug, Clone, PartialEq)]
11struct StageDependency {
12 stage: String,
13 path: String,
14 origin: Vec<String>,
15}
16
17macro_rules! linter_path {
18 ($session:expr, $part:expr, $block:block) => {
19 $session.push_path_part($part);
20 $block
21 $session.pop_path_part();
22 };
23}
24
25trait Linter {
26 fn analyze(&self, session: &mut LintSession);
27}
28
29impl Linter for Dofigen {
30 fn analyze(&self, session: &mut LintSession) {
31 linter_path!(session, "builders".into(), {
32 for (name, builder) in self.builders.iter() {
33 linter_path!(session, name.clone(), {
34 if name == "runtime" {
35 session.add_message(
36 MessageLevel::Error,
37 "The builder name 'runtime' is reserved".into(),
38 );
39 }
40 builder.analyze(session);
41 });
42 }
43 });
44
45 self.stage.analyze(session);
46
47 if let Some(user) = &self.stage.user {
49 if user.user == "root" || user.uid() == Some(0) {
50 session.messages.push(LintMessage {
51 level: MessageLevel::Warn,
52 message: "The runtime user should not be root".into(),
53 path: vec!["user".into()],
54 });
55 }
56 }
57
58 session.check_dependencies();
59 }
60}
61
62impl Linter for Stage {
63 fn analyze(&self, session: &mut LintSession) {
64 let name = session.current_path.last().cloned();
65
66 if let Some(name) = name.clone() {
68 if self.copy.is_empty() && self.run.run.is_empty() && self.root.is_none() {
69 session.add_message(
70 MessageLevel::Warn,
71 format!("The builder '{}' is empty and should be removed", name),
72 );
73 }
74 }
75
76 let name = name.unwrap_or("runtime".to_string());
77
78 let dependencies = self.get_dependencies(&session.current_path);
79 session.messages.append(
80 &mut dependencies
81 .iter()
82 .filter(|dep| dep.stage == "runtime")
83 .map(|dep| LintMessage {
84 level: MessageLevel::Error,
85 message: format!("The stage '{}' can't depend on the 'runtime'", &name,),
86 path: dep.origin.clone(),
87 })
88 .collect(),
89 );
90 let cache_paths = session.get_stage_cache_paths(self);
91 session.stage_infos.insert(
92 name,
93 StageLintInfo {
94 dependencies,
95 cache_paths,
96 },
97 );
98
99 if let FromContext::FromContext(Some(_)) = self.from {
101 linter_path!(session, "fromContext".into(), {
102 session.add_message(MessageLevel::Warn, WARN_MESSAGE_FROM_CONTEXT.to_string());
103 });
104 }
105
106 linter_path!(session, "copy".into(), {
107 for (position, copy) in self.copy.iter().enumerate() {
108 linter_path!(session, position.to_string(), {
109 copy.analyze(session);
110 });
111 }
112 });
113
114 if let Some(root) = &self.root {
115 linter_path!(session, "root".into(), {
116 root.analyze(session);
117 });
118 }
119
120 self.run.analyze(session);
121
122 if let Some(user) = &self.user {
124 if user.uid().is_none() {
125 linter_path!(session, "user".into(), {
126 session.add_message(
127 MessageLevel::Warn,
128 "UID should be used instead of username".to_string(),
129 );
130 });
131 }
132 }
133 }
134}
135
136impl Linter for CopyResource {
137 fn analyze(&self, session: &mut LintSession) {
138 match self {
139 CopyResource::Copy(copy) => copy.analyze(session),
140 _ => {}
141 }
142 }
143}
144
145impl Linter for Copy {
146 fn analyze(&self, session: &mut LintSession) {
147 match &self.from {
148 FromContext::FromContext(Some(_)) => {
149 linter_path!(session, "fromContext".into(), {
150 session.add_message(
151 MessageLevel::Warn,
152 format!(
153 "{} {}",
154 WARN_MESSAGE_FROM_CONTEXT, WARN_MESSAGE_FROM_CONTEXT_UNLESS
155 ),
156 );
157 });
158 }
159 _ => {}
160 }
161 }
162}
163
164impl Linter for Run {
165 fn analyze(&self, session: &mut LintSession) {
166 if self.run.is_empty() {
167 if !self.bind.is_empty() {
168 linter_path!(session, "bind".into(), {
169 session.add_message(
170 MessageLevel::Warn,
171 "The run list is empty but there are bind definitions".to_string(),
172 );
173 });
174 }
175
176 if !self.cache.is_empty() {
177 linter_path!(session, "cache".into(), {
178 session.add_message(
179 MessageLevel::Warn,
180 "The run list is empty but there are cache definitions".to_string(),
181 );
182 });
183 }
184 }
185
186 linter_path!(session, "run".into(), {
187 for (position, command) in self.run.iter().enumerate() {
188 linter_path!(session, position.to_string(), {
189 if command.starts_with("cd ") {
190 session.add_message(
191 MessageLevel::Warn,
192 "Avoid using 'cd' in the run command".to_string(),
193 );
194 }
195 });
196 }
197 });
198
199 linter_path!(session, "bind".into(), {
200 for (position, bind) in self.bind.iter().enumerate() {
201 linter_path!(session, position.to_string(), {
202 if let FromContext::FromContext(Some(_)) = bind.from {
203 linter_path!(session, "fromContext".into(), {
204 session.add_message(
205 MessageLevel::Warn,
206 format!(
207 "{} {}",
208 WARN_MESSAGE_FROM_CONTEXT, WARN_MESSAGE_FROM_CONTEXT_UNLESS
209 ),
210 );
211 });
212 }
213 });
214 }
215 });
216
217 linter_path!(session, "cache".into(), {
218 for (position, cache) in self.cache.iter().enumerate() {
219 linter_path!(session, position.to_string(), {
220 if let FromContext::FromContext(Some(_)) = cache.from {
221 linter_path!(session, "fromContext".into(), {
222 session.add_message(
223 MessageLevel::Warn,
224 format!(
225 "{} {}",
226 WARN_MESSAGE_FROM_CONTEXT, WARN_MESSAGE_FROM_CONTEXT_UNLESS
227 ),
228 );
229 });
230 }
231 });
232 }
233 });
234 }
235}
236
237trait StageDependencyGetter {
238 fn get_dependencies(&self, origin: &Vec<String>) -> Vec<StageDependency>;
239}
240
241impl StageDependencyGetter for Stage {
242 fn get_dependencies(&self, origin: &Vec<String>) -> Vec<StageDependency> {
243 let mut dependencies = vec![];
244 if let FromContext::FromBuilder(builder) = &self.from {
245 dependencies.push(StageDependency {
246 stage: builder.clone(),
247 path: "/".into(),
248 origin: [origin.clone(), vec!["from".into()]].concat(),
249 });
250 }
251 for (position, copy) in self.copy.iter().enumerate() {
252 dependencies.append(&mut copy.get_dependencies(
253 &[origin.clone(), vec!["copy".into(), position.to_string()]].concat(),
254 ));
255 }
256 dependencies.append(&mut self.run.get_dependencies(origin));
257 if let Some(root) = &self.root {
258 dependencies.append(
259 &mut root.get_dependencies(&[origin.clone(), vec!["root".into()]].concat()),
260 );
261 }
262 dependencies
263 }
264}
265
266impl StageDependencyGetter for Run {
267 fn get_dependencies(&self, origin: &Vec<String>) -> Vec<StageDependency> {
268 let mut dependencies = vec![];
269 for (position, cache) in self.cache.iter().enumerate() {
270 if let FromContext::FromBuilder(builder) = &cache.from {
271 dependencies.push(StageDependency {
272 stage: builder.clone(),
273 path: cache.source.clone().unwrap_or("/".into()),
274 origin: [origin.clone(), vec!["cache".into(), position.to_string()]].concat(),
275 });
276 }
277 }
278 for (position, bind) in self.bind.iter().enumerate() {
279 if let FromContext::FromBuilder(builder) = &bind.from {
280 dependencies.push(StageDependency {
281 stage: builder.clone(),
282 path: bind.source.clone().unwrap_or("/".into()),
283 origin: [origin.clone(), vec!["bind".into(), position.to_string()]].concat(),
284 });
285 }
286 }
287 dependencies
288 }
289}
290
291impl StageDependencyGetter for CopyResource {
292 fn get_dependencies(&self, origin: &Vec<String>) -> Vec<StageDependency> {
293 match self {
294 CopyResource::Copy(copy) => match ©.from {
295 FromContext::FromBuilder(builder) => copy
296 .paths
297 .iter()
298 .map(|path| StageDependency {
299 stage: builder.clone(),
300 path: path.clone(),
301 origin: origin.clone(),
302 })
303 .collect(),
304 _ => vec![],
305 },
306 _ => vec![],
307 }
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Default)]
312pub struct LintSession {
313 current_path: Vec<String>,
314 messages: Vec<LintMessage>,
315 stage_infos: HashMap<String, StageLintInfo>,
316 recursive_stage_dependencies: HashMap<String, Vec<String>>,
317}
318
319impl LintSession {
320 fn push_path_part(&mut self, part: String) {
321 self.current_path.push(part);
322 }
323
324 fn pop_path_part(&mut self) {
325 self.current_path.pop();
326 }
327
328 fn add_message(&mut self, level: MessageLevel, message: String) {
329 self.messages.push(LintMessage {
330 level,
331 message,
332 path: self.current_path.clone(),
333 });
334 }
335
336 pub fn messages(&self) -> Vec<LintMessage> {
337 self.messages.clone()
338 }
339
340 pub fn get_sorted_builders(&mut self) -> Vec<String> {
341 let mut stages: Vec<(String, Vec<String>)> = self
342 .stage_infos
343 .clone()
344 .keys()
345 .map(|name| {
346 (
347 name.clone(),
348 self.get_stage_recursive_dependencies(name.clone()),
349 )
350 })
351 .collect();
352
353 stages.sort_by(|(a_stage, a_deps), (b_stage, b_deps)| {
354 if a_deps.contains(b_stage) {
355 return std::cmp::Ordering::Greater;
356 }
357 if b_deps.contains(a_stage) {
358 return std::cmp::Ordering::Less;
359 }
360 a_stage.cmp(b_stage)
361 });
362
363 stages
364 .into_iter()
365 .map(|(stage, _)| stage)
366 .filter(|name| *name != "runtime")
367 .collect()
368 }
369
370 pub fn get_stage_recursive_dependencies(&mut self, stage: String) -> Vec<String> {
371 self.resolve_stage_recursive_dependencies(&mut vec![stage])
372 }
373
374 fn resolve_stage_recursive_dependencies(&mut self, path: &mut Vec<String>) -> Vec<String> {
375 let stage = &path.last().expect("The path is empty").clone();
376 if let Some(dependencies) = self.recursive_stage_dependencies.get(stage) {
377 return dependencies.clone();
378 }
379 let mut deps = HashSet::new();
380 let dependencies = self
381 .stage_infos
382 .get(stage)
383 .expect(format!("The stage info not found for stage '{}'", stage).as_str())
384 .dependencies
385 .clone();
386 for dependency in dependencies {
387 let dep_stage = &dependency.stage;
388 if path.contains(dep_stage) {
389 self.messages.push(LintMessage {
390 level: MessageLevel::Error,
391 message: format!(
392 "Circular dependency detected: {} -> {}",
393 path.join(" -> "),
394 dependency.stage
395 ),
396 path: dependency.origin.clone(),
397 });
398 continue;
399 }
400 deps.insert(dep_stage.clone());
401 if self.stage_infos.contains_key(dep_stage) {
402 path.push(dep_stage.clone());
403 deps.extend(self.resolve_stage_recursive_dependencies(path));
404 path.pop();
405 } }
407 let deps: Vec<String> = deps.into_iter().collect();
408 self.recursive_stage_dependencies
409 .insert(stage.clone(), deps.clone());
410 deps
411 }
412
413 fn check_dependencies(&mut self) {
415 let dependencies = self
416 .stage_infos
417 .values()
418 .flat_map(|info| info.dependencies.clone())
419 .collect::<Vec<_>>();
420
421 let caches = self
422 .stage_infos
423 .iter()
424 .map(|(name, info)| (name.clone(), info.cache_paths.clone()))
425 .collect::<HashMap<_, _>>();
426
427 let used_builders = dependencies
429 .iter()
430 .map(|dep| dep.stage.clone())
431 .collect::<HashSet<_>>();
432
433 let unused_builders = self
434 .stage_infos
435 .keys()
436 .filter(|name| name != &"runtime")
437 .map(|name| name.clone())
438 .filter(|name| !used_builders.contains(name))
439 .collect::<HashSet<_>>();
440
441 linter_path!(self, "builders".into(), {
442 for builder in unused_builders {
443 linter_path!(self, builder.clone(), {
444 self.add_message(
445 MessageLevel::Warn,
446 format!(
447 "The builder '{}' is not used and should be removed",
448 builder
449 ),
450 );
451 });
452 }
453 });
454
455 for dependency in dependencies {
456 if let Some(paths) = caches.get(&dependency.stage) {
457 paths
458 .iter()
459 .filter(|path| dependency.path.starts_with(*path))
460 .for_each(|path| {
461 self.messages.push(LintMessage {
462 level: MessageLevel::Error,
463 message: format!(
464 "Use of the '{}' builder cache path '{}'",
465 dependency.stage, path
466 ),
467 path: dependency.origin.clone(),
468 });
469 });
470 } else {
471 self.messages.push(LintMessage {
472 level: MessageLevel::Error,
473 message: format!("The builder '{}' not found", dependency.stage),
474 path: dependency.origin.clone(),
475 });
476 }
477 }
478 }
479
480 fn get_stage_cache_paths(&mut self, stage: &Stage) -> Vec<String> {
481 let mut paths = vec![];
482 paths.append(&mut self.get_run_cache_paths(
483 &stage.run,
484 &self.current_path.clone(),
485 &stage.workdir,
486 ));
487 if let Some(root) = &stage.root {
488 paths.append(&mut self.get_run_cache_paths(
489 root,
490 &[self.current_path.clone(), vec!["root".into()]].concat(),
491 &stage.workdir,
492 ));
493 }
494 paths
495 }
496
497 fn get_run_cache_paths(
498 &mut self,
499 run: &Run,
500 path: &Vec<String>,
501 workdir: &Option<String>,
502 ) -> Vec<String> {
503 let mut cache_paths = vec![];
504 for (position, cache) in run.cache.iter().enumerate() {
505 let target = cache.target.clone();
506 cache_paths.push(if target.starts_with("/") {
507 target.clone()
508 } else {
509 if let Some(workdir) = workdir {
510 format!("{}/{}", workdir, target)
511 }
512 else {
513 self.messages.push(LintMessage {
514 level: MessageLevel::Warn,
515 message: "The cache target should be absolute or a workdir should be defined in the stage".to_string(),
516 path: [path.clone(), vec!["cache".into(), position.to_string()]].concat(),
517 });
518 target.clone()
519 }
520 });
521 }
522 cache_paths
523 }
524
525 pub fn analyze(dofigen: &Dofigen) -> Self {
529 let mut session = Self::default();
530 dofigen.analyze(&mut session);
531
532 session
533 }
534}
535
536#[derive(Debug, Clone, PartialEq)]
537pub struct StageLintInfo {
538 dependencies: Vec<StageDependency>,
539 cache_paths: Vec<String>,
540}
541
542#[derive(Debug, Clone, PartialEq)]
543pub struct LintMessage {
544 pub level: MessageLevel,
545 pub path: Vec<String>,
546 pub message: String,
547}
548
549#[derive(Debug, Clone, PartialEq)]
550pub enum MessageLevel {
551 Warn,
552 Error,
553}
554
555#[cfg(test)]
556mod test {
557 use crate::Dofigen;
558
559 use super::*;
560 use pretty_assertions_sorted::assert_eq_sorted;
561
562 mod stage_dependencies {
563 use super::*;
564
565 #[test]
566 fn builders_dependencies() {
567 let dofigen = Dofigen {
568 builders: HashMap::from([
569 (
570 "builder1".into(),
571 Stage {
572 copy: vec![CopyResource::Copy(Copy {
573 from: FromContext::FromBuilder("builder2".into()),
574 paths: vec!["/path/to/copy".into()],
575 options: Default::default(),
576 ..Default::default()
577 })],
578 ..Default::default()
579 },
580 ),
581 (
582 "builder2".into(),
583 Stage {
584 copy: vec![CopyResource::Copy(Copy {
585 from: FromContext::FromBuilder("builder3".into()),
586 paths: vec!["/path/to/copy".into()],
587 options: Default::default(),
588 ..Default::default()
589 })],
590 ..Default::default()
591 },
592 ),
593 (
594 "builder3".into(),
595 Stage {
596 run: Run {
597 run: vec!["echo Hello".into()].into(),
598 ..Default::default()
599 },
600 ..Default::default()
601 },
602 ),
603 ]),
604 stage: Stage {
605 copy: vec![CopyResource::Copy(Copy {
606 from: FromContext::FromBuilder("builder1".into()),
607 paths: vec!["/path/to/copy".into()],
608 ..Default::default()
609 })],
610 ..Default::default()
611 },
612 ..Default::default()
613 };
614
615 let mut lint_session = LintSession::analyze(&dofigen);
616
617 let mut dependencies = lint_session.get_stage_recursive_dependencies("runtime".into());
618 dependencies.sort();
619 assert_eq_sorted!(dependencies, vec!["builder1", "builder2", "builder3"]);
620
621 dependencies = lint_session.get_stage_recursive_dependencies("builder1".into());
622 dependencies.sort();
623 assert_eq_sorted!(dependencies, vec!["builder2", "builder3"]);
624
625 dependencies = lint_session.get_stage_recursive_dependencies("builder2".into());
626 assert_eq_sorted!(dependencies, vec!["builder3"]);
627
628 dependencies = lint_session.get_stage_recursive_dependencies("builder3".into());
629 assert_eq_sorted!(dependencies, Vec::<String>::new());
630
631 let mut builders = lint_session.get_sorted_builders();
632 builders.sort();
633
634 assert_eq_sorted!(builders, vec!["builder1", "builder2", "builder3"]);
635
636 assert_eq_sorted!(lint_session.messages, vec![]);
637 }
638
639 #[test]
640 fn builders_circular_dependencies() {
641 let dofigen = Dofigen {
642 builders: HashMap::from([
643 (
644 "builder1".into(),
645 Stage {
646 copy: vec![CopyResource::Copy(Copy {
647 from: FromContext::FromBuilder("builder2".into()),
648 paths: vec!["/path/to/copy".into()],
649 options: Default::default(),
650 ..Default::default()
651 })],
652 ..Default::default()
653 },
654 ),
655 (
656 "builder2".into(),
657 Stage {
658 copy: vec![CopyResource::Copy(Copy {
659 from: FromContext::FromBuilder("builder3".into()),
660 paths: vec!["/path/to/copy".into()],
661 options: Default::default(),
662 ..Default::default()
663 })],
664 ..Default::default()
665 },
666 ),
667 (
668 "builder3".into(),
669 Stage {
670 copy: vec![CopyResource::Copy(Copy {
671 from: FromContext::FromBuilder("builder1".into()),
672 paths: vec!["/path/to/copy".into()],
673 options: Default::default(),
674 ..Default::default()
675 })],
676 ..Default::default()
677 },
678 ),
679 ]),
680 ..Default::default()
681 };
682
683 let mut lint_session = LintSession::analyze(&dofigen);
684
685 let mut dependencies = lint_session.get_stage_recursive_dependencies("runtime".into());
686 dependencies.sort();
687 assert_eq_sorted!(dependencies, Vec::<String>::new());
688
689 dependencies = lint_session.get_stage_recursive_dependencies("builder1".into());
690 dependencies.sort();
691 assert_eq_sorted!(dependencies, vec!["builder2", "builder3"]);
692
693 dependencies = lint_session.get_stage_recursive_dependencies("builder2".into());
694 assert_eq_sorted!(dependencies, vec!["builder3"]);
695
696 dependencies = lint_session.get_stage_recursive_dependencies("builder3".into());
697 assert_eq_sorted!(dependencies, Vec::<String>::new());
698
699 let mut builders = lint_session.get_sorted_builders();
700 builders.sort();
701
702 assert_eq_sorted!(builders, vec!["builder1", "builder2", "builder3"]);
703
704 assert_eq_sorted!(
705 lint_session.messages,
706 vec![LintMessage {
707 level: MessageLevel::Error,
708 path: vec![
709 "builders".into(),
710 "builder3".into(),
711 "copy".into(),
712 "0".into(),
713 ],
714 message:
715 "Circular dependency detected: builder1 -> builder2 -> builder3 -> builder1"
716 .into(),
717 },]
718 );
719 }
720
721 #[test]
722 fn builder_named_runtime() {
723 let dofigen = Dofigen {
724 builders: HashMap::from([(
725 "runtime".into(),
726 Stage {
727 run: Run {
728 run: vec!["echo Hello".into()].into(),
729 ..Default::default()
730 },
731 ..Default::default()
732 },
733 )]),
734 ..Default::default()
735 };
736
737 let mut lint_session = LintSession::analyze(&dofigen);
738
739 let mut builders = lint_session.get_sorted_builders();
740 builders.sort();
741
742 assert_eq_sorted!(builders, Vec::<String>::new());
743
744 assert_eq_sorted!(
745 lint_session.messages,
746 vec![LintMessage {
747 level: MessageLevel::Error,
748 path: vec!["builders".into(), "runtime".into(),],
749 message: "The builder name 'runtime' is reserved".into(),
750 },]
751 );
752 }
753
754 #[test]
755 fn builder_not_found() {
756 let dofigen = Dofigen {
757 stage: Stage {
758 from: FromContext::FromBuilder("builder1".into()),
759 ..Default::default()
760 },
761 ..Default::default()
762 };
763
764 let mut lint_session = LintSession::analyze(&dofigen);
765
766 let mut builders = lint_session.get_sorted_builders();
767 builders.sort();
768
769 assert_eq_sorted!(builders, Vec::<String>::new());
770
771 assert_eq_sorted!(
772 lint_session.messages,
773 vec![LintMessage {
774 level: MessageLevel::Error,
775 path: vec!["from".into(),],
776 message: "The builder 'builder1' not found".into(),
777 },]
778 );
779 }
780
781 #[test]
782 fn dependency_to_runtime() {
783 let dofigen = Dofigen {
784 builders: HashMap::from([(
785 "builder".into(),
786 Stage {
787 copy: vec![CopyResource::Copy(Copy {
788 from: FromContext::FromBuilder("runtime".into()),
789 paths: vec!["/path/to/copy".into()],
790 ..Default::default()
791 })],
792 ..Default::default()
793 },
794 )]),
795 stage: Stage {
796 run: Run {
797 run: vec!["echo Hello".into()].into(),
798 ..Default::default()
799 },
800 ..Default::default()
801 },
802 ..Default::default()
803 };
804
805 let mut lint_session = LintSession::analyze(&dofigen);
806
807 let mut builders = lint_session.get_sorted_builders();
808 builders.sort();
809
810 assert_eq_sorted!(builders, vec!["builder"]);
811
812 assert_eq_sorted!(
813 lint_session.messages,
814 vec![
815 LintMessage {
816 level: MessageLevel::Error,
817 path: vec![
818 "builders".into(),
819 "builder".into(),
820 "copy".into(),
821 "0".into()
822 ],
823 message: "The stage 'builder' can't depend on the 'runtime'".into(),
824 },
825 LintMessage {
826 level: MessageLevel::Warn,
827 path: vec!["builders".into(), "builder".into(),],
828 message: "The builder 'builder' is not used and should be removed".into(),
829 }
830 ]
831 );
832 }
833
834 #[test]
835 fn dependency_to_cache_path() {
836 let dofigen = Dofigen {
837 builders: HashMap::from([
838 (
839 "builder1".into(),
840 Stage {
841 run: Run {
842 run: vec!["echo Hello".into()].into(),
843 cache: vec![Cache {
844 target: "/path/to/cache".into(),
845 ..Default::default()
846 }],
847 ..Default::default()
848 },
849 ..Default::default()
850 },
851 ),
852 (
853 "builder2".into(),
854 Stage {
855 copy: vec![CopyResource::Copy(Copy {
856 from: FromContext::FromBuilder("builder1".into()),
857 paths: vec!["/path/to/cache/test".into()],
858 ..Default::default()
859 })],
860 ..Default::default()
861 },
862 ),
863 ]),
864 stage: Stage {
865 from: FromContext::FromBuilder("builder2".into()),
866 ..Default::default()
867 },
868 ..Default::default()
869 };
870
871 let mut lint_session = LintSession::analyze(&dofigen);
872
873 let mut builders = lint_session.get_sorted_builders();
874 builders.sort();
875
876 assert_eq_sorted!(builders, vec!["builder1", "builder2"]);
877
878 assert_eq_sorted!(
879 lint_session.messages,
880 vec![LintMessage {
881 level: MessageLevel::Error,
882 path: vec![
883 "builders".into(),
884 "builder2".into(),
885 "copy".into(),
886 "0".into()
887 ],
888 message: "Use of the 'builder1' builder cache path '/path/to/cache'".into(),
889 },]
890 );
891 }
892
893 #[test]
894 fn runtime_dependencies() {
895 let dofigen = Dofigen {
896 builders: HashMap::from([
897 (
898 "install-deps".to_string(),
899 Stage {
900 from: FromContext::FromImage(ImageName {
901 path: "php".to_string(),
902 version: Some(ImageVersion::Tag("8.3-fpm-alpine".to_string())),
903 ..Default::default()
904 }),
905 run: Run {
906 run: vec!["echo coucou".to_string()],
907 ..Default::default()
908 },
909 ..Default::default()
910 },
911 ),
912 (
913 "install-php-ext".to_string(),
914 Stage {
915 from: FromContext::FromBuilder("install-deps".to_string()),
916 run: Run {
917 run: vec!["echo coucou".to_string()],
918 ..Default::default()
919 },
920 ..Default::default()
921 },
922 ),
923 (
924 "get-composer".to_string(),
925 Stage {
926 from: FromContext::FromImage(ImageName {
927 path: "composer".to_string(),
928 version: Some(ImageVersion::Tag("latest".to_string())),
929 ..Default::default()
930 }),
931 run: Run {
932 run: vec!["echo coucou".to_string()],
933 ..Default::default()
934 },
935 ..Default::default()
936 },
937 ),
938 ]),
939 stage: Stage {
940 from: FromContext::FromBuilder("install-php-ext".to_string()),
941 copy: vec![CopyResource::Copy(Copy {
942 from: FromContext::FromBuilder("get-composer".to_string()),
943 paths: vec!["/usr/bin/composer".to_string()],
944 options: CopyOptions {
945 target: Some("/bin/".to_string()),
946 ..Default::default()
947 },
948 ..Default::default()
949 })],
950 ..Default::default()
951 },
952 ..Default::default()
953 };
954
955 let mut lint_session = LintSession::analyze(&dofigen);
956
957 let mut dependencies =
958 lint_session.get_stage_recursive_dependencies("install-deps".into());
959 dependencies.sort();
960 assert_eq_sorted!(dependencies, Vec::<String>::new());
961
962 dependencies = lint_session.get_stage_recursive_dependencies("install-php-ext".into());
963 assert_eq_sorted!(dependencies, vec!["install-deps"]);
964
965 dependencies = lint_session.get_stage_recursive_dependencies("get-composer".into());
966 assert_eq_sorted!(dependencies, Vec::<String>::new());
967
968 dependencies = lint_session.get_stage_recursive_dependencies("runtime".into());
969 dependencies.sort();
970 assert_eq_sorted!(
971 dependencies,
972 vec!["get-composer", "install-deps", "install-php-ext"]
973 );
974
975 let mut builders = lint_session.get_sorted_builders();
976 builders.sort();
977
978 assert_eq_sorted!(
979 builders,
980 vec!["get-composer", "install-deps", "install-php-ext"]
981 );
982
983 assert_eq_sorted!(lint_session.messages, vec![]);
984 }
985 }
986
987 mod builder {
988 use super::*;
989
990 #[test]
991 fn empty() {
992 let dofigen = Dofigen {
993 builders: HashMap::from([(
994 "builder".into(),
995 Stage {
996 from: FromContext::FromImage(ImageName {
997 path: "php".into(),
998 ..Default::default()
999 }),
1000 ..Default::default()
1001 },
1002 )]),
1003 stage: Stage {
1004 from: FromContext::FromBuilder("builder".into()),
1005 ..Default::default()
1006 },
1007 ..Default::default()
1008 };
1009
1010 let lint_session = LintSession::analyze(&dofigen);
1011
1012 assert_eq_sorted!(
1013 lint_session.messages,
1014 vec![LintMessage {
1015 level: MessageLevel::Warn,
1016 path: vec!["builders".into(), "builder".into()],
1017 message: "The builder 'builder' is empty and should be removed".into(),
1018 },]
1019 );
1020 }
1021
1022 #[test]
1023 fn unused() {
1024 let dofigen = Dofigen {
1025 builders: HashMap::from([(
1026 "builder".into(),
1027 Stage {
1028 from: FromContext::FromImage(ImageName {
1029 ..Default::default()
1030 }),
1031 run: Run {
1032 run: vec!["echo Hello".into()],
1033 ..Default::default()
1034 },
1035 ..Default::default()
1036 },
1037 )]),
1038 ..Default::default()
1039 };
1040
1041 let lint_session = LintSession::analyze(&dofigen);
1042
1043 assert_eq_sorted!(
1044 lint_session.messages,
1045 vec![LintMessage {
1046 level: MessageLevel::Warn,
1047 path: vec!["builders".into(), "builder".into()],
1048 message: "The builder 'builder' is not used and should be removed".into(),
1049 },]
1050 );
1051 }
1052 }
1053
1054 mod user {
1055 use super::*;
1056
1057 #[test]
1058 fn uid() {
1059 let dofigen = Dofigen {
1060 stage: Stage {
1061 user: Some(User::new("1000")),
1062 ..Default::default()
1063 },
1064 ..Default::default()
1065 };
1066
1067 let lint_session = LintSession::analyze(&dofigen);
1068
1069 assert_eq_sorted!(lint_session.messages, vec![]);
1070 }
1071
1072 #[test]
1073 fn username() {
1074 let dofigen = Dofigen {
1075 stage: Stage {
1076 user: Some(User::new("test")),
1077 ..Default::default()
1078 },
1079 ..Default::default()
1080 };
1081
1082 let lint_session = LintSession::analyze(&dofigen);
1083
1084 assert_eq_sorted!(
1085 lint_session.messages,
1086 vec![LintMessage {
1087 level: MessageLevel::Warn,
1088 path: vec!["user".into()],
1089 message: "UID should be used instead of username".into(),
1090 },]
1091 );
1092 }
1093 }
1094
1095 mod from_context {
1096 use super::*;
1097
1098 #[test]
1099 fn stage_and_copy() {
1100 let dofigen = Dofigen {
1101 stage: Stage {
1102 from: FromContext::FromContext(Some("php:8.3-fpm-alpine".into())),
1103 copy: vec![CopyResource::Copy(Copy {
1104 from: FromContext::FromContext(Some("composer:latest".into())),
1105 paths: vec!["/usr/bin/composer".into()],
1106 ..Default::default()
1107 })],
1108 ..Default::default()
1109 },
1110 ..Default::default()
1111 };
1112
1113 let lint_session = LintSession::analyze(&dofigen);
1114
1115 assert_eq_sorted!(lint_session.messages, vec![
1116 LintMessage {
1117 level: MessageLevel::Warn,
1118 path: vec!["fromContext".into()],
1119 message: "Prefer to use fromImage and fromBuilder instead of fromContext".into(),
1120 },
1121 LintMessage {
1122 level: MessageLevel::Warn,
1123 path: vec!["copy".into(), "0".into(), "fromContext".into()],
1124 message: "Prefer to use fromImage and fromBuilder instead of fromContext (unless it's really from a build context: https://docs.docker.com/reference/cli/docker/buildx/build/#build-context)".into(),
1125 }
1126 ]);
1127 }
1128
1129 #[test]
1130 fn root_bind() {
1131 let dofigen = Dofigen {
1132 builders: HashMap::from([(
1133 "builder".into(),
1134 Stage {
1135 root: Some(Run {
1136 bind: vec![Bind {
1137 from: FromContext::FromContext(Some("builder".into())),
1138 source: Some("/path/to/bind".into()),
1139 target: "/path/to/target".into(),
1140 ..Default::default()
1141 }],
1142 run: vec!["echo Hello".into()],
1143 ..Default::default()
1144 }),
1145 ..Default::default()
1146 },
1147 )]),
1148 stage: Stage {
1149 from: FromContext::FromBuilder("builder".into()),
1150 ..Default::default()
1151 },
1152 ..Default::default()
1153 };
1154
1155 let lint_session = LintSession::analyze(&dofigen);
1156
1157 assert_eq_sorted!(lint_session.messages, vec![
1158 LintMessage {
1159 level: MessageLevel::Warn,
1160 path: vec![
1161 "builders".into(),
1162 "builder".into(),
1163 "root".into(),
1164 "bind".into(),
1165 "0".into(),
1166 "fromContext".into(),
1167 ],
1168 message: "Prefer to use fromImage and fromBuilder instead of fromContext (unless it's really from a build context: https://docs.docker.com/reference/cli/docker/buildx/build/#build-context)".into(),
1169 }
1170 ]);
1171 }
1172 }
1173
1174 mod run {
1175 use super::*;
1176
1177 #[test]
1178 fn empty_run() {
1179 let dofigen = Dofigen {
1180 stage: Stage {
1181 run: Run {
1182 bind: vec![Bind {
1183 source: Some("/path/to/bind".into()),
1184 target: "/path/to/target".into(),
1185 ..Default::default()
1186 }],
1187 cache: vec![Cache {
1188 source: Some("/path/to/cache".into()),
1189 target: "/path/to/target".into(),
1190 ..Default::default()
1191 }],
1192 ..Default::default()
1193 },
1194 ..Default::default()
1195 },
1196 ..Default::default()
1197 };
1198
1199 let lint_session = LintSession::analyze(&dofigen);
1200
1201 assert_eq_sorted!(
1202 lint_session.messages,
1203 vec![
1204 LintMessage {
1205 level: MessageLevel::Warn,
1206 message: "The run list is empty but there are bind definitions".into(),
1207 path: vec!["bind".into()],
1208 },
1209 LintMessage {
1210 level: MessageLevel::Warn,
1211 message: "The run list is empty but there are cache definitions".into(),
1212 path: vec!["cache".into()],
1213 },
1214 ]
1215 );
1216 }
1217 }
1218}