# frozen_string_literal: true module MuxTf module Cli module Current extend TerraformHelpers extend PiotrbCliUtils::Util extend PiotrbCliUtils::CriCommandSupport extend PiotrbCliUtils::CmdLoop PLAN_FILENAME = 'foo.tfplan' class << self def run(args) version_check if args[0] == 'cli' cmd_loop return end folder_name = File.basename(Dir.getwd) log "Processing #{Paint[folder_name, :cyan]} ..." ENV['TF_IN_AUTOMATION'] = '1' ENV['TF_INPUT'] = '0' return launch_cmd_loop(:error) unless run_validate if ENV['TF_UPGRADE'] upgrade_status, upgrade_meta = run_upgrade return launch_cmd_loop(:error) unless upgrade_status == :ok end plan_status, @plan_meta = create_plan(PLAN_FILENAME) case plan_status when :ok log 'no changes, exiting', depth: 1 when :error log 'something went wrong', depth: 1 launch_cmd_loop(plan_status) when :changes log 'Printing Plan Summary ...', depth: 1 pretty_plan_summary(PLAN_FILENAME) launch_cmd_loop(plan_status) when :unknown launch_cmd_loop(plan_status) end rescue Exception => e # rubocop:disable Lint/RescueException puts Paint['Unhandled Exception!', :red] puts '=' * 20 puts e.full_message puts puts '< press enter to continue >' gets exit 1 end private def version_check if VersionCheck.has_updates? log Paint["="*80, :yellow] log "New version of #{Paint["mux_tf", :cyan]} is available!" log "You are currently on version: #{Paint[VersionCheck.current_gem_version, :yellow]}" log "Latest version found is: #{Paint[VersionCheck.latest_gem_version, :green]}" log "Run `#{Paint["gem update muf_tf", :green]}` to update!" log Paint["="*80, :yellow] end end def run_validate remedies = PlanFormatter.process_validation(validate) process_remedies(remedies) end def process_remedies(remedies) if remedies.delete? :init log 'Running terraform init ...', depth: 2 tf_init remedies = PlanFormatter.process_validation(validate) process_remedies(remedies) end unless remedies.empty? log "unprocessed remedies: #{remedies.to_a}", depth: 1 return false end true end def validate log 'Validating module ...', depth: 1 tf_validate.parsed_output end def create_plan(filename) log 'Preparing Plan ...', depth: 1 exit_code, meta = PlanFormatter.pretty_plan(filename) case exit_code when 0 [:ok, meta] when 1 [:error, meta] when 2 [:changes, meta] else log Paint["terraform plan exited with an unknown exit code: #{exit_code}", :yellow] [:unknown, meta] end end def launch_cmd_loop(status) return if ENV['NO_CMD'] case status when :error, :unknown log Paint['Dropping to command line so you can fix the issue!', :red] when :changes log Paint['Dropping to command line so you can review the changes.', :yellow] end cmd_loop(status) end def cmd_loop(status = nil) root_cmd = build_root_cmd folder_name = File.basename(Dir.getwd) puts root_cmd.help prompt = "#{folder_name} => " case status when :error, :unknown prompt = "[#{Paint[status.to_s, :red]}] #{prompt}" when :changes prompt = "[#{Paint[status.to_s, :yellow]}] #{prompt}" end run_cmd_loop(prompt) do |cmd| throw(:stop, :no_input) if cmd == '' args = Shellwords.split(cmd) root_cmd.run(args, {}, hard_exit: false) end end def build_root_cmd root_cmd = define_cmd(nil) root_cmd.add_command(plan_cmd) root_cmd.add_command(apply_cmd) root_cmd.add_command(shell_cmd) root_cmd.add_command(force_unlock_cmd) root_cmd.add_command(upgrade_cmd) root_cmd.add_command(interactive_cmd) root_cmd.add_command(exit_cmd) root_cmd end def plan_cmd define_cmd('plan', summary: 'Re-run plan') do |_opts, _args, _cmd| run_validate && run_plan end end def apply_cmd define_cmd('apply', summary: 'Apply the current plan') do |_opts, _args, _cmd| status = tf_apply(filename: PLAN_FILENAME) if status.success? throw :stop, :done else log 'Apply Failed!' end end end def shell_cmd define_cmd('shell', summary: 'Open your default terminal in the current folder') do |_opts, _args, _cmd| log Paint['Launching shell ...', :yellow] log Paint['When it exits you will be back at this prompt.', :yellow] system ENV['SHELL'] end end def force_unlock_cmd define_cmd('force-unlock', summary: 'Force unlock state after encountering a lock error!') do prompt = TTY::Prompt.new(interrupt: :noop) table = TTY::Table.new(header: %w[Field Value]) table << ['Lock ID', @plan_meta['ID']] table << ['Operation', @plan_meta['Operation']] table << ['Who', @plan_meta['Who']] table << ['Created', @plan_meta['Created']] puts table.render(:unicode, padding: [0, 1]) if @plan_meta && @plan_meta['error'] == 'lock' done = catch(:abort) do if @plan_meta['Operation'] != 'OperationTypePlan' throw :abort unless prompt.yes?( "Are you sure you want to force unlock a lock for operation: #{@plan_meta['Operation']}", default: false ) end throw :abort unless prompt.yes?( 'Are you sure you want to force unlock this lock?', default: false ) status = tf_force_unlock(id: @plan_meta['ID']) if status.success? log 'Done!' else log Paint["Failed with status: #{status}", :red] end true end log Paint['Aborted', :yellow] unless done else log Paint['No lock error or no plan ran!', :red] end end end def upgrade_cmd define_cmd('upgrade', summary: 'Upgrade modules/plguins') do |_opts, _args, _cmd| status, meta = run_upgrade if status != :ok log meta.inspect unless meta.empty? log 'Upgrade Failed!' end end end def interactive_cmd define_cmd('interactive', summary: 'Apply interactively') do |_opts, _args, _cmd| status = run_shell([tf_plan_summrary_cmd, PLAN_FILENAME, '-i'], return_status: true) if status != 0 log 'Interactive Apply Failed!' else run_plan end end end def run_plan plan_status, @plan_meta = create_plan(PLAN_FILENAME) case plan_status when :ok log 'no changes', depth: 1 when :error log 'something went wrong', depth: 1 when :changes log 'Printing Plan Summary ...', depth: 1 pretty_plan_summary(PLAN_FILENAME) when :unknown # nothing end end def run_upgrade exit_code, meta = PlanFormatter.process_upgrade case exit_code when 0 [:ok, meta] when 1 [:error, meta] # when 2 # [:changes, meta] else log Paint["terraform init upgrade exited with an unknown exit code: #{exit_code}", :yellow] [:unknown, meta] end end def tf_plan_summrary_cmd @tf_plan_summrary_cmd ||= File.expand_path(File.join(__dir__, '..', '..', '..', 'exe', 'tf_plan_summary')) end def pretty_plan_summary(filename) run_with_each_line([tf_plan_summrary_cmd, filename]) do |raw_line| log raw_line.rstrip, depth: 2 end end end end end end