terraform_landscape 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3adeba30b54a83c182ee6956d97f42e16fe1bb14
4
+ data.tar.gz: ab302a4d55105e6a0a3bc70e31c497e64dc3b489
5
+ SHA512:
6
+ metadata.gz: 065d407040083b704ddef29a67080db8b618f557ed498c9623ff26ced98ddbee28538fad417253370155d904974e06abea1a5d1741acaee63e1d6b15f7ceed44
7
+ data.tar.gz: 7602f283d8a02775dc4e07c5b93fd6c5763cc4e1d5f939fae3d1c042f47d0e3c387f48a2d3e472c9cb1d7610595f46829953f60bb7a356a9fbbfaaa28f2d4e72
data/bin/landscape ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'terraform_landscape'
4
+
5
+ output = TerraformLandscape::Output.new(STDOUT)
6
+ cli = TerraformLandscape::CLI.new(output)
7
+ exit cli.run(ARGV)
@@ -0,0 +1,67 @@
1
+ # Defines grammar for parsing `terraform plan` output.
2
+
3
+ grammar TerraformPlan
4
+ rule plan
5
+ [\s]* list:resource_list "\n"* {
6
+ def to_ast
7
+ list.to_ast
8
+ end
9
+ }
10
+ end
11
+
12
+ rule resource_list
13
+ item:resource "\n\n" list:resource_list {
14
+ def to_ast
15
+ [item.to_ast] + list.to_ast
16
+ end
17
+ }
18
+ /
19
+ item:resource "\n" {
20
+ def to_ast
21
+ [item.to_ast]
22
+ end
23
+ }
24
+ end
25
+
26
+ rule resource
27
+ header:resource_header "\n" attrs:attribute_list {
28
+ def to_ast
29
+ header.to_ast.merge(attributes: attrs.to_ast)
30
+ end
31
+ }
32
+ end
33
+
34
+ rule resource_header
35
+ change:('~' / '-' / '+') _ type:[a-zA-Z0-9_-]+ '.' name:[a-zA-Z0-9_-]+ {
36
+ def to_ast
37
+ { change: change.text_value.to_sym, resource_type: type.text_value, resource_name: name.text_value }
38
+ end
39
+ }
40
+ end
41
+
42
+ rule attribute_list
43
+ _ item:attribute "\n" attrs:attribute_list {
44
+ def to_ast
45
+ item.to_ast.merge(attrs.to_ast)
46
+ end
47
+ }
48
+ /
49
+ _ item:attribute {
50
+ def to_ast
51
+ item.to_ast
52
+ end
53
+ }
54
+ end
55
+
56
+ rule attribute
57
+ attribute_name:[^\s:]* ':' _? attribute_value:[^\n]+ {
58
+ def to_ast
59
+ { attribute_name.text_value => attribute_value.text_value }
60
+ end
61
+ }
62
+ end
63
+
64
+ rule _
65
+ [ ]+
66
+ end
67
+ end
@@ -0,0 +1,7 @@
1
+ require 'terraform_landscape/constants'
2
+ require 'terraform_landscape/version'
3
+ require 'terraform_landscape/output'
4
+ require 'terraform_landscape/errors'
5
+ require 'terraform_landscape/printer'
6
+ require 'terraform_landscape/terraform_plan'
7
+ require 'terraform_landscape/cli'
@@ -0,0 +1,58 @@
1
+ require 'optparse'
2
+
3
+ module TerraformLandscape
4
+ # Handles option parsing for the command line application.
5
+ class ArgumentsParser
6
+ # Parses command line options into an options hash.
7
+ #
8
+ # @param args [Array<String>] arguments passed via the command line
9
+ #
10
+ # @return [Hash] parsed options
11
+ def parse(args)
12
+ @options = {}
13
+ @options[:command] = :pretty_print # Default command
14
+
15
+ OptionParser.new do |parser|
16
+ parser.banner = "Usage: landscape [options] [plan-output-file]"
17
+
18
+ add_info_options parser
19
+ end.parse!(args)
20
+
21
+ # Any remaining arguments are assumed to be the output file
22
+ @options[:plan_output_file] = args.first
23
+
24
+ @options
25
+ rescue OptionParser::InvalidOption => ex
26
+ raise InvalidCliOptionError,
27
+ "#{ex.message}\nRun `landscape --help` to " \
28
+ 'see a list of available options.'
29
+ end
30
+
31
+ private
32
+
33
+ # Register informational flags.
34
+ def add_info_options(parser)
35
+ parser.on('--[no-]color', 'Force output to be colorized') do |color|
36
+ @options[:color] = color
37
+ end
38
+
39
+ parser.on('-d', '--debug', 'Enable debug mode for more verbose output') do
40
+ @options[:debug] = true
41
+ end
42
+
43
+ parser.on_tail('-h', '--help', 'Display help documentation') do
44
+ @options[:command] = :display_help
45
+ @options[:help_message] = parser.help
46
+ end
47
+
48
+ parser.on_tail('-v', '--version', 'Display version') do
49
+ @options[:command] = :display_version
50
+ end
51
+
52
+ parser.on_tail('-V', '--verbose-version', 'Display verbose version information') do
53
+ @options[:command] = :display_version
54
+ @options[:verbose_version] = true
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,51 @@
1
+ require 'commander'
2
+
3
+ module TerraformLandscape
4
+ # Command line application interface.
5
+ class CLI
6
+ include Commander::Methods
7
+
8
+ def initialize(output)
9
+ @output = output
10
+ end
11
+
12
+ # Parses the given command line arguments and executes appropriate logic
13
+ # based on those arguments.
14
+ #
15
+ # @param args [Array<String>] command line arguments
16
+ #
17
+ # @return [Integer] exit status code
18
+ def run(args)
19
+ program :name, 'Terraform Landscape'
20
+ program :version, VERSION
21
+ program :description, 'Pretty-print your Terraform plan output'
22
+
23
+ define_commands
24
+
25
+ run!
26
+ 0 # OK
27
+ end
28
+
29
+ private
30
+
31
+ def define_commands
32
+ command :print do |c|
33
+ c.action do
34
+ print
35
+ end
36
+ end
37
+
38
+ global_option '--no-color', 'Do not output any color' do
39
+ String.disable_colorization = true
40
+ @output.color_enabled = false
41
+ end
42
+
43
+ default_command :print
44
+ end
45
+
46
+ def print
47
+ printer = Printer.new(@output)
48
+ printer.process_stream(STDIN)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Global application constants.
4
+ module TerraformLandscape
5
+ HOME = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze
6
+
7
+ REPO_URL = 'https://github.com/coinbase/terraform_landscape'.freeze
8
+ BUG_REPORT_URL = "#{REPO_URL}/issues".freeze
9
+ end
@@ -0,0 +1,33 @@
1
+ # Collection of errors that can be raised by the framework.
2
+ module TerraformLandscape
3
+ # Abstract error. Separates LintTrappings errors from other kinds of
4
+ # errors in the exception hierarchy.
5
+ #
6
+ # @abstract
7
+ class Error < StandardError
8
+ # Returns the status code that should be output if this error goes
9
+ # unhandled.
10
+ #
11
+ # Ideally these should resemble exit codes from the sysexits documentation
12
+ # where it makes sense.
13
+ def self.exit_status(*args)
14
+ if args.any?
15
+ @exit_status = args.first
16
+ elsif @exit_status
17
+ @exit_status
18
+ else
19
+ ancestors.each do |ancestor|
20
+ return 70 if ancestor == TerraformLandscape::Error # No exit status defined
21
+ return ancestor.exit_status if ancestor.exit_status
22
+ end
23
+ end
24
+ end
25
+
26
+ def exit_status
27
+ self.class.exit_status
28
+ end
29
+ end
30
+
31
+ # Raised when there was a problem parsing a document.
32
+ class ParseError < Error; end
33
+ end
@@ -0,0 +1,118 @@
1
+ module TerraformLandscape
2
+ # Encapsulates all communication to an output source.
3
+ class Output
4
+ # Whether colored output via ANSI escape sequences is enabled.
5
+ # @return [true,false]
6
+ attr_accessor :color_enabled
7
+
8
+ # Creates a logger which outputs nothing.
9
+ # @return [TerraformLandscape::Output]
10
+ def self.silent
11
+ new(File.open(File::NULL, 'w'))
12
+ end
13
+
14
+ # Creates a new {SlimLint::Logger} instance.
15
+ #
16
+ # @param out [IO] the output destination.
17
+ def initialize(out)
18
+ @out = out
19
+ @color_enabled = tty?
20
+ end
21
+
22
+ # Print the specified output.
23
+ #
24
+ # @param output [String] the output to send
25
+ # @param newline [true,false] whether to append a newline
26
+ def puts(output, newline = true)
27
+ @out.print(output)
28
+ @out.print("\n") if newline
29
+ end
30
+
31
+ # Print the specified output without a newline.
32
+ #
33
+ # @param output [String] the output to send
34
+ def print(output)
35
+ puts(output, false)
36
+ end
37
+
38
+ # Print the specified output in bold face.
39
+ # If output destination is not a TTY, behaves the same as {#log}.
40
+ #
41
+ # @param args [Array<String>]
42
+ def bold(*args)
43
+ color('1', *args)
44
+ end
45
+
46
+ # Print the specified output in a color indicative of error.
47
+ # If output destination is not a TTY, behaves the same as {#log}.
48
+ #
49
+ # @param args [Array<String>]
50
+ def error(*args)
51
+ color(31, *args)
52
+ end
53
+
54
+ # Print the specified output in a bold face and color indicative of error.
55
+ # If output destination is not a TTY, behaves the same as {#log}.
56
+ #
57
+ # @param args [Array<String>]
58
+ def bold_error(*args)
59
+ color('1;31', *args)
60
+ end
61
+
62
+ # Print the specified output in a color indicative of success.
63
+ # If output destination is not a TTY, behaves the same as {#log}.
64
+ #
65
+ # @param args [Array<String>]
66
+ def success(*args)
67
+ color(32, *args)
68
+ end
69
+
70
+ # Print the specified output in a color indicative of a warning.
71
+ # If output destination is not a TTY, behaves the same as {#log}.
72
+ #
73
+ # @param args [Array<String>]
74
+ def warning(*args)
75
+ color(33, *args)
76
+ end
77
+
78
+ # Print the specified output in a color indicating something worthy of
79
+ # notice.
80
+ # If output destination is not a TTY, behaves the same as {#log}.
81
+ #
82
+ # @param args [Array<String>]
83
+ def notice(*args)
84
+ color(35, *args)
85
+ end
86
+
87
+ # Print the specified output in a color indicating information.
88
+ # If output destination is not a TTY, behaves the same as {#log}.
89
+ #
90
+ # @param args [Array<String>]
91
+ def info(*args)
92
+ color(36, *args)
93
+ end
94
+
95
+ # Print a blank line.
96
+ def newline
97
+ puts('')
98
+ end
99
+
100
+ # Whether this logger is outputting to a TTY.
101
+ #
102
+ # @return [true,false]
103
+ def tty?
104
+ @out.respond_to?(:tty?) && @out.tty?
105
+ end
106
+
107
+ private
108
+
109
+ # Print output in the specified color.
110
+ #
111
+ # @param code [Integer,String] ANSI color code
112
+ # @param output [String] output to print
113
+ # @param newline [Boolean] whether to append a newline
114
+ def color(code, output, newline = true)
115
+ puts(color_enabled ? "\033[#{code}m#{output}\033[0m" : output, newline)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,56 @@
1
+ require 'stringio'
2
+
3
+ module TerraformLandscape
4
+ class Printer
5
+ def initialize(output)
6
+ @output = output
7
+ end
8
+
9
+ def process_stream(io)
10
+ buffer = StringIO.new
11
+ begin
12
+ block_size = 1024
13
+
14
+ done = false
15
+ until done
16
+ readable_fds, = IO.select([io])
17
+ next unless readable_fds
18
+
19
+ readable_fds.each do |f|
20
+ begin
21
+ buffer << f.read_nonblock(block_size)
22
+ rescue IO::WaitReadable # rubocop:disable Lint/HandleExceptions
23
+ # Ignore; we'll call IO.select again
24
+ rescue EOFError
25
+ done = true
26
+ end
27
+ end
28
+ end
29
+ ensure
30
+ io.close
31
+ end
32
+
33
+ plan_output = buffer.string
34
+ scrubbed_output = plan_output.gsub(/\e\[\d+m/, '')
35
+
36
+ # Remove preface
37
+ if (match = scrubbed_output.match(/^Path:[^\n]+/))
38
+ scrubbed_output = scrubbed_output[match.end(0)..-1]
39
+ else
40
+ raise ParseError, 'Output does not contain proper preface'
41
+ end
42
+
43
+ # Remove postface
44
+ if (match = scrubbed_output.match(/^Plan:[^\n]+/))
45
+ plan_summary = scrubbed_output[match.begin(0)..match.end(0)]
46
+ scrubbed_output = scrubbed_output[0...match.begin(0)]
47
+ else
48
+ raise ParseError, 'Output does not container proper postface'
49
+ end
50
+
51
+ plan = TerraformPlan.from_output(scrubbed_output)
52
+ plan.display(@output)
53
+ @output.puts plan_summary
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,188 @@
1
+ require 'colorize'
2
+ require 'diffy'
3
+ require 'json'
4
+ require 'treetop'
5
+
6
+ ########################################################################
7
+ # Represents the parsed output of `terraform plan`.
8
+ #
9
+ # This allows us to easily inspect the plan and present a more readable
10
+ # explanation of the plan to the user.
11
+ ########################################################################
12
+ class TerraformLandscape::TerraformPlan
13
+ GRAMMAR_FILE = File.expand_path(File.join(File.dirname(__FILE__),
14
+ '..', '..', 'grammar',
15
+ 'terraform_plan.treetop'))
16
+
17
+ CHANGE_SYMBOL_TO_COLOR = {
18
+ :~ => :yellow,
19
+ :- => :red,
20
+ :+ => :green
21
+ }.freeze
22
+
23
+ DEFAULT_DIFF_CONTEXT_LINES = 5
24
+
25
+ class ParseError < StandardError; end
26
+
27
+ class << self
28
+ def from_output(string)
29
+ return new([]) if string.strip.empty?
30
+ tree = parser.parse(string)
31
+ raise ParseError, parser.failure_reason unless tree
32
+ new(tree.to_ast)
33
+ end
34
+
35
+ private
36
+
37
+ def parser
38
+ @parser ||=
39
+ begin
40
+ Treetop.load(GRAMMAR_FILE)
41
+ TerraformPlanParser.new
42
+ end
43
+ end
44
+ end
45
+
46
+ # Create a plan from an abstract syntax tree (AST).
47
+ def initialize(plan_ast, options = {})
48
+ @ast = plan_ast
49
+ @diff_context_lines = options.fetch(:diff_context_lines, DEFAULT_DIFF_CONTEXT_LINES)
50
+ end
51
+
52
+ def display(output)
53
+ @out = output
54
+ @ast.each do |resource|
55
+ display_resource(resource)
56
+ @out.newline
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def display_resource(resource)
63
+ change_color = CHANGE_SYMBOL_TO_COLOR[resource[:change]]
64
+
65
+ @out.puts "#{resource[:change]} #{resource[:resource_type]}." \
66
+ "#{resource[:resource_name]}".colorize(change_color)
67
+
68
+ # Determine longest attribute name so we align all values at same indentation
69
+ attribute_value_indent_amount = attribute_indent_amount_for_resource(resource)
70
+
71
+ resource[:attributes].each do |attribute_name, attribute_value|
72
+ display_attribute(resource,
73
+ change_color,
74
+ attribute_name,
75
+ attribute_value,
76
+ attribute_value_indent_amount)
77
+ end
78
+ end
79
+
80
+ def attribute_indent_amount_for_resource(resource)
81
+ longest_name_length = resource[:attributes].keys.reduce(0) do |longest, name|
82
+ name.length > longest ? name.length : longest
83
+ end
84
+ longest_name_length + 8
85
+ end
86
+
87
+ def json?(value)
88
+ ['{', '['].include?(value.to_s[0]) &&
89
+ (JSON.parse(value) rescue nil) # rubocop:disable Style/RescueModifier
90
+ end
91
+
92
+ def to_pretty_json(value)
93
+ JSON.pretty_generate(JSON.parse(value),
94
+ {
95
+ indent: ' ',
96
+ space: ' ',
97
+ object_nl: "\n",
98
+ array_nl: "\n"
99
+ })
100
+ end
101
+
102
+ def display_diff(old, new, indent)
103
+ @out.print Diffy::Diff.new(old, new, { context: @diff_context_lines })
104
+ .to_s(String.disable_colorization ? :text : :color)
105
+ .gsub("\n", "\n" + indent)
106
+ .strip
107
+ end
108
+
109
+ def display_attribute(
110
+ resource,
111
+ change_color,
112
+ attribute_name,
113
+ attribute_value,
114
+ attribute_value_indent_amount
115
+ )
116
+ attribute_value_indent = ' ' * attribute_value_indent_amount
117
+
118
+ if resource[:change] == :~
119
+ display_modified_attribute(change_color,
120
+ attribute_name,
121
+ attribute_value,
122
+ attribute_value_indent,
123
+ attribute_value_indent_amount)
124
+ else
125
+ display_added_or_removed_attribute(change_color,
126
+ attribute_name,
127
+ attribute_value,
128
+ attribute_value_indent,
129
+ attribute_value_indent_amount)
130
+ end
131
+ end
132
+
133
+ def display_modified_attribute( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
134
+ change_color,
135
+ attribute_name,
136
+ attribute_value,
137
+ attribute_value_indent,
138
+ attribute_value_indent_amount
139
+ )
140
+ # Since the attribute line is always of the form
141
+ # "old value" => "new value", we can add curly braces and parse with
142
+ # `eval` to obtain a hash with a single key/value.
143
+ old, new = eval("{#{attribute_value}}").to_a.first # rubocop:disable Lint/Eval
144
+
145
+ return if old == new # Don't show unchanged attributes
146
+
147
+ @out.print " #{attribute_name}:".ljust(attribute_value_indent_amount, ' ')
148
+ .colorize(change_color)
149
+
150
+ if json?(new)
151
+ # Value looks like JSON, so prettify it to make it more readable
152
+ fancy_old = "#{to_pretty_json(old)}\n"
153
+ fancy_new = "#{to_pretty_json(new)}\n"
154
+ display_diff(fancy_old, fancy_new, attribute_value_indent)
155
+ elsif old.include?("\n") || new.include?("\n")
156
+ # Multiline content, so display nicer diff
157
+ display_diff("#{old}\n", "#{new}\n", attribute_value_indent)
158
+ else
159
+ # Typical values, so just show before/after
160
+ @out.print '"' + old.colorize(:red) + '"'
161
+ @out.print ' => '.colorize(:light_black)
162
+ @out.print '"' + new.colorize(:green) + '"'
163
+ end
164
+
165
+ @out.newline
166
+ end
167
+
168
+ def display_added_or_removed_attribute(
169
+ change_color,
170
+ attribute_name,
171
+ attribute_value,
172
+ attribute_value_indent,
173
+ attribute_value_indent_amount
174
+ )
175
+ @out.print " #{attribute_name}:".ljust(attribute_value_indent_amount, ' ')
176
+ .colorize(change_color)
177
+
178
+ evaluated_string = eval(attribute_value) # rubocop:disable Lint/Eval
179
+ if json?(evaluated_string)
180
+ @out.print to_pretty_json(evaluated_string).gsub("\n", "\n" + attribute_value_indent)
181
+ .colorize(change_color)
182
+ else
183
+ @out.print "\"#{evaluated_string.colorize(change_color)}\""
184
+ end
185
+
186
+ @out.newline
187
+ end
188
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Defines the gem version.
4
+ module TerraformLandscape
5
+ VERSION = '0.0.1'.freeze
6
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: terraform_landscape
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Coinbase
8
+ - Shane da Silva
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2017-02-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: colorize
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.7'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0.7'
28
+ - !ruby/object:Gem::Dependency
29
+ name: commander
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '4.4'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '4.4'
42
+ - !ruby/object:Gem::Dependency
43
+ name: diffy
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: treetop
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.6'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.6'
70
+ description: Improve output of Terraform plans with color and indentation
71
+ email:
72
+ - shane@coinbase.com
73
+ executables:
74
+ - landscape
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - bin/landscape
79
+ - grammar/terraform_plan.treetop
80
+ - lib/terraform_landscape.rb
81
+ - lib/terraform_landscape/arguments_parser.rb
82
+ - lib/terraform_landscape/cli.rb
83
+ - lib/terraform_landscape/constants.rb
84
+ - lib/terraform_landscape/errors.rb
85
+ - lib/terraform_landscape/output.rb
86
+ - lib/terraform_landscape/printer.rb
87
+ - lib/terraform_landscape/terraform_plan.rb
88
+ - lib/terraform_landscape/version.rb
89
+ homepage: https://github.com/coinbase/terraform_landscape
90
+ licenses:
91
+ - Apache 2.0
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '2'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.6.10
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Pretty-print Terraform plan output
113
+ test_files: []