Skip to main content

xianyun_deploy/deployer/kustomize/
mod.rs

1//! A generic kustomize deployer implementation.
2//!
3//! Ported from `internal/deployer/kustomize`. It builds a kustomization
4//! directory with the kustomize backend and applies the rendered
5//! manifests with the kubectl backend, making arbitrary non-chart
6//! manifests declarable in caproni.yaml like any other deployer.
7
8pub mod builtin;
9pub mod camaterial;
10pub mod files;
11pub mod hostname;
12pub mod manager;
13
14use std::path::PathBuf;
15
16/// A cleanup callback for a temporary directory, always safe to call.
17/// Ported from the Go `cleanup func()` return value used throughout this
18/// package (`os.MkdirTemp` callers).
19pub(crate) type Cleanup = Box<dyn FnOnce() + Send>;
20
21/// Creates a fresh, uniquely-named temporary directory under the system
22/// temp directory, named `<prefix>XXXXXX` with the trailing `X`s replaced
23/// by `libc::mkdtemp`. Ported from the Go original's repeated
24/// `os.MkdirTemp("", "<prefix>-*")` calls.
25pub(crate) fn mkdtemp(prefix: &str) -> std::io::Result<PathBuf> {
26    use std::os::unix::ffi::OsStrExt;
27
28    let template_path = std::env::temp_dir().join(format!("{prefix}XXXXXX"));
29
30    let mut template = std::ffi::CString::new(template_path.as_os_str().as_bytes())
31        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?
32        .into_bytes_with_nul();
33
34    // SAFETY: `template` is a NUL-terminated, mutable buffer ending in
35    // "XXXXXX" as `mkdtemp(3)` requires; it is not read or written by
36    // any other code while this call is in progress.
37    let result = unsafe { libc::mkdtemp(template.as_mut_ptr().cast::<libc::c_char>()) };
38
39    if result.is_null() {
40        return Err(std::io::Error::last_os_error());
41    }
42
43    let path_bytes = &template[..template.len() - 1]; // drop the NUL terminator
44    Ok(PathBuf::from(std::ffi::OsStr::from_bytes(path_bytes)))
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn mkdtemp_creates_a_fresh_directory() {
53        let dir = mkdtemp("caproni-mkdtemp-test-").unwrap();
54        assert!(dir.is_dir());
55        std::fs::remove_dir_all(&dir).unwrap();
56    }
57}
58
59/// Hand-rolled test doubles for [`xianyun_backends::kustomize::Backend`]
60/// and [`xianyun_backends::kubectl::Backend`], shared by this module's
61/// own tests (`builtin`, `files`, `manager`). Mirrors the Go tests'
62/// mockery-generated `kustomizemocks.NewMockBackend` /
63/// `kubectlmocks.NewMockBackend`, but hand-rolled per the crate's
64/// existing convention (see `deployer::namespace::manager`'s
65/// `MockKubectlBackend`) since no shared mock of either `Backend` trait
66/// exists yet anywhere in the workspace.
67#[cfg(test)]
68pub(crate) mod test_support {
69    use std::sync::Mutex;
70    use std::time::Duration;
71
72    use xianyun_backends::kubectl::{Backend as KubectlBackend, KubectlError, VersionResponse};
73    use xianyun_backends::kustomize::{Backend as KustomizeBackend, KustomizeError};
74
75    type BuildFn = Box<dyn FnMut(&str) -> Result<Vec<u8>, KustomizeError> + Send>;
76
77    /// A test double for [`KustomizeBackend`] with a single settable
78    /// `build` expectation. Calling `build` without one configured
79    /// panics, matching mockery's "unexpected call" behavior.
80    #[derive(Default)]
81    pub(crate) struct MockKustomizeBackend {
82        build_fn: Mutex<Option<BuildFn>>,
83    }
84
85    impl MockKustomizeBackend {
86        pub(crate) fn new() -> Self {
87            Self::default()
88        }
89
90        pub(crate) fn expect_build<F>(self, f: F) -> Self
91        where
92            F: FnMut(&str) -> Result<Vec<u8>, KustomizeError> + Send + 'static,
93        {
94            *self.build_fn.lock().unwrap() = Some(Box::new(f));
95            self
96        }
97    }
98
99    impl KustomizeBackend for MockKustomizeBackend {
100        fn build(&self, dir: &str) -> Result<Vec<u8>, KustomizeError> {
101            let mut guard = self.build_fn.lock().unwrap();
102            match guard.as_mut() {
103                Some(f) => f(dir),
104                None => panic!("MockKustomizeBackend::build called without an expectation"),
105            }
106        }
107    }
108
109    type ApplyFn = Box<dyn FnMut(&[u8], &str) -> Result<(), KubectlError> + Send>;
110    type DeleteManifestFn = Box<dyn FnMut(&[u8], &str) -> Result<(), KubectlError> + Send>;
111
112    /// A test double for [`KubectlBackend`] with settable `apply` and
113    /// `delete_manifest` expectations -- the only two methods this
114    /// module's manager calls. Every other method panics if called,
115    /// matching mockery's "unexpected call" behavior for
116    /// unconfigured methods.
117    #[derive(Default)]
118    pub(crate) struct MockKubectlBackend {
119        apply_fn: Mutex<Option<ApplyFn>>,
120        delete_manifest_fn: Mutex<Option<DeleteManifestFn>>,
121    }
122
123    impl MockKubectlBackend {
124        pub(crate) fn new() -> Self {
125            Self::default()
126        }
127
128        pub(crate) fn expect_apply<F>(self, f: F) -> Self
129        where
130            F: FnMut(&[u8], &str) -> Result<(), KubectlError> + Send + 'static,
131        {
132            *self.apply_fn.lock().unwrap() = Some(Box::new(f));
133            self
134        }
135
136        pub(crate) fn expect_delete_manifest<F>(self, f: F) -> Self
137        where
138            F: FnMut(&[u8], &str) -> Result<(), KubectlError> + Send + 'static,
139        {
140            *self.delete_manifest_fn.lock().unwrap() = Some(Box::new(f));
141            self
142        }
143    }
144
145    impl KubectlBackend for MockKubectlBackend {
146        fn get(
147            &self,
148            _resource_type: &str,
149            _name: &str,
150            _namespace: &str,
151        ) -> Result<String, KubectlError> {
152            panic!("MockKubectlBackend::get: unexpected call")
153        }
154
155        fn delete_pods_by_label(
156            &self,
157            _namespace: &str,
158            _label_selector: &str,
159        ) -> Result<usize, KubectlError> {
160            panic!("MockKubectlBackend::delete_pods_by_label: unexpected call")
161        }
162
163        fn get_version(
164            &self,
165            _timeout: Duration,
166            _interval: Duration,
167        ) -> Result<VersionResponse, KubectlError> {
168            panic!("MockKubectlBackend::get_version: unexpected call")
169        }
170
171        fn scale(
172            &self,
173            _resource_type: &str,
174            _name: &str,
175            _namespace: &str,
176            _replicas: i64,
177        ) -> Result<(), KubectlError> {
178            panic!("MockKubectlBackend::scale: unexpected call")
179        }
180
181        fn apply(&self, json_data: &[u8], namespace: &str) -> Result<(), KubectlError> {
182            let mut guard = self.apply_fn.lock().unwrap();
183            match guard.as_mut() {
184                Some(f) => f(json_data, namespace),
185                None => panic!("MockKubectlBackend::apply called without an expectation"),
186            }
187        }
188
189        fn delete_manifest(&self, data: &[u8], namespace: &str) -> Result<(), KubectlError> {
190            let mut guard = self.delete_manifest_fn.lock().unwrap();
191            match guard.as_mut() {
192                Some(f) => f(data, namespace),
193                None => panic!("MockKubectlBackend::delete_manifest called without an expectation"),
194            }
195        }
196
197        fn delete(
198            &self,
199            _resource_type: &str,
200            _name: &str,
201            _namespace: &str,
202        ) -> Result<(), KubectlError> {
203            panic!("MockKubectlBackend::delete: unexpected call")
204        }
205
206        fn wait(
207            &self,
208            _resource_type: &str,
209            _name: &str,
210            _namespace: &str,
211            _condition: &str,
212            _timeout: Duration,
213        ) -> Result<(), KubectlError> {
214            panic!("MockKubectlBackend::wait: unexpected call")
215        }
216
217        fn wait_for_rollout_status(
218            &self,
219            _resource_type: &str,
220            _name: &str,
221            _namespace: &str,
222            _timeout: Duration,
223        ) -> Result<(), KubectlError> {
224            panic!("MockKubectlBackend::wait_for_rollout_status: unexpected call")
225        }
226
227        fn wait_for_pod_ready_by_label(
228            &self,
229            _namespace: &str,
230            _label_selector: &str,
231            _timeout: Duration,
232        ) -> Result<(), KubectlError> {
233            panic!("MockKubectlBackend::wait_for_pod_ready_by_label: unexpected call")
234        }
235
236        fn wait_for_job_complete(
237            &self,
238            _name: &str,
239            _namespace: &str,
240            _timeout: Duration,
241        ) -> Result<(), KubectlError> {
242            panic!("MockKubectlBackend::wait_for_job_complete: unexpected call")
243        }
244
245        fn wait_for_job_created(
246            &self,
247            _name: &str,
248            _namespace: &str,
249            _timeout: Duration,
250        ) -> Result<(), KubectlError> {
251            panic!("MockKubectlBackend::wait_for_job_created: unexpected call")
252        }
253
254        fn list_resource_names(
255            &self,
256            _kind: &str,
257            _namespace: &str,
258            _label_selector: &str,
259        ) -> Result<Vec<String>, KubectlError> {
260            panic!("MockKubectlBackend::list_resource_names: unexpected call")
261        }
262
263        fn ensure_namespace(&self, _name: &str) -> Result<(), KubectlError> {
264            panic!("MockKubectlBackend::ensure_namespace: unexpected call")
265        }
266
267        fn delete_namespace(&self, _name: &str) -> Result<(), KubectlError> {
268            panic!("MockKubectlBackend::delete_namespace: unexpected call")
269        }
270
271        fn namespace_exists(&self, _name: &str) -> Result<bool, KubectlError> {
272            panic!("MockKubectlBackend::namespace_exists: unexpected call")
273        }
274
275        fn wait_for_namespace_ready(
276            &self,
277            _name: &str,
278            _timeout: Duration,
279        ) -> Result<(), KubectlError> {
280            panic!("MockKubectlBackend::wait_for_namespace_ready: unexpected call")
281        }
282
283        fn run_job(
284            &self,
285            _image: &str,
286            _namespace: &str,
287            _timeout: Duration,
288            _command: &[&str],
289        ) -> Result<String, KubectlError> {
290            panic!("MockKubectlBackend::run_job: unexpected call")
291        }
292
293        fn get_raw(&self, _path: &str) -> Result<String, KubectlError> {
294            panic!("MockKubectlBackend::get_raw: unexpected call")
295        }
296    }
297}