Helmule MVP
Basic functionality is there, helmule can mirror helm chart with small modifications
This commit is contained in:
21
lib/Cargo.toml
Normal file
21
lib/Cargo.toml
Normal 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
144
lib/src/cli.rs
Normal 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
86
lib/src/config.rs
Normal 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
327
lib/src/git.rs
Normal 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
75
lib/src/helm/chart.rs
Normal 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(())
|
||||
}
|
||||
}
|
76
lib/src/helm/git_repository.rs
Normal file
76
lib/src/helm/git_repository.rs
Normal 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()
|
||||
}
|
||||
}
|
133
lib/src/helm/helm_repository.rs
Normal file
133
lib/src/helm/helm_repository.rs
Normal 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
4
lib/src/helm/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod chart;
|
||||
pub mod git_repository;
|
||||
pub mod helm_repository;
|
||||
pub mod repository;
|
36
lib/src/helm/repository.rs
Normal file
36
lib/src/helm/repository.rs
Normal 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
7
lib/src/include.rs
Normal 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
23
lib/src/lib.rs
Normal 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
18
lib/src/output.rs
Normal 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
55
lib/src/template.rs
Normal 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
56
lib/src/workdir.rs
Normal 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(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user