Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/repo add with optionals #15

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions src/chart.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
181 changes: 181 additions & 0 deletions src/helm_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use std::process::Command;
use fluvio_command::CommandExt;
use tracing::{instrument, warn};

use super::InstallArg;
use super::UninstallArg;
use super::HelmError;
use super::Chart;
use super::InstalledChart;
use super::RepoAddArg;

/// 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<Self, HelmError> {
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(())
}

pub fn repo_add_with_optionals(&self, optionals: RepoAddArg) -> Result<(), HelmError> {
let mut command: Command = optionals.into();
command.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<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)
}

/// Get all the available versions
#[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)
}

/// 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<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)
}

/// 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<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)
}

/// get helm package version
#[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())
}
}

/// 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<u8>) -> Result<(), HelmError> {
if !stderr.is_empty() {
let stderr = String::from_utf8(stderr)?;
if stderr.contains("Kubernetes cluster unreachable") {
return Err(HelmError::FailedToConnect);
}
}

Ok(())
}
136 changes: 136 additions & 0 deletions src/install_arg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use std::path::PathBuf;
use std::process::Command;

/// Installer Argument
#[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,
}
}

/// set chart version
pub fn version<S: Into<String>>(mut self, version: S) -> Self {
self.version = Some(version.into());
self
}

/// set namepsace
pub fn namespace<S: Into<String>>(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<K: Into<String>, V: Into<String>>(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<PathBuf>) -> 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 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
}
}

18 changes: 18 additions & 0 deletions src/installed_chart.rs
Original file line number Diff line number Diff line change
@@ -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,
}
Loading