Helmule MVP

Basic functionality is there, helmule can mirror helm chart with small
modifications
This commit is contained in:
2024-01-22 08:52:11 +01:00
parent 2f8170cf95
commit aabcb21f3b
53 changed files with 4817 additions and 5 deletions

21
lib/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "helmzoo_lib"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { workspace = true }
serde_json ={ workspace = true }
serde_yaml = { workspace = true }
tempfile = { workspace = true }
base64 = { workspace = true }
console = "0.15.8"
dialoguer = "0.11.0"
env_logger = "0.10.1"
indicatif = "0.17.7"
log = "0.4.20"
which = "6.0.0"
handlebars = "5.0.0"
chrono = "0.4.31"

144
lib/src/cli.rs Normal file
View File

@ -0,0 +1,144 @@
use std::{
error::Error,
fs::{self, read_dir},
path::{Path, PathBuf},
process::Command,
};
use which::which;
use crate::output::message_info;
pub fn cli_exec(command: String) -> Result<String, Box<dyn Error>> {
message_info(&format!("executing: {}", command));
let expect = format!("command has failed: {}", command);
let output = Command::new("sh")
.arg("-c")
.arg(command)
.output()
.expect(&expect);
let stderr = String::from_utf8_lossy(&output.stderr);
if !&output.status.success() {
return Err(Box::from(stderr));
};
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
pub fn cli_exec_from_dir(command: String, dir: String) -> Result<String, Box<dyn Error>> {
message_info(&format!("executing: {} from {}", command, dir));
let expect = format!("command has failed: {}", command);
let output = Command::new("sh")
.arg("-c")
.current_dir(dir)
.arg(command)
.output()
.expect(&expect);
let stderr = String::from_utf8_lossy(&output.stderr);
if !&output.status.success() {
return Err(Box::from(stderr));
};
let mut stdout = String::from_utf8_lossy(&output.stdout).to_string();
stdout.pop();
Ok(stdout)
}
// A helper that checks wheter all the required binaries are installed
pub fn check_prerequisites(bins: Vec<String>) -> Result<(), Box<dyn Error>> {
message_info(&"checking prerequisites".to_string());
for bin in bins {
message_info(&format!("checking {}", bin));
which(bin)?;
}
Ok(())
}
pub fn get_full_path(rel_path: String) -> Result<String, Box<dyn Error>> {
match PathBuf::from(&rel_path)
.canonicalize()?
.into_os_string()
.into_string()
{
Ok(path) => Ok(path),
Err(_) => Err(Box::from(format!(
"{} can't be converted into a full path",
rel_path
))),
}
}
pub fn get_full_path_dir(rel_path: String) -> Result<String, Box<dyn Error>> {
let res = match PathBuf::from(&rel_path).parent() {
Some(res) => res.canonicalize()?.into_os_string().into_string(),
None => PathBuf::from(&rel_path)
.canonicalize()?
.into_os_string()
.into_string(),
};
match res {
Ok(path) => Ok(path),
Err(_) => Err(Box::from(format!(
"{} can't be converted into a full path",
rel_path
))),
}
}
/// Copy files from source to destination recursively.
pub fn copy_recursively(
source: impl AsRef<Path>,
destination: impl AsRef<Path>,
) -> Result<(), Box<dyn Error>> {
for entry in read_dir(source)? {
let entry = entry?;
let filetype = entry.file_type()?;
if filetype.is_dir() {
copy_recursively(entry.path(), destination.as_ref().join(entry.file_name()))?;
} else {
message_info(&format!("trying to copy {:?}", entry.path()));
fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
pub fn is_path_relative(path: String) -> bool {
!path.starts_with('/')
}
#[cfg(test)]
mod tests {
use super::{cli_exec, cli_exec_from_dir};
use tempfile::TempDir;
#[test]
fn test_stderr() {
let command = ">&2 echo \"error\" && exit 1";
let test = cli_exec(command.to_string());
assert_eq!(test.err().unwrap().to_string(), "error\n".to_string());
}
#[test]
fn test_stdout() {
let command = "echo test";
let test = cli_exec(command.to_string());
assert_eq!(test.unwrap().to_string(), "test\n".to_string());
}
#[test]
fn test_stdout_current_dir() {
let dir = TempDir::new().unwrap();
let dir_str = dir.path().to_str().unwrap().to_string();
let command = "echo $PWD";
let test = cli_exec_from_dir(command.to_string(), dir_str.clone());
assert!(test.unwrap().to_string().contains(dir_str.as_str()));
}
#[test]
fn test_stderr_current_dir() {
let dir = TempDir::new().unwrap();
let dir_str = dir.path().to_str().unwrap().to_string();
let command = ">&2 echo \"error\" && exit 1";
let test = cli_exec_from_dir(command.to_string(), dir_str.clone());
assert_eq!(test.err().unwrap().to_string(), "error\n".to_string());
}
}

86
lib/src/config.rs Normal file
View File

@ -0,0 +1,86 @@
use std::{error::Error, ffi::OsStr, fs::File, path::Path};
use serde::de::DeserializeOwned;
pub trait ConfigImpl {
fn apply_includes(&mut self, config_path: String) -> Result<(), Box<dyn Error>>;
}
pub fn read_config<T: DeserializeOwned>(path: String) -> Result<T, Box<dyn Error>> {
let config_content = File::open(path.clone())?;
let config = match get_extension_from_filename(&path) {
Some(ext) => match ext {
"yaml" | "yml" => serde_yaml::from_reader(config_content)?,
_ => return Err(Box::from(format!("{} files are not supported", ext))),
},
None => return Err(Box::from("can't read file without extension")),
};
Ok(config)
}
fn get_extension_from_filename(filename: &str) -> Option<&str> {
Path::new(filename).extension().and_then(OsStr::to_str)
}
#[cfg(test)]
mod tests {
use super::{get_extension_from_filename, read_config};
use serde::{Deserialize, Serialize};
use std::{error::Error, fs::File, io::Write};
use tempfile::tempdir;
#[test]
fn test_extension_getter() {
let filepath = "/tmp/config.yaml";
let extension = get_extension_from_filename(filepath);
assert_eq!(extension, Some("yaml"));
}
#[test]
fn test_extension_getter_empty() {
let filepath = "/tmp/config";
let extension = get_extension_from_filename(filepath);
assert_eq!(extension, None);
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct DummyConfig {
string: String,
amounts: Vec<DummyProperty>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct DummyProperty {
amount: i32,
}
fn prepare_test_file(name: &str, data: &str) -> Result<String, Box<dyn Error>> {
let dir = tempdir()?;
let file_path = dir.into_path().join(&name);
let mut file = File::create(file_path.clone())?;
file.write_all(data.as_bytes())?;
let path = file_path.into_os_string().to_str().unwrap().to_string();
Ok(path)
}
#[test]
fn test_config_reader() -> Result<(), Box<dyn Error>> {
let content = "---
string: test
amounts:
- amount: 4
- amount: 5
";
let file_path = prepare_test_file("config.yaml", content)?;
let config_data: DummyConfig;
config_data = read_config(file_path)?;
let expected = DummyConfig {
string: "test".to_string(),
amounts: vec![DummyProperty { amount: 4 }, DummyProperty { amount: 5 }],
};
assert_eq!(expected, config_data);
Ok(())
}
}

327
lib/src/git.rs Normal file
View File

@ -0,0 +1,327 @@
use std::error::Error;
use serde::{Deserialize, Serialize};
use crate::cli::{cli_exec, cli_exec_from_dir};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GitOptions {
bin: String,
workdir: Option<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct CheckoutOptions {
// Checkout with -b if branch doesn't exist
pub create: bool,
#[serde(alias = "ref")]
pub git_ref: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct PushOptions {
pub rebase_to: Option<String>,
// If rebase, should be always set to true
pub force: bool,
pub brahcn: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct CommitOptions {
pub message: String,
pub add: bool,
}
impl GitOptions {
pub fn new(bin: String, workdir: Option<String>) -> Self {
Self { bin, workdir }
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Git {
pub url: String,
pub repo_path: String,
}
impl Git {
pub fn new(url: String, repo_path: String) -> Self {
Self { url, repo_path }
}
pub fn clone(&self, git_opts: GitOptions) -> Result<(), Box<dyn Error>> {
let cmd = format!("{} clone {} {}", git_opts.bin, self.url, self.repo_path);
match self.exec(cmd, git_opts.workdir.clone()) {
Ok(_) => Ok(()),
Err(err) => Err(err),
}
}
pub fn checkout(
&self,
git_opts: GitOptions,
opts: CheckoutOptions,
) -> Result<(), Box<dyn Error>> {
let cmd = format!(
"{} -C {} checkout {}",
git_opts.bin, self.repo_path, opts.git_ref
);
match self.exec(cmd, git_opts.workdir.clone()) {
Ok(_) => Ok(()),
Err(err) => match opts.create {
true => {
let cmd = format!(
"{} -C {} checkout -b {}",
git_opts.bin, self.repo_path, opts.git_ref
);
match self.exec(cmd, git_opts.workdir.clone()) {
Ok(_) => Ok(()),
Err(err) => Err(err),
}
}
false => Err(err),
},
}
}
pub fn commit(&self, git_opts: GitOptions, opts: CommitOptions) -> Result<(), Box<dyn Error>> {
if opts.add {
let cmd = format!("{} -C {} add .", git_opts.bin, self.repo_path);
let _ = self.exec(cmd, git_opts.workdir.clone())?;
}
let cmd = format!(
"{} diff --staged --quiet || {} -C {} commit -m \"{}\"",
git_opts.bin, git_opts.bin, self.repo_path, opts.message
);
match self.exec(cmd, git_opts.workdir.clone()) {
Ok(_) => Ok(()),
Err(err) => Err(err),
}
}
// TODO: Add tests for rebase and force
pub fn push(&self, git_opts: GitOptions, opts: PushOptions) -> Result<(), Box<dyn Error>> {
if let Some(branch) = opts.rebase_to {
let cmd = format!("{} -C {} rebase {}", git_opts.bin, self.repo_path, branch);
let _ = self.exec(cmd, git_opts.workdir.clone())?;
}
let mut args = String::new();
if opts.force {
args = format!("{} --force", args);
}
let cmd = format!(
"{} -C {} push --set-upstream origin {} {}",
git_opts.bin, self.repo_path, opts.brahcn, args
);
let _ = self.exec(cmd, git_opts.workdir.clone());
Ok(())
}
// TODO: Add tests
pub fn init(&self, git_opts: GitOptions) -> Result<(), Box<dyn Error>> {
let cmd = format!("{} -C {} init", git_opts.bin, self.repo_path);
let _ = self.exec(cmd, git_opts.workdir.clone())?;
let cmd = format!(
"{} -C {} remote add origin {}",
git_opts.bin, self.repo_path, self.url
);
let _ = self.exec(cmd, git_opts.workdir.clone())?;
Ok(())
}
fn exec(&self, cmd: String, workdir: Option<String>) -> Result<String, Box<dyn Error>> {
match workdir {
Some(workdir) => cli_exec_from_dir(cmd, workdir),
None => cli_exec(cmd),
}
}
}
#[cfg(test)]
mod tests {
use crate::cli::cli_exec_from_dir;
use crate::git::{CheckoutOptions, CommitOptions, Git, PushOptions};
use std::error::Error;
use std::path::Path;
use tempfile::tempdir;
use super::GitOptions;
fn prepare_a_repo() -> Result<String, Box<dyn Error>> {
let tmp_dir = tempdir()?
.into_path()
.into_os_string()
.into_string()
.unwrap();
cli_exec_from_dir("git init".to_string(), tmp_dir.clone())?;
Ok(tmp_dir)
}
fn prepare_a_workdir() -> Result<String, Box<dyn Error>> {
let tmp_dir = tempdir()?
.into_path()
.into_os_string()
.into_string()
.unwrap();
Ok(tmp_dir)
}
#[test]
fn test_pull_no_wd() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = format!("{}/test", tmp_dir);
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts)?;
let result = Path::new(&git_dir).exists();
assert!(result);
Ok(())
}
#[test]
fn test_pull_wd() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
println!("{}", tmp_dir.clone());
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts)?;
let result = Path::new(&format!("{}/{}", tmp_dir, git_dir)).exists();
assert!(result);
Ok(())
}
#[test]
fn test_checkout_no_create() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
cli_exec_from_dir("git checkout -b test".to_string(), git.url.clone())?;
cli_exec_from_dir(
"touch test.txt && git add . && git commit -m test".to_string(),
git.url.clone(),
)?;
cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?;
let checkout_options = CheckoutOptions {
create: false,
git_ref: "test".to_string(),
};
git.clone(git_opts.clone())?;
git.checkout(git_opts, checkout_options)?;
let result = cli_exec_from_dir(
"git rev-parse --abbrev-ref HEAD".to_string(),
format!("{}/{}", tmp_dir.clone(), git_dir.clone()),
)?;
assert_eq!(result, "test");
Ok(())
}
#[test]
fn test_checkout_no_create_err() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?;
cli_exec_from_dir(
"touch test.txt && git add . && git commit -m test".to_string(),
git.url.clone(),
)?;
git.clone(git_opts.clone())?;
let checkout_options = CheckoutOptions {
create: false,
git_ref: "test".to_string(),
};
let res = git.checkout(git_opts, checkout_options);
assert!(res.is_err());
Ok(())
}
#[test]
fn test_checkout_create() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?;
cli_exec_from_dir(
"touch test.txt && git add . && git commit -m test".to_string(),
git.url.clone(),
)?;
git.clone(git_opts.clone())?;
let checkout_options = CheckoutOptions {
create: true,
git_ref: "test".to_string(),
};
git.checkout(git_opts, checkout_options)?;
let result = cli_exec_from_dir(
"git rev-parse --abbrev-ref HEAD".to_string(),
format!("{}/{}", tmp_dir.clone(), git_dir.clone()),
)?;
assert_eq!(result, "test");
Ok(())
}
#[test]
fn test_commit() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone());
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts.clone())?;
let commit_options = CommitOptions {
message: "test commit".to_string(),
add: false,
};
cli_exec_from_dir("touch test.txt && git add .".to_string(), full_path.clone())?;
git.commit(git_opts, commit_options)?;
let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), full_path)?;
assert_eq!(result, "test commit\n");
Ok(())
}
#[test]
fn test_commit_add() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone());
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts.clone())?;
let commit_options = CommitOptions {
message: "test commit".to_string(),
add: true,
};
cli_exec_from_dir("touch test.txt".to_string(), full_path.clone())?;
git.commit(git_opts, commit_options)?;
let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), full_path)?;
assert_eq!(result, "test commit\n");
Ok(())
}
#[test]
fn test_push_no_rebase() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone());
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts.clone())?;
let push_options = PushOptions {
rebase_to: None,
force: false,
brahcn: "main".to_string(),
};
cli_exec_from_dir("git checkout -b main".to_string(), full_path.clone())?;
cli_exec_from_dir(
"touch test.txt && git add . && git commit -m 'test commit'".to_string(),
full_path.clone(),
)?;
git.push(git_opts, push_options)?;
cli_exec_from_dir("git checkout main".to_string(), git.url.clone())?;
let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), git.url)?;
assert_eq!(result, "test commit\n");
Ok(())
}
}

