From b7c9b4205f143ce41e00a57c8779f95190d2ffea Mon Sep 17 00:00:00 2001 From: Dag Andersen Date: Tue, 22 Oct 2024 12:04:49 +0200 Subject: [PATCH] Add support for 'changed files' and 'watch-pattern' annotation (#50) --- .github/workflows/generate-diff.yml | 2 +- .github/workflows/mkdocs.yaml | 4 + .github/workflows/pr-build.yml | 3 + Cargo.lock | 6 +- Cargo.toml | 4 +- .../label-selectors/my-app-labels.yaml | 22 ++ .../watch-pattern/broken-regex.yaml | 22 ++ .../watch-pattern/valid-regex.yaml | 22 ++ makefile | 18 +- src/argocd.rs | 6 +- src/extract.rs | 4 +- src/main.rs | 108 +++++-- src/no_apps_found.rs | 62 ++++ src/parsing.rs | 293 ++++++++++++++---- src/utils.rs | 17 +- 15 files changed, 471 insertions(+), 122 deletions(-) create mode 100644 examples/helm/applications/label-selectors/my-app-labels.yaml create mode 100644 examples/helm/applications/watch-pattern/broken-regex.yaml create mode 100644 examples/helm/applications/watch-pattern/valid-regex.yaml create mode 100644 src/no_apps_found.rs diff --git a/.github/workflows/generate-diff.yml b/.github/workflows/generate-diff.yml index 24daf85..7778452 100644 --- a/.github/workflows/generate-diff.yml +++ b/.github/workflows/generate-diff.yml @@ -9,7 +9,7 @@ on: - "examples/**" jobs: - build: + render-diff: runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/mkdocs.yaml b/.github/workflows/mkdocs.yaml index 24bfd20..44ab61c 100644 --- a/.github/workflows/mkdocs.yaml +++ b/.github/workflows/mkdocs.yaml @@ -4,8 +4,12 @@ on: branches: - master - main + paths: + - "docs/**" + permissions: contents: write + jobs: deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index a5ab7ae..d29f34a 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -4,6 +4,9 @@ on: pull_request: branches: - "main" + paths: + - "src/**" + - Cargo.toml jobs: build-amd64: diff --git a/Cargo.lock b/Cargo.lock index b942c00..ce7b7e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ [[package]] name = "argocd-diff-preview" -version = "0.0.19" +version = "0.0.20" dependencies = [ "base64", "env_logger", @@ -533,9 +533,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.131" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "67d42a0bd4ac281beff598909bb56a86acaf979b84483e1c79c10dcaf98f8cf3" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index fd34af2..95d82b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "argocd-diff-preview" -version = "0.0.19" +version = "0.0.20" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,7 +10,7 @@ tokio = {version="1.40.0",features = ["full"]} base64 = "0.22.1" serde = "1.0.210" serde_yaml = "0.9.33" -serde_json = "1.0.128" +serde_json = "1.0.131" walkdir = "2.5.0" schemars = "0.8.21" structopt = { version = "0.3" } diff --git a/examples/helm/applications/label-selectors/my-app-labels.yaml b/examples/helm/applications/label-selectors/my-app-labels.yaml new file mode 100644 index 0000000..3c111d7 --- /dev/null +++ b/examples/helm/applications/label-selectors/my-app-labels.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: my-app-labels + namespace: argocd + labels: + team: my-team +spec: + project: default + destination: + name: in-cluster + namespace: default + sources: + - repoURL: https://github.com/dag-andersen/argocd-diff-preview + ref: local-files + - path: examples/helm/charts/myApp + repoURL: https://github.com/dag-andersen/argocd-diff-preview + helm: + valueFiles: + - $local-files/examples/helm/values/values.yaml + valuesObject: + replicaCount: 5 diff --git a/examples/helm/applications/watch-pattern/broken-regex.yaml b/examples/helm/applications/watch-pattern/broken-regex.yaml new file mode 100644 index 0000000..3ac7320 --- /dev/null +++ b/examples/helm/applications/watch-pattern/broken-regex.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: my-app-watch-pattern-broken-regex + namespace: argocd + annotations: + argocd-diff-preview/watch-pattern: '\' +spec: + project: default + destination: + name: in-cluster + namespace: default + sources: + - repoURL: https://github.com/dag-andersen/argocd-diff-preview + ref: local-files + - path: examples/helm/charts/myApp + repoURL: https://github.com/dag-andersen/argocd-diff-preview + helm: + valueFiles: + - $local-files/examples/helm/values/values.yaml + valuesObject: + replicaCount: 5 diff --git a/examples/helm/applications/watch-pattern/valid-regex.yaml b/examples/helm/applications/watch-pattern/valid-regex.yaml new file mode 100644 index 0000000..ea219e3 --- /dev/null +++ b/examples/helm/applications/watch-pattern/valid-regex.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: my-app-watch-pattern-valid-regex + namespace: argocd + annotations: + argocd-diff-preview/watch-pattern: "examples/helm/values/.*" +spec: + project: default + destination: + name: in-cluster + namespace: default + sources: + - repoURL: https://github.com/dag-andersen/argocd-diff-preview + ref: local-files + - path: examples/helm/charts/myApp + repoURL: https://github.com/dag-andersen/argocd-diff-preview + helm: + valueFiles: + - $local-files/examples/helm/values/values.yaml + valuesObject: + replicaCount: 5 diff --git a/makefile b/makefile index a1da078..b08e9c1 100644 --- a/makefile +++ b/makefile @@ -10,10 +10,18 @@ pull-repostory: 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 - -local-test-cargo: pull-repostory - cargo run -- -b "$(base_branch)" -t "$(target_branch)" --repo $(github_org)/$(gitops_repo) -r "$(regex)" --debug --diff-ignore "$(diff-ignore)" --timeout $(timeout) -l "$(selector)" +run-with-cargo: pull-repostory + cargo run -- -b "$(base_branch)" \ + -t "$(target_branch)" \ + --repo $(github_org)/$(gitops_repo) \ + --debug \ + -r "$(regex)" \ + --diff-ignore "$(diff_ignore)" \ + --timeout $(timeout) \ + -l "$(selector)" \ + --files-changed="$(files_changed)" -local-test-docker: pull-repostory +run-with-docker: pull-repostory docker build . -f $(docker_file) -t image docker run \ --network=host \ @@ -27,6 +35,8 @@ local-test-docker: pull-repostory -e TARGET_BRANCH=$(target_branch) \ -e REPO=$(github_org)/$(gitops_repo) \ -e FILE_REGEX="$(regex)" \ - -e DIFF_IGNORE="$(diff-ignore)" \ + -e DIFF_IGNORE="$(diff_ignore)" \ -e TIMEOUT=$(timeout) \ + -e SELECTOR="$(selector)" \ + -e FILES_CHANGED="$(files_changed)" \ image diff --git a/src/argocd.rs b/src/argocd.rs index 908ddb3..5e690e4 100644 --- a/src/argocd.rs +++ b/src/argocd.rs @@ -60,8 +60,6 @@ pub async fn install_argo_cd(options: ArgoCDOptions<'_>) -> Result<(), Box) -> Result<(), Box Result<(), Box> { - info!("🌚 Getting resources from {}", branch_type); + info!("🌚 Getting resources from {}-branch", branch_type); let app_file = apps_file(branch_type); @@ -111,10 +111,12 @@ pub async fn get_resources( Some(msg) if TIMEOUT_MESSAGES.iter().any(|e| msg.contains(e)) => { + debug!("Application: {} timed out with error: {}", name, msg); list_of_timed_out_apps.push(name.to_string().clone()); other_errors.push((name.to_string(), msg.to_string())); } Some(msg) => { + debug!("Application: {} failed with error: {}", name, msg); other_errors.push((name.to_string(), msg.to_string())); } _ => (), diff --git a/src/main.rs b/src/main.rs index 4464be0..ef629a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use crate::utils::{check_if_folder_exists, create_folder_if_not_exists, run_command}; use log::{debug, error, info}; +use parsing::{applications_to_string, GetApplicationOptions}; use regex::Regex; use std::fs; use std::path::PathBuf; @@ -14,6 +15,7 @@ mod diff; mod extract; mod kind; mod minikube; +mod no_apps_found; mod parsing; mod utils; @@ -75,9 +77,14 @@ struct Opt { /// Max diff message character count. Default: 65536 (GitHub comment limit) #[structopt(long, env)] max_diff_length: Option, + /// Label selector to filter on, supports '=', '==', and '!='. (e.g. -l key1=value1,key2=value2). #[structopt(long, short = "l", env)] selector: Option, + + /// List of files changed between the two branches. Input must be a comma or space separated string. When provided, only Applications watching these files will be rendered. + #[structopt(long, env)] + files_changed: Option, } #[derive(Debug)] @@ -174,6 +181,19 @@ async fn main() -> Result<(), Box> { .as_deref() .filter(|f| !f.trim().is_empty()); let max_diff_length = opt.max_diff_length; + let files_changed: Option> = opt + .files_changed + .map(|f| f.trim().to_string()) + .filter(|f| !f.is_empty()) + .map(|f| { + (if f.contains(',') { + f.split(',') + } else { + f.split(' ') + }) + .map(|s| s.trim().to_string()) + .collect() + }); // select local cluster tool let tool = match opt.local_cluster_tool { @@ -216,9 +236,12 @@ async fn main() -> Result<(), Box> { if let Some(a) = max_diff_length { info!("✨ - max-diff-length: {}", a); } + if let Some(a) = files_changed.clone() { + info!("✨ - files-changed: {:?}", a); + } // label selectors can be fined in the following format: key1==value1,key2=value2,key3!=value3 - let selector = opt.selector.map(|s| { + let selector = opt.selector.filter(|s| !s.trim().is_empty()).map(|s| { let labels: Vec = s .split(",") .filter(|l| !l.trim().is_empty()) @@ -291,16 +314,39 @@ async fn main() -> Result<(), Box> { let cluster_name = CLUSTER_NAME; + // 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, + }, + &file_regex, + &selector, + &files_changed, + repo, + ) + .await?; + + let found_base_apps = !base_apps.is_empty(); + let found_target_apps = !target_apps.is_empty(); + + 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?; + 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?, } - - argocd::install_argo_cd(argocd::ArgoCDOptions { - version: argocd_version, - debug: opt.debug, - }) - .await?; create_folder_if_not_exists(secrets_folder); match apply_folder(secrets_folder) { @@ -312,36 +358,34 @@ async fn main() -> Result<(), Box> { } } - // remove .git from repo - let repo = repo.trim_end_matches(".git"); - let base_apps = parsing::get_applications_as_string( - BASE_BRANCH_FOLDER, - &base_branch_name, - &file_regex, - &selector, - repo, - ) - .await?; - let target_apps = parsing::get_applications_as_string( - TARGET_BRANCH_FOLDER, - &target_branch_name, - &file_regex, - &selector, - repo, - ) + argocd::install_argo_cd(argocd::ArgoCDOptions { + version: argocd_version, + debug: opt.debug, + }) .await?; - fs::write(apps_file(&Branch::Base), base_apps)?; - fs::write(apps_file(&Branch::Target), &target_apps)?; + fs::write(apps_file(&Branch::Base), applications_to_string(base_apps))?; + fs::write( + apps_file(&Branch::Target), + applications_to_string(target_apps), + )?; - // Cleanup + // Cleanup output folder clean_output_folder(output_folder); - extract::get_resources(&Branch::Base, timeout, output_folder).await?; - extract::delete_applications().await; - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - extract::get_resources(&Branch::Target, timeout, output_folder).await?; + // Extract resources from Argo CD + if found_base_apps { + extract::get_resources(&Branch::Base, timeout, output_folder).await?; + if found_target_apps { + 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?; + } + // Delete cluster match tool { ClusterTool::Kind => kind::delete_cluster(&cluster_name), ClusterTool::Minikube => minikube::delete_cluster(), @@ -378,7 +422,7 @@ fn apply_manifest(file_name: &str) -> Result { .arg("-f") .arg(file_name) .output() - .expect(format!("failed to apply manifest: {}", file_name).as_str()); + .unwrap_or_else(|_| panic!("failed to apply manifest: {}", file_name)); match output.status.success() { true => Ok(output), false => Err(output), diff --git a/src/no_apps_found.rs b/src/no_apps_found.rs new file mode 100644 index 0000000..8e43ad9 --- /dev/null +++ b/src/no_apps_found.rs @@ -0,0 +1,62 @@ +use std::error::Error; +use std::fs; + +use crate::Selector; + +// Message to show when no applications were found + +pub async fn write_message( + output_folder: &str, + selector: &Option>, + changed_files: &Option>, +) -> Result<(), Box> { + let message = get_message(selector, changed_files); + + let markdown = generate_markdown(&message); + let markdown_path = format!("{}/diff.md", output_folder); + fs::write(&markdown_path, markdown)?; + + Ok(()) +} + +const MARKDOWN_TEMPLATE: &str = r#" +## Argo CD Diff Preview + +%message% +"#; + +fn generate_markdown(message: &str) -> String { + MARKDOWN_TEMPLATE + .replace("%message%", message) + .trim_start() + .to_string() +} + +pub fn get_message( + selector: &Option>, + changed_files: &Option>, +) -> String { + let selector_string = |s: &Vec| { + s.iter() + .map(|s| s.to_string()) + .collect::>() + .join(",") + }; + + match (selector, changed_files) { + (Some(s), Some(f)) => format!( + "Found no changed Applications that matched '{}' and watched these files: '{}'", + selector_string(s), + f.join("`, `") + ), + (Some(s), None) => format!( + "Found no changed Applications that matched '{}'", + selector_string(s) + ), + (None, Some(f)) => format!( + "Found no changed Applications that watched these files: '{}'", + f.join("`, `") + ), + (None, None) => "Found no Applications".to_string(), + } +} diff --git a/src/parsing.rs b/src/parsing.rs index 9e3babb..ff59057 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -1,5 +1,5 @@ use crate::{Operator, Selector}; -use log::{debug, info}; +use log::{debug, info, warn}; use regex::Regex; use serde_yaml::Mapping; use std::{error::Error, io::BufRead}; @@ -9,29 +9,76 @@ struct K8sResource { yaml: serde_yaml::Value, } -struct Application { +pub struct Application { file_name: String, yaml: serde_yaml::Value, kind: ApplicationKind, } +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()) + } +} + enum ApplicationKind { Application, ApplicationSet, } -pub async fn get_applications_as_string( +const ANNOTATION_WATCH_PATTERN: &str = "argocd-diff-preview/watch-pattern"; +const ANNOTATION_IGNORE: &str = "argocd-diff-preview/ignore"; + +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>, + regex: &Option, + selector: &Option>, + files_changed: &Option>, + repo: &str, +) -> Result<(Vec, Vec), Box> { + let base_apps = get_applications( + base_branch.directory, + base_branch.branch, + regex, + selector, + files_changed, + repo, + ) + .await?; + let target_apps = get_applications( + target_branch.directory, + target_branch.branch, + regex, + selector, + files_changed, + repo, + ) + .await?; + + Ok((base_apps, target_apps)) +} + +pub async fn get_applications( directory: &str, branch: &str, regex: &Option, selector: &Option>, + files_changed: &Option>, repo: &str, -) -> Result> { +) -> Result, Box> { let yaml_files = get_yaml_files(directory, regex).await; - let k8s_resources = parse_yaml(yaml_files).await; - let applications = get_applications(k8s_resources, selector); - let output = patch_applications(applications, branch, repo).await?; - Ok(output) + let k8s_resources = parse_yaml(directory, yaml_files).await; + let applications = from_resource_to_application(k8s_resources, selector, files_changed); + if !applications.is_empty() { + return patch_applications(applications, branch, repo).await; + } + Ok(applications) } async fn get_yaml_files(directory: &str, regex: &Option) -> Vec { @@ -50,27 +97,41 @@ async fn get_yaml_files(directory: &str, regex: &Option) -> Vec { .map(|s| s == "yaml" || s == "yml") .unwrap_or(false) }) - .map(|e| format!("{}", e.path().display())) + .map(|e| { + format!( + "{}", + e.path() + .iter() + .skip(1) + .collect::() + .display() + ) + }) .filter(|f| regex.is_none() || regex.as_ref().unwrap().is_match(f)) .collect(); match regex { Some(r) => debug!( - "🤖 Found {} yaml files matching regex: {}", + "🤖 Found {} yaml files in dir '{}' matching regex: {}", yaml_files.len(), + directory, r.as_str() ), - None => debug!("🤖 Found {} yaml files", yaml_files.len()), + None => debug!( + "🤖 Found {} yaml files in dir '{}'", + yaml_files.len(), + directory + ), } yaml_files } -async fn parse_yaml(files: Vec) -> Vec { +async fn parse_yaml(directory: &str, files: Vec) -> Vec { files.iter() .flat_map(|f| { - debug!("Found file: {}", f); - let file = std::fs::File::open(f).unwrap(); + debug!("In dir '{}' found yaml file: {}", directory, f); + let file = std::fs::File::open(format!("{}/{}",directory,f)).unwrap(); let reader = std::io::BufReader::new(file); let lines = reader.lines().map(|l| l.unwrap()); @@ -106,7 +167,7 @@ async fn patch_applications( applications: Vec, branch: &str, repo: &str, -) -> Result> { +) -> Result, Box> { info!("🤖 Patching applications for branch: {}", branch); let point_destination_to_in_cluster = |spec: &mut Mapping| { @@ -186,24 +247,17 @@ async fn patch_applications( branch ); - // convert back to yaml string - let mut output = String::new(); - for r in applications { - output.push_str(&serde_yaml::to_string(&r.yaml)?); - output.push_str("---\n"); - } - - Ok(output) + Ok(applications) } -fn get_applications( +fn from_resource_to_application( k8s_resources: Vec, selector: &Option>, + files_changed: &Option>, ) -> Vec { - k8s_resources + let apps: Vec = k8s_resources .into_iter() .filter_map(|r| { - debug!("Processing file: {}", r.file_name); let kind = r.yaml["kind"] .as_str() @@ -214,52 +268,157 @@ fn get_applications( _ => None, })?; - if r.yaml["metadata"]["annotations"]["argocd-diff-preview/ignore"].as_str() - == Some("true") - { + Some(Application { + kind, + file_name: r.file_name, + yaml: r.yaml, + }) + }) + .collect(); + + match (selector, files_changed) { + (Some(s), Some(f)) => info!( + "🤖 Will only run on Applications that match '{}' and watch these files: '{}'", + s.iter() + .map(|s| s.to_string()) + .collect::>() + .join(","), + f.join("`, `") + ), + (Some(s), None) => info!( + "🤖 Will only run on Applications that match '{}'", + s.iter() + .map(|s| s.to_string()) + .collect::>() + .join(",") + ), + (None, Some(f)) => info!( + "🤖 Will only run on Applications that watch these files: '{}'", + f.join("`, `") + ), + (None, None) => {}, + } + + let number_of_apps_before_filtering = apps.len(); + + 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.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), + 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 'argocd-diff-preview/ignore=true' in file: {}", - r.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), - r.file_name + "Ignoring application {:?} due to label selector mismatch in file: {}", + a.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), + a.file_name ); return None; + } else { + debug!( + "Selected application {:?} due to label selector match in file: {}", + a.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), + a.file_name + ); } + } - // loop over labels and check if the selector matches - if let Some(selector) = selector { - let labels: Vec<(&str, &str)> = { - match r.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 selector mismatch in file: {}", - r.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), - r.file_name - ); - return None; - } else { - debug!( - "Selected application {:?} due to selector match in file: {}", - r.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), - r.file_name - ); - } + // Check watch pattern annotation + let pattern_annotation = a.yaml["metadata"]["annotations"][ANNOTATION_WATCH_PATTERN].as_str(); + let pattern: Option> = pattern_annotation.map(Regex::new); + match (files_changed, pattern) { + (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.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), + a.file_name + ); + } + // Check if the application changed and the regex pattern matches. + (Some(files_changed), Some(Ok(pattern))) if files_changed.iter().any(|f| pattern.is_match(f)) => { + debug!( + "Selected application {:?} due to regex pattern '{}' matching changed files", + a.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), + pattern + ); + } + (_, Some(Ok(pattern))) => { + debug!( + "Ignoring application {:?} due to regex pattern '{}' not matching changed files", + a.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), + pattern + ); + return None; + }, + (_, Some(Err(e))) => { + warn!( + "🚨 Ignoring application {:?} due to invalid regex pattern '{}' ({})", + a.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), + pattern_annotation.unwrap(), + a.file_name + ); + debug!("Error: {}", e); + return None; + } + (_, None) => { + debug!( + "Ignoring application {:?} due to missing '{}' annotation ({})", + a.yaml["metadata"]["name"].as_str().unwrap_or("unknown"), + &ANNOTATION_WATCH_PATTERN, + a.file_name + ); + return None; } + } - Some(Application { - kind, - file_name: r.file_name, - yaml: r.yaml, - }) - }) - .collect() + Some(a) + }).collect(); + + if number_of_apps_before_filtering != filtered_apps.len() { + info!( + "🤖 Found {} applications before filtering", + number_of_apps_before_filtering + ); + info!( + "🤖 Found {} applications after filtering", + filtered_apps.len() + ); + } else { + info!("🤖 Found {} applications", number_of_apps_before_filtering); + } + + filtered_apps +} + +pub fn applications_to_string(applications: Vec) -> String { + applications + .iter() + .map(|a| a.to_string()) + .collect::>() + .join("---\n") } diff --git a/src/utils.rs b/src/utils.rs index ae584fb..ef39f9a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,7 +20,10 @@ pub async fn run_command(command: &str, current_dir: Option<&str>) -> Result, current_dir: Option<&str>) -> Result { +pub async fn run_command_from_list( + command: Vec<&str>, + current_dir: Option<&str>, +) -> Result { let output = Command::new(command[0]) .args(&command[1..]) .env( @@ -29,13 +32,13 @@ pub async fn run_command_from_list(command: Vec<&str>, current_dir: Option<&str> ) .current_dir(current_dir.unwrap_or(".")) .output() - .expect(format!("Failed to execute command: {}", command.join(" ")).as_str()); + .unwrap_or_else(|_| panic!("Failed to execute command: {}", command.join(" "))); - if !output.status.success() { - return Err(output); + if output.status.success() { + Ok(output) + } else { + Err(output) } - - Ok(output) } pub fn spawn_command(command: &str, current_dir: Option<&str>) { @@ -46,5 +49,5 @@ pub fn spawn_command(command: &str, current_dir: Option<&str>) { .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() - .expect(format!("Failed to execute command: {}", command).as_str()); + .unwrap_or_else(|_| panic!("Failed to execute command: {}", command)); }