Skip to main content

cgn_helm/
lib.rs

1//! Thin wrapper around the `helm` binary.
2//!
3//! `cgn-ctl install` orchestrates a Cognitora install by invoking helm under
4//! the hood. Rust's ecosystem doesn't have a real Helm SDK, but shelling
5//! out is fine: helm is a single static Go binary, we ship it embedded
6//! in our release tarballs, and the surface we use is stable.
7
8#![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
17/// Locate the helm binary. Honours `$CGN_HELM_BIN`, falls back to PATH lookup.
18pub 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
26/// `helm version --short` round-trip, used by `cgn-ctl preflight`.
27pub 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/// Install or upgrade a chart (`helm upgrade --install`).
44#[derive(Debug, Clone)]
45pub struct Install {
46    pub release: String,
47    pub chart: PathBuf, // local chart dir or oci:// URL
48    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
90/// `helm uninstall <release> -n <ns>`.
91pub 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
98/// `helm template ... | yq` equivalent for offline rendering. Returns YAML.
99pub 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
126/// Convenience: render `Vec<u8>` of YAML for a chart from a serialisable
127/// `values` struct. Useful in tests and `cgn-ctl install --dry-run`.
128pub 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}