diff --git a/.github/workflows/generate-diff.yml b/.github/workflows/generate-diff.yml index c384ced5..98b83c80 100644 --- a/.github/workflows/generate-diff.yml +++ b/.github/workflows/generate-diff.yml @@ -25,6 +25,15 @@ jobs: ref: main path: main + - name: Set ArgoCD Custom Values + run: | + cat > values.yaml << "EOF" + # set whatever helm values you want + configs: + cm: + kustomize.buildOptions: --load-restrictor LoadRestrictionsNone --enable-helm + EOF + - name: Generate Diff run: | docker run \ @@ -33,9 +42,10 @@ jobs: -v $(pwd)/main:/base-branch \ -v $(pwd)/pull-request:/target-branch \ -v $(pwd)/output:/output \ + -v $(pwd)/values.yaml:/argocd-config/values.yaml \ -e TARGET_BRANCH=${{ github.head_ref }} \ -e REPO=${{ github.repository }} \ - dagandersen/argocd-diff-preview:v0.0.26 + dagandersen/argocd-diff-preview:v0.0.28 --debug - name: Post diff as comment run: | diff --git a/.gitignore b/.gitignore index 37f73e63..0e429b49 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ output/ /apps_target_branch.yaml /apps_base_branch.yaml venv/ +base-branch/ +target-branch/ \ No newline at end of file diff --git a/README.md b/README.md index 2e172d54..0c87e456 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ The safest way to make changes to you Helm Charts and Kustomize Overlays in your > -v $(pwd)/target-branch:/target-branch \ > -e TARGET_BRANCH=helm-example-3 \ > -e REPO=dag-andersen/argocd-diff-preview \ -> dagandersen/argocd-diff-preview:v0.0.26 +> dagandersen/argocd-diff-preview:v0.0.27 > ``` > > and the output would be something like this: @@ -118,7 +118,7 @@ jobs: -v $(pwd)/output:/output \ -e TARGET_BRANCH=${{ github.head_ref }} \ -e REPO=${{ github.repository }} \ - dagandersen/argocd-diff-preview:v0.0.26 + dagandersen/argocd-diff-preview:v0.0.27 - name: Post diff as comment run: | diff --git a/docs/application-selection.md b/docs/application-selection.md index 1ec5c3da..e1424a99 100644 --- a/docs/application-selection.md +++ b/docs/application-selection.md @@ -78,7 +78,7 @@ jobs: -e TARGET_BRANCH=${{ github.head_ref }} \ -e REPO=${{ github.repository }} \ -e FILES_CHANGED="${{ steps.changed-files.outputs.all_changed_files }}" - dagandersen/argocd-diff-preview:v0.0.26 + dagandersen/argocd-diff-preview:v0.0.27 ``` ## Ignoring individual applications diff --git a/docs/demo.md b/docs/demo.md index 641d4886..9d7c696a 100644 --- a/docs/demo.md +++ b/docs/demo.md @@ -17,7 +17,7 @@ docker run \ -v $(pwd)/target-branch:/target-branch \ -e TARGET_BRANCH=helm-example-3 \ -e REPO=dag-andersen/argocd-diff-preview \ - dagandersen/argocd-diff-preview:v0.0.26 + dagandersen/argocd-diff-preview:v0.0.27 ``` and the output would be something like this: diff --git a/docs/github-actions-workflow.md b/docs/github-actions-workflow.md index 5ed4066b..d4246c53 100644 --- a/docs/github-actions-workflow.md +++ b/docs/github-actions-workflow.md @@ -39,7 +39,7 @@ jobs: -v $(pwd)/output:/output \ -e TARGET_BRANCH=${{ github.head_ref }} \ -e REPO=${{ github.repository }} \ - dagandersen/argocd-diff-preview:v0.0.26 + dagandersen/argocd-diff-preview:v0.0.27 - name: Post diff as comment run: | @@ -88,7 +88,7 @@ In the simple code examples above, we do not provide the cluster with any creden -v $(pwd)/secrets:/secrets \ ⬅️ Mount the secrets folder -e TARGET_BRANCH=${{ github.head_ref }} \ -e REPO=${{ github.repository }} \ - dagandersen/argocd-diff-preview:v0.0.26 + dagandersen/argocd-diff-preview:v0.0.27 ``` For more info, see the [Argo CD docs](https://argo-cd.readthedocs.io/en/stable/operator-manual/argocd-repo-creds-yaml/) \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md index adecfc93..4be7c6d5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -28,7 +28,7 @@ -e TARGET_BRANCH= \ -e BASE_BRANCH= \ -e REPO=/ \ - dagandersen/argocd-diff-preview:v0.0.26 + dagandersen/argocd-diff-preview:v0.0.27 ``` If base-branch(`BASE_BRANCH`) is not specified it will default to `main`. @@ -52,7 +52,7 @@ *Example for downloading and running on macOS:* ```bash - curl -LJO https://github.com/dag-andersen/argocd-diff-preview/releases/download/v0.0.26/argocd-diff-preview-Darwin-x86_64.tar.gz + curl -LJO https://github.com/dag-andersen/argocd-diff-preview/releases/download/v0.0.27/argocd-diff-preview-Darwin-x86_64.tar.gz tar -xvf argocd-diff-preview-Darwin-x86_64.tar.gz sudo mv argocd-diff-preview /usr/local/bin argocd-diff-preview --help diff --git a/docs/options.md b/docs/options.md index af98ee89..c4f01b09 100644 --- a/docs/options.md +++ b/docs/options.md @@ -14,6 +14,9 @@ FLAGS: -h, --help Prints help information + --keep-cluster-alive + Keep cluster alive after the tool finishes + -V, --version Prints version information @@ -22,6 +25,10 @@ OPTIONS: Argo CD Helm Chart version [env: ARGOCD_CHART_VERSION=] + --argocd-namespace + Namespace to use for Argo CD + [env: ARGOCD_NAMESPACE=] [default: argocd] + -b, --base-branch Base branch name [env: BASE_BRANCH=] [default: main] @@ -86,4 +93,4 @@ OPTIONS: --timeout Set timeout for waiting for Applications to become 'OutOfSync' [env: TIMEOUT=] [default: 180] -``` \ No newline at end of file +``` diff --git a/makefile b/makefile index 50ee5ae6..a5207798 100644 --- a/makefile +++ b/makefile @@ -11,6 +11,9 @@ pull-repository: cd base-branch && gh repo clone $(github_org)/$(gitops_repo) -- --depth=1 --branch "$(base_branch)" && cp -r $(gitops_repo)/. . && rm -rf .git && echo "*" > .gitignore && rm -rf $(gitops_repo) && cd - cd target-branch && gh repo clone $(github_org)/$(gitops_repo) -- --depth=1 --branch "$(target_branch)" && cp -r $(gitops_repo)/. . && rm -rf .git && echo "*" > .gitignore && rm -rf $(gitops_repo) && cd - +docker-build: + docker build . -f $(docker_file) -t image + run-with-cargo: pull-repository cargo run -- -b "$(base_branch)" \ -t "$(target_branch)" \ @@ -24,8 +27,7 @@ run-with-cargo: pull-repository --argocd-namespace "$(argocd_namespace)" \ --files-changed="$(files_changed)" -run-with-docker: pull-repository - docker build . -f $(docker_file) -t image +run-with-docker: pull-repository docker-build docker run \ --network=host \ -v ~/.kube:/root/.kube \ diff --git a/src/argo_resource.rs b/src/argo_resource.rs index f7a05c9b..1eae0ac3 100644 --- a/src/argo_resource.rs +++ b/src/argo_resource.rs @@ -8,22 +8,24 @@ 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)] +#[derive(PartialEq, Clone)] pub enum ApplicationKind { Application, ApplicationSet, } +#[derive(Clone)] pub struct ArgoResource { - pub file_name: String, pub yaml: serde_yaml::Value, pub kind: ApplicationKind, pub name: String, + // Where the resource was found + pub file_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 ArgoResource { + pub fn as_string(&self) -> Result> { + Ok(serde_yaml::to_string(&self.yaml)?) } } diff --git a/src/argocd.rs b/src/argocd.rs index 57a9fc99..89f65768 100644 --- a/src/argocd.rs +++ b/src/argocd.rs @@ -116,9 +116,8 @@ impl ArgoCDInstallation { } } let password_encoded = password_encoded.unwrap().stdout; - let password_decoded = BASE64_STANDARD.decode(password_encoded).map_err(|e| { + let password_decoded = BASE64_STANDARD.decode(password_encoded).inspect_err(|e| { error!("❌ Failed to decode password: {}", e); - e })?; String::from_utf8(password_decoded).inspect_err(|_e| { diff --git a/src/extract.rs b/src/extract.rs index 807e1f59..57208612 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -211,7 +211,10 @@ pub async fn get_resources( ); // info about where it was stored - info!("💾 Resources stored in: '{}/'", destination_folder); + info!( + "💾 Resources stored in: '{}/'", + destination_folder + ); Ok(()) } diff --git a/src/main.rs b/src/main.rs index 6227f590..74a9a34b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use error::{CommandError, CommandOutput}; use log::{debug, error, info}; use regex::Regex; use selector::Selector; +use std::collections::HashMap; use std::fs; use std::path::PathBuf; use std::str::FromStr; @@ -128,13 +129,12 @@ impl FromStr for ClusterTool { #[tokio::main] async fn main() -> Result<(), Box> { - run().await.map_err(|e| { + run().await.inspect_err(|e| { let opt = Opt::from_args(); error!("❌ {}", e); if !opt.keep_cluster_alive { cleanup_cluster(opt.local_cluster_tool, &opt.cluster_name); } - e }) } @@ -194,7 +194,7 @@ async fn run() -> Result<(), Box> { let cluster_tool = &opt.local_cluster_tool; - let repo_regex = Regex::new(r"^[a-zA-Z0-9-]+/[a-zA-Z0-9\._-]+$").unwrap(); + let repo_regex = Regex::new(r"^[a-zA-Z0-9-]+/[a-zA-Z0-9\._-]+$")?; if !repo_regex.is_match(repo) { error!("❌ Invalid repository format. Please use OWNER/REPO"); return Err("Invalid repository format".into()); @@ -294,6 +294,9 @@ async fn run() -> Result<(), Box> { opt.ignore_invalid_watch_pattern, )?; + let base_apps = unique_names(base_apps, &base_branch); + let target_apps = unique_names(target_apps, &target_branch); + let found_base_apps = !base_apps.is_empty(); let found_target_apps = !target_apps.is_empty(); @@ -305,21 +308,25 @@ async fn run() -> Result<(), Box> { return Ok(()); } - info!( - "💾 Writing applications from '{}' to ./{}", - base_branch.name, - base_branch.app_file() - ); - utils::write_to_file(base_branch.app_file(), &applications_to_string(base_apps))?; - info!( - "💾 Writing applications from '{}' to ./{}", - target_branch.name, - target_branch.app_file() - ); - utils::write_to_file( - target_branch.app_file(), - &applications_to_string(target_apps), - )?; + { + info!( + "💾 Writing {} applications from '{}' to ./{}", + base_apps.len(), + base_branch.name, + base_branch.app_file() + ); + utils::write_to_file(base_branch.app_file(), &applications_to_string(base_apps)?)?; + info!( + "💾 Writing {} applications from '{}' to ./{}", + target_apps.len(), + target_branch.name, + target_branch.app_file() + ); + utils::write_to_file( + target_branch.app_file(), + &applications_to_string(target_apps)?, + )?; + } match cluster_tool { ClusterTool::Kind => kind::create_cluster(&cluster_name)?, @@ -334,7 +341,7 @@ async fn run() -> Result<(), Box> { Ok(_) => info!("🤷 No secrets found in {}", secrets_folder), Err(e) => { error!("❌ Failed to apply secrets"); - return Err(e); + return Err(e.into()); } } @@ -404,6 +411,44 @@ fn clean_output_folder(output_folder: &str) -> Result<(), Box> { Ok(()) } +// Give new name for duplicate names in Vec +fn unique_names(apps: Vec, branch: &Branch) -> Vec { + let mut duplicate_names: HashMap> = HashMap::new(); + apps.into_iter().for_each(|a| { + duplicate_names.entry(a.name.clone()).or_default().push(a); + }); + let mut new_vec: Vec = vec![]; + let mut duplicate_counter = 0; + for (name, apps) in duplicate_names { + if apps.len() > 1 { + duplicate_counter += 1; + debug!( + "Found {} duplicate applications with name: '{}'", + apps.len(), + name + ); + let mut sorted_apps = apps.clone(); + sorted_apps.sort_by_key(|a| a.as_string().unwrap_or_default()); + for (i, app) in sorted_apps.into_iter().enumerate() { + let new_name = format!("{}-{}", name, i + 1); + let mut new_app = app.clone(); + new_app.name.clone_from(&new_name); + new_app.yaml["metadata"]["name"] = serde_yaml::Value::String(new_name); + new_vec.push(new_app); + } + } else { + new_vec.push(apps[0].clone()); + } + } + if duplicate_counter > 0 { + info!( + "🔍 Found {} duplicate applications names for branch: {}. Suffixing with -1, -2, -3, etc.", + duplicate_counter, branch.name + ); + } + new_vec +} + fn cleanup_cluster(tool: ClusterTool, cluster_name: &str) { match tool { ClusterTool::Kind if kind::cluster_exists(cluster_name) => { @@ -433,9 +478,9 @@ fn apply_manifest(file_name: &str) -> Result { }) } -fn apply_folder(folder_name: &str) -> Result> { +fn apply_folder(folder_name: &str) -> Result { if !PathBuf::from(folder_name).is_dir() { - return Err(format!("{} is not a directory", folder_name).into()); + return Err(format!("{} is not a directory", folder_name)); } let mut count = 0; if let Ok(entries) = fs::read_dir(folder_name) { @@ -445,7 +490,7 @@ 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(e.stderr.into()), + Err(e) => return Err(e.stderr), } } } @@ -453,10 +498,17 @@ fn apply_folder(folder_name: &str) -> Result> { Ok(count) } -pub fn applications_to_string(applications: Vec) -> String { - applications +pub fn applications_to_string(applications: Vec) -> Result> { + let output = applications .iter() - .map(|a| a.to_string()) - .collect::>() - .join("---\n") + .map(|a| { + a.as_string().inspect_err(|e| { + error!( + "❌ Failed to convert application '{}' (path: {}) to valid YAML: {}", + a.name, a.file_name, e + ); + }) + }) + .collect::, Box>>()?; + Ok(output.join("---\n")) } diff --git a/src/parsing.rs b/src/parsing.rs index 6936710e..452a76ed 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -68,16 +68,43 @@ pub fn get_applications_for_branches<'a>( if duplicate_yaml.is_empty() { Ok((base_apps, target_apps)) } else { + let base_apps_before = base_apps.len(); + let target_apps_before = target_apps.len(); + // remove duplicates - let base_apps = base_apps + let base_apps: Vec = base_apps .into_iter() .filter(|a| !duplicate_yaml.contains(&a.yaml)) .collect(); - let target_apps = target_apps + let target_apps: Vec = target_apps .into_iter() .filter(|a| !duplicate_yaml.contains(&a.yaml)) .collect(); + info!( + "🤖 Skipped {} applications for branch: '{}' because they have not changed after patching", + base_apps_before - base_apps.len(), + base_branch.name + ); + + info!( + "🤖 Skipped {} applications for branch: '{}' because they have not changed after patching", + target_apps_before - target_apps.len(), + target_branch.name + ); + + info!( + "🤖 Using the remaining {} applications for branch: '{}'", + base_apps.len(), + base_branch.name + ); + + info!( + "🤖 Using the remaining {} applications for branch: '{}'", + target_apps.len(), + target_branch.name + ); + Ok((base_apps, target_apps)) } } @@ -209,7 +236,6 @@ fn patch_applications( if app.is_err() { info!("❌ Failed to patch application: {}", app_name); - return app; } app })