lib/terraform_landscape/printer.rb
require 'stringio' module TerraformLandscape # Takes output from Terraform executable and outputs it in a prettified # format. class Printer def initialize(output) @output = output end def process_stream(io, options = {}) # rubocop:disable Metrics/MethodLength apply = nil buffer = StringIO.new original_tf_output = StringIO.new begin block_size = 1024 done = false until done readable_fds, = IO.select([io]) next unless readable_fds readable_fds.each do |f| begin new_output = f.read_nonblock(block_size) original_tf_output << new_output buffer << strip_ansi(new_output) rescue IO::WaitReadable # rubocop:disable Lint/HandleExceptions # Ignore; we'll call IO.select again rescue EOFError done = true end end apply = apply_prompt(buffer.string.encode('UTF-8', invalid: :replace, replace: '')) done = true if apply end begin process_string(buffer.string) @output.print apply if apply rescue ParseError, TerraformPlan::ParseError => e raise e if options[:trace] @output.warning FALLBACK_MESSAGE @output.print original_tf_output.string end @output.write_from(io) ensure io.close end end def process_string(plan_output) # rubocop:disable Metrics/MethodLength scrubbed_output = strip_ansi(plan_output) # Our grammar assumes output with Unix line endings scrubbed_output.gsub!("\r\n", "\n") # Remove initialization messages like # "- Downloading plugin for provider "aws" (1.1.0)..." # "- module.base_network" # as these break the parser which thinks "-" is a resource deletion scrubbed_output.gsub!(/^- .*\.\.\.$/, '') scrubbed_output.gsub!(/^- module\..*$/, '') # Remove separation lines that appear after refreshing state scrubbed_output.gsub!(/^-+$/, '') if (matches = scrubbed_output.scan(/^Warning:.*$/)) matches.each do |warning| @output.puts warning.colorize(:yellow) end end # Remove preface if (match = scrubbed_output.match(/^Path:[^\n]+/)) scrubbed_output = scrubbed_output[match.end(0)..-1] elsif (match = scrubbed_output.match(/^Terraform.+following\sactions:/)) scrubbed_output = scrubbed_output[match.end(0)..-1] elsif (match = scrubbed_output.match(/^\s*(~|\+|\-)/)) scrubbed_output = scrubbed_output[match.begin(0)..-1] elsif scrubbed_output =~ /^(No changes\.|This plan does nothing)/ @output.puts 'No changes.' return else raise ParseError, 'Output does not contain proper preface' end # Remove postface if (match = scrubbed_output.match(/^Plan:[^\n]+/)) plan_summary = scrubbed_output[match.begin(0)..match.end(0)] scrubbed_output = scrubbed_output[0...match.begin(0)] end plan = TerraformPlan.from_output(scrubbed_output) plan.display(@output) @output.puts plan_summary end private def strip_ansi(string) string.gsub(/\e\[\d+m/, '') end def apply_prompt(output) return unless output =~ /Enter a value:\s+$/ output[/Do you want to perform these actions.*$/m, 0] end end end