# frozen_string_literal: true require "bundler" 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 install mux_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) { 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 } 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| plan = PlanSummaryHandler.from_file(PLAN_FILENAME) begin abort_message = catch :abort do plan.run_interactive end if abort_message log Paint["Aborted: #{abort_message}", :red] else run_plan end rescue Exception => e log e.full_message log "Interactive Apply Failed!" 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 pretty_plan_summary(filename) plan = PlanSummaryHandler.from_file(filename) plan.flat_summary.each do |line| log line, depth: 2 end log "", depth: 2 log plan.summary, depth: 2 end end end end end