From 2f66a84ec497cb820b5dc2cd6cfa4d6223cf6155 Mon Sep 17 00:00:00 2001 From: Tyler Maginnis Date: Tue, 27 Aug 2024 02:10:07 -0400 Subject: [PATCH] Add feature to skip upgrade if no changes detected (#379) * Implement no-upgrade-if-unchanged based on the --detailed-exit-code from helm-diff * Revert config.yaml * Add the comma causing the linting error from catgo fmt * Remove the trailing { for cargo linting * Align the comment for cargo linting * Align the comment for cargo linting * Code formatting for cargo linting * Add the ',' for cargo linting * Remainder of cargo linting * Resolve CR * Add -b --bypass-skip-upgrade-on-no-changes #clap flag * Based on result of char cargo test, change chars to strings * "Rust auto-linting, revert config.yaml context" * Implement --yes || -y for -b Upgrade suboption for CI environments * Implement -b -y || -n (--bypass-skip-upgrade-on-no-changes --no is the implicit case * Cargo linting fix * Update documentation * Fix some clippy errors introduced - partial reversion in Main function * Add the UpgradeControl enum * Suppress exit_code 2 * Suppress the single exit_code warning for unused variables in text.rs * Remove UI prompt intererfered with TUI. Alter the way specific debug messages are written to avoid interfering with TUI mode --- README.md | 29 +++++++++++ src/command.rs | 4 +- src/helm.rs | 64 +++++++++++++++++++++--- src/main.rs | 121 +++++++++++++++++++++++++++++++++++++++------ src/output/text.rs | 2 + 5 files changed, 195 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index cd58fe4..3b7851f 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,35 @@ Only do this if you really want to deploy: cargo run -- --vdir ./example/helm-values upgrade ``` +## Additional Options + +### Bypass Upgrade on No Changes + +The `-b` or `--bypass-upgrade-on-no-changes` option allows you to bypass the upgrade process if no changes are detected. This can be useful to save time and resources when you are confident that no changes have been made to the release values. + +### Suboptions + +#### Yes + +The `-y` or `--yes` suboption can be used in conjunction with the `--bypass-upgrade-on-no-changes` option to automatically proceed with the upgrade even if no changes are detected. This is useful for automated scripts where manual intervention is not possible. + +#### No + +The `-n` or `--no` suboption can be used in conjunction with the `--bypass-upgrade-on-no-changes` option to automatically skip the upgrade if no changes are detected. This is useful when you want to ensure that upgrades are only performed when necessary without manual intervention. + +Example usage: + +```sh +# Bypass upgrade on no changes and automatically proceed with the upgrade +cargo run -- --vdir ./example/helm-values upgrade --bypass-upgrade-on-no-changes --yes +cargo run -- --vdir ./example/helm-values upgrade -b -y + + +# Bypass upgrade on no changes and automatically skip the upgrade +cargo run -- --vdir ./example/helm-values upgrade --bypass-upgrade-on-no-changes --no +cargo run -- --vdir ./example/helm-values upgrade -b -y +``` + ## Config layout You can have zero or more environments. diff --git a/src/command.rs b/src/command.rs index a1e4dda..0292105 100644 --- a/src/command.rs +++ b/src/command.rs @@ -22,6 +22,7 @@ pub struct CommandSuccess { pub stdout: String, pub stderr: String, pub duration: Duration, + pub exit_code: i32, // This field stores the exit code of the command to determine if it was successful or not, and whether or not there were any diffs. } impl CommandSuccess { @@ -176,7 +177,7 @@ impl CommandLine { let kind = match output { Err(err) => Err(CommandErrorKind::FailedToStart { err }), Ok(output) => { - if output.status.success() { + if output.status.success() || exit_code == 2 { Ok(()) } else { Err(CommandErrorKind::BadExitCode {}) @@ -189,6 +190,7 @@ impl CommandLine { cmd: self.clone(), stdout, stderr, + exit_code, duration, }), Err(kind) => Err(CommandError { diff --git a/src/helm.rs b/src/helm.rs index 53ee397..e4af019 100644 --- a/src/helm.rs +++ b/src/helm.rs @@ -118,6 +118,8 @@ pub struct HelmResult { pub installation: Arc, pub result: CommandResult, pub command: Command, + // The exit code contains information about whether there were any diffs. + pub exit_code: i32, } impl HelmResult { @@ -130,6 +132,7 @@ impl HelmResult { installation: installation.clone(), result, command, + exit_code: 0, } } @@ -366,20 +369,35 @@ pub async fn template( } } +// The DiffResult struct is used to store the exit code of the diff command. +pub enum DiffResult { + NoChanges, + Changes, + Errors, + Unknown, +} +async fn log_debug_message(tx: &MultiOutput, message: &str) { + tx.send(Message::Log(log!(Level::DEBUG, message))).await; +} + /// Run the helm diff command. pub async fn diff( installation: &Arc, helm_repos: &HelmReposLock, tx: &MultiOutput, -) -> Result<()> { +) -> Result { + // Retrieve the helm chart for the given installation. let chart = helm_repos.get_helm_chart(&installation.chart_reference)?; + // Get the chart arguments from the chart. let (chart, mut chart_args) = get_args_from_chart(&chart); + // Construct the arguments for the helm diff command. let mut args = vec![ "diff".into(), "upgrade".into(), installation.name.clone().into(), chart, + "--detailed-exitcode".into(), // This flag ensures that the exit code will indicate if there are changes or errors. "--context=3".into(), "--no-color".into(), "--allow-unreleased".into(), @@ -389,22 +407,52 @@ pub async fn diff( installation.clone().context.clone().into(), ]; + // Append additional arguments from the installation configuration. args.append(&mut add_values_files(installation)); args.append(&mut chart_args); args.append(&mut get_template_parameters(installation)); + // Create a CommandLine instance with the helm path and the constructed arguments. let command_line = CommandLine(helm_path(), args); + // Run the command and await the result. let result = command_line.run().await; - let has_errors = result.is_err(); - let i_result = HelmResult::from_result(installation, result, Command::Diff); + // Check if there were any errors in the command execution. + let _has_errors = result.is_err(); + // Create a HelmResult instance from the command result. + let mut i_result = HelmResult::from_result(installation, result, Command::Diff); + + // Evaluate the detailed exit code - any non-zero exit code indicates changes (1) or errors (2). + let diff_result = if let Ok(command_success) = &i_result.result { + i_result.exit_code = command_success.exit_code; + match command_success.exit_code { + 0 => { + log_debug_message(tx, "No changes detected!").await; // Exit code 0 indicates no changes. + DiffResult::NoChanges + } + 1 => { + log_debug_message(tx, "Errors encountered!").await; // Exit code 1 indicates errors. + DiffResult::Errors + } + 2 => { + log_debug_message(tx, "Changes detected!").await; // Exit code 2 indicates changes. + DiffResult::Changes + } + _ => { + log_debug_message(tx, "Unknown exit code").await; // Any other exit code is considered unknown. + DiffResult::Unknown + } + } + } else { + log_debug_message(tx, "Other exception encountered").await; // If the command result is an error, return Unknown. + DiffResult::Unknown + }; + + // Wrap the HelmResult in an Arc and send it via the MultiOutput channel. let i_result = Arc::new(i_result); tx.send(Message::InstallationResult(i_result)).await; - if has_errors { - Err(anyhow::anyhow!("diff operation failed")) - } else { - Ok(()) - } + // Return the diff result. + Ok(diff_result) } /// Run the helm upgrade command. diff --git a/src/main.rs b/src/main.rs index c91d5aa..6899028 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,7 @@ use tracing_subscriber::{Layer, Registry}; mod command; mod helm; +use helm::DiffResult; use helm::{HelmChart, Installation}; use helm::{HelmRepo, InstallationId}; @@ -65,6 +66,14 @@ pub struct Update { pub value: String, } +/// For command line options, we need to control the upgrade process. +#[derive(Clone, Copy)] // Add Clone and Copy traits +enum UpgradeControl { + BypassAndAssumeYes, + BypassAndAssumeNo, + Normal, +} + impl FromStr for Update { type Err = anyhow::Error; @@ -189,21 +198,55 @@ async fn run_job( helm_repos: &HelmReposLock, installation: &Arc, tx: &MultiOutput, + upgrade_control: &UpgradeControl, ) -> Result<()> { match command { Request::Upgrade { .. } => { - helm::upgrade(installation, helm_repos, tx, true).await?; - helm::upgrade(installation, helm_repos, tx, false).await + let diff_result = helm::diff(installation, helm_repos, tx).await?; + match diff_result { + DiffResult::NoChanges => { + match upgrade_control { + UpgradeControl::BypassAndAssumeYes => { + helm::upgrade(installation, helm_repos, tx, true).await?; + helm::upgrade(installation, helm_repos, tx, false).await?; + } + UpgradeControl::BypassAndAssumeNo | UpgradeControl::Normal => { + // Do nothing, as these are implicit or default cases + } + } + } + DiffResult::Changes => { + helm::upgrade(installation, helm_repos, tx, true).await?; + helm::upgrade(installation, helm_repos, tx, false).await?; + } + DiffResult::Errors | DiffResult::Unknown => { + // Handle errors or unknown cases if needed. + } + } + Ok(()) + } + Request::Diff { .. } => { + helm::diff(installation, helm_repos, tx).await?; + Ok(()) } - Request::Diff { .. } => helm::diff(installation, helm_repos, tx).await, Request::Test { .. } => { helm::outdated(installation, helm_repos, tx).await?; helm::lint(installation, tx).await?; - helm::template(installation, helm_repos, tx).await + helm::template(installation, helm_repos, tx).await?; + Ok(()) + } + Request::Template { .. } => { + helm::template(installation, helm_repos, tx).await?; + Ok(()) + } + Request::Outdated { .. } => { + helm::outdated(installation, helm_repos, tx).await?; + Ok(()) + } + Request::Update { updates, .. } => { + helm::update(installation, tx, updates).await?; + Ok(()) } - Request::Template { .. } => helm::template(installation, helm_repos, tx).await, - Request::Outdated { .. } => helm::outdated(installation, helm_repos, tx).await, - Request::Update { updates, .. } => helm::update(installation, tx, updates).await, } } @@ -262,7 +305,17 @@ struct Args { #[derive(Subcommand, Debug, Clone)] enum Request { /// Upgrade/install releases. - Upgrade {}, + Upgrade { + /// Bypass skip upgrade on no changes. + #[clap(long, short = 'b')] + bypass_skip_upgrade_on_no_changes: bool, + /// Assume yes or no. + #[clap(long, short = 'y', conflicts_with = "no", default_value_t = false)] + yes: bool, + /// Assume no. + #[clap(long, short = 'n', conflicts_with = "yes", default_value_t = false)] + no: bool, + }, /// Diff releases with current state. Diff {}, @@ -380,6 +433,28 @@ async fn main() -> Result<()> { tracing_subscriber::fmt::init(); let args = Args::parse(); + // Extract the bypass_skip_upgrade_on_no_changes and bypass_assume_yes flags if the command is Upgrade + let upgrade_control = if let Request::Upgrade { + bypass_skip_upgrade_on_no_changes, + yes, + no, + } = args.command + { + if bypass_skip_upgrade_on_no_changes { + if yes { + UpgradeControl::BypassAndAssumeYes + } else if no { + UpgradeControl::BypassAndAssumeNo + } else { + UpgradeControl::Normal + } + } else { + UpgradeControl::Normal + } + } else { + UpgradeControl::Normal + }; + let output_types = if args.output.is_empty() { vec![OutputFormat::Text] } else { @@ -424,7 +499,7 @@ async fn main() -> Result<()> { .await; // Save the error for now so we can clean up. - let rc = do_task(command, &args, &output_pipe).await; + let rc = do_task(command, &args, &output_pipe, upgrade_control).await; // Log the error. if let Err(err) = &rc { @@ -456,7 +531,12 @@ async fn main() -> Result<()> { rc } -async fn do_task(command: Arc, args: &Args, output: &output::MultiOutput) -> Result<()> { +async fn do_task( + command: Arc, + args: &Args, + output: &output::MultiOutput, + upgrade_control: UpgradeControl, +) -> Result<()> { // let mut helm_repos = HelmRepos::new(); let (skipped_list, todo) = generate_todo(args)?; @@ -468,7 +548,7 @@ async fn do_task(command: Arc, args: &Args, output: &output::MultiOutpu } // let jobs: Jobs = (command, todo); - run_jobs_concurrently(command, todo, output, skipped).await + run_jobs_concurrently(command, todo, output, skipped, &upgrade_control).await } type SkippedResult = Arc; @@ -663,15 +743,16 @@ async fn run_jobs_concurrently( todo: Vec>, output: &output::MultiOutput, skipped: InstallationSet, + upgrade_control: &UpgradeControl, ) -> Result<()> { let required_repos = if request.requires_helm_repos() { get_required_repos(&todo) } else { vec![] }; - let rc = with_helm_repos(required_repos, |repos| async { - run_jobs_concurrently_with_repos(request, todo, output, skipped, repos).await + run_jobs_concurrently_with_repos(request, todo, output, skipped, repos, *upgrade_control) + .await }) .await; @@ -684,6 +765,7 @@ async fn run_jobs_concurrently_with_repos( output: &output::MultiOutput, skipped: InstallationSet, helm_repos: Arc, + upgrade_control: UpgradeControl, ) -> Result<()> { let do_depends = request.do_depends(); // let skip_depends = !matches!(jobs.0, Task::Upgrade | Task::Test); @@ -706,7 +788,14 @@ async fn run_jobs_concurrently_with_repos( let request = request.clone(); let helm_repos = helm_repos.clone(); tokio::spawn(async move { - worker_thread(&request, &helm_repos, &tx_dispatch, &output).await + worker_thread( + &request, + &helm_repos, + &tx_dispatch, + &output, + &upgrade_control, + ) + .await }) }) .collect(); @@ -802,6 +891,7 @@ async fn worker_thread( helm_repos: &HelmReposLock, tx_dispatch: &mpsc::Sender, output: &MultiOutput, + upgrade_control: &UpgradeControl, ) -> Result<()> { let mut errors = false; @@ -822,10 +912,9 @@ async fn worker_thread( .await; // Execute the job - let result = run_job(command, helm_repos, &install, output).await; + let result = run_job(command, helm_repos, &install, output, upgrade_control).await; match &result { Ok(()) => { - // Tell dispatcher job is done so it can update the dependancies tx_dispatch .send(Dispatch::Done(HashIndex::get_hash_index(&install))) .await?; diff --git a/src/output/text.rs b/src/output/text.rs index 20c88a1..7797c16 100644 --- a/src/output/text.rs +++ b/src/output/text.rs @@ -216,6 +216,8 @@ fn process_message(msg: &Arc, state: &mut State) { command, result, installation, + #[allow(unused_variables)] // Suppress warning for this variable + exit_code, } = hr.as_ref(); let result_str = hr.result_line();