ferro_cli/doctor/checks/
copy_dirs_dockerignore_collision.rs1use crate::doctor::check::{CheckCategory, CheckResult, DoctorCheck};
5use crate::project::read_deploy_metadata;
6use std::fs;
7use std::path::Path;
8
9pub struct CopyDirsDockerignoreCollisionCheck;
10
11const NAME: &str = "copy_dirs_dockerignore_collision";
12
13impl DoctorCheck for CopyDirsDockerignoreCollisionCheck {
14 fn name(&self) -> &'static str {
15 NAME
16 }
17 fn run(&self, root: &Path) -> CheckResult {
18 check_impl(root)
19 }
20 fn category(&self) -> CheckCategory {
21 CheckCategory::Deploy
22 }
23}
24
25pub(crate) fn check_impl(root: &Path) -> CheckResult {
26 let dockerignore = root.join(".dockerignore");
27 if !dockerignore.is_file() {
28 return CheckResult::ok(NAME, "skipped (.dockerignore absent)");
29 }
30 let metadata = match read_deploy_metadata(root) {
31 Ok(m) => m,
32 Err(_) => return CheckResult::ok(NAME, "skipped (deploy metadata absent)"),
33 };
34 if metadata.copy_dirs.is_empty() {
35 return CheckResult::ok(NAME, "no copy_dirs declared");
36 }
37 let ignore_content = match fs::read_to_string(&dockerignore) {
38 Ok(s) => s,
39 Err(e) => return CheckResult::error(NAME, format!("failed to read .dockerignore: {e}")),
40 };
41 let ignore_lines: Vec<&str> = ignore_content
42 .lines()
43 .map(str::trim)
44 .filter(|l| !l.is_empty() && !l.starts_with('#') && !l.starts_with('!'))
45 .collect();
46
47 let mut collisions: Vec<String> = Vec::new();
48 for entry in &metadata.copy_dirs {
49 let entry_head = entry.split('/').next().unwrap_or(entry);
50 for line in &ignore_lines {
51 let stripped = line.trim_end_matches('/');
52 if stripped == entry_head || stripped == entry.as_str() {
53 collisions.push(format!("'{entry}' excluded by .dockerignore rule '{line}'"));
54 break;
55 }
56 }
57 }
58
59 if collisions.is_empty() {
60 CheckResult::ok(NAME, "copy_dirs entries not excluded by .dockerignore")
61 } else {
62 CheckResult::error(
63 NAME,
64 format!(
65 "{} copy_dirs entries collide with .dockerignore",
66 collisions.len()
67 ),
68 )
69 .with_details(collisions.join("; "))
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::doctor::check::CheckStatus;
77 use tempfile::TempDir;
78
79 fn write(p: &Path, body: &str) {
80 if let Some(parent) = p.parent() {
81 fs::create_dir_all(parent).unwrap();
82 }
83 fs::write(p, body).unwrap();
84 }
85
86 #[test]
87 fn name_and_category() {
88 assert_eq!(CopyDirsDockerignoreCollisionCheck.name(), NAME);
89 assert_eq!(
90 CopyDirsDockerignoreCollisionCheck.category(),
91 CheckCategory::Deploy
92 );
93 }
94
95 #[test]
96 fn skipped_when_dockerignore_absent() {
97 let td = TempDir::new().unwrap();
98 write(
99 &td.path().join("Cargo.toml"),
100 "[package]\nname=\"x\"\nversion=\"0.1.0\"\n[package.metadata.ferro.deploy]\ncopy_dirs=[\"data\"]\n",
101 );
102 let r = check_impl(td.path());
103 assert_eq!(r.status, CheckStatus::Ok);
104 assert!(r.message.contains("skipped"));
105 }
106
107 #[test]
108 fn errors_when_copy_dir_collides() {
109 let td = TempDir::new().unwrap();
110 write(
111 &td.path().join("Cargo.toml"),
112 "[package]\nname=\"x\"\nversion=\"0.1.0\"\n[package.metadata.ferro.deploy]\ncopy_dirs=[\"data\"]\n",
113 );
114 write(
115 &td.path().join(".dockerignore"),
116 "# comment\ntarget/\ndata/\n*.log\n",
117 );
118 let r = check_impl(td.path());
119 assert_eq!(r.status, CheckStatus::Error);
120 assert!(r.details.as_ref().unwrap().contains("data"));
121 assert!(r.details.as_ref().unwrap().contains("data/"));
122 }
123
124 #[test]
125 fn ok_when_no_collision() {
126 let td = TempDir::new().unwrap();
127 write(
128 &td.path().join("Cargo.toml"),
129 "[package]\nname=\"x\"\nversion=\"0.1.0\"\n[package.metadata.ferro.deploy]\ncopy_dirs=[\"migrations\"]\n",
130 );
131 write(&td.path().join(".dockerignore"), "target/\ndata/\n");
132 let r = check_impl(td.path());
133 assert_eq!(r.status, CheckStatus::Ok);
134 }
135
136 #[test]
137 fn negation_lines_ignored() {
138 let td = TempDir::new().unwrap();
139 write(
140 &td.path().join("Cargo.toml"),
141 "[package]\nname=\"x\"\nversion=\"0.1.0\"\n[package.metadata.ferro.deploy]\ncopy_dirs=[\"data\"]\n",
142 );
143 write(&td.path().join(".dockerignore"), "!data/\n");
144 let r = check_impl(td.path());
145 assert_eq!(r.status, CheckStatus::Ok);
146 }
147}