Init commit

This commit is contained in:
2024-01-10 12:33:05 +01:00
commit 09f81a5899
32 changed files with 2737 additions and 0 deletions

45
src/config/extension.rs Normal file
View File

@ -0,0 +1,45 @@
use std::{fs, path::Path};
use log::info;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Extension {
name: Option<String>,
target_dir: String,
source_dir: String,
}
impl Extension {
pub(crate) fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
let extension_name = match self.name.clone() {
Some(res) => res,
None => "Unnamed".to_string(),
};
info!("applying extension: '{}'", extension_name);
let target_dir = format!("{}/{}", chart_local_path, self.target_dir);
info!("trying to create a dir: {}", target_dir);
fs::create_dir(target_dir.clone())?;
info!("copying {} to {}", self.source_dir, target_dir);
copy_recursively(self.source_dir.clone(), target_dir)?;
Ok(())
}
}
/// Copy files from source to destination recursively.
pub fn copy_recursively(
source: impl AsRef<Path>,
destination: impl AsRef<Path>,
) -> Result<(), Box<dyn std::error::Error>> {
for entry in fs::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 {
info!("trying to copy {:?}", entry.path());
fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}

184
src/config/mod.rs Normal file
View File

@ -0,0 +1,184 @@
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::fs::File;
pub(crate) mod extension;
pub(crate) mod patch;
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Config {
pub(crate) repositories: Vec<Repository>,
pub(crate) charts: Vec<Chart>,
pub(crate) mirrors: Vec<Mirror>,
}
impl Config {
pub(crate) fn new(config_path: String) -> Result<Self, Box<dyn std::error::Error>> {
info!("reading the config file");
let config_content = File::open(config_path)?;
let config: Config = serde_yaml::from_reader(config_content)?;
Ok(config)
}
}
pub(crate) enum RepositoryKind {
Helm,
Git,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Repository {
// A name of the repository to be references by charts
pub(crate) name: String,
// Helm repository data
pub(crate) helm: Option<Helm>,
pub(crate) git: Option<Git>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Mirror {
pub(crate) name: String,
pub(crate) git: Option<GitMirror>,
pub(crate) custom_command: Option<CustomCommandsMirror>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct CustomCommandsMirror {
pub(crate) package: Vec<String>,
pub(crate) upload: Vec<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct GitMirror {
pub(crate) url: String,
pub(crate) path: Option<String>,
pub(crate) branch: String,
pub(crate) commit: Option<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Helm {
// A url of the helm repository
pub(crate) url: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Git {
pub(crate) url: String,
#[serde(alias = "ref")]
pub(crate) git_ref: String,
pub(crate) path: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Chart {
// A name of the helm chart
pub(crate) name: String,
// A reference to repository by name
pub(crate) repository: String,
pub(crate) mirrors: Vec<String>,
// Versions to be mirrored
pub(crate) version: Option<String>,
// A repository object
pub(crate) extensions: Option<Vec<extension::Extension>>,
pub(crate) patches: Option<Vec<patch::Patch>>,
#[serde(skip_serializing)]
pub(crate) repository_obj: Option<Repository>,
#[serde(skip_serializing)]
pub(crate) mirror_objs: Option<Vec<Mirror>>,
}
impl Chart {
pub(crate) fn populate_repository(
&mut self,
repositories: Vec<Repository>,
) -> Result<(), Box<dyn std::error::Error>> {
for repository in repositories {
if repository.name == self.repository {
self.repository_obj = Some(repository);
return Ok(());
}
}
//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);
return Err(Box::from(error_msg));
}
// TODO: Handle the "mirror not found" error
pub(crate) fn populate_mirrors(
&mut self,
mirrors: Vec<Mirror>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut mirror_objs: Vec<Mirror> = vec![];
for mirror_global in mirrors.clone() {
for mirror_name in self.mirrors.clone() {
if mirror_name == mirror_global.name.clone() {
mirror_objs.push(mirror_global.clone());
}
}
}
if mirror_objs.len() > 0 {
self.mirror_objs = Some(mirror_objs);
}
Ok(())
}
pub(crate) fn get_helm_repository_url(&self) -> String {
match self.repository_obj.clone() {
Some(res) => res.helm.unwrap().url,
None => {
warn!("repository object is not filled for chart {}", self.name);
return "".to_string();
}
}
}
pub(crate) fn get_git_repository_url(&self) -> String {
match self.repository_obj.clone() {
Some(res) => res.git.unwrap().url,
None => {
warn!("repository object is not filled for chart {}", self.name);
return "".to_string();
}
}
}
pub(crate) fn get_git_repository_ref(&self) -> String {
match self.repository_obj.clone() {
Some(res) => res.git.unwrap().git_ref,
None => {
warn!("repository object is not filled for chart {}", self.name);
return "".to_string();
}
}
}
pub(crate) fn get_git_repository_path(&self) -> String {
match self.repository_obj.clone() {
Some(res) => res.git.unwrap().path,
None => {
warn!("repository object is not filled for chart {}", self.name);
return "".to_string();
}
}
}
pub(crate) fn get_repo_kind(&self) -> Result<RepositoryKind, Box<dyn std::error::Error>> {
match &self.repository_obj {
Some(res) => {
if res.helm.is_some() {
return Ok(RepositoryKind::Helm);
} else if res.git.is_some() {
return Ok(RepositoryKind::Git);
} else {
return Err(Box::from("unknown repository kind is found"));
}
}
None => {
return Err(Box::from(
"repository object is not filled up for the chart",
));
}
}
}
}

200
src/config/patch.rs Normal file
View File

@ -0,0 +1,200 @@
use std::{
fs::{self, read_dir, remove_dir_all, File, OpenOptions},
io::Write,
path::{Path, PathBuf},
};
use log::{error, info};
use serde::{Deserialize, Serialize};
use crate::helpers::cli::{cli_exec, cli_exec_from_dir};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct RegexpPatch {
pub(crate) path: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct GitPatch {
path: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) enum YqOperations {
Add,
Delete,
Replace,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct YqPatch {
file: String,
op: YqOperations,
key: String,
value: Option<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct CustomCommandPatch {
commands: Vec<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Patch {
regexp: Option<RegexpPatch>,
git: Option<GitPatch>,
custom_command: Option<CustomCommandPatch>,
yq: Option<YqPatch>,
}
impl Patch {
pub(crate) fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
let patch_action = patch_action_from_definition(self.clone())?;
patch_action.apply(chart_local_path)
}
}
trait PatchInterface {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>>;
}
impl PatchInterface for YqPatch {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
let cmd = match self.op {
YqOperations::Add => format!(
"yq -i '{} += \"{}\"' {}",
self.key,
self.value.clone().unwrap(),
self.file
),
YqOperations::Delete => format!("yq -i \'del({})\' {}", self.key, self.file),
YqOperations::Replace => format!(
"yq e -i \'{} = \"{}\"\' {}",
self.key,
self.value.clone().unwrap(),
self.file
),
};
cli_exec_from_dir(cmd, chart_local_path)?;
Ok(())
}
}
impl PatchInterface for RegexpPatch {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
for entry in read_dir(self.path.clone())? {
let entry = entry?;
let filetype = entry.file_type()?;
if filetype.is_dir() {
error!(
"reading dirs is not supported yet, skipping {:?}",
entry.path()
);
} else {
info!("reading a patch file: {:?}", entry.path());
let config_content = File::open(entry.path())?;
for patch_des in serde_yaml::Deserializer::from_reader(config_content) {
let patch: crate::patch::regexp::RegexpPatch =
match crate::patch::regexp::RegexpPatch::deserialize(patch_des) {
Ok(patch) => patch,
Err(err) => return Err(Box::from(err)),
};
info!("applying patch: {}", patch.name);
let after = match patch.after {
Some(after) => after,
None => "".to_string(),
};
match patch.before {
Some(before) => {
let patch_regexp = regex::Regex::new(before.as_str())?;
for target in patch.targets {
let file_path = format!("{}/{}", chart_local_path, target);
let file_content = fs::read_to_string(file_path.clone())?;
let new_content =
patch_regexp.replace_all(file_content.as_str(), after.clone());
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(file_path.clone())?;
file.write(new_content.as_bytes())?;
}
}
None => {
for target in patch.targets {
let file_path = format!("{}/{}", chart_local_path, target);
let file_content = fs::read_to_string(file_path.clone())?;
let new_content = format!("{}\n{}", file_content, after);
let mut file = OpenOptions::new()
.write(true)
.append(false)
.open(file_path.clone())?;
file.write(new_content.as_bytes())?;
}
}
};
}
}
}
Ok(())
}
}
impl PatchInterface for GitPatch {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
if !is_git_repo(chart_local_path.clone()) {
init_git_repo(chart_local_path.clone())?;
};
let cmd = format!("git -C {} apply {}", chart_local_path, self.path);
cli_exec(cmd)?;
remove_dir_all(chart_local_path + "/.git")?;
Ok(())
}
}
impl PatchInterface for CustomCommandPatch {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
for cmd in self.commands.clone() {
cli_exec_from_dir(cmd, chart_local_path.clone())?;
}
Ok(())
}
}
fn patch_action_from_definition(
patch: Patch,
) -> Result<Box<dyn PatchInterface>, Box<dyn std::error::Error>> {
if let Some(regexp) = patch.regexp {
return Ok(Box::new(RegexpPatch { path: regexp.path }));
} else if let Some(git) = patch.git {
return Ok(Box::new(GitPatch {
path: {
let path = PathBuf::from(git.path);
let can_path = fs::canonicalize(&path).ok().unwrap();
can_path.into_os_string().into_string().ok().unwrap()
},
}));
} else if let Some(custom_command) = patch.custom_command {
return Ok(Box::new(CustomCommandPatch {
commands: custom_command.commands,
}));
} else if let Some(yq) = patch.yq {
if yq.op != YqOperations::Delete && yq.value.is_none() {
return Err(Box::from("yq patch of non kind 'delete' requires a value"));
};
return Ok(Box::from(yq));
} else {
return Err(Box::from("unknown patch type"));
};
}
fn is_git_repo(path: String) -> bool {
let dot_git_path = path + ".git";
Path::new(dot_git_path.as_str()).exists()
}
pub(crate) fn init_git_repo(path: String) -> Result<(), Box<dyn std::error::Error>> {
cli_exec(format!("git -C {} init .", path))?;
cli_exec(format!("git -C {} add .", path))?;
cli_exec(format!("git -C {} commit -m 'Init commit'", path))?;
Ok(())
}

66
src/helpers/cli.rs Normal file
View File

@ -0,0 +1,66 @@
use std::process::{Command, ExitStatus};
use log::info;
pub(crate) fn cli_exec(command: String) -> Result<String, Box<dyn std::error::Error>> {
info!("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(crate) fn cli_exec_from_dir(
command: String,
dir: String,
) -> Result<String, Box<dyn std::error::Error>> {
info!("executing: {}", command);
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)
}
#[cfg(test)]
mod tests {
use crate::helpers::cli::{cli_exec, cli_exec_from_dir};
#[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 = tempfile::tempdir().unwrap();
let command = "echo $PWD";
let dir_str = dir.into_path().into_os_string().into_string().unwrap();
let test = cli_exec_from_dir(command.to_string(), dir_str.clone());
assert!(test.unwrap().to_string().contains(dir_str.as_str()));
}
}

21
src/helpers/copy.rs Normal file
View File

@ -0,0 +1,21 @@
use std::{fs, path::Path};
use log::info;
/// Copy files from source to destination recursively.
pub(crate) fn copy_recursively(
source: impl AsRef<Path>,
destination: impl AsRef<Path>,
) -> Result<(), Box<dyn std::error::Error>> {
for entry in fs::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 {
info!("trying to copy {:?}", entry.path());
fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}

2
src/helpers/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub(crate) mod cli;
pub(crate) mod copy;

180
src/main.rs Normal file
View File

@ -0,0 +1,180 @@
pub(crate) mod config;
pub(crate) mod helpers;
pub(crate) mod mirror;
pub(crate) mod patch;
pub(crate) mod source;
use clap::Parser;
use log::{error, info};
use std::fs;
use std::{fs::create_dir, path::PathBuf, process::exit};
use tempfile::TempDir;
use crate::config::patch::init_git_repo;
/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Name of the working dir
#[arg(short, long)]
workdir: Option<String>,
/// Path to the configuration file
#[arg(short, long)]
config: String,
/// Dry run
#[arg(short, long, default_value = "false")]
dry_run: bool,
/// Init git patch. Use it if you want to create git patch for a chart
/// It's going to pull a chart and init a git repo there, so you can
/// apply changes and create a patch file
/// It's not going to try mirroring changes, but will apply extensions
/// and patches that are already defined
#[arg(long)]
init_git_patch: Option<Vec<String>>,
}
fn main() {
env_logger::init();
let args = Args::parse();
// Prepare the workdir
let workdir_path = match args.workdir {
Some(res) => match create_dir(res.clone()) {
Ok(_) => {
let path = PathBuf::from(res);
let can_path = fs::canonicalize(&path).ok().unwrap();
can_path.into_os_string().into_string().ok().unwrap()
}
Err(err) => {
error!("{}", err);
exit(1);
}
},
None => {
let tmp_dir = match TempDir::new() {
Ok(res) => res,
Err(err) => {
error!("{}", err);
exit(1);
}
};
match tmp_dir.path().to_str() {
Some(res) => res.to_string(),
None => {
exit(1);
}
}
}
};
// Read the config
let config = match config::Config::new(args.config) {
Ok(res) => res,
Err(err) => {
error!("{}", err);
exit(1);
}
};
for mut chart in config.clone().charts {
match chart.populate_repository(config.repositories.clone()) {
Ok(_) => {
info!("repo is populated for chart {}", chart.name);
}
Err(err) => {
error!("{}", err);
exit(1);
}
}
match chart.populate_mirrors(config.mirrors.clone()) {
Ok(_) => {
info!("mirrors arepopulated for chart {}", chart.name)
}
Err(err) => {
error!("{}", err);
exit(1);
}
}
let chart_repo = match source::repo_from_chart(chart.clone()) {
Ok(res) => res,
Err(err) => {
error!("{}", err);
exit(1);
}
};
match chart_repo.pull(workdir_path.clone()) {
Ok(res) => {
info!(
"succesfully pulled chart {} into {}",
chart.name.clone(),
res.path,
);
if let Some(extensions) = chart.extensions.clone() {
for extension in extensions {
if let Err(err) = extension.apply(res.clone().path) {
error!("{}", err);
exit(1);
}
}
}
if let Some(patches) = chart.patches.clone() {
for patch in patches {
if let Err(err) = patch.apply(res.clone().path) {
error!("{}", err);
exit(1);
}
}
}
if let Some(init_git_patch) = args.init_git_patch.clone() {
if init_git_patch.contains(&chart.name) {
info!(
"init git patch mode is enabled, go to {} to make your changes",
res.path
);
match init_git_repo(res.path) {
Ok(_) => {
info!("not mirroring, because of the init git patch mode");
}
Err(err) => {
error!("{}", err);
exit(1);
}
};
break;
}
}
if let Some(mirrors) = chart.mirror_objs.clone() {
for mirror_obj in mirrors {
match mirror::mirror_from_mirror_obj(mirror_obj.clone()) {
Ok(mirror) => {
match mirror.push(workdir_path.clone(), res.clone(), args.dry_run) {
Ok(_) => info!(
"mirrored {} to {}",
chart.name.clone(),
mirror_obj.name
),
Err(err) => {
error!("{}", err);
exit(1);
}
};
}
Err(err) => {
error!("{}", err);
exit(1);
}
}
}
}
}
Err(err) => {
error!("{}", err);
exit(1);
}
}
}
// Populate charts
// Download helm charts from config
// If workdir is not provided, create a temporary di
}

View File

@ -0,0 +1,33 @@
use crate::helpers::cli::cli_exec_from_dir;
use super::Target;
pub(crate) struct CustomCommands {
pub(crate) package: Vec<String>,
pub(crate) upload: Vec<String>,
}
impl Target for CustomCommands {
fn push(
&self,
workdir_path: String,
chart_local: crate::source::ChartLocal,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
for cmd_tmpl in self.package.clone() {
let mut reg = super::register_handlebars();
reg.register_template_string("cmd", cmd_tmpl)?;
let cmd = reg.render("cmd", &chart_local)?;
cli_exec_from_dir(cmd, workdir_path.clone())?;
}
if !dry_run {
for cmd_tmpl in self.upload.clone() {
let mut reg = super::register_handlebars();
reg.register_template_string("cmd", cmd_tmpl)?;
let cmd = reg.render("cmd", &chart_local)?;
cli_exec_from_dir(cmd, workdir_path.clone())?;
}
}
Ok(())
}
}

60
src/mirror/git.rs Normal file
View File

@ -0,0 +1,60 @@
use crate::{helpers::cli::cli_exec_from_dir, source::ChartLocal};
use dircpy::*;
use super::Target;
pub(crate) struct Git {
pub(crate) git_dir: String,
pub(crate) url: String,
pub(crate) path: String,
pub(crate) branch: String,
pub(crate) commit: Option<String>,
}
impl Target for Git {
fn push(
&self,
workdir_path: String,
chart_local: ChartLocal,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let cmd = format!("git clone {} {}", self.url, self.git_dir);
cli_exec_from_dir(cmd, workdir_path.clone())?;
let git_repo_path = format!("{}/{}", workdir_path, self.git_dir);
// Prepare branch
let mut reg = super::register_handlebars();
reg.register_template_string("branch", self.branch.clone())?;
let branch = reg.render("branch", &chart_local)?;
let cmd = format!("git checkout {}", branch);
if let Err(_) = cli_exec_from_dir(cmd, git_repo_path.clone()) {
let cmd = format!("git checkout -b {}", branch);
cli_exec_from_dir(cmd, git_repo_path.clone())?;
};
// Prepare path
reg.register_template_string("path", self.path.clone())?;
let path = reg.render("path", &chart_local)?;
let repo_local_full_path = format!("{}/{}", git_repo_path, path);
CopyBuilder::new(chart_local.path.clone(), repo_local_full_path.clone())
.overwrite_if_size_differs(true)
.run()?;
// Prepare the commit message
let commit_message = match self.commit.clone() {
Some(commit) => commit,
None => "helmuled {{ name }}-{{ version }}".to_string(),
};
reg.register_template_string("commit", commit_message.clone())?;
let commit = reg.render("commit", &chart_local)?;
let cmd = format!(
"git add . && git diff --staged --quiet || git commit -m '{}'",
commit
);
cli_exec_from_dir(cmd, repo_local_full_path.clone())?;
if !dry_run {
let cmd = format!("git push --set-upstream origin {}", branch);
cli_exec_from_dir(cmd, repo_local_full_path)?;
}
Ok(())
}
}

53
src/mirror/mod.rs Normal file
View File

@ -0,0 +1,53 @@
use chrono::prelude::*;
use handlebars::{handlebars_helper, Handlebars};
use time::{format_description::parse, OffsetDateTime};
use crate::{config::Mirror, source::ChartLocal};
pub(crate) mod custom_command;
pub(crate) mod git;
pub(crate) trait Target {
fn push(
&self,
workdir_path: String,
chart_local: ChartLocal,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>>;
}
pub(crate) fn mirror_from_mirror_obj(
mirror: Mirror,
) -> Result<Box<dyn Target>, Box<dyn std::error::Error>> {
if let Some(git) = mirror.git {
return Ok(Box::from(git::Git {
git_dir: mirror.name.clone(),
url: git.url,
path: match git.path {
Some(path) => path,
None => "".to_string(),
},
branch: git.branch,
commit: git.commit,
}));
} else if let Some(command) = mirror.custom_command {
return Ok(Box::from(custom_command::CustomCommands {
package: command.package,
upload: command.upload,
}));
}
Err(Box::from(format!(
"a kind is unknown for the mirror {}",
mirror.name
)))
}
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(crate) 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
}

1
src/patch/mod.rs Normal file
View File

@ -0,0 +1 @@
pub(crate) mod regexp;

9
src/patch/regexp.rs Normal file
View File

@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct RegexpPatch {
pub(crate) name: String,
pub(crate) targets: Vec<String>,
pub(crate) before: Option<String>,
pub(crate) after: Option<String>,
}

69
src/source/git.rs Normal file
View File

@ -0,0 +1,69 @@
use std::fs::{self, rename};
use crate::helpers::cli::{cli_exec, cli_exec_from_dir};
use base64::{engine::general_purpose, Engine as _};
use super::{ChartLocal, Repo};
pub(crate) struct Git {
git_url: String,
git_ref: String,
path: String,
pub(crate) chart: String,
}
impl From<crate::config::Chart> for Git {
fn from(value: crate::config::Chart) -> Self {
Git {
git_url: value.get_git_repository_url(),
git_ref: value.get_git_repository_ref(),
path: value.get_git_repository_path(),
chart: value.name,
}
}
}
impl Repo for Git {
fn pull(&self, workdir_path: String) -> Result<ChartLocal, Box<dyn std::error::Error>> {
let repo_local_name = general_purpose::STANDARD_NO_PAD.encode(self.git_url.clone());
let cmd = format!(
"git clone {} {}/{}",
self.git_url, workdir_path, repo_local_name
);
cli_exec(cmd)?;
let cmd = format!(
"git -C {}/{} checkout {}",
workdir_path, repo_local_name, self.git_ref
);
cli_exec(cmd)?;
let old_dir_name = format!(
"{}/{}/{}/{}",
workdir_path, repo_local_name, self.path, self.chart
);
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::<super::Version>(&helm_stdout) {
Ok(res) => {
new_dir_name = format!("{}/{}-{}", workdir_path, self.chart, 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_local_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(ChartLocal {
name: self.chart.clone(),
version,
path: new_dir_name,
repo_url: self.git_url.clone(),
})
}
}

105
src/source/helm.rs Normal file
View File

@ -0,0 +1,105 @@
use super::{ChartLocal, Repo};
use crate::helpers::cli::{cli_exec, cli_exec_from_dir};
use base64::{engine::general_purpose, Engine as _};
use std::fs::rename;
pub(crate) enum RepoKind {
Default,
Oci,
}
const LATEST_VERSION: &str = "latest";
pub(crate) struct Helm {
pub(crate) chart: String,
pub(crate) repository_url: String,
pub(crate) version: String,
}
impl From<crate::config::Chart> for Helm {
fn from(value: crate::config::Chart) -> Self {
Helm {
chart: value.name.clone(),
repository_url: value.get_helm_repository_url(),
version: match value.version {
Some(res) => res,
None => LATEST_VERSION.to_string(),
},
}
}
}
impl Helm {
fn repo_kind_from_url(&self) -> Result<RepoKind, Box<dyn std::error::Error>> {
let prefix = self
.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_default(&self, workdir_path: String) -> Result<ChartLocal, Box<dyn std::error::Error>> {
// Add repo and update
let repo_local_name = general_purpose::STANDARD_NO_PAD.encode(self.repository_url.clone());
let cmd = format!("helm repo add {} {}", repo_local_name, self.repository_url);
cli_exec(cmd)?;
cli_exec("helm repo update".to_string())?;
let args = match self.version.as_str() {
LATEST_VERSION => "".to_string(),
_ => format!("--version {}", self.version.clone()),
};
let cmd = format!(
"helm pull {}/{} {} --destination {} --untar",
repo_local_name, &self.chart, args, workdir_path
);
cli_exec(cmd)?;
// Get the version
let cmd = format!("helm show chart {}/{}", workdir_path, &self.chart);
let helm_stdout = cli_exec(cmd)?;
let old_dir_name = format!("{}/{}", workdir_path, &self.chart);
let new_dir_name: String;
match serde_yaml::from_str::<super::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)?;
let cmd = "helm show chart . | yq '.version'".to_string();
let version = cli_exec_from_dir(cmd, new_dir_name.clone())?;
Ok(ChartLocal {
name: self.chart.clone(),
version,
path: new_dir_name,
repo_url: self.repository_url.clone(),
})
}
}
impl Repo for Helm {
fn pull(&self, workdir_path: String) -> Result<ChartLocal, Box<dyn std::error::Error>> {
let repository_kind = self.repo_kind_from_url()?;
let path = match repository_kind {
RepoKind::Default => self.pull_default(workdir_path)?,
RepoKind::Oci => {
todo!()
}
};
Ok(path)
}
}

46
src/source/mod.rs Normal file
View File

@ -0,0 +1,46 @@
use crate::config::Chart;
use serde::{Deserialize, Serialize};
pub(crate) mod git;
pub(crate) mod helm;
#[derive(Deserialize, Serialize, Clone)]
pub(crate) struct ChartLocal {
pub(crate) name: String,
pub(crate) version: String,
pub(crate) path: String,
pub(crate) repo_url: String,
}
impl ChartLocal {
pub(crate) fn test(&self) -> String {
"test-me-if-you-can".to_string()
}
}
pub(crate) trait Repo {
fn pull(&self, workdir_path: String) -> Result<ChartLocal, Box<dyn std::error::Error>>;
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Version {
pub(crate) version: String,
}
pub(crate) fn repo_from_chart(chart: Chart) -> Result<Box<dyn Repo>, Box<dyn std::error::Error>> {
match chart.get_repo_kind() {
Ok(res) => {
return match res {
crate::config::RepositoryKind::Helm => {
let helm: helm::Helm = chart.into();
Ok(Box::new(helm))
}
crate::config::RepositoryKind::Git => {
let git: git::Git = chart.into();
Ok(Box::new(git))
}
}
}
Err(err) => return Err(Box::from(err)),
};
}