class MuxTf::PlanFormatter

Public Class Methods

init_status_to_remedies(status, meta) click to toggle source
# File lib/mux_tf/plan_formatter.rb, line 87
def init_status_to_remedies(status, meta)
  remedies = Set.new
  if status != 0
    if meta[:need_reconfigure]
      remedies << :reconfigure
    else
      p [status, meta]
      remedies << :unknown
    end
  end
  remedies
end
pretty_plan(filename, targets: []) click to toggle source
# File lib/mux_tf/plan_formatter.rb, line 9
def pretty_plan(filename, targets: [])
  pastel = Pastel.new

  phase = :init

  meta = {}

  parser = StatefulParser.new(normalizer: pastel.method(:strip))
  parser.state(:info, /^Acquiring state lock/)
  parser.state(:error, /(╷|Error locking state|Error:)/, %i[none blank info])
  parser.state(:refreshing, /^.+: Refreshing state... \[id=/, %i[none info])
  parser.state(:refreshing, /Refreshing Terraform state in-memory prior to plan.../, %i[none blank info])
  parser.state(:refresh_done, /^----------+$/, [:refreshing])
  parser.state(:refresh_done, /^$/, [:refreshing])
  parser.state(:plan_info, /Terraform will perform the following actions:/, [:refresh_done, :none])
  parser.state(:plan_summary, /^Plan:/, [:plan_info])

  parser.state(:error_lock_info, /Lock Info/, [:error])
  parser.state(:error, /^$/, [:error_lock_info])

  parser.state(:plan_error, /^╷|Error: /, %i[refreshing refresh_done])

  status = tf_plan(out: filename, detailed_exitcode: true, compact_warnings: true, targets: targets) { |raw_line|
    parser.parse(raw_line.rstrip) do |state, line|
      case state
      when :none
        if line.blank?
          # nothing
        else
          p [state, line]
        end
      when :info
        if /Acquiring state lock. This may take a few moments.../.match?(line)
          log "Acquiring state lock ...", depth: 2
        else
          p [state, line]
        end
      when :error
        meta["error"] = "lock"
        log Paint[line, :red], depth: 2
      when :plan_error
        if phase != :plan_error
          puts
          phase = :plan_error
        end
        meta["error"] = "refresh"
        log Paint[line, :red], depth: 2
      when :error_lock_info
        if line =~ /^  ([^ ]+):\s+([^ ].+)$/
          meta[$LAST_MATCH_INFO[1]] = $LAST_MATCH_INFO[2]
        end
        log Paint[line, :red], depth: 2
      when :refreshing
        if phase != :refreshing
          phase = :refreshing
          log "Refreshing state ", depth: 2, newline: false
        else
          print "."
        end
      when :refresh_done
        if phase != :refresh_done
          phase = :refresh_done
          puts
        else
          # nothing
        end
      when :plan_info
        log line, depth: 2
      when :plan_summary
        log line, depth: 2
      else
        p [state, pastel.strip(line)]
      end
    end
  }
  [status.status, meta]
end
process_validation(info) click to toggle source
# File lib/mux_tf/plan_formatter.rb, line 235
def process_validation(info)
  remedies = Set.new

  if info["error_count"] > 0 || info["warning_count"] > 0
    log "Encountered #{Paint[info["error_count"], :red]} Errors and #{Paint[info["warning_count"], :yellow]} Warnings!", depth: 2
    info["diagnostics"].each do |dinfo|
      color = dinfo["severity"] == "error" ? :red : :yellow
      log "#{Paint[dinfo["severity"].capitalize, color]}: #{dinfo["summary"]}", depth: 3
      if dinfo["detail"]&.include?("terraform init")
        remedies << :init
      else
        log dinfo["detail"], depth: 4 if dinfo["detail"]
        if dinfo["range"]
          log format_validation_range(dinfo["range"], color), depth: 4
        end

        remedies << :unknown if dinfo["severity"] == "error"
      end
    end
  end

  remedies
end
run_tf_init(upgrade: nil, reconfigure: nil) click to toggle source
# File lib/mux_tf/plan_formatter.rb, line 100
def run_tf_init(upgrade: nil, reconfigure: nil)
  pastel = Pastel.new

  phase = :init

  meta = {}

  parser = StatefulParser.new(normalizer: pastel.method(:strip))

  parser.state(:modules_init, /^Initializing modules\.\.\./)
  parser.state(:modules_upgrade, /^Upgrading modules\.\.\./)
  parser.state(:backend, /^Initializing the backend\.\.\./, [:modules_init, :modules_upgrade])
  parser.state(:plugins, /^Initializing provider plugins\.\.\./, [:backend])

  parser.state(:plugin_warnings, /^$/, [:plugins])
  parser.state(:backend_error, /Error:/, [:backend])

  status = tf_init(upgrade: upgrade, reconfigure: reconfigure) { |raw_line|
    stripped_line = pastel.strip(raw_line.rstrip)

    parser.parse(raw_line.rstrip) do |state, line|
      case state
      when :modules_init
        if phase != state
          phase = state
          log "Initializing modules ", depth: 1
          next
        end
        case stripped_line
        when /^Downloading (?<repo>[^ ]+) (?<version>[^ ]+) for (?<module>[^ ]+)\.\.\./
          print "D"
        when /^- (?<module>[^ ]+) in (?<path>.+)$/
          print "."
        when ""
          puts
        else
          p [state, stripped_line]
        end
      when :modules_upgrade
        if phase != state
          # first line
          phase = state
          log "Upgrding modules ", depth: 1, newline: false
          next
        end
        case stripped_line
        when /^- (?<module>[^ ]+) in (?<path>.+)$/
          # info = $~.named_captures
          # log "- #{info["module"]}", depth: 2
          print "."
        when /^Downloading (?<repo>[^ ]+) (?<version>[^ ]+) for (?<module>[^ ]+)\.\.\./
          # info = $~.named_captures
          # log "Downloading #{info["module"]} from #{info["repo"]} @ #{info["version"]}"
          print "D"
        when ""
          puts
        else
          p [state, stripped_line]
        end
      when :backend
        if phase != state
          # first line
          phase = state
          log "Initializing the backend ", depth: 1, newline: false
          next
        end
        case stripped_line
        when /^Successfully configured/
          log line, depth: 2
        when /unless the backend/
          log line, depth: 2
        when ""
          puts
        else
          p [state, stripped_line]
        end
      when :backend_error
        if raw_line.match "terraform init -reconfigure"
          meta[:need_reconfigure] = true
          log Paint["module needs to be reconfigured", :red], depth: 2
        end
      when :plugins
        if phase != state
          # first line
          phase = state
          log "Initializing provider plugins ...", depth: 1
          next
        end
        case stripped_line
        when /^- Reusing previous version of (?<module>.+) from the dependency lock file$/
          info = $LAST_MATCH_INFO.named_captures
          log "- [FROM-LOCK] #{info["module"]}", depth: 2
        when /^- (?<module>.+) is built in to Terraform$/
          info = $LAST_MATCH_INFO.named_captures
          log "- [BUILTIN] #{info["module"]}", depth: 2
        when /^- Finding (?<module>[^ ]+) versions matching "(?<version>.+)"\.\.\./
          info = $LAST_MATCH_INFO.named_captures
          log "- [FIND] #{info["module"]} matching #{info["version"].inspect}", depth: 2
        when /^- Finding latest version of (?<module>.+)\.\.\.$/
          info = $LAST_MATCH_INFO.named_captures
          log "- [FIND] #{info["module"]}", depth: 2
        when /^- Installing (?<module>[^ ]+) v(?<version>.+)\.\.\.$/
          info = $LAST_MATCH_INFO.named_captures
          log "- [INSTALLING] #{info["module"]} v#{info["version"]}", depth: 2
        when /^- Installed (?<module>[^ ]+) v(?<version>.+) \(signed by( a)? (?<signed>.+)\)$/
          info = $LAST_MATCH_INFO.named_captures
          log "- [INSTALLED] #{info["module"]} v#{info["version"]} (#{info["signed"]})", depth: 2
        when /^- Using previously-installed (?<module>[^ ]+) v(?<version>.+)$/
          info = $LAST_MATCH_INFO.named_captures
          log "- [USING] #{info["module"]} v#{info["version"]}", depth: 2
        when /^- Downloading plugin for provider "(?<provider>[^"]+)" \((?<provider_path>[^)]+)\) (?<version>.+)\.\.\.$/
          info = $LAST_MATCH_INFO.named_captures
          log "- #{info["provider"]} #{info["version"]}", depth: 2
        when "- Checking for available provider plugins..."
          # noop
        else
          p [state, line]
        end
      when :plugin_warnings
        if phase != state
          # first line
          phase = state
          next
        end

        log Paint[line, :yellow], depth: 1
      else
        p [state, line]
      end
    end
  }

  [status.status, meta]
