1#![forbid(unsafe_code)]
9
10use std::path::{Path, PathBuf};
11use std::process::Stdio;
12
13use cgn_core::{Error, Result};
14use serde::Serialize;
15use tokio::process::Command;
16
17pub fn locate_helm() -> Result<PathBuf> {
19 if let Ok(p) = std::env::var("CGN_HELM_BIN") {
20 return Ok(PathBuf::from(p));
21 }
22 which::which("helm")
23 .map_err(|_| Error::Unavailable("helm binary not found in PATH; set CGN_HELM_BIN".into()))
24}
25
26pub async fn version() -> Result<String> {
28 let bin = locate_helm()?;
29 let out = Command::new(bin)
30 .args(["version", "--short"])
31 .output()
32 .await
33 .map_err(|e| Error::Internal(format!("helm version: {e}")))?;
34 if !out.status.success() {
35 return Err(Error::Unavailable(format!(
36 "helm version: {}",
37 String::from_utf8_lossy(&out.stderr)
38 )));
39 }
40 Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
41}
42
43#[derive(Debug, Clone)]
45pub struct Install {
46 pub release: String,
47 pub chart: PathBuf, pub namespace: String,
49 pub create_namespace: bool,
50 pub values: Vec<PathBuf>,
51 pub set: Vec<(String, String)>,
52 pub wait: bool,
53 pub timeout: Option<String>,
54}
55
56impl Install {
57 pub async fn run(&self) -> Result<String> {
58 let bin = locate_helm()?;
59 let mut cmd = Command::new(bin);
60 cmd.args([
61 "upgrade",
62 "--install",
63 &self.release,
64 self.chart
65 .to_str()
66 .ok_or_else(|| Error::InvalidArgument("chart path utf-8".into()))?,
67 "--namespace",
68 &self.namespace,
69 ]);
70 if self.create_namespace {
71 cmd.arg("--create-namespace");
72 }
73 if self.wait {
74 cmd.arg("--wait");
75 }
76 if let Some(t) = &self.timeout {
77 cmd.args(["--timeout", t]);
78 }
79 for v in &self.values {
80 cmd.arg("-f").arg(v);
81 }
82 for (k, v) in &self.set {
83 cmd.arg("--set").arg(format!("{k}={v}"));
84 }
85 cmd.stdin(Stdio::null());
86 run(cmd, "helm upgrade").await
87 }
88}
89
90pub async fn uninstall(release: &str, namespace: &str) -> Result<String> {
92 let bin = locate_helm()?;
93 let mut cmd = Command::new(bin);
94 cmd.args(["uninstall", release, "--namespace", namespace]);
95 run(cmd, "helm uninstall").await
96}
97
98pub async fn template(chart: &Path, values: &[(String, String)]) -> Result<String> {
100 let bin = locate_helm()?;
101 let mut cmd = Command::new(bin);
102 cmd.args(["template", "cognitora", chart.to_str().unwrap_or(".")]);
103 for (k, v) in values {
104 cmd.arg("--set").arg(format!("{k}={v}"));
105 }
106 run(cmd, "helm template").await
107}
108
109async fn run(mut cmd: Command, what: &str) -> Result<String> {
110 let out = cmd
111 .stderr(Stdio::piped())
112 .stdout(Stdio::piped())
113 .output()
114 .await
115 .map_err(|e| Error::Internal(format!("{what}: spawn: {e}")))?;
116 if !out.status.success() {
117 return Err(Error::Internal(format!(
118 "{what} exit {}: {}",
119 out.status,
120 String::from_utf8_lossy(&out.stderr)
121 )));
122 }
123 Ok(String::from_utf8_lossy(&out.stdout).into_owned())
124}
125
126pub async fn template_with_values<V: Serialize>(chart: &Path, values: &V) -> Result<String> {
129 let yaml = serde_yaml::to_string(values).map_err(|e| Error::Internal(format!("yaml: {e}")))?;
130 let tmp = tempfile::NamedTempFile::new().map_err(Error::Io)?;
131 std::fs::write(tmp.path(), yaml).map_err(Error::Io)?;
132 let bin = locate_helm()?;
133 let mut cmd = Command::new(bin);
134 cmd.args(["template", "cognitora", chart.to_str().unwrap_or(".")])
135 .arg("-f")
136 .arg(tmp.path());
137 run(cmd, "helm template").await
138}