xianyun_deploy/deployer/helm/
chartsource.rs1use tracing::info;
4
5use xianyun_backends::helm::{Backend as HelmBackend, chartref::redact_chart_ref};
6
7use super::HelmError;
8use super::manager::Manager;
9
10type MaterializedChart = (std::path::PathBuf, Box<dyn FnOnce()>);
13
14impl<B: HelmBackend> Manager<B> {
15 pub(super) fn materialize_url_chart(
32 &self,
33 chart_ref: &str,
34 ) -> Result<MaterializedChart, HelmError> {
35 let dir = tempfile::Builder::new()
36 .prefix("caproni-chart-")
37 .tempdir()
38 .map_err(|e| HelmError::Io(format!("creating chart unpack directory: {e}")))?;
39
40 info!(chart = %redact_chart_ref(chart_ref), "fetching helm chart");
41
42 let dir_path = dir.path().to_path_buf();
43
44 self.backend
45 .pull(chart_ref, &dir_path.display().to_string())
46 .map_err(|e| HelmError::Backend {
47 context: format!("fetching chart {}", redact_chart_ref(chart_ref)),
48 source: e,
49 })?;
50
51 let root = super::chartdeps::locate_unpacked_chart(&dir_path)?;
52
53 self.resolve_chart_dependencies(&root)?;
54
55 Ok((root, Box::new(move || drop(dir))))
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use std::sync::{Arc, Mutex};
62
63 use xianyun_executor::exec::ExecError;
64
65 use super::super::manager::Manager;
66 use super::super::test_support::MockHelmBackend;
67
68 const TEST_CHART_URL: &str = "https://gitlab.com/g/p/-/archive/abc123/p-abc123.tar.gz";
69 const TEST_REPO_URL: &str = "https://charts.jetstack.io";
70
71 const SATISFIED_LIST: &str =
72 "NAME\tVERSION\tREPOSITORY\tSTATUS\ncert-manager\t1.16.2\thttps://charts.jetstack.io\tok\n";
73 const UNSATISFIED_LIST: &str = "NAME\tVERSION\tREPOSITORY\tSTATUS\ncert-manager\t1.16.2\thttps://charts.jetstack.io\tmissing\n";
74
75 fn stub_pull_for(chart_ref: &'static str, dirs: Arc<Mutex<Vec<String>>>) -> MockHelmBackend {
79 stub_pull_for_calls(chart_ref, dirs, 1)
80 }
81
82 fn stub_pull_for_calls(
83 chart_ref: &'static str,
84 dirs: Arc<Mutex<Vec<String>>>,
85 calls: usize,
86 ) -> MockHelmBackend {
87 let mut backend = MockHelmBackend::new();
88 for _ in 0..calls {
89 backend = stub_pull_once(backend, chart_ref, dirs.clone());
90 }
91 backend
92 }
93
94 fn stub_pull_once(
95 backend: MockHelmBackend,
96 chart_ref: &'static str,
97 dirs: Arc<Mutex<Vec<String>>>,
98 ) -> MockHelmBackend {
99 backend.expect_pull(move |got_chart_ref, untar_dir| {
100 assert_eq!(got_chart_ref, chart_ref);
101
102 let entries: Vec<_> = std::fs::read_dir(untar_dir).unwrap().collect();
103 assert!(
104 entries.is_empty(),
105 "helm must never unpack into a directory that already holds a chart"
106 );
107
108 dirs.lock().unwrap().push(untar_dir.to_string());
109
110 std::fs::create_dir_all(format!("{untar_dir}/p-abc123.tar.gz")).unwrap();
111 std::fs::create_dir_all(format!("{untar_dir}/gitlab")).unwrap();
112 std::fs::write(format!("{untar_dir}/gitlab/Chart.yaml"), "name: gitlab\n").unwrap();
113 std::fs::write(
114 format!("{untar_dir}/gitlab/Chart.lock"),
115 format!(
116 "dependencies:\n- name: cert-manager\n repository: {TEST_REPO_URL}\n version: 1.16.2\n"
117 ),
118 )
119 .unwrap();
120
121 Ok(())
122 })
123 }
124
125 #[test]
127 fn test_materialize_url_chart_unpacks_the_chart_and_cleans_up_after_it() {
129 let dirs = Arc::new(Mutex::new(Vec::new()));
130 let backend = stub_pull_for(TEST_CHART_URL, dirs.clone())
131 .expect_dependency_list(|_| Ok(SATISFIED_LIST.to_string()));
132
133 let mgr = Manager::new(backend);
134
135 let (root, cleanup) = mgr.materialize_url_chart(TEST_CHART_URL).unwrap();
136
137 let dirs = dirs.lock().unwrap().clone();
138 assert_eq!(dirs.len(), 1);
139 assert_eq!(
140 root.display().to_string(),
141 format!("{}/gitlab", dirs[0]),
142 "the chart root, not the directory helm unpacked into"
143 );
144 assert!(root.join("Chart.yaml").is_file());
145
146 cleanup();
147
148 assert!(
149 !std::path::Path::new(&dirs[0]).exists(),
150 "cleanup should remove the unpack directory"
151 );
152 }
153
154 #[test]
155 fn test_materialize_url_chart_resolves_a_chart_that_does_not_vendor_its_dependencies() {
157 let dirs = Arc::new(Mutex::new(Vec::new()));
158 let backend = stub_pull_for(TEST_CHART_URL, dirs)
159 .expect_dependency_list(|_| Ok(UNSATISFIED_LIST.to_string()))
160 .expect_repo_list(|| Ok("[]".to_string()))
161 .expect_repo_add(|_, url, force_update| {
162 assert_eq!(url, TEST_REPO_URL);
163 assert!(force_update);
164 Ok(())
165 })
166 .expect_dependency_build(|_, skip_refresh| {
167 assert!(skip_refresh);
168 Ok(())
169 });
170
171 let mgr = Manager::new(backend);
172
173 let (_root, cleanup) = mgr.materialize_url_chart(TEST_CHART_URL).unwrap();
174 cleanup();
175 }
176
177 #[test]
178 fn test_materialize_url_chart_reports_a_failed_pull() {
180 let backend = MockHelmBackend::new().expect_pull(|got_chart_ref, _| {
181 assert_eq!(got_chart_ref, TEST_CHART_URL);
182 Err(ExecError::RunFailed(
183 "failed to fetch the chart".to_string(),
184 ))
185 });
186
187 let mgr = Manager::new(backend);
188
189 let result = mgr.materialize_url_chart(TEST_CHART_URL);
190 let err = match result {
191 Ok(_) => panic!("expected an error"),
192 Err(e) => e,
193 };
194
195 assert!(err.to_string().contains("failed to fetch the chart"));
196 }
197
198 #[test]
199 fn test_materialize_url_chart_reports_an_archive_with_no_chart_in_it() {
201 let backend = MockHelmBackend::new().expect_pull(|got_chart_ref, _| {
203 assert_eq!(got_chart_ref, TEST_CHART_URL);
204 Ok(())
205 });
206
207 let mgr = Manager::new(backend);
208
209 let result = mgr.materialize_url_chart(TEST_CHART_URL);
210 let err = match result {
211 Ok(_) => panic!("expected an error"),
212 Err(e) => e,
213 };
214
215 assert!(matches!(err, super::HelmError::ChartNotUnpacked(_)));
216 }
217
218 #[test]
219 fn test_materialize_url_chart_each_call_unpacks_into_a_fresh_directory() {
221 let dirs = Arc::new(Mutex::new(Vec::new()));
222 let backend = stub_pull_for_calls(TEST_CHART_URL, dirs.clone(), 2)
223 .expect_dependency_list(|_| Ok(SATISFIED_LIST.to_string()))
224 .expect_dependency_list(|_| Ok(SATISFIED_LIST.to_string()));
225
226 let mgr = Manager::new(backend);
227
228 for _ in 0..2 {
229 let (_root, cleanup) = mgr.materialize_url_chart(TEST_CHART_URL).unwrap();
230 cleanup();
231 }
232
233 let dirs = dirs.lock().unwrap().clone();
234 assert_eq!(dirs.len(), 2);
235 assert_ne!(
236 dirs[0], dirs[1],
237 "a reused directory would let helm merge two charts"
238 );
239 }
240}