xianyun_deploy/deployer/helm/
chartresolve.rs1use std::path::Path;
4
5use tracing::{debug, info, warn};
6
7use xianyun_backends::helm::Backend as HelmBackend;
8
9use super::HelmError;
10use super::chartdeps::{chart_repo_name, dependencies_satisfied, locked_repositories};
11use super::manager::Manager;
12
13impl<B: HelmBackend> Manager<B> {
14 pub(super) fn resolve_chart_dependencies(&self, chart_dir: &Path) -> Result<(), HelmError> {
16 let list_output = self
17 .backend
18 .dependency_list(&chart_dir.display().to_string())
19 .map_err(|e| HelmError::Backend {
20 context: "listing chart dependencies".to_string(),
21 source: e,
22 })?;
23
24 if dependencies_satisfied(&list_output) {
25 debug!(chart = %chart_dir.display(), "chart dependencies already satisfied");
26 return Ok(());
27 }
28
29 self.register_locked_repositories(chart_dir);
30
31 info!(chart = %chart_dir.display(), "resolving chart dependencies");
32
33 self.backend
34 .dependency_build(&chart_dir.display().to_string(), true)
35 .map_err(|e| HelmError::Backend {
36 context: format!(
37 "chart {} (run `helm dependency update` there if Chart.lock is out of step \
38 with Chart.yaml, or `helm repo add` any private repository it needs)",
39 chart_dir.display()
40 ),
41 source: e,
42 })
43 }
44
45 fn register_locked_repositories(&self, chart_dir: &Path) {
47 let repos = match locked_repositories(chart_dir) {
48 Ok(repos) => repos,
49 Err(e) => {
50 warn!(chart = %chart_dir.display(), error = %e, "could not read the chart's dependency lock");
51 return;
52 }
53 };
54
55 if repos.is_empty() {
56 return;
57 }
58
59 let existing = match self.get_existing_helm_repos() {
60 Ok(existing) => existing,
61 Err(e) => {
62 warn!(error = %e, "could not read the configured helm repositories");
63 return;
64 }
65 };
66
67 for repo_url in repos {
68 if let Some(registered) = self.find_helm_repo_by_url(&existing, &repo_url) {
69 if let Err(e) = self.backend.repo_update(®istered.name) {
74 warn!(
75 name = %registered.name, url = %repo_url, error = %e,
76 "could not refresh a chart dependency's repository"
77 );
78 }
79
80 continue;
81 }
82
83 let name = chart_repo_name(&repo_url);
84
85 info!(name = %name, url = %repo_url, "registering a chart dependency's repository");
86
87 if let Err(e) = self.backend.repo_add(&name, &repo_url, true) {
90 warn!(
91 name = %name, url = %repo_url, error = %e,
92 "could not register a chart dependency's repository"
93 );
94 }
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use xianyun_executor::exec::ExecError;
102
103 use super::super::chartdeps::chart_repo_name;
104 use super::super::manager::Manager;
105 use super::super::test_support::MockHelmBackend;
106
107 const TEST_REPO_URL: &str = "https://charts.jetstack.io";
108
109 const SATISFIED_LIST: &str =
110 "NAME\tVERSION\tREPOSITORY\tSTATUS\ncert-manager\t1.16.2\thttps://charts.jetstack.io\tok\n";
111 const UNSATISFIED_LIST: &str = "NAME\tVERSION\tREPOSITORY\tSTATUS\ncert-manager\t1.16.2\thttps://charts.jetstack.io\tmissing\n";
112
113 fn lock_with_test_repo() -> tempfile::TempDir {
114 let dir = tempfile::tempdir().unwrap();
115 std::fs::write(
116 dir.path().join("Chart.lock"),
117 format!(
118 "dependencies:\n- name: cert-manager\n repository: {TEST_REPO_URL}\n version: 1.16.2\ndigest: sha256:abc\n"
119 ),
120 )
121 .unwrap();
122 dir
123 }
124
125 fn err_repo_unreachable() -> ExecError {
126 ExecError::RunFailed(
127 "looks like the repository is not a valid chart repository".to_string(),
128 )
129 }
130
131 fn err_lock_out_of_sync() -> ExecError {
132 ExecError::RunFailed("the lock file (Chart.lock) is out of sync".to_string())
133 }
134
135 fn err_helm_missing() -> ExecError {
136 ExecError::RunFailed("exec: helm: executable file not found".to_string())
137 }
138
139 #[test]
141 fn test_resolve_chart_dependencies_satisfied_chart_is_left_alone() {
143 let dir = lock_with_test_repo();
144 let dir_str = dir.path().display().to_string();
145
146 let backend = MockHelmBackend::new().expect_dependency_list(move |chart_dir| {
148 assert_eq!(chart_dir, dir_str);
149 Ok(SATISFIED_LIST.to_string())
150 });
151
152 let mgr = Manager::new(backend);
153
154 mgr.resolve_chart_dependencies(dir.path()).unwrap();
155 }
156
157 #[test]
158 fn test_resolve_chart_dependencies_registers_an_unknown_repository_then_builds() {
160 let dir = lock_with_test_repo();
161 let want_name = chart_repo_name(TEST_REPO_URL);
162
163 let backend = MockHelmBackend::new()
164 .expect_dependency_list(|_| Ok(UNSATISFIED_LIST.to_string()))
165 .expect_repo_list(|| Ok("[]".to_string()))
166 .expect_repo_add(move |name, url, force_update| {
167 assert_eq!(name, want_name);
168 assert_eq!(url, TEST_REPO_URL);
169 assert!(force_update);
170 Ok(())
171 })
172 .expect_dependency_build(|_, skip_refresh| {
174 assert!(skip_refresh);
175 Ok(())
176 });
177
178 let mgr = Manager::new(backend);
179
180 mgr.resolve_chart_dependencies(dir.path()).unwrap();
181 }
182
183 #[test]
184 fn test_resolve_chart_dependencies_refreshes_an_already_registered_repository() {
186 let dir = lock_with_test_repo();
187
188 let backend = MockHelmBackend::new()
191 .expect_dependency_list(|_| Ok(UNSATISFIED_LIST.to_string()))
192 .expect_repo_list(|| {
193 Ok(r#"[{"name":"jetstack","url":"https://charts.jetstack.io/"}]"#.to_string())
194 })
195 .expect_repo_update(|name| {
196 assert_eq!(name, "jetstack");
197 Ok(())
198 })
199 .expect_dependency_build(|_, _| Ok(()));
200
201 let mgr = Manager::new(backend);
202
203 mgr.resolve_chart_dependencies(dir.path()).unwrap();
204 }
205
206 #[test]
207 fn test_resolve_chart_dependencies_preparation_failures_still_reach_the_build() {
209 {
215 let dir = lock_with_test_repo();
216 let backend = MockHelmBackend::new()
217 .expect_dependency_list(|_| Ok(UNSATISFIED_LIST.to_string()))
218 .expect_repo_list(|| Ok("[]".to_string()))
219 .expect_repo_add(|_, _, _| Err(err_repo_unreachable()))
220 .expect_dependency_build(|_, _| Ok(()));
221 Manager::new(backend)
222 .resolve_chart_dependencies(dir.path())
223 .unwrap();
224 }
225
226 {
228 let dir = lock_with_test_repo();
229 let backend = MockHelmBackend::new()
230 .expect_dependency_list(|_| Ok(UNSATISFIED_LIST.to_string()))
231 .expect_repo_list(|| {
232 Ok(r#"[{"name":"jetstack","url":"https://charts.jetstack.io"}]"#.to_string())
233 })
234 .expect_repo_update(|_| Err(err_repo_unreachable()))
235 .expect_dependency_build(|_, _| Ok(()));
236 Manager::new(backend)
237 .resolve_chart_dependencies(dir.path())
238 .unwrap();
239 }
240
241 {
243 let dir = lock_with_test_repo();
244 let backend = MockHelmBackend::new()
245 .expect_dependency_list(|_| Ok(UNSATISFIED_LIST.to_string()))
246 .expect_repo_list(|| Ok("not json".to_string()))
247 .expect_dependency_build(|_, _| Ok(()));
248 Manager::new(backend)
249 .resolve_chart_dependencies(dir.path())
250 .unwrap();
251 }
252
253 {
255 let dir = lock_with_test_repo();
256 std::fs::write(dir.path().join("Chart.lock"), "dependencies: [oh dear\n").unwrap();
257 let backend = MockHelmBackend::new()
258 .expect_dependency_list(|_| Ok(UNSATISFIED_LIST.to_string()))
259 .expect_dependency_build(|_, _| Ok(()));
260 Manager::new(backend)
261 .resolve_chart_dependencies(dir.path())
262 .unwrap();
263 }
264 }
265
266 #[test]
267 fn test_resolve_chart_dependencies_reports_a_failed_build() {
269 let dir = lock_with_test_repo();
270
271 let backend = MockHelmBackend::new()
275 .expect_dependency_list(|_| Ok(UNSATISFIED_LIST.to_string()))
276 .expect_repo_list(|| Ok("[]".to_string()))
277 .expect_repo_add(|_, _, _| Ok(()))
278 .expect_dependency_build(|_, _| Err(err_lock_out_of_sync()));
279
280 let err = Manager::new(backend)
281 .resolve_chart_dependencies(dir.path())
282 .unwrap_err();
283
284 let message = err.to_string();
285 assert!(message.contains("the lock file (Chart.lock) is out of sync"));
286 assert!(message.contains("helm dependency update"));
287 }
288
289 #[test]
290 fn test_resolve_chart_dependencies_reports_a_failed_probe() {
292 let dir = lock_with_test_repo();
293
294 let backend = MockHelmBackend::new().expect_dependency_list(|_| Err(err_helm_missing()));
295
296 let err = Manager::new(backend)
297 .resolve_chart_dependencies(dir.path())
298 .unwrap_err();
299
300 assert!(
301 err.to_string()
302 .contains("exec: helm: executable file not found")
303 );
304 }
305
306 #[test]
307 fn test_resolve_chart_dependencies_registers_every_repository_the_lock_names() {
309 let dir = tempfile::tempdir().unwrap();
310 std::fs::write(
311 dir.path().join("Chart.lock"),
312 "dependencies:\n\
313 - name: cert-manager\n repository: https://charts.jetstack.io\n version: 1.16.2\n\
314 - name: prometheus\n repository: https://prometheus-community.github.io/helm-charts\n version: 25.8.0\n\
315 - name: in-tree\n repository: \"\"\n version: 1.0.0\n",
316 )
317 .unwrap();
318
319 let added = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
320 let added_1 = added.clone();
321 let added_2 = added.clone();
322
323 let backend = MockHelmBackend::new()
324 .expect_dependency_list(|_| Ok(UNSATISFIED_LIST.to_string()))
325 .expect_repo_list(|| Ok("[]".to_string()))
326 .expect_repo_add(move |_, url, _| {
327 added_1.lock().unwrap().push(url.to_string());
328 Ok(())
329 })
330 .expect_repo_add(move |_, url, _| {
331 added_2.lock().unwrap().push(url.to_string());
332 Ok(())
333 })
334 .expect_dependency_build(|_, _| Ok(()));
335
336 Manager::new(backend)
337 .resolve_chart_dependencies(dir.path())
338 .unwrap();
339
340 assert_eq!(
341 *added.lock().unwrap(),
342 vec![
343 "https://charts.jetstack.io".to_string(),
344 "https://prometheus-community.github.io/helm-charts".to_string(),
345 ]
346 );
347 }
348}