end

Private Class Methods

format_validation_range(range, color) click to toggle source
# File lib/mux_tf/plan_formatter.rb, line 261
def format_validation_range(range, color)
  # filename: "../../../modules/pods/jane_pod/main.tf"
  # start:
  #   line: 151
  #   column: 27
  #   byte: 6632
  # end:
  #   line: 151
  #   column: 53
  #   byte: 6658

  context_lines = 3

  lines = range["start"]["line"]..range["end"]["line"]
  columns = range["start"]["column"]..range["end"]["column"]

  # on ../../../modules/pods/jane_pod/main.tf line 151, in module "jane":
  # 151:   jane_resources_preset = var.jane_resources_presetx
  output = []
  lines_info = lines.size == 1 ? "#{lines.first}:#{columns.first}" : "#{lines.first}:#{columns.first} to #{lines.last}:#{columns.last}"
  output << "on: #{range["filename"]} line#{lines.size > 1 ? "s" : ""}: #{lines_info}"

  if File.exist?(range["filename"])
    file_lines = File.read(range["filename"]).split("\n")
    extract_range = ([lines.first - context_lines, 0].max)..([lines.last + context_lines, file_lines.length - 1].min)
    file_lines.each_with_index do |line, index|
      if extract_range.cover?(index + 1)
        if lines.cover?(index + 1)
          start_col = 1
          end_col = :max
          if index + 1 == lines.first
            start_col = columns.first
          elsif index + 1 == lines.last
            start_col = columns.last
          end
          painted_line = paint_line(line, color, start_col: start_col, end_col: end_col)
          output << "#{Paint[">", color]} #{index + 1}: #{painted_line}"
        else
          output << "  #{index + 1}: #{line}"
        end
      end
    end
  end

  output
end
paint_line(line, *paint_options, start_col: 1, end_col: :max) click to toggle source
# File lib/mux_tf/plan_formatter.rb, line 308
def paint_line(line, *paint_options, start_col: 1, end_col: :max)
  end_col = line.length if end_col == :max
  prefix = line[0, start_col - 1]
  suffix = line[end_col..-1]
  middle = line[start_col - 1..end_col - 1]
  "#{prefix}#{Paint[middle, *paint_options]}#{suffix}"
end