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: HashMap<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 let mut sorted: Vec<String> = vec![];
354
355 loop {
356 let mut part: Vec<String> = stages
357 .extract_if(|_name, deps| deps.is_empty())
358 .map(|(name, _deps)| name)
359 .collect();
360
361 if part.is_empty() {
362 break;
364 }
365
366 part.sort();
367
368 for name in part.iter() {
369 for deps in stages.values_mut() {
370 deps.retain(|dep| dep != name);
371 }
372 }
373
374 sorted.append(&mut part);
375
376 if stages.is_empty() {
377 break;
378 }
379 }
380
381 sorted
382 .into_iter()
383 .filter(|name| name != "runtime")
384 .collect()
385 }
386
387 pub fn get_stage_recursive_dependencies(&mut self, stage: String) -> Vec<String> {
388 self.resolve_stage_recursive_dependencies(&mut vec![stage])
389 }
390
391 fn resolve_stage_recursive_dependencies(&mut self, path: &mut Vec<String>) -> Vec<String> {
392 let stage = &path.last().expect("The path is empty").clone();
393 if let Some(dependencies) = self.recursive_stage_dependencies.get(stage) {
394 return dependencies.clone();
395 }
396 let mut deps = HashSet::new();
397 let dependencies = self
398 .stage_infos
399 .get(stage)
400 .expect(format!("The stage info not found for stage '{}'", stage).as_str())
401 .dependencies
402 .clone();
403 for dependency in dependencies {
404 let dep_stage = &dependency.stage;
405 if path.contains(dep_stage) {
406 self.messages.push(LintMessage {
407 level: MessageLevel::Error,
408 message: format!(
409 "Circular dependency detected: {} -> {}",
410 path.join(" -> "),
411 dependency.stage
412 ),
413 path: dependency.origin.clone(),
414 });
415 continue;
416 }
417 deps.insert(dep_stage.clone());
418 if self.stage_infos.contains_key(dep_stage) {
419 path.push(dep_stage.clone());
420 deps.extend(self.resolve_stage_recursive_dependencies(path));
421 path.pop();
422 } }
424 let deps: Vec<String> = deps.into_iter().collect();
425 self.recursive_stage_dependencies
426 .insert(stage.clone(), deps.clone());
427 deps
428 }
429
430 fn check_dependencies(&mut self) {
432 let dependencies = self
433 .stage_infos
434 .values()
435 .flat_map(|info| info.dependencies.clone())
436 .collect::<Vec<_>>();
437
438 let caches = self
439 .stage_infos
440 .iter()
441 .map(|(name, info)| (name.clone(), info.cache_paths.clone()))
442 .collect::<HashMap<_, _>>();
443
444 let used_builders = dependencies
446 .iter()
447 .map(|dep| dep.stage.clone())
448 .collect::<HashSet<_>>();
449
450 let unused_builders = self
451 .stage_infos
452 .keys()
453 .filter(|name| name != &"runtime")
454 .map(|name| name.clone())
455 .filter(|name| !used_builders.contains(name))
456 .collect::<HashSet<_>>();
457
458 linter_path!(self, "builders".into(), {
459 for builder in unused_builders {
460 linter_path!(self, builder.clone(), {
461 self.add_message(
462 MessageLevel::Warn,
463 format!(
464 "The builder '{}' is not used and should be removed",
465 builder
466 ),
467 );
468 });
469 }
470 });
471
472 for dependency in dependencies {
473 if let Some(paths) = caches.get(&dependency.stage) {
474 paths
475 .iter()
476 .filter(|path| dependency.path.starts_with(*path))
477 .for_each(|path| {
478 self.messages.push(LintMessage {
479 level: MessageLevel::Error,
480 message: format!(
481 "Use of the '{}' builder cache path '{}'",
482 dependency.stage, path
483 ),
484 path: dependency.origin.clone(),
485 });
486 });
487 } else {
488 self.messages.push(LintMessage {
489 level: MessageLevel::Error,
490 message: format!("The builder '{}' not found", dependency.stage),
491 path: dependency.origin.clone(),
492 });
493 }
494 }
495 }
496
497 fn get_stage_cache_paths(&mut self, stage: &Stage) -> Vec<String> {
498 let mut paths = vec![];
499 paths.append(&mut self.get_run_cache_paths(
500 &stage.run,
501 &self.current_path.clone(),
502 &stage.workdir,
503 ));
504 if let Some(root) = &stage.root {
505 paths.append(&mut self.get_run_cache_paths(
506 root,
507 &[self.current_path.clone(), vec!["root".into()]].concat(),
508 &stage.workdir,
509 ));
510 }
511 paths
512 }
513
514 fn get_run_cache_paths(
515 &mut self,
516 run: &Run,
517 path: &Vec<String>,
518 workdir: &Option<String>,
519 ) -> Vec<String> {
520 let mut cache_paths = vec![];
521 for (position, cache) in run.cache.iter().enumerate() {
522 let target = cache.target.clone();
523 cache_paths.push(if target.starts_with("/") {
524 target.clone()
525 } else {
526 if let Some(workdir) = workdir {
527 format!("{}/{}", workdir, target)
528 }
529 else {
530 self.messages.push(LintMessage {
531 level: MessageLevel::Warn,
532 message: "The cache target should be absolute or a workdir should be defined in the stage".to_string(),
533 path: [path.clone(), vec!["cache".into(), position.to_string()]].concat(),
534 });
535 target.clone()
536 }
537 });
538 }
539 cache_paths
540 }
541
542 pub fn analyze(dofigen: &Dofigen) -> Self {
546 let mut session = Self::default();
547 dofigen.analyze(&mut session);
548
549 session
550 }
551}
552
553#[derive(Debug, Clone, PartialEq)]
554pub struct StageLintInfo {
555 dependencies: Vec<StageDependency>,
556 cache_paths: Vec<String>,
557}
558
559#[derive(Debug, Clone, PartialEq)]
560pub struct LintMessage {
561 pub level: MessageLevel,
562 pub path: Vec<String>,
563 pub message: String,
564}
565
566#[derive(Debug, Clone, PartialEq)]
567pub enum MessageLevel {
568 Warn,
569 Error,
570}
571
572#[cfg(test)]
573mod test {
574 use crate::Dofigen;
575
576 use super::*;
577 use pretty_assertions_sorted::assert_eq_sorted;
578
579 mod stage_dependencies {
580 use super::*;
581
582 #[test]
583 fn builders_dependencies() {
584 let dofigen = Dofigen {
585 builders: HashMap::from([
586 (
587 "builder1".into(),
588 Stage {
589 copy: vec![CopyResource::Copy(Copy {
590 from: FromContext::FromBuilder("builder2".into()),
591 paths: vec!["/path/to/copy".into()],
592 options: Default::default(),
593 ..Default::default()
594 })],
595 ..Default::default()
596 },
597 ),
598 (
599 "builder2".into(),
600 Stage {
601 copy: vec![CopyResource::Copy(Copy {
602 from: FromContext::FromBuilder("builder3".into()),
603 paths: vec!["/path/to/copy".into()],
604 options: Default::default(),
605 ..Default::default()
606 })],
607 ..Default::default()
608 },
609 ),
610 (
611 "builder3".into(),
612 Stage {
613 run: Run {
614 run: vec!["echo Hello".into()].into(),
615 ..Default::default()
616 },
617 ..Default::default()
618 },
619 ),
620 ]),
621 stage: Stage {
622 copy: vec![CopyResource::Copy(Copy {
623 from: FromContext::FromBuilder("builder1".into()),
624 paths: vec!["/path/to/copy".into()],
625 ..Default::default()
626 })],
627 ..Default::default()
628 },
629 ..Default::default()
630 };
631
632 let mut lint_session = LintSession::analyze(&dofigen);
633
634 let mut dependencies = lint_session.get_stage_recursive_dependencies("runtime".into());
635 dependencies.sort();
636 assert_eq_sorted!(dependencies, vec!["builder1", "builder2", "builder3"]);
637
638 dependencies = lint_session.get_stage_recursive_dependencies("builder1".into());
639 dependencies.sort();
640 assert_eq_sorted!(dependencies, vec!["builder2", "builder3"]);
641
642 dependencies = lint_session.get_stage_recursive_dependencies("builder2".into());
643 assert_eq_sorted!(dependencies, vec!["builder3"]);
644
645 dependencies = lint_session.get_stage_recursive_dependencies("builder3".into());
646 assert_eq_sorted!(dependencies, Vec::<String>::new());
647
648 let builders = lint_session.get_sorted_builders();
649
650 assert_eq_sorted!(builders, vec!["builder3", "builder2", "builder1",]);
651
652 assert_eq_sorted!(lint_session.messages, vec![]);
653 }
654
655 #[test]
656 fn builders_circular_dependencies() {
657 let dofigen = Dofigen {
658 builders: HashMap::from([
659 (
660 "builder1".into(),
661 Stage {
662 copy: vec![CopyResource::Copy(Copy {
663 from: FromContext::FromBuilder("builder2".into()),
664 paths: vec!["/path/to/copy".into()],
665 options: Default::default(),
666 ..Default::default()
667 })],
668 ..Default::default()
669 },
670 ),
671 (
672 "builder2".into(),
673 Stage {
674 copy: vec![CopyResource::Copy(Copy {
675 from: FromContext::FromBuilder("builder3".into()),
676 paths: vec!["/path/to/copy".into()],
677 options: Default::default(),
678 ..Default::default()
679 })],
680 ..Default::default()
681 },
682 ),
683 (
684 "builder3".into(),
685 Stage {
686 copy: vec![CopyResource::Copy(Copy {
687 from: FromContext::FromBuilder("builder1".into()),
688 paths: vec!["/path/to/copy".into()],
689 options: Default::default(),
690 ..Default::default()
691 })],
692 ..Default::default()
693 },
694 ),
695 ]),
696 ..Default::default()
697 };
698
699 let mut lint_session = LintSession::analyze(&dofigen);
700
701 let mut dependencies = lint_session.get_stage_recursive_dependencies("runtime".into());
702 dependencies.sort();
703 assert_eq_sorted!(dependencies, Vec::<String>::new());
704
705 dependencies = lint_session.get_stage_recursive_dependencies("builder1".into());
706 dependencies.sort();
707 assert_eq_sorted!(dependencies, vec!["builder2", "builder3"]);
708
709 dependencies = lint_session.get_stage_recursive_dependencies("builder2".into());
710 assert_eq_sorted!(dependencies, vec!["builder3"]);
711
712 dependencies = lint_session.get_stage_recursive_dependencies("builder3".into());
713 assert_eq_sorted!(dependencies, Vec::<String>::new());
714
715 let mut builders = lint_session.get_sorted_builders();
716 builders.sort();
717
718 assert_eq_sorted!(builders, vec!["builder1", "builder2", "builder3"]);
719
720 assert_eq_sorted!(
721 lint_session.messages,
722 vec![LintMessage {
723 level: MessageLevel::Error,
724 path: vec![
725 "builders".into(),
726 "builder3".into(),
727 "copy".into(),
728 "0".into(),
729 ],
730 message:
731 "Circular dependency detected: builder1 -> builder2 -> builder3 -> builder1"
732 .into(),
733 },]
734 );
735 }
736
737 #[test]
738 fn builder_named_runtime() {
739 let dofigen = Dofigen {
740 builders: HashMap::from([(
741 "runtime".into(),
742 Stage {
743 run: Run {
744 run: vec!["echo Hello".into()].into(),
745 ..Default::default()
746 },
747 ..Default::default()
748 },
749 )]),
750 ..Default::default()
751 };
752
753 let mut lint_session = LintSession::analyze(&dofigen);
754
755 let mut builders = lint_session.get_sorted_builders();
756 builders.sort();
757
758 assert_eq_sorted!(builders, Vec::<String>::new());
759
760 assert_eq_sorted!(
761 lint_session.messages,
762 vec![LintMessage {
763 level: MessageLevel::Error,
764 path: vec!["builders".into(), "runtime".into(),],
765 message: "The builder name 'runtime' is reserved".into(),
766 },]
767 );
768 }
769
770 #[test]
771 fn builder_not_found() {
772 let dofigen = Dofigen {
773 stage: Stage {
774 from: FromContext::FromBuilder("builder1".into()),
775 ..Default::default()
776 },
777 ..Default::default()
778 };
779
780 let mut lint_session = LintSession::analyze(&dofigen);
781
782 let builders = lint_session.get_sorted_builders();
783
784 assert_eq_sorted!(builders, Vec::<String>::new());
785
786 assert_eq_sorted!(
787 lint_session.messages,
788 vec![LintMessage {
789 level: MessageLevel::Error,
790 path: vec!["from".into(),],
791 message: "The builder 'builder1' not found".into(),
792 },]
793 );
794 }
795
796 #[test]
797 fn dependency_to_runtime() {
798 let dofigen = Dofigen {
799 builders: HashMap::from([(
800 "builder".into(),
801 Stage {
802 copy: vec![CopyResource::Copy(Copy {
803 from: FromContext::FromBuilder("runtime".into()),
804 paths: vec!["/path/to/copy".into()],
805 ..Default::default()
806 })],
807 ..Default::default()
808 },
809 )]),
810 stage: Stage {
811 run: Run {
812 run: vec!["echo Hello".into()].into(),
813 ..Default::default()
814 },
815 ..Default::default()
816 },
817 ..Default::default()
818 };
819
820 let mut lint_session = LintSession::analyze(&dofigen);
821
822 let mut builders = lint_session.get_sorted_builders();
823 builders.sort();
824
825 assert_eq_sorted!(builders, vec!["builder"]);
826
827 assert_eq_sorted!(
828 lint_session.messages,
829 vec![
830 LintMessage {
831 level: MessageLevel::Error,
832 path: vec![
833 "builders".into(),
834 "builder".into(),
835 "copy".into(),
836 "0".into()
837 ],
838 message: "The stage 'builder' can't depend on the 'runtime'".into(),
839 },
840 LintMessage {
841 level: MessageLevel::Warn,
842 path: vec!["builders".into(), "builder".into(),],
843 message: "The builder 'builder' is not used and should be removed".into(),
844 }
845 ]
846 );
847 }
848
849 #[test]
850 fn dependency_to_cache_path() {
851 let dofigen = Dofigen {
852 builders: HashMap::from([
853 (
854 "builder1".into(),
855 Stage {
856 run: Run {
857 run: vec!["echo Hello".into()].into(),
858 cache: vec![Cache {
859 target: "/path/to/cache".into(),
860 ..Default::default()
861 }],
862 ..Default::default()
863 },
864 ..Default::default()
865 },
866 ),
867 (
868 "builder2".into(),
869 Stage {
870 copy: vec![CopyResource::Copy(Copy {
871 from: FromContext::FromBuilder("builder1".into()),
872 paths: vec!["/path/to/cache/test".into()],
873 ..Default::default()
874 })],
875 ..Default::default()
876 },
877 ),
878 ]),
879 stage: Stage {
880 from: FromContext::FromBuilder("builder2".into()),
881 ..Default::default()
882 },
883 ..Default::default()
884 };
885
886 let mut lint_session = LintSession::analyze(&dofigen);
887
888 let mut builders = lint_session.get_sorted_builders();
889 builders.sort();
890
891 assert_eq_sorted!(builders, vec!["builder1", "builder2"]);
892
893 assert_eq_sorted!(
894 lint_session.messages,
895 vec![LintMessage {
896 level: MessageLevel::Error,
897 path: vec![
898 "builders".into(),
899 "builder2".into(),
900 "copy".into(),
901 "0".into()
902 ],
903 message: "Use of the 'builder1' builder cache path '/path/to/cache'".into(),
904 },]
905 );
906 }
907
908 #[test]
909 fn runtime_dependencies() {
910 let dofigen = Dofigen {
911 builders: HashMap::from([
912 (
913 "install-deps".to_string(),
914 Stage {
915 from: FromContext::FromImage(ImageName {
916 path: "php".to_string(),
917 version: Some(ImageVersion::Tag("8.3-fpm-alpine".to_string())),
918 ..Default::default()
919 }),
920 run: Run {
921 run: vec!["echo coucou".to_string()],
922 ..Default::default()
923 },
924 ..Default::default()
925 },
926 ),
927 (
928 "install-php-ext".to_string(),
929 Stage {
930 from: FromContext::FromBuilder("install-deps".to_string()),
931 run: Run {
932 run: vec!["echo coucou".to_string()],
933 ..Default::default()
934 },
935 ..Default::default()
936 },
937 ),
938 (
939 "get-composer".to_string(),
940 Stage {
941 from: FromContext::FromImage(ImageName {
942 path: "composer".to_string(),
943 version: Some(ImageVersion::Tag("latest".to_string())),
944 ..Default::default()
945 }),
946 run: Run {
947 run: vec!["echo coucou".to_string()],
948 ..Default::default()
949 },
950 ..Default::default()
951 },
952 ),
953 ]),
954 stage: Stage {
955 from: FromContext::FromBuilder("install-php-ext".to_string()),
956 copy: vec![CopyResource::Copy(Copy {
957 from: FromContext::FromBuilder("get-composer".to_string()),
958 paths: vec!["/usr/bin/composer".to_string()],
959 options: CopyOptions {
960 target: Some("/bin/".to_string()),
961 ..Default::default()
962 },
963 ..Default::default()
964 })],
965 ..Default::default()
966 },
967 ..Default::default()
968 };
969
970 let mut lint_session = LintSession::analyze(&dofigen);
971
972 let mut dependencies =
973 lint_session.get_stage_recursive_dependencies("install-deps".into());
974 dependencies.sort();
975 assert_eq_sorted!(dependencies, Vec::<String>::new());
976
977 dependencies = lint_session.get_stage_recursive_dependencies("install-php-ext".into());
978 assert_eq_sorted!(dependencies, vec!["install-deps"]);
979
980 dependencies = lint_session.get_stage_recursive_dependencies("get-composer".into());
981 assert_eq_sorted!(dependencies, Vec::<String>::new());
982
983 dependencies = lint_session.get_stage_recursive_dependencies("runtime".into());
984 dependencies.sort();
985 assert_eq_sorted!(
986 dependencies,
987 vec!["get-composer", "install-deps", "install-php-ext"]
988 );
989
990 let mut builders = lint_session.get_sorted_builders();
991 builders.sort();
992
993 assert_eq_sorted!(
994 builders,
995 vec!["get-composer", "install-deps", "install-php-ext"]
996 );
997
998 assert_eq_sorted!(lint_session.messages, vec![]);
999 }
1000 }
1001
1002 mod builder {
1003 use super::*;
1004
1005 #[test]
1006 fn empty() {
1007 let dofigen = Dofigen {
1008 builders: HashMap::from([(
1009 "builder".into(),
1010 Stage {
1011 from: FromContext::FromImage(ImageName {
1012 path: "php".into(),
1013 ..Default::default()
1014 }),
1015 ..Default::default()
1016 },
1017 )]),
1018 stage: Stage {
1019 from: FromContext::FromBuilder("builder".into()),
1020 ..Default::default()
1021 },
1022 ..Default::default()
1023 };
1024
1025 let lint_session = LintSession::analyze(&dofigen);
1026
1027 assert_eq_sorted!(
1028 lint_session.messages,
1029 vec![LintMessage {
1030 level: MessageLevel::Warn,
1031 path: vec!["builders".into(), "builder".into()],
1032 message: "The builder 'builder' is empty and should be removed".into(),
1033 },]
1034 );
1035 }
1036
1037 #[test]
1038 fn unused() {
1039 let dofigen = Dofigen {
1040 builders: HashMap::from([(
1041 "builder".into(),
1042 Stage {
1043 from: FromContext::FromImage(ImageName {
1044 ..Default::default()
1045 }),
1046 run: Run {
1047 run: vec!["echo Hello".into()],
1048 ..Default::default()
1049 },
1050 ..Default::default()
1051 },
1052 )]),
1053 ..Default::default()
1054 };
1055
1056 let lint_session = LintSession::analyze(&dofigen);
1057
1058 assert_eq_sorted!(
1059 lint_session.messages,
1060 vec![LintMessage {
1061 level: MessageLevel::Warn,
1062 path: vec!["builders".into(), "builder".into()],
1063 message: "The builder 'builder' is not used and should be removed".into(),
1064 },]
1065 );
1066 }
1067 }
1068
1069 mod user {
1070 use super::*;
1071
1072 #[test]
1073 fn uid() {
1074 let dofigen = Dofigen {
1075 stage: Stage {
1076 user: Some(User::new("1000")),
1077 ..Default::default()
1078 },
1079 ..Default::default()
1080 };
1081
1082 let lint_session = LintSession::analyze(&dofigen);
1083
1084 assert_eq_sorted!(lint_session.messages, vec![]);
1085 }
1086
1087 #[test]
1088 fn username() {
1089 let dofigen = Dofigen {
1090 stage: Stage {
1091 user: Some(User::new("test")),
1092 ..Default::default()
1093 },
1094 ..Default::default()
1095 };
1096
1097 let lint_session = LintSession::analyze(&dofigen);
1098
1099 assert_eq_sorted!(
1100 lint_session.messages,
1101 vec![LintMessage {
1102 level: MessageLevel::Warn,
1103 path: vec!["user".into()],
1104 message: "UID should be used instead of username".into(),
1105 },]
1106 );
1107 }
1108 }
1109
1110 mod from_context {
1111 use super::*;
1112
1113 #[test]
1114 fn stage_and_copy() {
1115 let dofigen = Dofigen {
1116 stage: Stage {
1117 from: FromContext::FromContext(Some("php:8.3-fpm-alpine".into())),
1118 copy: vec![CopyResource::Copy(Copy {
1119 from: FromContext::FromContext(Some("composer:latest".into())),
1120 paths: vec!["/usr/bin/composer".into()],
1121 ..Default::default()
1122 })],
1123 ..Default::default()
1124 },
1125 ..Default::default()
1126 };
1127
1128 let lint_session = LintSession::analyze(&dofigen);
1129
1130 assert_eq_sorted!(lint_session.messages, vec![
1131 LintMessage {
1132 level: MessageLevel::Warn,
1133 path: vec!["fromContext".into()],
1134 message: "Prefer to use fromImage and fromBuilder instead of fromContext".into(),
1135 },
1136 LintMessage {
1137 level: MessageLevel::Warn,
1138 path: vec!["copy".into(), "0".into(), "fromContext".into()],
1139 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(),
1140 }
1141 ]);
1142 }
1143
1144 #[test]
1145 fn root_bind() {
1146 let dofigen = Dofigen {
1147 builders: HashMap::from([(
1148 "builder".into(),
1149 Stage {
1150 root: Some(Run {
1151 bind: vec![Bind {
1152 from: FromContext::FromContext(Some("builder".into())),
1153 source: Some("/path/to/bind".into()),
1154 target: "/path/to/target".into(),
1155 ..Default::default()
1156 }],
1157 run: vec!["echo Hello".into()],
1158 ..Default::default()
1159 }),
1160 ..Default::default()
1161 },
1162 )]),
1163 stage: Stage {
1164 from: FromContext::FromBuilder("builder".into()),
1165 ..Default::default()
1166 },
1167 ..Default::default()
1168 };
1169
1170 let lint_session = LintSession::analyze(&dofigen);
1171
1172 assert_eq_sorted!(lint_session.messages, vec![
1173 LintMessage {
1174 level: MessageLevel::Warn,
1175 path: vec![
1176 "builders".into(),
1177 "builder".into(),
1178 "root".into(),
1179 "bind".into(),
1180 "0".into(),
1181 "fromContext".into(),
1182 ],
1183 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(),
1184 }
1185 ]);
1186 }
1187 }
1188
1189 mod run {
1190 use super::*;
1191
1192 #[test]
1193 fn empty_run() {
1194 let dofigen = Dofigen {
1195 stage: Stage {
1196 run: Run {
1197 bind: vec![Bind {
1198 source: Some("/path/to/bind".into()),
1199 target: "/path/to/target".into(),
1200 ..Default::default()
1201 }],
1202 cache: vec![Cache {
1203 source: Some("/path/to/cache".into()),
1204 target: "/path/to/target".into(),
1205 ..Default::default()
1206 }],
1207 ..Default::default()
1208 },
1209 ..Default::default()
1210 },
1211 ..Default::default()
1212 };
1213
1214 let lint_session = LintSession::analyze(&dofigen);
1215
1216 assert_eq_sorted!(
1217 lint_session.messages,
1218 vec![
1219 LintMessage {
1220 level: MessageLevel::Warn,
1221 message: "The run list is empty but there are bind definitions".into(),
1222 path: vec!["bind".into()],
1223 },
1224 LintMessage {
1225 level: MessageLevel::Warn,
1226 message: "The run list is empty but there are cache definitions".into(),
1227 path: vec!["cache".into()],
1228 },
1229 ]
1230 );
1231 }
1232 }
1233}