use std::path::PathBuf;
use std::process::Command;
use serde::Deserialize;
use tracing::{instrument, warn};
mod error;
pub use crate::error::HelmError;
use fluvio_command::CommandExt;
#[derive(Debug)]
pub struct InstallArg {
pub name: String,
pub chart: String,
pub version: Option<String>,
pub namespace: Option<String>,
pub opts: Vec<(String, String)>,
pub values: Vec<PathBuf>,
pub develop: bool,
}
impl InstallArg {
pub fn new<N: Into<String>, C: Into<String>>(name: N, chart: C) -> Self {
Self {
name: name.into(),
chart: chart.into(),
version: None,
namespace: None,
opts: vec![],
values: vec![],
develop: false,
}
}
pub fn version<S: Into<String>>(mut self, version: S) -> Self {
self.version = Some(version.into());
self
}
pub fn namespace<S: Into<String>>(mut self, ns: S) -> Self {
self.namespace = Some(ns.into());
self
}
pub fn opts(mut self, options: Vec<(String, String)>) -> Self {
self.opts = options;
self
}
pub fn opt<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.opts.push((key.into(), value.into()));
self
}
pub fn develop(mut self) -> Self {
self.develop = true;
self
}
pub fn values(mut self, values: Vec<PathBuf>) -> Self {
self.values = values;
self
}
pub fn value(&mut self, value: PathBuf) -> &mut Self {
self.values.push(value);
self
}
pub fn install(&self) -> Command {
let mut command = Command::new("helm");
command.args(&["install", &self.name, &self.chart]);
self.apply_args(&mut command);
command
}
pub fn upgrade(&self) -> Command {
let mut command = Command::new("helm");
command.args(&["upgrade", "--install", &self.name, &self.chart]);
self.apply_args(&mut command);
command
}
fn apply_args(&self, command: &mut Command) {
if let Some(namespace) = &self.namespace {
command.args(&["--namespace", namespace]);
}
if self.develop {
command.arg("--devel");
}
if let Some(version) = &self.version {
command.args(&["--version", version]);
}
for value_path in &self.values {
command.arg("--values").arg(value_path);
}
for (key, val) in &self.opts {
command.arg("--set").arg(format!("{}={}", key, val));
}
}
}
impl Into<Command> for InstallArg {
fn into(self) -> Command {
let mut command = Command::new("helm");
command.args(&["install", &self.name, &self.chart]);
if let Some(namespace) = &self.namespace {
command.args(&["--namespace", namespace]);
}
if self.develop {
command.arg("--devel");
}
if let Some(version) = &self.version {
command.args(&["--version", version]);
}
for value_path in &self.values {
command.arg("--values").arg(value_path);
}
for (key, val) in &self.opts {
command.arg("--set").arg(format!("{}={}", key, val));
}
command
}
}
#[derive(Debug)]
pub struct UninstallArg {
pub release: String,
pub namespace: Option<String>,
pub ignore_not_found: bool,
pub dry_run: bool,
pub timeout: Option<String>,
}
impl UninstallArg {
pub fn new(release: String) -> Self {
Self {
release,
namespace: None,
ignore_not_found: false,
dry_run: false,
timeout: None,
}
}
pub fn namespace(mut self, ns: String) -> Self {
self.namespace = Some(ns);
self
}
pub fn ignore_not_found(mut self) -> Self {
self.ignore_not_found = true;
self
}
pub fn dry_run(mut self) -> Self {
self.dry_run = true;
self
}
pub fn timeout(mut self, timeout: String) -> Self {
self.timeout = Some(timeout);
self
}
}
impl Into<Command> for UninstallArg {
fn into(self) -> Command {
let mut command = Command::new("helm");
command.args(&["uninstall", &self.release]);
if let Some(namespace) = &self.namespace {
command.args(&["--namespace", namespace]);
}
if self.dry_run {
command.arg("--dry-run");
}
for timeout in &self.timeout {
command.arg("--timeout").arg(timeout);
}
command
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct HelmClient {}
impl HelmClient {
pub fn new() -> Result<Self, HelmError> {
let output = Command::new("helm").arg("version").result()?;
let out_str = String::from_utf8(output.stdout).map_err(HelmError::Utf8Error)?;
if !out_str.contains("version") {
return Err(HelmError::HelmVersionNotFound(out_str));
}
Ok(Self {})
}
#[instrument(skip(self))]
pub fn install(&self, args: &InstallArg) -> Result<(), HelmError> {
let mut command = args.install();
command.result()?;
Ok(())
}
#[instrument(skip(self))]
pub fn upgrade(&self, args: &InstallArg) -> Result<(), HelmError> {
let mut command = args.upgrade();
command.result()?;
Ok(())
}
pub fn uninstall(&self, uninstall: UninstallArg) -> Result<(), HelmError> {
if uninstall.ignore_not_found {
let app_charts = self
.get_installed_chart_by_name(&uninstall.release, uninstall.namespace.as_deref())?;
if app_charts.is_empty() {
warn!("Chart does not exists, {}", &uninstall.release);
return Ok(());
}
}
let mut command: Command = uninstall.into();
command.result()?;
Ok(())
}
#[instrument(skip(self))]
pub fn repo_add(&self, chart: &str, location: &str) -> Result<(), HelmError> {
Command::new("helm")
.args(&["repo", "add", chart, location])
.result()?;
Ok(())
}
#[instrument(skip(self))]
pub fn repo_update(&self) -> Result<(), HelmError> {
Command::new("helm").args(&["repo", "update"]).result()?;
Ok(())
}
#[instrument(skip(self))]
pub fn search_repo(&self, chart: &str, version: &str) -> Result<Vec<Chart>, HelmError> {
let mut command = Command::new("helm");
command
.args(&["search", "repo", chart])
.args(&["--version", version])
.args(&["--output", "json"]);
let output = command.result()?;
check_helm_stderr(output.stderr)?;
serde_json::from_slice(&output.stdout).map_err(HelmError::Serde)
}
#[instrument(skip(self))]
pub fn versions(&self, chart: &str) -> Result<Vec<Chart>, HelmError> {
let mut command = Command::new("helm");
command
.args(&["search", "repo"])
.args(&["--versions", chart])
.args(&["--output", "json", "--devel"]);
let output = command.result()?;
check_helm_stderr(output.stderr)?;
serde_json::from_slice(&output.stdout).map_err(HelmError::Serde)
}
#[instrument(skip(self))]
pub fn chart_version_exists(&self, name: &str, version: &str) -> Result<bool, HelmError> {
let versions = self.search_repo(name, version)?;
let count = versions
.iter()
.filter(|chart| chart.name == name && chart.version == version)
.count();
Ok(count > 0)
}
#[instrument(skip(self))]
pub fn get_installed_chart_by_name(
&self,
name: &str,
namespace: Option<&str>,
) -> Result<Vec<InstalledChart>, HelmError> {
let exact_match = format!("^{}$", name);
let mut command = Command::new("helm");
command
.arg("list")
.arg("--filter")
.arg(exact_match)
.arg("--output")
.arg("json");
if let Some(ns) = namespace {
command.args(&["--namespace", ns]);
}
let output = command.result()?;
check_helm_stderr(output.stderr)?;
serde_json::from_slice(&output.stdout).map_err(HelmError::Serde)
}
#[instrument(skip(self))]
pub fn get_helm_version(&self) -> Result<String, HelmError> {
let helm_version = Command::new("helm")
.arg("version")
.arg("--short")
.output()
.map_err(HelmError::HelmNotInstalled)?;
let version_text = String::from_utf8(helm_version.stdout).map_err(HelmError::Utf8Error)?;
Ok(version_text[1..].trim().to_string())
}
}
fn check_helm_stderr(stderr: Vec<u8>) -> Result<(), HelmError> {
if !stderr.is_empty() {
let stderr = String::from_utf8(stderr)?;
if stderr.contains("Kubernetes cluster unreachable") {
return Err(HelmError::FailedToConnect);
}
}
Ok(())
}
#[derive(Debug, Deserialize)]
pub struct Chart {
name: String,
version: String,
}
impl Chart {
pub fn version(&self) -> &str {
&self.version
}
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Deserialize)]
pub struct InstalledChart {
pub name: String,
pub app_version: String,
pub revision: String,
pub updated: String,
pub status: String,
pub chart: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_get_installed_charts() {
const JSON_RESPONSE: &str = r#"[{"name":"test_chart","namespace":"default","revision":"50","updated":"2021-03-17 08:42:54.546347741 +0000 UTC","status":"deployed","chart":"test_chart-1.2.32-rc2","app_version":"1.2.32-rc2"}]"#;
let installed_charts: Vec<InstalledChart> =
serde_json::from_slice(JSON_RESPONSE.as_bytes()).expect("can not parse json");
assert_eq!(installed_charts.len(), 1);
let test_chart = installed_charts
.get(0)
.expect("can not grab the first result");
assert_eq!(test_chart.name, "test_chart");
assert_eq!(test_chart.chart, "test_chart-1.2.32-rc2");
}
}