Skip to main content

xianyun_deploy/deployer/helm/
chartresolve.rs

1//! Ported from `internal/deployer/helm/chartresolve.go`.
2
3use 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    /// Ported from `resolveChartDependencies`.
15    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    /// Ported from `registerLockedRepositories`.
46    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                // Already the user's, credentials and all, so it is
70                // refreshed rather than re-added: the build reads the cached
71                // index, and a stale one does not hold the version the lock
72                // pins.
73                if let Err(e) = self.backend.repo_update(&registered.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            // RepoAdd fetches the index as part of adding, so the build that
88            // follows needs no refresh of its own.
89            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    // Ported from internal/deployer/helm/chartresolve_test.go
140    #[test]
141    // Ported from internal/deployer/helm/chartresolve_test.go:42 (TestResolveChartDependencies_SatisfiedChartIsLeftAlone)
142    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        // Only the probe is expected; any other call would be wasted work.
147        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    // Ported from internal/deployer/helm/chartresolve_test.go:56 (TestResolveChartDependencies_RegistersAnUnknownRepositoryThenBuilds)
159    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            // --skip-refresh: RepoAdd has just fetched the index.
173            .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    // Ported from internal/deployer/helm/chartresolve_test.go:75 (TestResolveChartDependencies_RefreshesAnAlreadyRegisteredRepository)
185    fn test_resolve_chart_dependencies_refreshes_an_already_registered_repository() {
186        let dir = lock_with_test_repo();
187
188        // Registered under the user's own name, possibly with credentials, so
189        // it is not re-added. Its index is refreshed instead.
190        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    // Ported from internal/deployer/helm/chartresolve_test.go:95 (TestResolveChartDependencies_PreparationFailuresStillReachTheBuild)
208    fn test_resolve_chart_dependencies_preparation_failures_still_reach_the_build() {
209        // Everything before the build is advisory: it makes a repository
210        // usable, and helm reports what is actually wrong with the chart
211        // better than a guess here would.
212
213        // a repository that cannot be registered
214        {
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        // a repository whose index cannot be refreshed
227        {
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        // a repository list that cannot be parsed
242        {
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        // a lock that cannot be read
254        {
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    // Ported from internal/deployer/helm/chartresolve_test.go:156 (TestResolveChartDependencies_ReportsAFailedBuild)
268    fn test_resolve_chart_dependencies_reports_a_failed_build() {
269        let dir = lock_with_test_repo();
270
271        // No fallback: an out-of-step lock is the user's to resolve, and the
272        // error says so, because helm's own message only reaches them under
273        // --debug.
274        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    // Ported from internal/deployer/helm/chartresolve_test.go:175 (TestResolveChartDependencies_ReportsAFailedProbe)
291    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    // Ported from internal/deployer/helm/chartresolve_test.go:188 (TestResolveChartDependencies_RegistersEveryRepositoryTheLockNames)
308    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}