75
lib/src/helm/chart.rs Normal file
View File

@ -0,0 +1,75 @@
use std::error::Error;
use serde::{Deserialize, Serialize};
use super::repository::{Repository, RepositoryImpl};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)]
pub struct Chart {
// A name of the helm chart
pub name: String,
// A reference to repository by name
pub repository: String,
pub mirrors: Vec<String>,
// Versions to be mirrored
#[serde(default = "latest")]
pub version: String,
}
pub(crate) fn latest() -> String {
"latest".to_string()
}
impl Chart {
pub fn find_repo(
&self,
repositories: Vec<Repository>,
) -> Result<Box<dyn RepositoryImpl>, Box<dyn Error>> {
for repository in repositories {
if repository.name == self.repository {
if let Some(helm) = repository.helm {
return Ok(Box::from(helm));
} else if let Some(git) = repository.git {
return Ok(Box::from(git));
} else {
return Err(Box::from("unsupported kind of repository"));
}
}
}
//let err = error!("repo {} is not found in the repo list", self.repository);
let error_msg = format!("repo {} is not found in the repo list", self.repository);
Err(Box::from(error_msg))
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use crate::helm::{
chart::latest,
helm_repository::HelmRepo,
repository::{Repository, RepositoryImpl},
};
use super::Chart;
#[test]
fn test_find_repo() -> Result<(), Box<dyn Error>> {
let chart = Chart {
name: "test".to_string(),
repository: "test".to_string(),
mirrors: vec!["test".to_string()],
version: latest(),
};
let repo = Repository {
name: "test".to_string(),
helm: Some(HelmRepo {
url: "test.rocks".to_string(),
}),
};
let res = chart.find_repo(vec![repo])?;
assert_eq!(res.get_url(), "test.rocks".to_string());
Ok(())
}
}

View File

@ -0,0 +1,76 @@
use crate::cli::cli_exec_from_dir;
use crate::git::CheckoutOptions;
use crate::git::GitOptions;
use crate::{cli::cli_exec, helm::repository::Version};
use std::error::Error;
use std::fs::{self, rename};
use base64::{engine::general_purpose, Engine};
use serde::Deserialize;
use serde::Serialize;
use crate::git::Git;
use super::{chart::Chart, repository::RepositoryImpl};
// A struct that represents a git repo with a chart
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GitRepo {
pub url: String,
#[serde(alias = "ref")]
pub git_ref: String,
pub path: String,
#[serde(default = "default_git_bin")]
pub(crate) git_bin: String,
}
fn default_git_bin() -> String {
"git".to_string()
}
impl RepositoryImpl for GitRepo {
fn pull_chart(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>> {
let repo_name = general_purpose::STANDARD_NO_PAD.encode(self.get_url().clone());
let git_instance = Git::new(self.get_url(), repo_name.clone());
let git_opts = GitOptions::new(self.git_bin.clone(), Some(workdir_path.clone()));
git_instance.clone(git_opts.clone())?;
let checkout_opts = CheckoutOptions {
create: true,
git_ref: self.git_ref.clone(),
};
git_instance.checkout(git_opts, checkout_opts)?;
let old_dir_name = format!(
"{}/{}/{}/{}",
workdir_path,
repo_name,
self.path,
chart.name.clone()
);
let cmd = format!("helm show chart {}", old_dir_name);
let helm_stdout = cli_exec(cmd)?;
let new_dir_name: String;
match serde_yaml::from_str::<Version>(&helm_stdout) {
Ok(res) => {
new_dir_name = format!("{}/{}-{}", workdir_path, chart.name.clone(), res.version);
rename(old_dir_name, new_dir_name.clone())?;
}
Err(err) => return Err(Box::from(err)),
};
// Cleaning up
fs::remove_dir_all(format!("{}/{}", workdir_path, repo_name))?;
// Get the version
let cmd = "helm show chart . | yq '.version'".to_string();
let _version = cli_exec_from_dir(cmd, new_dir_name.clone())?;
Ok(new_dir_name)
}
fn get_url(&self) -> String {
self.url.clone()
}
}

View File

@ -0,0 +1,133 @@
use std::{error::Error, fs::rename};
use base64::{engine::general_purpose, Engine};
use serde::{Deserialize, Serialize};
use crate::cli::{cli_exec, cli_exec_from_dir};
use super::{
chart::Chart,
repository::{RepositoryImpl, Version},
};
// A struct that represents a regular helm repo
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct HelmRepo {
// A URL of the helm repository
pub url: String,
}
const LATEST_VERSION: &str = "latest";
impl RepositoryImpl for HelmRepo {
fn pull_chart(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>> {
match self.repo_kind_from_url(self.clone())? {
RepoKind::Default => self.pull_default(chart, workdir_path),
RepoKind::Oci => self.pull_oci(chart, workdir_path),
}
}
fn get_url(&self) -> String {
self.url.clone()
}
}
pub(crate) enum RepoKind {
Default,
Oci,
}
impl HelmRepo {
fn repo_kind_from_url(&self, repository: HelmRepo) -> Result<RepoKind, Box<dyn Error>> {
let prefix = repository
.url
.chars()
.take_while(|&ch| ch != ':')
.collect::<String>();
match prefix.as_str() {
"oci" => Ok(RepoKind::Oci),
"https" | "http" => Ok(RepoKind::Default),
_ => Err(Box::from(format!(
"repo kind is not defined by the prefix: {}",
prefix
))),
}
}
fn pull_oci(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>> {
let args = match chart.version.as_str() {
LATEST_VERSION => "".to_string(),
_ => format!("--version {}", chart.version.clone()),
};
let repo = match self.get_url().ends_with('/') {
true => {
let mut repo = self.get_url().clone();
repo.pop();
repo
}
false => self.get_url().clone(),
};
let cmd = format!(
"helm pull {}/{} {} --destination {} --untar",
repo, chart.name, args, workdir_path
);
cli_exec(cmd)?;
// Get the version
let cmd = format!("helm show chart {}/{}", workdir_path, chart.name);
let helm_stdout = cli_exec(cmd)?;
let old_dir_name = format!("{}/{}", workdir_path, chart.name);
let new_dir_name: String;
match serde_yaml::from_str::<Version>(&helm_stdout) {
Ok(res) => {
new_dir_name = format!("{}-{}", old_dir_name, res.version);
rename(old_dir_name, new_dir_name.clone())?;
}
Err(err) => return Err(Box::from(err)),
};
// TODO: Do we really need it?
let cmd = "helm show chart . | yq '.version'".to_string();
let _version = cli_exec_from_dir(cmd, new_dir_name.clone())?;
Ok(new_dir_name)
}
fn pull_default(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>> {
// Add repo and update
let repo_local_name = general_purpose::STANDARD_NO_PAD.encode(self.get_url());
let cmd = format!("helm repo add {} {}", repo_local_name, self.get_url());
cli_exec(cmd)?;
cli_exec("helm repo update".to_string())?;
let args = match chart.version.as_str() {
LATEST_VERSION => "".to_string(),
_ => format!("--version {}", chart.version.clone()),
};
let cmd = format!(
"helm pull {}/{} {} --destination {} --untar",
repo_local_name, chart.name, args, workdir_path
);
cli_exec(cmd)?;
// Get the version
let cmd = format!("helm show chart {}/{}", workdir_path, chart.name);
let helm_stdout = cli_exec(cmd)?;
let old_dir_name = format!("{}/{}", workdir_path, chart.name);
let new_dir_name: String;
match serde_yaml::from_str::<Version>(&helm_stdout) {
Ok(res) => {
new_dir_name = format!("{}-{}", old_dir_name, res.version);
rename(old_dir_name, new_dir_name.clone())?;
}
Err(err) => return Err(Box::from(err)),
};
//cleaning up
let cmd = format!("helm repo remove {}", repo_local_name);
cli_exec(cmd)?;
// TODO: Do we really need it?
let cmd = "helm show chart . | yq '.version'".to_string();
let _version = cli_exec_from_dir(cmd, new_dir_name.clone())?;
Ok(new_dir_name)
}
}

4
lib/src/helm/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod chart;
pub mod git_repository;
pub mod helm_repository;
pub mod repository;

View File

@ -0,0 +1,36 @@
use std::error::Error;
use serde::{Deserialize, Serialize};
use super::{chart::Chart, git_repository::GitRepo, helm_repository::HelmRepo};
// A struct that represents a helm repository
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Repository {
// A name of the repo. It's going to be used by tools
// to get a URL, so it can be any string
pub name: String,
pub helm: Option<HelmRepo>,
pub git: Option<GitRepo>,
}
// Supported kinds of helm repos
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) enum RepositoryKind {
// Regular helm repos and OCI
Helm,
// Git, it's not supposed to use helm-git plugin
// but instead it's just using git to get a repo
// and then look for charts in the repo
Git,
}
pub trait RepositoryImpl {
fn pull_chart(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>>;
fn get_url(&self) -> String;
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Version {
pub(crate) version: String,
}

7
lib/src/include.rs Normal file
View File

@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Include<T> {
pub path: String,
pub kind: T,
}

23
lib/src/lib.rs Normal file
View File

@ -0,0 +1,23 @@
pub mod cli;
pub mod config;
pub mod git;
pub mod helm;
pub mod include;
pub mod output;
pub mod template;
pub mod workdir;
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

18
lib/src/output.rs Normal file
View File

@ -0,0 +1,18 @@
use console::style;
use std::error::Error;
pub fn message_empty(msg: &str) {
println!(" {}", style(msg).blue());
}
pub fn message_info(msg: &str) {
let prefix = format!("{}", style("-->"));
let msg = format!("{} {}", prefix, msg,);
println!(" {}", style(msg).blue());
}
pub fn message_error(err: Box<dyn Error>) {
let prefix = format!("{}", style("!->").red());
let msg = format!("{} {}", prefix, err);
println!(" {}", style(msg).red());
}

55
lib/src/template.rs Normal file
View File

@ -0,0 +1,55 @@
use chrono::prelude::*;
use handlebars::{handlebars_helper, Handlebars};
use serde::Serialize;
handlebars_helper!(date_helper: | | Utc::now().format("%Y-%m-%d").to_string());
handlebars_helper!(time_helper: | | Utc::now().format("%H-%M-%S").to_string());
pub fn register_handlebars() -> Handlebars<'static> {
let mut handlebars = Handlebars::new();
handlebars.register_helper("date", Box::new(date_helper));
handlebars.register_helper("time", Box::new(time_helper));
handlebars
}
pub fn render<T>(string: String, data: &T) -> Result<String, Box<dyn std::error::Error>>
where
T: Serialize,
{
let mut reg = register_handlebars();
let tmpl_name = "template";
reg.register_template_string(tmpl_name, string)?;
let result = reg.render(tmpl_name, data)?;
Ok(result)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use chrono::Utc;
use crate::template::register_handlebars;
#[test]
fn test_date_helper() {
let mut reg = register_handlebars();
reg.register_template_string("test", "{{ date }}").unwrap();
let result = reg
.render("test", &HashMap::<String, String>::new())
.unwrap();
let expected = Utc::now().format("%Y-%m-%d").to_string();
assert_eq!(result, expected);
}
#[test]
fn test_time_helper() {
let mut reg = register_handlebars();
reg.register_template_string("test", "{{ time }}").unwrap();
let result = reg
.render("test", &HashMap::<String, String>::new())
.unwrap();
let expected = Utc::now().format("%H-%M-%S").to_string();
assert_eq!(result, expected);
}
}

56
lib/src/workdir.rs Normal file
View File

@ -0,0 +1,56 @@
use tempfile::tempdir;
use crate::output::message_info;
use std::{error::Error, fs::create_dir, path::PathBuf};
pub fn setup_workdir(path: Option<String>) -> Result<String, Box<dyn Error>> {
let path = match path {
Some(res) => {
message_info(&format!("trying to create a dir: {}", res));
match create_dir(res.clone()) {
Ok(_) => PathBuf::from(res),
Err(err) => {
let _msg = format!("couldn't create dir {}", res);
return Err(Box::from(err));
}
}
}
None => {
message_info("trying to create a temporary dir");
// I'm using into_path to prevent the dir from being removed
tempdir()?.into_path()
}
};
Ok(path.into_os_string().into_string().unwrap())
}
#[cfg(test)]
mod tests {
use std::{error::Error, fs::remove_dir_all, path::Path};
use tempfile::tempdir;
use crate::workdir::setup_workdir;
#[test]
fn test_temporary_dir() -> Result<(), Box<dyn Error>> {
let wd = setup_workdir(None)?;
let result = Path::new(&wd).exists();
assert!(result);
remove_dir_all(wd)?;
Ok(())
}
#[test]
fn test_specified_dir() -> Result<(), Box<dyn Error>> {
let dir = tempdir()?;
let path = dir.path().join("test").to_str().unwrap().to_string();
let wd = setup_workdir(Some(path.clone()))?;
let result = Path::new(&wd).exists();
assert!(result);
assert!(setup_workdir(Some(path)).is_err());
remove_dir_all(dir)?;
Ok(())
}
}