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