From 61b647d9ce2692036ba7ffba6567f043a0ca3fa5 Mon Sep 17 00:00:00 2001 From: Dag Andersen Date: Mon, 2 Dec 2024 14:48:40 -0700 Subject: [PATCH] Delete cluster on error (#71) --- Cargo.lock | 34 ++--- Cargo.toml | 8 +- src/argo_resource.rs | 302 +++++++++++++++++++++++++++++++++++++++++++ src/argocd.rs | 112 ++++++++-------- src/branch.rs | 34 +++++ src/diff.rs | 51 ++++---- src/error.rs | 27 ++++ src/extract.rs | 107 +++++++-------- src/filter_apps.rs | 126 ------------------ src/kind.rs | 72 ++++++----- src/main.rs | 294 ++++++++++++++++++----------------------- src/minikube.rs | 58 +++++---- src/no_apps_found.rs | 2 +- src/parsing.rs | 226 ++++++++++---------------------- src/selector.rs | 69 ++++++++++ src/utils.rs | 47 ++++--- 16 files changed, 880 insertions(+), 689 deletions(-) create mode 100644 src/argo_resource.rs create mode 100644 src/branch.rs create mode 100644 src/error.rs delete mode 100644 src/filter_apps.rs create mode 100644 src/selector.rs diff --git a/Cargo.lock b/Cargo.lock index d078f32..2d3b17d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ [[package]] name = "argocd-diff-preview" -version = "0.0.24" +version = "0.0.25" dependencies = [ "base64", "env_logger", @@ -395,9 +395,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -491,7 +491,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.58", + "syn 2.0.90", ] [[package]] @@ -502,22 +502,22 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.90", ] [[package]] @@ -528,14 +528,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.131" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67d42a0bd4ac281beff598909bb56a86acaf979b84483e1c79c10dcaf98f8cf3" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -624,9 +624,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -644,9 +644,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -668,7 +668,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.90", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ee89443..c624e2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "argocd-diff-preview" -version = "0.0.24" +version = "0.0.25" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = {version="1.40.0",features = ["full"]} +tokio = {version="1.41.1",features = ["full"]} base64 = "0.22.1" -serde = "1.0.210" +serde = "1.0.215" serde_yaml = "0.9.33" -serde_json = "1.0.131" +serde_json = "1.0.133" walkdir = "2.5.0" schemars = "0.8.21" structopt = { version = "0.3" } diff --git a/src/argo_resource.rs b/src/argo_resource.rs new file mode 100644 index 0000000..fa80ac6 --- /dev/null +++ b/src/argo_resource.rs @@ -0,0 +1,302 @@ +use log::{debug, error, warn}; +use regex::Regex; +use std::error::Error; + +use crate::{parsing::K8sResource, selector::Operator, Selector}; + +const ANNOTATION_WATCH_PATTERN: &str = "argocd-diff-preview/watch-pattern"; +const ANNOTATION_IGNORE: &str = "argocd-diff-preview/ignore"; + +#[derive(PartialEq)] +pub enum ApplicationKind { + Application, + ApplicationSet, +} + +pub struct ArgoResource { + pub file_name: String, + pub yaml: serde_yaml::Value, + pub kind: ApplicationKind, + pub name: String, +} + +impl std::fmt::Display for ArgoResource { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", serde_yaml::to_string(&self.yaml).unwrap()) + } +} + +impl PartialEq for ArgoResource { + fn eq(&self, other: &Self) -> bool { + self.yaml == other.yaml + } +} + +impl ArgoResource { + pub fn set_namespace(mut self, namespace: &str) -> ArgoResource { + self.yaml["metadata"]["namespace"] = serde_yaml::Value::String(namespace.to_string()); + self + } + + pub fn set_project_to_default(mut self) -> Result> { + let spec = match self.kind { + ApplicationKind::Application => self.yaml["spec"].as_mapping_mut(), + ApplicationKind::ApplicationSet => { + self.yaml["spec"]["template"]["spec"].as_mapping_mut() + } + }; + + match spec { + None => Err(format!("No 'spec' key found in Application: {}", self.name).into()), + Some(spec) => { + spec["project"] = serde_yaml::Value::String("default".to_string()); + Ok(self) + } + } + } + + pub fn point_destination_to_in_cluster(mut self) -> Result> { + let spec = match self.kind { + ApplicationKind::Application => self.yaml["spec"].as_mapping_mut(), + ApplicationKind::ApplicationSet => { + self.yaml["spec"]["template"]["spec"].as_mapping_mut() + } + }; + + match spec { + None => Err(format!("No 'spec' key found in Application: {}", self.name).into()), + Some(spec) if spec.contains_key("destination") => { + spec["destination"]["name"] = serde_yaml::Value::String("in-cluster".to_string()); + spec["destination"] + .as_mapping_mut() + .map(|a| a.remove("server")); + Ok(self) + } + Some(_) => Err(format!( + "No 'spec.destination' key found in Application: {}", + self.name + ) + .into()), + } + } + + pub fn remove_sync_policy(mut self) -> ArgoResource { + let spec = match self.kind { + ApplicationKind::Application => self.yaml["spec"].as_mapping_mut(), + ApplicationKind::ApplicationSet => { + self.yaml["spec"]["template"]["spec"].as_mapping_mut() + } + }; + match spec { + Some(spec) => { + spec.remove("syncPolicy"); + } + None => debug!( + "Can't remove 'syncPolicy' because 'spec' key not found in file: {}", + self.file_name + ), + } + self + } + + pub fn redirect_sources( + mut self, + repo: &str, + branch: &str, + ) -> Result> { + let spec = match self.kind { + ApplicationKind::Application => self.yaml["spec"].as_mapping_mut(), + ApplicationKind::ApplicationSet => { + self.yaml["spec"]["template"]["spec"].as_mapping_mut() + } + }; + + match spec { + None => Err(format!("No 'spec' key found in Application: {}", self.name).into()), + Some(spec) if spec.contains_key("source") => { + if spec["source"]["chart"].as_str().is_some() { + return Ok(self); + } + match spec["source"]["repoURL"].as_str() { + Some(url) if url.to_lowercase().contains(&repo.to_lowercase()) => { + spec["source"]["targetRevision"] = + serde_yaml::Value::String(branch.to_string()); + } + _ => debug!( + "Found no 'repoURL' under spec.source in file: {}", + self.file_name + ), + } + Ok(self) + } + Some(spec) if spec.contains_key("sources") => { + if let Some(sources) = spec["sources"].as_sequence_mut() { + for source in sources { + if source["chart"].as_str().is_some() { + continue; + } + match source["repoURL"].as_str() { + Some(url) if url.to_lowercase().contains(&repo.to_lowercase()) => { + source["targetRevision"] = + serde_yaml::Value::String(branch.to_string()); + } + _ => debug!( + "Found no 'repoURL' under spec.sources[] in file: {}", + self.file_name + ), + } + } + } + Ok(self) + } + Some(_) => Err(format!( + "No 'spec.source' or 'spec.sources' key found in Application: {}", + self.name + ) + .into()), + } + } + + pub fn from_k8s_resource(k8s_resource: K8sResource) -> Option { + let kind = k8s_resource.yaml["kind"] + .as_str() + .and_then(|kind| match kind { + "Application" => Some(ApplicationKind::Application), + "ApplicationSet" => Some(ApplicationKind::ApplicationSet), + _ => None, + })?; + + match k8s_resource.yaml["metadata"]["name"].as_str() { + Some(name) => Some(ArgoResource { + kind, + file_name: k8s_resource.file_name, + name: name.to_string(), + yaml: k8s_resource.yaml, + }), + _ => None, + } + } + + pub fn filter( + self, + selector: &Option>, + files_changed: &Option>, + ignore_invalid_watch_pattern: bool, + ) -> Option { + // check if the application should be ignored + if self.yaml["metadata"]["annotations"][ANNOTATION_IGNORE].as_str() == Some("true") { + debug!( + "Ignoring application {:?} due to '{}=true' in file: {}", + self.name, ANNOTATION_IGNORE, self.file_name + ); + return None; + } + + // loop over labels and check if the selector matches + if let Some(selector) = selector { + let labels: Vec<(&str, &str)> = { + match self.yaml["metadata"]["labels"].as_mapping() { + Some(m) => m + .iter() + .flat_map(|(k, v)| Some((k.as_str()?, v.as_str()?))) + .collect(), + None => Vec::new(), + } + }; + let selected = selector.iter().all(|l| match l.operator { + Operator::Eq => labels.iter().any(|(k, v)| k == &l.key && v == &l.value), + Operator::Ne => labels.iter().all(|(k, v)| k != &l.key || v != &l.value), + }); + if !selected { + debug!( + "Ignoring application {:?} due to label selector mismatch in file: {}", + self.name, self.file_name + ); + return None; + } else { + debug!( + "Selected application {:?} due to label selector match in file: {}", + self.name, self.file_name + ); + } + } + + // Check watch pattern annotation + let pattern_annotation = + self.yaml["metadata"]["annotations"][ANNOTATION_WATCH_PATTERN].as_str(); + let list_of_regex_results = pattern_annotation.map(|s| { + s.split(',') + .map(|s| Regex::new(s.trim())) + .collect::>>() + }); + + // Return early if a regex pattern is invalid + if let Some(pattern_vec) = &list_of_regex_results { + if let Some(p) = pattern_vec.iter().filter_map(|r| r.as_ref().err()).next() { + if ignore_invalid_watch_pattern { + warn!("๐Ÿšจ Ignoring application {:?} due to invalid regex pattern in '{}' ({}) - Error: {}", + self.name, + pattern_annotation.unwrap_or("unknown"), + self.file_name, + p); + } else { + error!( + "๐Ÿšจ Application {:?} has an invalid regex pattern in '{}' ({}) - Error: {}", + self.name, + pattern_annotation.unwrap_or("unknown"), + self.file_name, + p + ); + panic!("Invalid regex pattern in annotation"); + } + } + } + + let patterns: Option> = + list_of_regex_results.map(|v| v.into_iter().flat_map(|r| r.ok()).collect()); + + match (files_changed, patterns) { + (None, _) => {} + // Check if the application changed. + (Some(files_changed), _) if files_changed.contains(&self.file_name) => { + debug!( + "Selected application {:?} due to file change in file: {}", + self.name, self.file_name + ); + } + // Check if the application changed and the regex pattern matches. + (Some(files_changed), Some(pattern)) + if files_changed + .iter() + .any(|f| pattern.iter().any(|r| r.is_match(f))) => + { + debug!( + "Selected application {:?} due to regex pattern '{}' matching changed files", + self.name, + pattern + .iter() + .map(|r| r.as_str()) + .collect::>() + .join(", "), + ); + } + (_, Some(pattern)) => { + debug!( + "Ignoring application {:?} due to regex pattern '{}' not matching changed files", + self.name, + pattern.iter().map(|r| r.as_str()).collect::>().join(", "), + ); + return None; + } + (_, None) => { + debug!( + "Ignoring application {:?} due to missing '{}' annotation ({})", + self.name, &ANNOTATION_WATCH_PATTERN, self.file_name + ); + return None; + } + } + + Some(self) + } +} diff --git a/src/argocd.rs b/src/argocd.rs index da3c992..102c4d1 100644 --- a/src/argocd.rs +++ b/src/argocd.rs @@ -1,7 +1,10 @@ -use crate::run_command; +use crate::{ + error::{CommandError, CommandOutput}, + utils::run_command, +}; use base64::prelude::*; use log::{debug, error, info}; -use std::{error::Error, process::Output}; +use std::error::Error; pub struct ArgoCDOptions<'a> { pub version: Option<&'a str>, @@ -10,15 +13,11 @@ pub struct ArgoCDOptions<'a> { const CONFIG_PATH: &str = "argocd-config"; -pub async fn create_namespace() -> Result<(), Box> { - match run_command("kubectl create ns argocd", None).await { - Ok(_) => (), - Err(e) => { - error!("โŒ Failed to create namespace argocd"); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } - } - +pub fn create_namespace() -> Result<(), Box> { + run_command("kubectl create ns argocd", None).map_err(|e| { + error!("โŒ Failed to create namespace argocd"); + CommandError::new(e) + })?; debug!("๐Ÿฆ‘ Namespace argocd created successfully"); Ok(()) } @@ -51,18 +50,14 @@ pub async fn install_argo_cd(options: ArgoCDOptions<'_>) -> Result<(), Box (), - Err(e) => { - error!("โŒ Failed to add argo repo"); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } - } + .map_err(|e| { + error!("โŒ Failed to add argo repo"); + CommandError::new(e) + })?; let helm_install_command = format!( "helm install argocd argo/argo-cd -n argocd {} {} {}", @@ -74,25 +69,24 @@ pub async fn install_argo_cd(options: ArgoCDOptions<'_>) -> Result<(), Box (), - Err(e) => { - error!("โŒ Failed to install Argo CD"); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } - } + run_command(&helm_install_command, None).map_err(|e| { + error!("โŒ Failed to install Argo CD"); + CommandError::new(e) + })?; info!("๐Ÿฆ‘ Waiting for Argo CD to start..."); // wait for argocd-server to be ready - run_command( + match run_command( "kubectl wait --for=condition=available deployment/argocd-server -n argocd --timeout=300s", None, - ) - .await - .expect("failed to wait for argocd-server"); - - info!("๐Ÿฆ‘ Argo CD is now available"); + ) { + Ok(_) => info!("๐Ÿฆ‘ Argo CD is now available"), + Err(_) => { + error!("โŒ Failed to wait for argocd-server"); + return Err("Failed to wait for argocd-server".to_string().into()); + } + } info!("๐Ÿฆ‘ Logging in to Argo CD through CLI..."); @@ -102,16 +96,16 @@ pub async fn install_argo_cd(options: ArgoCDOptions<'_>) -> Result<(), Box = None; + let mut password_encoded: Option = None; let mut counter = 0; while password_encoded.is_none() { - password_encoded = match run_command(&command, None).await { + password_encoded = match run_command(command, None) { Ok(a) => Some(a), - Err(e) => { - if counter == 5 { - error!("โŒ Failed to get secret {}", secret_name); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } + Err(e) if counter == 5 => { + error!("โŒ Failed to get secret {}", secret_name); + return Err(Box::new(CommandError::new(e))); + } + Err(_) => { counter += 1; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; debug!("โณ Retrying to get secret {}", secret_name); @@ -120,11 +114,15 @@ pub async fn install_argo_cd(options: ArgoCDOptions<'_>) -> Result<(), Box) -> Result<(), Box debug!( - "๐Ÿ”ง Configmap argocd-cmd-params-cm and argocd-cm:\n{}\n{}", - command, - String::from_utf8_lossy(&o.stdout) + "๐Ÿ”ง ConfigMap argocd-cmd-params-cm and argocd-cm:\n{}\n{}", + command, &o.stdout ), Err(e) => { - error!("โŒ Failed to get configmap"); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) + error!("โŒ Failed to get ConfigMaps"); + return Err(Box::new(CommandError::new(e))); } } } diff --git a/src/branch.rs b/src/branch.rs new file mode 100644 index 0000000..2a36223 --- /dev/null +++ b/src/branch.rs @@ -0,0 +1,34 @@ +pub enum BranchType { + Base, + Target, +} + +pub struct Branch { + pub name: String, + pub branch_type: BranchType, +} + +impl Branch { + pub fn app_file(&self) -> &'static str { + match self.branch_type { + BranchType::Base => "apps_base_branch.yaml", + BranchType::Target => "apps_target_branch.yaml", + } + } + + pub fn folder_name(&self) -> &str { + match self.branch_type { + BranchType::Base => "base-branch", + BranchType::Target => "target-branch", + } + } +} + +impl std::fmt::Display for BranchType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + BranchType::Base => write!(f, "base"), + BranchType::Target => write!(f, "target"), + } + } +} diff --git a/src/diff.rs b/src/diff.rs index eee6541..aa8d6fd 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -1,13 +1,14 @@ +use crate::error::CommandOutput; use crate::utils::run_command; use crate::Branch; use log::{debug, info}; +use std::error::Error; use std::fs; -use std::{error::Error, process::Output}; -pub async fn generate_diff( +pub fn generate_diff( output_folder: &str, - base_branch_name: &str, - target_branch_name: &str, + base_branch: &Branch, + target_branch: &Branch, diff_ignore: Option, line_count: Option, max_char_count: Option, @@ -16,7 +17,7 @@ pub async fn generate_diff( info!( "๐Ÿ”ฎ Generating diff between {} and {}", - base_branch_name, target_branch_name + base_branch.name, target_branch.name ); let patterns_to_ignore = match diff_ignore { @@ -24,27 +25,25 @@ pub async fn generate_diff( None => "".to_string(), }; - let parse_diff_output = |output: Result| -> String { - let o = match output { - Err(e) if !e.stderr.is_empty() => panic!( - "Error running diff command with error: {}", - String::from_utf8_lossy(&e.stderr) - ), - Ok(e) => String::from_utf8_lossy(&e.stdout).trim_end().to_string(), - Err(e) => String::from_utf8_lossy(&e.stdout).trim_end().to_string(), + let parse_diff_output = + |output: Result| -> Result> { + let o = match output { + Err(e) if !e.stderr.trim().is_empty() => { + return Err(format!("Error running command: {}", e.stderr).into()) + } + Ok(e) => e.stdout.trim_end().to_string(), + Err(e) => e.stdout.trim_end().to_string(), + }; + if o.trim().is_empty() { + Ok("No changes found".to_string()) + } else { + Ok(o) + } }; - if o.trim().is_empty() { - "No changes found".to_string() - } else { - o - } - }; let summary_diff_command = format!( "git --no-pager diff --compact-summary --no-index {} {} {}", - patterns_to_ignore, - Branch::Base, - Branch::Target + patterns_to_ignore, base_branch.branch_type, target_branch.branch_type ); debug!( @@ -53,19 +52,19 @@ pub async fn generate_diff( ); let summary_as_string = - parse_diff_output(run_command(&summary_diff_command, Some(output_folder)).await); + parse_diff_output(run_command(&summary_diff_command, Some(output_folder)))?; let diff_command = &format!( "git --no-pager diff --no-prefix -U{} --no-index {} {} {}", line_count.unwrap_or(10), patterns_to_ignore, - Branch::Base, - Branch::Target + base_branch.branch_type, + target_branch.branch_type, ); debug!("Getting diff with command: {}", diff_command); - let diff_as_string = parse_diff_output(run_command(diff_command, Some(output_folder)).await); + let diff_as_string = parse_diff_output(run_command(diff_command, Some(output_folder)))?; let remaining_max_chars = max_diff_message_char_count - markdown_template_length() - summary_as_string.len(); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ccdedd3 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,27 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub struct CommandOutput { + pub stdout: String, + pub stderr: String, +} + +#[derive(Debug)] +pub struct CommandError { + stderr: String, +} + +impl CommandError { + pub fn new(s: CommandOutput) -> Self { + CommandError { stderr: s.stderr } + } +} + +impl Error for CommandError {} + +impl fmt::Display for CommandError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.stderr) + } +} diff --git a/src/extract.rs b/src/extract.rs index b2abfd0..777ac59 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1,9 +1,10 @@ -use crate::utils::run_command; -use crate::{apply_manifest, apps_file, Branch}; +use crate::error::CommandError; +use crate::utils::{run_command, spawn_command}; +use crate::{apply_manifest, Branch}; use log::{debug, error, info}; +use serde_yaml::Value; use std::collections::HashSet; use std::fs; -use std::process::{Command, Stdio}; use std::{collections::BTreeMap, error::Error}; static ERROR_MESSAGES: [&str; 10] = [ @@ -30,22 +31,22 @@ static TIMEOUT_MESSAGES: [&str; 7] = [ ]; pub async fn get_resources( - branch_type: &Branch, + branch: &Branch, timeout: u64, output_folder: &str, ) -> Result<(), Box> { - info!("๐ŸŒš Getting resources from {}-branch", branch_type); + info!("๐ŸŒš Getting resources from {}-branch", branch.branch_type); - let app_file = apps_file(branch_type); + let app_file = branch.app_file(); - if fs::metadata(app_file).unwrap().len() != 0 { - if let Err(e) = apply_manifest(app_file) { + if fs::metadata(app_file)?.len() != 0 { + apply_manifest(app_file).map_err(|e| { error!( "โŒ Failed to apply applications for branch: {}", - branch_type + branch.name ); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } + CommandError::new(e) + })?; } let mut set_of_processed_apps = HashSet::new(); @@ -54,27 +55,32 @@ pub async fn get_resources( let start_time = std::time::Instant::now(); loop { - let output = run_command("kubectl get applications -n argocd -oyaml", None) - .await - .expect("failed to get applications"); - let applications: serde_yaml::Value = - serde_yaml::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap(); + let command = "kubectl get applications -n argocd -oyaml"; + let applications: Result = match run_command(command, None) { + Ok(o) => serde_yaml::from_str(&o.stdout), + Err(e) => return Err(format!("โŒ Failed to get applications: {}", e.stderr).into()), + }; - let items = applications["items"].as_sequence().unwrap(); - if items.is_empty() { - break; - } + let applications = match applications { + Ok(applications) => applications, + Err(_) => { + return Err(format!("โŒ Failed to parse yaml from command: {}", command).into()); + } + }; - if items.len() == set_of_processed_apps.len() { - break; - } + let applications = match applications["items"].as_sequence() { + None => break, + Some(apps) if apps.is_empty() => break, + Some(apps) if apps.len() == set_of_processed_apps.len() => break, + Some(apps) => apps, + }; let mut list_of_timed_out_apps = vec![]; let mut other_errors = vec![]; let mut apps_left = 0; - for item in items { + for item in applications { let name = item["metadata"]["name"].as_str().unwrap(); if set_of_processed_apps.contains(name) { continue; @@ -82,15 +88,15 @@ pub async fn get_resources( match item["status"]["sync"]["status"].as_str() { Some("OutOfSync") | Some("Synced") => { debug!("Getting manifests for application: {}", name); - match run_command(&format!("argocd app manifests {}", name), None).await { + match run_command(&format!("argocd app manifests {}", name), None) { Ok(o) => { fs::write( - format!("{}/{}/{}", output_folder, branch_type, name), + format!("{}/{}/{}", output_folder, branch.branch_type, name), &o.stdout, )?; debug!("Got manifests for application: {}", name) } - Err(e) => error!("error: {}", String::from_utf8_lossy(&e.stderr)), + Err(e) => error!("error: {}", e.stderr), } set_of_processed_apps.insert(name.to_string().clone()); continue; @@ -148,7 +154,7 @@ pub async fn get_resources( return Err("Failed to process applications".into()); } - if items.len() == set_of_processed_apps.len() { + if applications.len() == set_of_processed_apps.len() { tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; continue; } @@ -178,12 +184,11 @@ pub async fn get_resources( list_of_timed_out_apps.len(), ); for app in &list_of_timed_out_apps { - match run_command(&format!("argocd app get {} --refresh", app), None).await { + match run_command(&format!("argocd app get {} --refresh", app), None) { Ok(_) => info!("๐Ÿ”„ Refreshing application: {}", app), Err(e) => error!( "โš ๏ธ Failed to refresh application: {} with {}", - app, - String::from_utf8_lossy(&e.stderr) + app, &e.stderr ), } } @@ -193,7 +198,7 @@ pub async fn get_resources( info!( "โณ Waiting for {} out of {} applications to become 'OutOfSync'. Retrying in 5 seconds. Timeout in {} seconds...", apps_left, - items.len(), + applications.len(), timeout - time_elapsed ); } @@ -204,13 +209,13 @@ pub async fn get_resources( info!( "๐ŸŒš Got all resources from {} applications for {}", set_of_processed_apps.len(), - branch_type + branch.name ); Ok(()) } -pub async fn delete_applications() { +pub async fn delete_applications() -> Result<(), Box> { info!("๐Ÿงผ Removing applications"); loop { debug!("๐Ÿ—‘ Deleting ApplicationSets"); @@ -218,35 +223,22 @@ pub async fn delete_applications() { match run_command( "kubectl delete applicationsets.argoproj.io --all -n argocd", None, - ) - .await - { + ) { Ok(_) => debug!("๐Ÿ—‘ Deleted ApplicationSets"), Err(e) => { - error!( - "โŒ Failed to delete applicationsets: {}", - String::from_utf8_lossy(&e.stderr) - ) + error!("โŒ Failed to delete applicationsets: {}", &e.stderr) } }; debug!("๐Ÿ—‘ Deleting Applications"); - let args = "kubectl delete applications.argoproj.io --all -n argocd" - .split_whitespace() - .collect::>(); - let mut child = Command::new(args[0]) - .args(&args[1..]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("failed to execute process"); - + let mut child = spawn_command( + "kubectl delete applications.argoproj.io --all -n argocd", + None, + ); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; if run_command("kubectl get applications -A --no-headers", None) - .await - .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) - .map(|e| e.trim().is_empty()) + .map(|e| e.stdout.trim().is_empty()) .unwrap_or_default() { let _ = child.kill(); @@ -255,9 +247,7 @@ pub async fn delete_applications() { tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; if run_command("kubectl get applications -A --no-headers", None) - .await - .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) - .map(|e| e.trim().is_empty()) + .map(|e| e.stdout.trim().is_empty()) .unwrap_or_default() { let _ = child.kill(); @@ -269,5 +259,6 @@ pub async fn delete_applications() { Err(e) => error!("โŒ Failed to delete applications: {}", e), }; } - info!("๐Ÿงผ Removed applications successfully") + info!("๐Ÿงผ Removed applications successfully"); + Ok(()) } diff --git a/src/filter_apps.rs b/src/filter_apps.rs deleted file mode 100644 index 005ec25..0000000 --- a/src/filter_apps.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::{parsing::Application, Operator, Selector}; -use log::{debug, error, warn}; -use regex::Regex; - -const ANNOTATION_IGNORE: &str = "argocd-diff-preview/ignore"; -const ANNOTATION_WATCH_PATTERN: &str = "argocd-diff-preview/watch-pattern"; - -pub fn filter( - apps: Vec, - selector: &Option>, - files_changed: &Option>, - ignore_invalid_watch_pattern: bool, -) -> Vec { - let filtered_apps: Vec = apps.into_iter().filter_map(|a| { - - // check if the application should be ignored - if a.yaml["metadata"]["annotations"][ANNOTATION_IGNORE].as_str() - == Some("true") - { - debug!( - "Ignoring application {:?} due to '{}=true' in file: {}", - a.name, - ANNOTATION_IGNORE, - a.file_name - ); - return None; - } - - // loop over labels and check if the selector matches - if let Some(selector) = selector { - let labels: Vec<(&str, &str)> = { - match a.yaml["metadata"]["labels"].as_mapping() { - Some(m) => m.iter() - .flat_map(|(k, v)| Some((k.as_str()?, v.as_str()?))) - .collect(), - None => Vec::new(), - } - }; - let selected = selector.iter().all(|l| match l.operator { - Operator::Eq => labels.iter().any(|(k, v)| k == &l.key && v == &l.value), - Operator::Ne => labels.iter().all(|(k, v)| k != &l.key || v != &l.value), - }); - if !selected { - debug!( - "Ignoring application {:?} due to label selector mismatch in file: {}", - a.name, - a.file_name - ); - return None; - } else { - debug!( - "Selected application {:?} due to label selector match in file: {}", - a.name, - a.file_name - ); - } - } - - // Check watch pattern annotation - let pattern_annotation = a.yaml["metadata"]["annotations"][ANNOTATION_WATCH_PATTERN].as_str(); - let list_of_regex_results = pattern_annotation.map(|s| s.split(',').map(|s| Regex::new(s.trim())).collect::>>()); - - // Return early if a regex pattern is invalid - if let Some(pattern_vec) = &list_of_regex_results { - if let Some(p) = pattern_vec.iter().filter_map(|r| r.as_ref().err()).next() { - if ignore_invalid_watch_pattern { - warn!("๐Ÿšจ Ignoring application {:?} due to invalid regex pattern in '{}' ({}) - Error: {}", - a.name, - pattern_annotation.unwrap_or("unknown"), - a.file_name, - p); - } else { - error!("๐Ÿšจ Application {:?} has an invalid regex pattern in '{}' ({}) - Error: {}", - a.name, - pattern_annotation.unwrap_or("unknown"), - a.file_name, - p); - panic!("Invalid regex pattern in annotation"); - } - } - } - - let patterns: Option> = list_of_regex_results.map(|v| v.into_iter().flat_map(|r| r.ok()).collect()); - - match (files_changed, patterns) { - (None, _) => {} - // Check if the application changed. - (Some(files_changed), _) if files_changed.contains(&a.file_name) => { - debug!( - "Selected application {:?} due to file change in file: {}", - a.name, - a.file_name - ); - } - // Check if the application changed and the regex pattern matches. - (Some(files_changed), Some(pattern)) if files_changed.iter().any(|f| pattern.iter().any(|r| r.is_match(f))) => { - debug!( - "Selected application {:?} due to regex pattern '{}' matching changed files", - a.name, - pattern.iter().map(|r| r.as_str()).collect::>().join(", "), - ); - } - (_, Some(pattern)) => { - debug!( - "Ignoring application {:?} due to regex pattern '{}' not matching changed files", - a.name, - pattern.iter().map(|r| r.as_str()).collect::>().join(", "), - ); - return None; - }, - (_, None) => { - debug!( - "Ignoring application {:?} due to missing '{}' annotation ({})", - a.name, - &ANNOTATION_WATCH_PATTERN, - a.file_name - ); - return None; - } - } - - Some(a) - }).collect(); - - filtered_apps -} diff --git a/src/kind.rs b/src/kind.rs index 0d32a69..c17a194 100644 --- a/src/kind.rs +++ b/src/kind.rs @@ -1,55 +1,63 @@ -use crate::{run_command, utils::spawn_command}; -use log::{error, info}; +use crate::{ + error::CommandError, + utils::{run_command, spawn_command}, +}; +use log::{debug, error, info}; use std::error::Error; -pub async fn is_installed() -> bool { - run_command("which kind", None).await.is_ok() +pub fn is_installed() -> bool { + run_command("which kind", None).is_ok() } -pub async fn create_cluster(cluster_name: &str) -> Result<(), Box> { +pub fn create_cluster(cluster_name: &str) -> Result<(), Box> { // check if docker is running - match run_command("docker ps", None).await { - Ok(_) => (), - Err(e) => { - error!("โŒ Docker is not running"); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } - } + run_command("docker ps", None).map_err(|o| { + error!("โŒ Docker is not running"); + CommandError::new(o) + })?; info!("๐Ÿš€ Creating cluster..."); - match run_command( + run_command( &format!("kind delete cluster --name {}", cluster_name), None, ) - .await - { - Ok(_) => (), - Err(e) => { - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } - }; + .map_err(CommandError::new)?; - match run_command( + run_command( &format!("kind create cluster --name {}", cluster_name), None, ) - .await - { - Ok(_) => { - info!("๐Ÿš€ Cluster created successfully"); - Ok(()) - } - Err(e) => { - error!("โŒ Failed to Create cluster"); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) + .map(|_| { + info!("๐Ÿš€ Cluster created successfully"); + Ok(()) + }) + .map_err(|e| { + error!("โŒ Failed to create cluster"); + CommandError::new(e) + })? +} + +pub fn cluster_exists(cluster_name: &str) -> bool { + match run_command("kind get clusters", None) { + Ok(o) if o.stdout.trim() == cluster_name => true, + Ok(o) => { + debug!("โŒ Cluster '{}' not found in: {}", cluster_name, o.stdout); + false } + Err(_) => false, } } -pub fn delete_cluster(cluster_name: &str) { +pub fn delete_cluster(cluster_name: &str, wait: bool) { info!("๐Ÿ’ฅ Deleting cluster..."); - spawn_command( + let mut child = spawn_command( &format!("kind delete cluster --name {}", cluster_name), None, ); + if wait { + match child.wait() { + Ok(_) => info!("๐Ÿ’ฅ Cluster deleted successfully"), + Err(e) => error!("โŒ Failed to delete cluster: {}", e), + } + } } diff --git a/src/main.rs b/src/main.rs index cb77035..3f5ce98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,26 @@ -use crate::utils::{check_if_folder_exists, create_folder_if_not_exists, run_command}; +use argo_resource::ArgoResource; +use branch::{Branch, BranchType}; +use error::CommandOutput; use log::{debug, error, info}; -use parsing::{applications_to_string, GetApplicationOptions}; use regex::Regex; +use selector::Selector; use std::fs; use std::path::PathBuf; -use std::{ - error::Error, - io::Write, - process::{Command, Output}, -}; +use std::str::FromStr; +use std::{error::Error, io::Write}; use structopt::StructOpt; +use utils::{check_if_folder_exists, create_folder_if_not_exists, run_command_from_list}; +mod argo_resource; mod argocd; +mod branch; mod diff; +mod error; mod extract; -mod filter_apps; mod kind; mod minikube; mod no_apps_found; mod parsing; +mod selector; mod utils; #[derive(Debug, StructOpt)] @@ -72,8 +75,8 @@ struct Opt { secrets_folder: String, /// Local cluster tool. Options: kind, minikube, auto. Default: Auto - #[structopt(long, env)] - local_cluster_tool: Option, + #[structopt(long, env, default_value = "auto")] + local_cluster_tool: ClusterTool, /// Max diff message character count. Default: 65536 (GitHub comment limit) #[structopt(long, env)] @@ -102,58 +105,30 @@ enum ClusterTool { Minikube, } -enum Branch { - Base, - Target, -} - -impl std::fmt::Display for Branch { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Branch::Base => write!(f, "base"), - Branch::Target => write!(f, "target"), - } - } -} - -enum Operator { - Eq, - Ne, -} - -struct Selector { - key: String, - value: String, - operator: Operator, -} - -impl std::fmt::Display for Selector { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Selector { - key, - value, - operator, - } => match operator { - Operator::Eq => write!(f, "{}={}", key, value), - Operator::Ne => write!(f, "{}!={}", key, value), - }, +impl FromStr for ClusterTool { + type Err = &'static str; + fn from_str(day: &str) -> Result { + match day.to_lowercase().as_str() { + "kind" => Ok(ClusterTool::Kind), + "minikube" => Ok(ClusterTool::Minikube), + "auto" if kind::is_installed() => Ok(ClusterTool::Kind), + "auto" if minikube::is_installed() => Ok(ClusterTool::Minikube), + _ => Err("No local cluster tool found. Please install kind or minikube"), } } } -fn apps_file(branch: &Branch) -> &'static str { - match branch { - Branch::Base => "apps_base_branch.yaml", - Branch::Target => "apps_target_branch.yaml", - } -} - -const BASE_BRANCH_FOLDER: &str = "base-branch"; -const TARGET_BRANCH_FOLDER: &str = "target-branch"; - #[tokio::main] async fn main() -> Result<(), Box> { + run().await.map_err(|e| { + let opt = Opt::from_args(); + error!("โŒ {}", e); + cleanup_cluster(opt.local_cluster_tool, &opt.cluster_name); + e + }) +} + +async fn run() -> Result<(), Box> { let opt = Opt::from_args(); // Start timer @@ -174,7 +149,8 @@ async fn main() -> Result<(), Box> { let file_regex = opt .file_regex .filter(|f| !f.trim().is_empty()) - .map(|f| Regex::new(&f).unwrap()); + .map(|f| Regex::new(&f)) + .transpose()?; let base_branch_name = opt.base_branch.trim(); let target_branch_name = opt.target_branch.trim(); @@ -204,25 +180,16 @@ async fn main() -> Result<(), Box> { }); // select local cluster tool - let tool = match opt.local_cluster_tool { - Some(t) if t == "kind" => ClusterTool::Kind, - Some(t) if t == "minikube" => ClusterTool::Minikube, - _ if kind::is_installed().await => ClusterTool::Kind, - _ if minikube::is_installed().await => ClusterTool::Minikube, - _ => { - error!("โŒ No local cluster tool found. Please install kind or minikube"); - panic!("No local cluster tool found") - } - }; + let cluster_tool = &opt.local_cluster_tool; let repo_regex = Regex::new(r"^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$").unwrap(); if !repo_regex.is_match(repo) { error!("โŒ Invalid repository format. Please use OWNER/REPO"); - panic!("Invalid repository format"); + return Err("Invalid repository format".into()); } info!("โœจ Running with:"); - info!("โœจ - local-cluster-tool: {:?}", tool); + info!("โœจ - local-cluster-tool: {:?}", cluster_tool); info!("โœจ - base-branch: {}", base_branch_name); info!("โœจ - target-branch: {}", target_branch_name); info!("โœจ - secrets-folder: {}", secrets_folder); @@ -251,48 +218,22 @@ async fn main() -> Result<(), Box> { info!("โœจ Ignoring invalid watch patterns Regex on Applications"); } + let base_branch = Branch { + name: base_branch_name.to_string(), + branch_type: BranchType::Base, + }; + + let target_branch = Branch { + name: target_branch_name.to_string(), + branch_type: BranchType::Target, + }; + // label selectors can be fined in the following format: key1==value1,key2=value2,key3!=value3 let selector = opt.selector.filter(|s| !s.trim().is_empty()).map(|s| { let labels: Vec = s .split(',') .filter(|l| !l.trim().is_empty()) - .map(|l| { - let not_equal = l.split("!=").collect::>(); - let equal_double = l.split("==").collect::>(); - let equal_single = l.split('=').collect::>(); - let selector = match (not_equal.len(), equal_double.len(), equal_single.len()) { - (2, _, _) => Selector { - key: not_equal[0].trim().to_string(), - value: not_equal[1].trim().to_string(), - operator: Operator::Ne, - }, - (_, 2, _) => Selector { - key: equal_double[0].trim().to_string(), - value: equal_double[1].trim().to_string(), - operator: Operator::Eq, - }, - (_, _, 2) => Selector { - key: equal_single[0].trim().to_string(), - value: equal_single[1].trim().to_string(), - operator: Operator::Eq, - }, - _ => { - error!("โŒ Invalid label selector format: {}", l); - panic!("Invalid label selector format"); - } - }; - if selector.key.is_empty() - || selector.key.contains('!') - || selector.key.contains('=') - || selector.value.is_empty() - || selector.value.contains('!') - || selector.value.contains('=') - { - error!("โŒ Invalid label selector format: {}", l); - panic!("Invalid label selector format"); - } - selector - }) + .map(|l| Selector::from(l).expect("Invalid label selector format")) .collect(); labels }); @@ -307,20 +248,20 @@ async fn main() -> Result<(), Box> { ); } - if !check_if_folder_exists(&BASE_BRANCH_FOLDER) { + if !check_if_folder_exists(base_branch.folder_name()) { error!( "โŒ Base branch folder does not exist: {}", - BASE_BRANCH_FOLDER + base_branch.folder_name() ); - panic!("Base branch folder does not exist"); + return Err("Base branch folder does not exist".into()); } - if !check_if_folder_exists(&TARGET_BRANCH_FOLDER) { + if !check_if_folder_exists(target_branch.folder_name()) { error!( "โŒ Target branch folder does not exist: {}", - TARGET_BRANCH_FOLDER + target_branch.folder_name() ); - panic!("Target branch folder does not exist"); + return Err("Target branch folder does not exist".into()); } let cluster_name = opt.cluster_name; @@ -328,21 +269,14 @@ async fn main() -> Result<(), Box> { // remove .git from repo let repo = repo.trim_end_matches(".git"); let (base_apps, target_apps) = parsing::get_applications_for_both_branches( - GetApplicationOptions { - directory: BASE_BRANCH_FOLDER, - branch: &base_branch_name, - }, - GetApplicationOptions { - directory: TARGET_BRANCH_FOLDER, - branch: &target_branch_name, - }, + &base_branch, + &target_branch, &file_regex, &selector, &files_changed, repo, opt.ignore_invalid_watch_pattern, - ) - .await?; + )?; let found_base_apps = !base_apps.is_empty(); let found_target_apps = !target_apps.is_empty(); @@ -350,25 +284,31 @@ async fn main() -> Result<(), Box> { if !found_base_apps && !found_target_apps { info!("๐Ÿ‘€ Nothing to compare"); info!("๐Ÿ‘€ If this doesn't seem right, try running the tool with '--debug' to get more details about what is happening"); - no_apps_found::write_message(output_folder, &selector, &files_changed).await?; + no_apps_found::write_message(output_folder, &selector, &files_changed)?; info!("๐ŸŽ‰ Done in {} seconds", start.elapsed().as_secs()); return Ok(()); } - match tool { - ClusterTool::Kind => kind::create_cluster(&cluster_name).await?, - ClusterTool::Minikube => minikube::create_cluster().await?, + fs::write(base_branch.app_file(), applications_to_string(base_apps))?; + fs::write( + target_branch.app_file(), + applications_to_string(target_apps), + )?; + + match cluster_tool { + ClusterTool::Kind => kind::create_cluster(&cluster_name)?, + ClusterTool::Minikube => minikube::create_cluster()?, } - argocd::create_namespace().await?; + argocd::create_namespace()?; - create_folder_if_not_exists(secrets_folder); + create_folder_if_not_exists(secrets_folder)?; match apply_folder(secrets_folder) { Ok(count) if count > 0 => info!("๐Ÿคซ Applied {} secrets", count), Ok(_) => info!("๐Ÿคท No secrets found in {}", secrets_folder), Err(e) => { error!("โŒ Failed to apply secrets"); - panic!("error: {}", e) + return Err(e); } } @@ -378,74 +318,86 @@ async fn main() -> Result<(), Box> { }) .await?; - fs::write(apps_file(&Branch::Base), applications_to_string(base_apps))?; - fs::write( - apps_file(&Branch::Target), - applications_to_string(target_apps), - )?; - // Cleanup output folder - clean_output_folder(output_folder); + clean_output_folder(output_folder)?; // Extract resources from Argo CD if found_base_apps { - extract::get_resources(&Branch::Base, timeout, output_folder).await?; + extract::get_resources(&base_branch, timeout, output_folder).await?; if found_target_apps { - extract::delete_applications().await; + extract::delete_applications().await?; tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; } } if found_target_apps { - extract::get_resources(&Branch::Target, timeout, output_folder).await?; + extract::get_resources(&target_branch, timeout, output_folder).await?; } // Delete cluster - match tool { - ClusterTool::Kind => kind::delete_cluster(&cluster_name), - ClusterTool::Minikube => minikube::delete_cluster(), + match cluster_tool { + ClusterTool::Kind => kind::delete_cluster(&cluster_name, false), + ClusterTool::Minikube => minikube::delete_cluster(false), } diff::generate_diff( output_folder, - &base_branch_name, - &target_branch_name, + &base_branch, + &target_branch, diff_ignore, line_count, max_diff_length, - ) - .await?; + )?; info!("๐ŸŽ‰ Done in {} seconds", start.elapsed().as_secs()); Ok(()) } -fn clean_output_folder(output_folder: &str) { - create_folder_if_not_exists(output_folder); - fs::remove_dir_all(format!("{}/{}", output_folder, Branch::Base)).unwrap_or_default(); - fs::remove_dir_all(format!("{}/{}", output_folder, Branch::Target)).unwrap_or_default(); - fs::create_dir(format!("{}/{}", output_folder, Branch::Base)) - .expect("Unable to create directory"); - fs::create_dir(format!("{}/{}", output_folder, Branch::Target)) - .expect("Unable to create directory"); +fn clean_output_folder(output_folder: &str) -> Result<(), Box> { + create_folder_if_not_exists(output_folder)?; + fs::remove_dir_all(format!("{}/{}", output_folder, BranchType::Base)).unwrap_or_default(); + fs::remove_dir_all(format!("{}/{}", output_folder, BranchType::Target)).unwrap_or_default(); + { + let dir = format!("{}/{}", output_folder, BranchType::Base); + match fs::create_dir(&dir) { + Ok(_) => (), + Err(_) => return Err(format!("โŒ Failed to create directory: {}", dir).into()), + } + } + { + let dir = format!("{}/{}", output_folder, BranchType::Target); + match fs::create_dir(&dir) { + Ok(_) => (), + Err(_) => return Err(format!("โŒ Failed to create directory: {}", dir).into()), + } + } + Ok(()) } -fn apply_manifest(file_name: &str) -> Result { - let output = Command::new("kubectl") - .arg("apply") - .arg("-f") - .arg(file_name) - .output() - .unwrap_or_else(|_| panic!("failed to apply manifest: {}", file_name)); - match output.status.success() { - true => Ok(output), - false => Err(output), +fn cleanup_cluster(tool: ClusterTool, cluster_name: &str) { + match tool { + ClusterTool::Kind if kind::cluster_exists(cluster_name) => { + info!("๐Ÿงผ Cleaning up..."); + kind::delete_cluster(cluster_name, true) + } + ClusterTool::Minikube if minikube::cluster_exists() => { + info!("๐Ÿงผ Cleaning up..."); + minikube::delete_cluster(true) + } + _ => debug!("๐Ÿงผ No cluster to clean up"), } } -fn apply_folder(folder_name: &str) -> Result { +fn apply_manifest(file_name: &str) -> Result { + run_command_from_list(vec!["kubectl", "apply", "-f", file_name], None).map_err(|e| { + error!("โŒ Failed to apply manifest: {}", file_name); + e + }) +} + +fn apply_folder(folder_name: &str) -> Result> { if !PathBuf::from(folder_name).is_dir() { - return Err(format!("{} is not a directory", folder_name)); + return Err(format!("{} is not a directory", folder_name).into()); } let mut count = 0; if let Ok(entries) = fs::read_dir(folder_name) { @@ -455,10 +407,18 @@ fn apply_folder(folder_name: &str) -> Result { if file_name.ends_with(".yaml") || file_name.ends_with(".yml") { match apply_manifest(file_name) { Ok(_) => count += 1, - Err(e) => return Err(String::from_utf8_lossy(&e.stderr).to_string()), + Err(e) => return Err(e.stderr.into()), } } } } Ok(count) } + +pub fn applications_to_string(applications: Vec) -> String { + applications + .iter() + .map(|a| a.to_string()) + .collect::>() + .join("---\n") +} diff --git a/src/minikube.rs b/src/minikube.rs index 924a7a4..f1c30b6 100644 --- a/src/minikube.rs +++ b/src/minikube.rs @@ -1,42 +1,46 @@ -use crate::{run_command, utils::spawn_command}; +use crate::{ + error::CommandError, + utils::{run_command, spawn_command}, +}; use log::{error, info}; use std::error::Error; -pub async fn is_installed() -> bool { - run_command("which minikube", None).await.is_ok() +pub fn is_installed() -> bool { + run_command("which minikube", None).is_ok() } -pub async fn create_cluster() -> Result<(), Box> { +pub fn create_cluster() -> Result<(), Box> { // check if docker is running - match run_command("docker ps", None).await { - Ok(_) => (), - Err(e) => { - error!("โŒ Docker is not running"); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } - } + run_command("docker ps", None).map_err(|o| { + error!("โŒ Docker is not running"); + CommandError::new(o) + })?; info!("๐Ÿš€ Creating cluster..."); - match run_command("minikube delete", None).await { - Ok(o) => o, - Err(e) => { - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } - }; + run_command("minikube delete", None).map_err(CommandError::new)?; - match run_command("minikube start", None).await { - Ok(_) => { + run_command("minikube start", None) + .map(|_| { info!("๐Ÿš€ Cluster created successfully"); Ok(()) - } - Err(e) => { - error!("โŒ Failed to Create cluster"); - panic!("error: {}", String::from_utf8_lossy(&e.stderr)) - } - } + }) + .map_err(|e| { + error!("โŒ Failed to create cluster"); + CommandError::new(e) + })? +} + +pub fn cluster_exists() -> bool { + run_command("minikube status", None).is_ok() } -pub fn delete_cluster() { +pub fn delete_cluster(wait: bool) { info!("๐Ÿ’ฅ Deleting cluster..."); - spawn_command("minikube delete", None); + let mut child = spawn_command("minikube delete", None); + if wait { + match child.wait() { + Ok(_) => info!("๐Ÿ’ฅ Cluster deleted successfully"), + Err(e) => error!("โŒ Failed to delete cluster: {}", e), + } + } } diff --git a/src/no_apps_found.rs b/src/no_apps_found.rs index 89e609b..9e31d82 100644 --- a/src/no_apps_found.rs +++ b/src/no_apps_found.rs @@ -5,7 +5,7 @@ use crate::Selector; // Message to show when no applications were found -pub async fn write_message( +pub fn write_message( output_folder: &str, selector: &Option>, changed_files: &Option>, diff --git a/src/parsing.rs b/src/parsing.rs index f99f1a1..109f889 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -1,72 +1,48 @@ -use crate::{filter_apps::filter, Selector}; +use crate::{argo_resource::ArgoResource, Branch, Selector}; use log::{debug, info}; use regex::Regex; -use serde_yaml::{Mapping, Value}; +use serde_yaml::Value; use std::{error::Error, io::BufRead}; -struct K8sResource { - file_name: String, - yaml: serde_yaml::Value, -} - -pub struct Application { +pub struct K8sResource { pub file_name: String, pub yaml: serde_yaml::Value, - kind: ApplicationKind, - pub name: String, -} - -impl std::fmt::Display for Application { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", serde_yaml::to_string(&self.yaml).unwrap()) - } } -impl PartialEq for Application { - fn eq(&self, other: &Self) -> bool { - self.yaml == other.yaml +impl Clone for K8sResource { + fn clone(&self) -> Self { + K8sResource { + file_name: self.file_name.clone(), + yaml: self.yaml.clone(), + } } } -enum ApplicationKind { - Application, - ApplicationSet, -} - -pub struct GetApplicationOptions<'a> { - pub directory: &'a str, - pub branch: &'a str, -} - -pub async fn get_applications_for_both_branches<'a>( - base_branch: GetApplicationOptions<'a>, - target_branch: GetApplicationOptions<'a>, +pub fn get_applications_for_both_branches<'a>( + base_branch: &Branch, + target_branch: &Branch, regex: &Option, selector: &Option>, files_changed: &Option>, repo: &str, ignore_invalid_watch_pattern: bool, -) -> Result<(Vec, Vec), Box> { +) -> Result<(Vec, Vec), Box> { let base_apps = get_applications( - base_branch.directory, - base_branch.branch, + base_branch, regex, selector, files_changed, repo, ignore_invalid_watch_pattern, - ) - .await?; + )?; let target_apps = get_applications( - target_branch.directory, - target_branch.branch, + target_branch, regex, selector, files_changed, repo, ignore_invalid_watch_pattern, - ) - .await?; + )?; let duplicate_yaml = base_apps .iter() @@ -103,17 +79,16 @@ pub async fn get_applications_for_both_branches<'a>( } } -pub async fn get_applications( - directory: &str, - branch: &str, +pub fn get_applications( + branch: &Branch, regex: &Option, selector: &Option>, files_changed: &Option>, repo: &str, ignore_invalid_watch_pattern: bool, -) -> Result, Box> { - let yaml_files = get_yaml_files(directory, regex).await; - let k8s_resources = parse_yaml(directory, yaml_files).await; +) -> Result, Box> { + let yaml_files = get_yaml_files(branch.folder_name(), regex); + let k8s_resources = parse_yaml(branch.folder_name(), yaml_files); let applications = from_resource_to_application( k8s_resources, selector, @@ -121,12 +96,12 @@ pub async fn get_applications( ignore_invalid_watch_pattern, ); if !applications.is_empty() { - return patch_applications(applications, branch, repo).await; + return patch_applications(applications, branch, repo); } Ok(applications) } -async fn get_yaml_files(directory: &str, regex: &Option) -> Vec { +fn get_yaml_files(directory: &str, regex: &Option) -> Vec { use walkdir::WalkDir; info!("๐Ÿค– Fetching all files in dir: {}", directory); @@ -172,7 +147,7 @@ async fn get_yaml_files(directory: &str, regex: &Option) -> Vec { yaml_files } -async fn parse_yaml(directory: &str, files: Vec) -> Vec { +fn parse_yaml(directory: &str, files: Vec) -> Vec { files.iter() .flat_map(|f| { debug!("In dir '{}' found yaml file: {}", directory, f); @@ -208,90 +183,52 @@ async fn parse_yaml(directory: &str, files: Vec) -> Vec { .collect() } -async fn patch_applications( - applications: Vec, - branch: &str, +fn patch_applications( + applications: Vec, + branch: &Branch, repo: &str, -) -> Result, Box> { - info!("๐Ÿค– Patching applications for branch: {}", branch); - - let point_destination_to_in_cluster = |spec: &mut Mapping| { - if spec.contains_key("destination") { - spec["destination"]["name"] = serde_yaml::Value::String("in-cluster".to_string()); - spec["destination"] - .as_mapping_mut() - .map(|a| a.remove("server")); - } - }; - - let set_project_to_default = - |spec: &mut Mapping| spec["project"] = serde_yaml::Value::String("default".to_string()); - - let remove_sync_policy = |spec: &mut Mapping| spec.remove("syncPolicy"); - - let redirect_sources = |spec: &mut Mapping, file: &str| { - if spec.contains_key("source") { - if spec["source"]["chart"].as_str().is_some() { - return; - } - match spec["source"]["repoURL"].as_str() { - Some(url) if url.to_lowercase().contains(&repo.to_lowercase()) => { - spec["source"]["targetRevision"] = serde_yaml::Value::String(branch.to_string()) - } - _ => debug!("Found no 'repoURL' under spec.source in file: {}", file), - } - } else if spec.contains_key("sources") { - if let Some(sources) = spec["sources"].as_sequence_mut() { - for source in sources { - if source["chart"].as_str().is_some() { - continue; - } - match source["repoURL"].as_str() { - Some(url) if url.to_lowercase().contains(&repo.to_lowercase()) => { - source["targetRevision"] = - serde_yaml::Value::String(branch.to_string()); - } - _ => debug!("Found no 'repoURL' under spec.sources[] in file: {}", file), - } - } - } - } - }; +) -> Result, Box> { + info!("๐Ÿค– Patching applications for branch: {}", branch.name); - let applications: Vec = applications + let applications: Vec>> = applications .into_iter() - .map(|mut a| { - // Update namesapce - a.yaml["metadata"]["namespace"] = serde_yaml::Value::String("argocd".to_string()); - a - }) - .filter_map(|mut a| { - // Clean up the spec - let spec = match a.kind { - ApplicationKind::Application => a.yaml["spec"].as_mapping_mut()?, - ApplicationKind::ApplicationSet => { - a.yaml["spec"]["template"]["spec"].as_mapping_mut()? - } - }; - remove_sync_policy(spec); - set_project_to_default(spec); - point_destination_to_in_cluster(spec); - redirect_sources(spec, &a.file_name); - debug!( - "Collected resources from application: {:?} in file: {}", - a.name, a.file_name - ); - Some(a) + .map(|a| { + let app_name = a.name.clone(); + let app: Result> = a + .set_namespace("argocd") + .remove_sync_policy() + .set_project_to_default() + .and_then(|a| a.point_destination_to_in_cluster()) + .and_then(|a| a.redirect_sources(repo, &branch.name)); + + if app.is_err() { + info!("โŒ Failed to patch application: {}", app_name); + return app; + } + app }) .collect(); info!( "๐Ÿค– Patching {} Argo CD Application[Sets] for branch: {}", applications.len(), - branch + branch.name ); - Ok(applications) + let errors: Vec = applications + .iter() + .filter_map(|a| match a { + Ok(_) => None, + Err(e) => Some(e.to_string()), + }) + .collect(); + + if !errors.is_empty() { + return Err(errors.join("\n").into()); + } + + let apps = applications.into_iter().filter_map(|a| a.ok()).collect(); + Ok(apps) } fn from_resource_to_application( @@ -299,32 +236,10 @@ fn from_resource_to_application( selector: &Option>, files_changed: &Option>, ignore_invalid_watch_pattern: bool, -) -> Vec { - let apps: Vec = k8s_resources - .into_iter() - .filter_map(|r| { - let kind = - r.yaml["kind"] - .as_str() - .map(|s| s.to_string()) - .and_then(|kind| match kind.as_str() { - "Application" => Some(ApplicationKind::Application), - "ApplicationSet" => Some(ApplicationKind::ApplicationSet), - _ => None, - })?; - - let name = r.yaml["metadata"]["name"] - .as_str() - .unwrap_or("unknown") - .to_string(); - - Some(Application { - kind, - file_name: r.file_name, - name, - yaml: r.yaml, - }) - }) +) -> Vec { + let apps: Vec = k8s_resources + .iter() + .filter_map(|r| ArgoResource::from_k8s_resource(r.clone())) .collect(); match (selector, files_changed) { @@ -352,7 +267,10 @@ fn from_resource_to_application( let number_of_apps_before_filtering = apps.len(); - let filtered_apps: Vec = filter(apps, selector, files_changed, ignore_invalid_watch_pattern); + let filtered_apps: Vec = apps + .into_iter() + .filter_map(|a| a.filter(selector, files_changed, ignore_invalid_watch_pattern)) + .collect(); if number_of_apps_before_filtering != filtered_apps.len() { info!( @@ -369,11 +287,3 @@ fn from_resource_to_application( filtered_apps } - -pub fn applications_to_string(applications: Vec) -> String { - applications - .iter() - .map(|a| a.to_string()) - .collect::>() - .join("---\n") -} diff --git a/src/selector.rs b/src/selector.rs new file mode 100644 index 0000000..8084d06 --- /dev/null +++ b/src/selector.rs @@ -0,0 +1,69 @@ +use std::error::Error; + +use log::error; + +pub enum Operator { + Eq, + Ne, +} + +pub struct Selector { + pub key: String, + pub value: String, + pub operator: Operator, +} + +impl std::fmt::Display for Selector { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Selector { + key, + value, + operator, + } => match operator { + Operator::Eq => write!(f, "{}={}", key, value), + Operator::Ne => write!(f, "{}!={}", key, value), + }, + } + } +} + +impl Selector { + pub fn from(l: &str) -> Result> { + let not_equal = l.split("!=").collect::>(); + let equal_double = l.split("==").collect::>(); + let equal_single = l.split('=').collect::>(); + let selector = match (not_equal.len(), equal_double.len(), equal_single.len()) { + (2, _, _) => Selector { + key: not_equal[0].trim().to_string(), + value: not_equal[1].trim().to_string(), + operator: Operator::Ne, + }, + (_, 2, _) => Selector { + key: equal_double[0].trim().to_string(), + value: equal_double[1].trim().to_string(), + operator: Operator::Eq, + }, + (_, _, 2) => Selector { + key: equal_single[0].trim().to_string(), + value: equal_single[1].trim().to_string(), + operator: Operator::Eq, + }, + _ => { + error!("โŒ Invalid label selector format: {}", l); + return Err("Invalid label selector format".into()); + } + }; + if selector.key.is_empty() + || selector.key.contains('!') + || selector.key.contains('=') + || selector.value.is_empty() + || selector.value.contains('!') + || selector.value.contains('=') + { + error!("โŒ Invalid label selector format: {}", l); + return Err("Invalid label selector format".into()); + } + Ok(selector) + } +} diff --git a/src/utils.rs b/src/utils.rs index ef39f9a..4556fef 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,29 +1,33 @@ +use std::error::Error; use std::path::PathBuf; -use std::process::Stdio; -use std::{ - fs, - process::{Command, Output}, -}; +use std::process::{Child, Stdio}; +use std::{fs, process::Command}; -pub fn create_folder_if_not_exists(folder_name: &str) { +use crate::error::CommandOutput; + +pub fn create_folder_if_not_exists(folder_name: &str) -> Result<(), Box> { if !PathBuf::from(folder_name).is_dir() { - fs::create_dir(folder_name).expect("Unable to create directory"); + fs::create_dir(folder_name)?; } + Ok(()) } pub fn check_if_folder_exists(folder_name: &str) -> bool { PathBuf::from(folder_name).is_dir() } -pub async fn run_command(command: &str, current_dir: Option<&str>) -> Result { +pub fn run_command( + command: &str, + current_dir: Option<&str>, +) -> Result { let args = command.split_whitespace().collect::>(); - run_command_from_list(args, current_dir).await + run_command_from_list(args, current_dir) } -pub async fn run_command_from_list( +pub fn run_command_from_list( command: Vec<&str>, current_dir: Option<&str>, -) -> Result { +) -> Result { let output = Command::new(command[0]) .args(&command[1..]) .env( @@ -34,20 +38,29 @@ pub async fn run_command_from_list( .output() .unwrap_or_else(|_| panic!("Failed to execute command: {}", command.join(" "))); - if output.status.success() { - Ok(output) - } else { - Err(output) + match output.status.success() { + true => Ok(CommandOutput { + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }), + false => Err(CommandOutput { + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }), } } -pub fn spawn_command(command: &str, current_dir: Option<&str>) { +pub fn spawn_command(command: &str, current_dir: Option<&str>) -> Child { let args = command.split_whitespace().collect::>(); + spawn_command_from_list(args, current_dir) +} + +pub fn spawn_command_from_list(args: Vec<&str>, current_dir: Option<&str>) -> Child { Command::new(args[0]) .args(&args[1..]) .current_dir(current_dir.unwrap_or(".")) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() - .unwrap_or_else(|_| panic!("Failed to execute command: {}", command)); + .unwrap_or_else(|_| panic!("Failed to execute command: {}", args.join(" "))) }