diff --git a/Makefile b/Makefile index 9b76796..ef358e7 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ RUSTV=stable test-all: cargo build --all-features - cargo test --all + cargo test --all install-fmt: rustup component add rustfmt --toolchain $(RUSTV) diff --git a/src/chart.rs b/src/chart.rs new file mode 100644 index 0000000..b20439d --- /dev/null +++ b/src/chart.rs @@ -0,0 +1,19 @@ +use serde::Deserialize; + +/// A representation of a chart definition in a repo. +#[derive(Debug, Deserialize)] +pub struct Chart { + /// The chart name + name: String, + /// The chart version + version: String, +} + +impl Chart { + pub fn version(&self) -> &str { + &self.version + } + pub fn name(&self) -> &str { + &self.name + } +} diff --git a/src/helm_client.rs b/src/helm_client.rs new file mode 100644 index 0000000..881427b --- /dev/null +++ b/src/helm_client.rs @@ -0,0 +1,181 @@ +use fluvio_command::CommandExt; +use std::process::Command; +use tracing::{instrument, warn}; + +use super::Chart; +use super::HelmError; +use super::InstallArg; +use super::InstalledChart; +use super::UninstallArg; + +/// Client to manage helm operations +#[derive(Debug)] +#[non_exhaustive] +pub struct HelmClient {} + +impl HelmClient { + /// Creates a Rust client to manage our helm needs. + /// + /// This only succeeds if the helm command can be found. + pub fn new() -> Result { + let output = Command::new("helm").arg("version").result()?; + + // Convert command output into a string + let out_str = String::from_utf8(output.stdout).map_err(HelmError::Utf8Error)?; + + // Check that the version command gives a version. + // In the future, we can parse the version string and check + // for compatible CLI client version. + if !out_str.contains("version") { + return Err(HelmError::HelmVersionNotFound(out_str)); + } + + // If checks succeed, create Helm client + Ok(Self {}) + } + + /// Installs the given chart under the given name. + /// + #[instrument(skip(self))] + pub fn install(&self, args: &InstallArg) -> Result<(), HelmError> { + let mut command = args.install(); + command.result()?; + Ok(()) + } + + /// Upgrades the given chart + #[instrument(skip(self))] + pub fn upgrade(&self, args: &InstallArg) -> Result<(), HelmError> { + let mut command = args.upgrade(); + command.result()?; + Ok(()) + } + + /// Uninstalls specified chart library + 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(()) + } + + /// Adds a new helm repo with the given chart name and chart location + #[instrument(skip(self))] + pub fn repo_add(&self, chart: &str, location: &str) -> Result<(), HelmError> { + Command::new("helm") + .args(&["repo", "add", chart, location]) + .result()?; + Ok(()) + } + + /// Updates the local helm repository + #[instrument(skip(self))] + pub fn repo_update(&self) -> Result<(), HelmError> { + Command::new("helm").args(&["repo", "update"]).result()?; + Ok(()) + } + + /// Searches the repo for the named helm chart + #[instrument(skip(self))] + pub fn search_repo(&self, chart: &str, version: &str) -> Result, 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) + } + + /// Get all the available versions + #[instrument(skip(self))] + pub fn versions(&self, chart: &str) -> Result, 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) + } + + /// Checks that a given version of a given chart exists in the repo. + #[instrument(skip(self))] + pub fn chart_version_exists(&self, name: &str, version: &str) -> Result { + let versions = self.search_repo(name, version)?; + let count = versions + .iter() + .filter(|chart| chart.name() == name && chart.version() == version) + .count(); + Ok(count > 0) + } + + /// Returns the list of installed charts by name + #[instrument(skip(self))] + pub fn get_installed_chart_by_name( + &self, + name: &str, + namespace: Option<&str>, + ) -> Result, HelmError> { + let exact_match = format!("^{}$", name); + let mut command = Command::new("helm"); + command + .arg("list") + .arg("--filter") + .arg(exact_match) + .arg("--output") + .arg("json"); + + match namespace { + Some(ns) => { + command.args(&["--namespace", ns]); + } + None => { + // Search all namespaces + command.args(&["-A"]); + } + } + + let output = command.result()?; + check_helm_stderr(output.stderr)?; + serde_json::from_slice(&output.stdout).map_err(HelmError::Serde) + } + + /// get helm package version + #[instrument(skip(self))] + pub fn get_helm_version(&self) -> Result { + 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()) + } +} + +/// Check for errors in Helm's stderr output +/// +/// Returns `Ok(())` if everything is fine, or `HelmError` if something is wrong +fn check_helm_stderr(stderr: Vec) -> Result<(), HelmError> { + if !stderr.is_empty() { + let stderr = String::from_utf8(stderr)?; + if stderr.contains("Kubernetes cluster unreachable") { + return Err(HelmError::FailedToConnect); + } + } + + Ok(()) +} diff --git a/src/install_arg.rs b/src/install_arg.rs new file mode 100644 index 0000000..d7eae66 --- /dev/null +++ b/src/install_arg.rs @@ -0,0 +1,135 @@ +use std::path::PathBuf; +use std::process::Command; + +/// Installer Argument +#[derive(Debug)] +pub struct InstallArg { + pub name: String, + pub chart: String, + pub version: Option, + pub namespace: Option, + pub opts: Vec<(String, String)>, + pub values: Vec, + pub develop: bool, +} + +impl InstallArg { + pub fn new, C: Into>(name: N, chart: C) -> Self { + Self { + name: name.into(), + chart: chart.into(), + version: None, + namespace: None, + opts: vec![], + values: vec![], + develop: false, + } + } + + /// set chart version + pub fn version>(mut self, version: S) -> Self { + self.version = Some(version.into()); + self + } + + /// set namepsace + pub fn namespace>(mut self, ns: S) -> Self { + self.namespace = Some(ns.into()); + self + } + + /// reset array of options + pub fn opts(mut self, options: Vec<(String, String)>) -> Self { + self.opts = options; + self + } + + /// set a single option + pub fn opt, V: Into>(mut self, key: K, value: V) -> Self { + self.opts.push((key.into(), value.into())); + self + } + + /// set to use develop + pub fn develop(mut self) -> Self { + self.develop = true; + self + } + + /// set list of values + pub fn values(mut self, values: Vec) -> Self { + self.values = values; + self + } + + /// set one value + 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 From for Command { + fn from(arg: InstallArg) -> Self { + let mut command = Command::new("helm"); + command.args(&["install", &arg.name, &arg.chart]); + + if let Some(namespace) = &arg.namespace { + command.args(&["--namespace", namespace]); + } + + if arg.develop { + command.arg("--devel"); + } + + if let Some(version) = &arg.version { + command.args(&["--version", version]); + } + + for value_path in &arg.values { + command.arg("--values").arg(value_path); + } + + for (key, val) in &arg.opts { + command.arg("--set").arg(format!("{}={}", key, val)); + } + + command + } +} diff --git a/src/installed_chart.rs b/src/installed_chart.rs new file mode 100644 index 0000000..23858a2 --- /dev/null +++ b/src/installed_chart.rs @@ -0,0 +1,18 @@ +use serde::Deserialize; + +/// A representation of an installed chart. +#[derive(Debug, Deserialize)] +pub struct InstalledChart { + /// The chart name + pub name: String, + /// The version of the app this chart installed + pub app_version: String, + /// The chart revision + pub revision: String, + /// Date/time when the chart was last updated + pub updated: String, + /// Status of the installed chart + pub status: String, + /// The ID of the chart that is installed + pub chart: String, +} diff --git a/src/lib.rs b/src/lib.rs index c41973f..9bc4149 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,419 +1,17 @@ -use std::path::PathBuf; -use std::process::Command; - -use serde::Deserialize; -use tracing::{instrument, warn}; - +mod chart; mod error; -pub use crate::error::HelmError; -use fluvio_command::CommandExt; - -/// Installer Argument -#[derive(Debug)] -pub struct InstallArg { - pub name: String, - pub chart: String, - pub version: Option, - pub namespace: Option, - pub opts: Vec<(String, String)>, - pub values: Vec, - pub develop: bool, -} - -impl InstallArg { - pub fn new, C: Into>(name: N, chart: C) -> Self { - Self { - name: name.into(), - chart: chart.into(), - version: None, - namespace: None, - opts: vec![], - values: vec![], - develop: false, - } - } - - /// set chart version - pub fn version>(mut self, version: S) -> Self { - self.version = Some(version.into()); - self - } - - /// set namepsace - pub fn namespace>(mut self, ns: S) -> Self { - self.namespace = Some(ns.into()); - self - } - - /// reset array of options - pub fn opts(mut self, options: Vec<(String, String)>) -> Self { - self.opts = options; - self - } - - /// set a single option - pub fn opt, V: Into>(mut self, key: K, value: V) -> Self { - self.opts.push((key.into(), value.into())); - self - } - - /// set to use develop - pub fn develop(mut self) -> Self { - self.develop = true; - self - } - - /// set list of values - pub fn values(mut self, values: Vec) -> Self { - self.values = values; - self - } - - /// set one value - 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 From for Command { - fn from(arg: InstallArg) -> Self { - let mut command = Command::new("helm"); - command.args(&["install", &arg.name, &arg.chart]); - - if let Some(namespace) = &arg.namespace { - command.args(&["--namespace", namespace]); - } - - if arg.develop { - command.arg("--devel"); - } - - if let Some(version) = &arg.version { - command.args(&["--version", version]); - } - - for value_path in &arg.values { - command.arg("--values").arg(value_path); - } - - for (key, val) in &arg.opts { - command.arg("--set").arg(format!("{}={}", key, val)); - } - - command - } -} - -/// Uninstaller Argument -#[derive(Debug)] -pub struct UninstallArg { - pub release: String, - pub namespace: Option, - pub ignore_not_found: bool, - pub dry_run: bool, - pub timeout: Option, -} - -impl UninstallArg { - pub fn new(release: String) -> Self { - Self { - release, - namespace: None, - ignore_not_found: false, - dry_run: false, - timeout: None, - } - } - - /// set namepsace - pub fn namespace(mut self, ns: String) -> Self { - self.namespace = Some(ns); - self - } - - /// set ignore not found - pub fn ignore_not_found(mut self) -> Self { - self.ignore_not_found = true; - self - } - - /// set dry tun - pub fn dry_run(mut self) -> Self { - self.dry_run = true; - self - } - - /// set timeout - pub fn timeout(mut self, timeout: String) -> Self { - self.timeout = Some(timeout); - self - } -} - -impl From for Command { - fn from(arg: UninstallArg) -> Self { - let mut command = Command::new("helm"); - command.args(&["uninstall", &arg.release]); - - if let Some(namespace) = &arg.namespace { - command.args(&["--namespace", namespace]); - } - - if arg.dry_run { - command.arg("--dry-run"); - } - - for timeout in &arg.timeout { - command.arg("--timeout").arg(timeout); - } - - command - } -} - -/// Client to manage helm operations -#[derive(Debug)] -#[non_exhaustive] -pub struct HelmClient {} - -impl HelmClient { - /// Creates a Rust client to manage our helm needs. - /// - /// This only succeeds if the helm command can be found. - pub fn new() -> Result { - let output = Command::new("helm").arg("version").result()?; - - // Convert command output into a string - let out_str = String::from_utf8(output.stdout).map_err(HelmError::Utf8Error)?; - - // Check that the version command gives a version. - // In the future, we can parse the version string and check - // for compatible CLI client version. - if !out_str.contains("version") { - return Err(HelmError::HelmVersionNotFound(out_str)); - } - - // If checks succeed, create Helm client - Ok(Self {}) - } - - /// Installs the given chart under the given name. - /// - #[instrument(skip(self))] - pub fn install(&self, args: &InstallArg) -> Result<(), HelmError> { - let mut command = args.install(); - command.result()?; - Ok(()) - } - - /// Upgrades the given chart - #[instrument(skip(self))] - pub fn upgrade(&self, args: &InstallArg) -> Result<(), HelmError> { - let mut command = args.upgrade(); - command.result()?; - Ok(()) - } - - /// Uninstalls specified chart library - 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(()) - } - - /// Adds a new helm repo with the given chart name and chart location - #[instrument(skip(self))] - pub fn repo_add(&self, chart: &str, location: &str) -> Result<(), HelmError> { - Command::new("helm") - .args(&["repo", "add", chart, location]) - .result()?; - Ok(()) - } - - /// Updates the local helm repository - #[instrument(skip(self))] - pub fn repo_update(&self) -> Result<(), HelmError> { - Command::new("helm").args(&["repo", "update"]).result()?; - Ok(()) - } - - /// Searches the repo for the named helm chart - #[instrument(skip(self))] - pub fn search_repo(&self, chart: &str, version: &str) -> Result, HelmError> { - let mut command = Command::new("helm"); - command - .args(&["search", "repo", chart]) - .args(&["--version", version]) - .args(&["--output", "json"]); +mod helm_client; +mod install_arg; +mod installed_chart; +mod uninstall_arg; - let output = command.result()?; - - check_helm_stderr(output.stderr)?; - serde_json::from_slice(&output.stdout).map_err(HelmError::Serde) - } - - /// Get all the available versions - #[instrument(skip(self))] - pub fn versions(&self, chart: &str) -> Result, 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) - } - - /// Checks that a given version of a given chart exists in the repo. - #[instrument(skip(self))] - pub fn chart_version_exists(&self, name: &str, version: &str) -> Result { - let versions = self.search_repo(name, version)?; - let count = versions - .iter() - .filter(|chart| chart.name == name && chart.version == version) - .count(); - Ok(count > 0) - } - - /// Returns the list of installed charts by name - #[instrument(skip(self))] - pub fn get_installed_chart_by_name( - &self, - name: &str, - namespace: Option<&str>, - ) -> Result, HelmError> { - let exact_match = format!("^{}$", name); - let mut command = Command::new("helm"); - command - .arg("list") - .arg("--filter") - .arg(exact_match) - .arg("--output") - .arg("json"); - - match namespace { - Some(ns) => { - command.args(&["--namespace", ns]); - } - None => { - // Search all namespaces - command.args(&["-A"]); - } - } - - let output = command.result()?; - check_helm_stderr(output.stderr)?; - serde_json::from_slice(&output.stdout).map_err(HelmError::Serde) - } - - /// get helm package version - #[instrument(skip(self))] - pub fn get_helm_version(&self) -> Result { - 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()) - } -} - -/// Check for errors in Helm's stderr output -/// -/// Returns `Ok(())` if everything is fine, or `HelmError` if something is wrong -fn check_helm_stderr(stderr: Vec) -> Result<(), HelmError> { - if !stderr.is_empty() { - let stderr = String::from_utf8(stderr)?; - if stderr.contains("Kubernetes cluster unreachable") { - return Err(HelmError::FailedToConnect); - } - } - - Ok(()) -} - -/// A representation of a chart definition in a repo. -#[derive(Debug, Deserialize)] -pub struct Chart { - /// The chart name - name: String, - /// The chart version - version: String, -} - -impl Chart { - pub fn version(&self) -> &str { - &self.version - } - pub fn name(&self) -> &str { - &self.name - } -} +pub use crate::error::HelmError; -/// A representation of an installed chart. -#[derive(Debug, Deserialize)] -pub struct InstalledChart { - /// The chart name - pub name: String, - /// The version of the app this chart installed - pub app_version: String, - /// The chart revision - pub revision: String, - /// Date/time when the chart was last updated - pub updated: String, - /// Status of the installed chart - pub status: String, - /// The ID of the chart that is installed - pub chart: String, -} +pub use chart::Chart; +pub use helm_client::HelmClient; +pub use install_arg::InstallArg; +pub use installed_chart::InstalledChart; +pub use uninstall_arg::UninstallArg; #[cfg(test)] mod tests { diff --git a/src/uninstall_arg.rs b/src/uninstall_arg.rs new file mode 100644 index 0000000..ee10784 --- /dev/null +++ b/src/uninstall_arg.rs @@ -0,0 +1,68 @@ +use std::process::Command; + +/// Uninstaller Argument +#[derive(Debug)] +pub struct UninstallArg { + pub release: String, + pub namespace: Option, + pub ignore_not_found: bool, + pub dry_run: bool, + pub timeout: Option, +} + +impl UninstallArg { + pub fn new(release: String) -> Self { + Self { + release, + namespace: None, + ignore_not_found: false, + dry_run: false, + timeout: None, + } + } + + /// set namepsace + pub fn namespace(mut self, ns: String) -> Self { + self.namespace = Some(ns); + self + } + + /// set ignore not found + pub fn ignore_not_found(mut self) -> Self { + self.ignore_not_found = true; + self + } + + /// set dry tun + pub fn dry_run(mut self) -> Self { + self.dry_run = true; + self + } + + /// set timeout + pub fn timeout(mut self, timeout: String) -> Self { + self.timeout = Some(timeout); + self + } +} + +impl From for Command { + fn from(arg: UninstallArg) -> Self { + let mut command = Command::new("helm"); + command.args(&["uninstall", &arg.release]); + + if let Some(namespace) = &arg.namespace { + command.args(&["--namespace", namespace]); + } + + if arg.dry_run { + command.arg("--dry-run"); + } + + for timeout in &arg.timeout { + command.arg("--timeout").arg(timeout); + } + + command + } +}