syncable_cli/analyzer/kubelint/parser/
kustomize.rs

1//! Kustomize support for Kubernetes manifests.
2
3use crate::analyzer::kubelint::context::Object;
4use crate::analyzer::kubelint::parser::yaml;
5use std::path::Path;
6use std::process::Command;
7
8/// Render a Kustomize directory to Kubernetes objects.
9///
10/// This function shells out to `kustomize build` (or `kubectl kustomize`)
11/// to render the directory and then parses the resulting YAML.
12pub fn render_kustomize(dir: &Path) -> Result<Vec<Object>, KustomizeError> {
13    // Try kustomize binary first, fall back to kubectl kustomize
14    let output = if is_kustomize_available() {
15        let mut cmd = Command::new("kustomize");
16        cmd.arg("build").arg(dir);
17        cmd.output()
18            .map_err(|e| KustomizeError::BuildError(e.to_string()))?
19    } else if is_kubectl_kustomize_available() {
20        let mut cmd = Command::new("kubectl");
21        cmd.arg("kustomize").arg(dir);
22        cmd.output()
23            .map_err(|e| KustomizeError::BuildError(e.to_string()))?
24    } else {
25        return Err(KustomizeError::KustomizeNotFound);
26    };
27
28    if !output.status.success() {
29        let stderr = String::from_utf8_lossy(&output.stderr);
30        return Err(KustomizeError::BuildError(stderr.to_string()));
31    }
32
33    // Parse the rendered YAML
34    let yaml_content = String::from_utf8_lossy(&output.stdout);
35    yaml::parse_yaml_with_path(&yaml_content, dir)
36        .map_err(|e| KustomizeError::BuildError(e.to_string()))
37}
38
39/// Render Kustomize with specific options.
40pub fn render_kustomize_with_options(
41    dir: &Path,
42    enable_helm: bool,
43    load_restrictors: LoadRestrictors,
44) -> Result<Vec<Object>, KustomizeError> {
45    if !is_kustomize_available() && !is_kubectl_kustomize_available() {
46        return Err(KustomizeError::KustomizeNotFound);
47    }
48
49    let output = if is_kustomize_available() {
50        let mut cmd = Command::new("kustomize");
51        cmd.arg("build").arg(dir);
52
53        if enable_helm {
54            cmd.arg("--enable-helm");
55        }
56
57        match load_restrictors {
58            LoadRestrictors::None => {
59                cmd.arg("--load-restrictor=none");
60            }
61            LoadRestrictors::RootOnly => {
62                // Default behavior, no flag needed
63            }
64        }
65
66        cmd.output()
67            .map_err(|e| KustomizeError::BuildError(e.to_string()))?
68    } else {
69        // kubectl kustomize has limited options
70        let mut cmd = Command::new("kubectl");
71        cmd.arg("kustomize").arg(dir);
72
73        if enable_helm {
74            cmd.arg("--enable-helm");
75        }
76
77        cmd.output()
78            .map_err(|e| KustomizeError::BuildError(e.to_string()))?
79    };
80
81    if !output.status.success() {
82        let stderr = String::from_utf8_lossy(&output.stderr);
83        return Err(KustomizeError::BuildError(stderr.to_string()));
84    }
85
86    let yaml_content = String::from_utf8_lossy(&output.stdout);
87    yaml::parse_yaml_with_path(&yaml_content, dir)
88        .map_err(|e| KustomizeError::BuildError(e.to_string()))
89}
90
91/// Load restrictor options for kustomize.
92#[derive(Debug, Clone, Copy, Default)]
93pub enum LoadRestrictors {
94    /// No restrictions (can load from anywhere).
95    None,
96    /// Only load from root directory (default).
97    #[default]
98    RootOnly,
99}
100
101/// Check if a directory is a Kustomize directory.
102pub fn is_kustomize_dir(path: &Path) -> bool {
103    path.join("kustomization.yaml").exists()
104        || path.join("kustomization.yml").exists()
105        || path.join("Kustomization").exists()
106}
107
108/// Check if kustomize binary is available in PATH.
109pub fn is_kustomize_available() -> bool {
110    Command::new("kustomize")
111        .arg("version")
112        .output()
113        .map(|o| o.status.success())
114        .unwrap_or(false)
115}
116
117/// Check if kubectl kustomize is available.
118pub fn is_kubectl_kustomize_available() -> bool {
119    Command::new("kubectl")
120        .arg("kustomize")
121        .arg("--help")
122        .output()
123        .map(|o| o.status.success())
124        .unwrap_or(false)
125}
126
127/// Get kustomize version if available.
128pub fn kustomize_version() -> Option<String> {
129    // Try kustomize binary first
130    if let Some(version) = Command::new("kustomize")
131        .arg("version")
132        .output()
133        .ok()
134        .filter(|o| o.status.success())
135        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
136    {
137        return Some(version);
138    }
139
140    // Fall back to kubectl version
141    Command::new("kubectl")
142        .arg("version")
143        .arg("--client")
144        .arg("-o")
145        .arg("json")
146        .output()
147        .ok()
148        .filter(|o| o.status.success())
149        .map(|o| {
150            let output = String::from_utf8_lossy(&o.stdout);
151            format!("kubectl ({})", output.lines().next().unwrap_or("unknown"))
152        })
153}
154
155/// Kustomize errors.
156#[derive(Debug, Clone)]
157pub enum KustomizeError {
158    /// kustomize binary not found.
159    KustomizeNotFound,
160    /// Build error.
161    BuildError(String),
162}
163
164impl std::fmt::Display for KustomizeError {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        match self {
167            Self::KustomizeNotFound => {
168                write!(
169                    f,
170                    "kustomize binary not found in PATH (tried 'kustomize' and 'kubectl kustomize')"
171                )
172            }
173            Self::BuildError(msg) => write!(f, "Build error: {}", msg),
174        }
175    }
176}
177
178impl std::error::Error for KustomizeError {}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_is_kustomize_dir_detection() {
186        let temp_dir = std::env::temp_dir();
187        assert!(!is_kustomize_dir(&temp_dir)); // temp dir is not a Kustomize dir
188    }
189
190    #[test]
191    fn test_kustomize_availability() {
192        // Just verify the function runs without panicking
193        let _available = is_kustomize_available();
194        let _kubectl_available = is_kubectl_kustomize_available();
195    }
196}