mux_tf 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler"
4
+
3
5
  module MuxTf
4
6
  module Cli
5
7
  module Mux
@@ -7,44 +9,63 @@ module MuxTf
7
9
  extend PiotrbCliUtils::ShellHelpers
8
10
 
9
11
  class << self
12
+ def with_clean_env
13
+ backup = {}
14
+ Bundler.with_original_env do
15
+ ENV.keys.grep(/^(RBENV_|RUBYLIB)/).each do |key|
16
+ backup[key] = ENV[key]
17
+ ENV.delete(key)
18
+ end
19
+ yield
20
+ end
21
+ ensure
22
+ backup.each do |k, v|
23
+ ENV[k] = v
24
+ end
25
+ end
26
+
10
27
  def run(_args)
11
- Dotenv.load('.env.mux')
28
+ Dotenv.load(".env.mux")
12
29
 
13
- log 'Enumerating folders ...'
30
+ log "Enumerating folders ..."
14
31
  dirs = enumerate_terraform_dirs
15
32
 
16
- fail_with 'Error: - no subfolders detected! Aborting.' if dirs.empty?
33
+ fail_with "Error: - no subfolders detected! Aborting." if dirs.empty?
17
34
 
18
- tasks = dirs.map do |dir|
35
+ tasks = dirs.map { |dir|
19
36
  {
20
37
  name: dir,
21
38
  cwd: dir,
22
- cmd: File.expand_path(File.join(__dir__, '..', '..', '..', 'exe', 'tf_current'))
39
+ cmd: File.expand_path(File.join(__dir__, "..", "..", "..", "exe", "tf_current"))
23
40
  }
24
- end
41
+ }
25
42
 
26
43
  project = File.basename(Dir.getwd)
27
44
 
28
- if ENV['MUX_TF_AUTH_WRAPPER']
29
- log 'Warming up AWS connection ...'
30
- words = Shellwords.shellsplit(ENV['MUX_TF_AUTH_WRAPPER'])
31
- result = capture_shell([*words, 'aws', 'sts', 'get-caller-identity'], raise_on_error: true)
45
+ if ENV["MUX_TF_AUTH_WRAPPER"]
46
+ log "Warming up AWS connection ..."
47
+ words = Shellwords.shellsplit(ENV["MUX_TF_AUTH_WRAPPER"])
48
+ result = capture_shell([*words, "aws", "sts", "get-caller-identity"], raise_on_error: true)
32
49
  p JSON.parse(result)
33
50
  end
34
51
 
35
52
  if Tmux.session_running?(project)
36
- log 'Killing existing session ...'
53
+ log "Killing existing session ..."
37
54
  Tmux.kill_session(project)
38
55
  end
39
56
 
40
- log 'Starting new session ...'
41
- Tmux.new_session project
42
- Tmux.select_pane 'initial'
57
+ log "Starting new session ..."
58
+ with_clean_env do
59
+ Tmux.new_session project
60
+ end
61
+ Tmux.select_pane "initial"
43
62
 
44
- Tmux.set_hook 'pane-exited', 'select-layout tiled'
45
- Tmux.set_hook 'window-pane-changed', 'select-layout tiled'
63
+ # Tmux.set "remain-on-exit", "on"
46
64
 
47
- Tmux.set 'mouse', 'on'
65
+ Tmux.set_hook "pane-exited", "select-layout tiled"
66
+ Tmux.set_hook "window-pane-changed", "select-layout tiled"
67
+
68
+ Tmux.set "mouse", "on"
48
69
 
49
70
  window_id = Tmux.list_windows.first[:id]
50
71
 
@@ -60,17 +81,19 @@ module MuxTf
60
81
  end
61
82
  end
62
83
 
63
- log 'Almost done ...'
84
+ log "Almost done ..."
64
85
 
65
- initial_pane = Tmux.find_pane('initial')
86
+ initial_pane = Tmux.find_pane("initial")
66
87
  Tmux.kill_pane initial_pane[:id]
67
88
  Tmux.tile!
68
89
 
69
90
  puts "\e]0;tmux: #{project}\007"
70
91
 
71
- log 'Attaching ...'
72
- Tmux.attach(project, cc: !!ENV['MUXP_CC_MODE'])
73
- log 'Done!'
92
+ sleep 1
93
+
94
+ log "Attaching ..."
95
+ Tmux.attach(project, cc: !!ENV["MUXP_CC_MODE"])
96
+ log "Done!"
74
97
  end
75
98
 
76
99
  private
@@ -78,9 +101,9 @@ module MuxTf
78
101
  def enumerate_terraform_dirs
79
102
  ignored = []
80
103
 
81
- ignored += ENV['MUX_IGNORE'].split(',') if ENV['MUX_IGNORE']
104
+ ignored += ENV["MUX_IGNORE"].split(",") if ENV["MUX_IGNORE"]
82
105
 
83
- dirs = Dir['**/*/.terraform'].map { |n| n.gsub(%r{/\.terraform}, '') }
106
+ dirs = Dir["**/*/.terraform"].map { |n| n.gsub(%r{/\.terraform}, "") }
84
107
  dirs.reject! { |d| d.in?(ignored) }
85
108
 
86
109
  dirs
@@ -14,271 +14,45 @@ module MuxTf
14
14
  hierarchy: false
15
15
  }
16
16
 
17
- args = OptionParser.new do |opts|
18
- opts.on('-i') do |v|
17
+ args = OptionParser.new { |opts|
18
+ opts.on("-i") do |v|
19
19
  options[:interactive] = v
20
20
  end
21
- opts.on('-h') do |v|
21
+ opts.on("-h") do |v|
22
22
  options[:hierarchy] = v
23
23
  end
24
- end.parse!(args)
24
+ }.parse!(args)
25
25
 
26
26
  if options[:interactive]
27
- raise 'must specify plan file in interactive mode' if args[0].blank?
27
+ raise "must specify plan file in interactive mode" if args[0].blank?
28
28
  end
29
29
 
30
- data = if args[0]
31
- load_summary_from_file(args[0])
32
- else
33
- JSON.parse(STDIN.read)
34
- end
35
-
36
- parts = []
37
-
38
- data['resource_changes'].each do |v|
39
- next unless v['change']
40
-
41
- case v['change']['actions']
42
- when ['no-op']
43
- # do nothing
44
- when ['create']
45
- parts << {
46
- action: 'create',
47
- address: v['address'],
48
- deps: find_deps(data, v['address'])
49
- }
50
- when ['update']
51
- parts << {
52
- action: 'update',
53
- address: v['address'],
54
- deps: find_deps(data, v['address'])
55
- }
56
- when ['delete']
57
- parts << {
58
- action: 'delete',
59
- address: v['address'],
60
- deps: find_deps(data, v['address'])
61
- }
62
- when %w[delete create]
63
- parts << {
64
- action: 'replace',
65
- address: v['address'],
66
- deps: find_deps(data, v['address'])
67
- }
68
- when ['read']
69
- parts << {
70
- action: 'read',
71
- address: v['address'],
72
- deps: find_deps(data, v['address'])
73
- }
74
- else
75
- puts "[??] #{v['address']}"
76
- puts "UNKNOWN ACTIONS: #{v['change']['actions'].inspect}"
77
- puts 'TODO: update plan_summary to support this!'
78
- end
79
- end
80
-
81
- prune_unchanged_deps(parts)
82
-
83
- if options[:interactive]
84
- run_interactive(parts, args[0])
30
+ plan = if args[0]
31
+ PlanSummaryHandler.from_file(args[0])
85
32
  else
86
- if options[:hierarchy]
87
- print_nested(parts)
88
- else
89
- print_flat(parts)
90
- end
91
- print_summary(parts)
92
- end
93
- end
94
-
95
- def load_summary_from_file(file)
96
- if File.exist?("#{file}.json") && File.mtime("#{file}.json").to_f >= File.mtime(file).to_f
97
- JSON.parse(File.read("#{file}.json"))
98
- else
99
- puts 'Analyzing changes ...'
100
- result = tf_show(file, json: true)
101
- data = result.parsed_output
102
- File.open("#{file}.json", 'w') { |fh| fh.write(JSON.dump(data)) }
103
- data
104
- end
105
- end
106
-
107
- def print_summary(parts)
108
- summary = {}
109
- parts.each do |part|
110
- summary[part[:action]] ||= 0
111
- summary[part[:action]] += 1
112
- end
113
- pieces = summary.map do |k, v|
114
- color = color_for_action(k)
115
- "#{Paint[v, :yellow]} to #{Paint[k, color]}"
33
+ PlanSummaryHandler.from_data(JSON.parse(STDIN.read))
116
34
  end
117
35
 
118
- puts
119
- puts "Plan Summary: #{pieces.join(Paint[', ', :gray])}"
120
- end
121
-
122
- def print_flat(parts)
123
- parts.each do |part|
124
- puts "[#{format_action(part[:action])}] #{format_address(part[:address])}"
125
- end
126
- end
127
-
128
- def run_interactive(parts, _plan_name)
129
- prompt = TTY::Prompt.new
130
- result = prompt.multi_select('Update resources:', per_page: 99, echo: false) do |menu|
131
- parts.each do |part|
132
- label = "[#{format_action(part[:action])}] #{format_address(part[:address])}"
133
- menu.choice label, part[:address]
36
+ if options[:interactive]
37
+ abort_message = catch :abort do
38
+ plan.run_interactive
134
39
  end
135
- end
136
-
137
- if !result.empty?
138
- log 'Re-running apply with the selected resources ...'
139
- status = tf_apply(targets: result)
140
- unless status.success?
141
- log Paint["Failed! (#{status.status})", :red]
142
- exit status.status
40
+ if abort_message
41
+ log Paint["Aborted: #{abort_message}", :red]
143
42
  end
144
43
  else
145
- raise 'nothing selected'
146
- end
147
- end
148
-
149
- def print_nested(parts)
150
- parts = parts.deep_dup
151
- until parts.empty?
152
- part = parts.shift
153
- if part[:deps] == []
154
- indent = if part[:met_deps] && !part[:met_deps].empty?
155
- ' '
156
- else
157
- ''
158
- end
159
- message = "[#{format_action(part[:action])}]#{indent} #{format_address(part[:address])}"
160
- if part[:met_deps]
161
- message += " - (needs: #{part[:met_deps].join(', ')})"
162
- end
163
- puts message
164
- parts.each do |ipart|
165
- d = ipart[:deps].delete(part[:address])
166
- if d
167
- ipart[:met_deps] ||= []
168
- ipart[:met_deps] << d
169
- end
44
+ if options[:hierarchy]
45
+ plan.nested_summary.each do |line|
46
+ puts line
170
47
  end
171
48
  else
172
- parts.unshift part
173
- end
174
- end
175
- end
176
-
177
- def prune_unchanged_deps(parts)
178
- valid_addresses = parts.map { |part| part[:address] }
179
-
180
- parts.each do |part|
181
- part[:deps].select! { |dep| valid_addresses.include?(dep) }
182
- end
183
- end
184
-
185
- def find_config(module_root, module_name, address, parent_address)
186
- module_info = if parent_address.empty?
187
- module_root[module_name]
188
- elsif module_root && module_root[module_name]
189
- module_root[module_name]['module']
190
- else
191
- {}
192
- end
193
-
194
- if m = address.match(/^module\.([^.]+)\./)
195
- find_config(module_info['module_calls'], m[1], m.post_match, parent_address + ["module.#{m[1]}"])
196
- else
197
- if module_info['resources']
198
- resource = module_info['resources'].find do |resource|
199
- address == resource['address']
49
+ plan.flat_summary.each do |line|
50
+ puts line
200
51
  end
201
52
  end
202
- [resource, parent_address]
203
- end
204
- end
205
-
206
- def find_deps(data, address)
207
- result = []
208
-
209
- full_address = address
210
- m = address.match(/\[(.+)\]$/)
211
- if m
212
- address = m.pre_match
213
- index = m[1][0] == '"' ? m[1].gsub(/^"(.+)"$/, '\1') : m[1].to_i
214
- end
215
-
216
- if data['prior_state']['values']['root_module']['resources']
217
- resource = data['prior_state']['values']['root_module']['resources'].find do |resource|
218
- address == resource['address'] && index == resource['index']
219
- end
220
- end
221
-
222
- result += resource['depends_on'] if resource && resource['depends_on']
223
-
224
- resource, parent_address = find_config(data['configuration'], 'root_module', address, [])
225
- if resource
226
- deps = []
227
- resource['expressions'].each do |_k, v|
228
- deps << v['references'] if v.is_a?(Hash) && v['references']
229
- end
230
- result += deps.map { |s| (parent_address + [s]).join('.') }
231
- end
232
-
233
- result
234
- end
235
-
236
- def color_for_action(action)
237
- case action
238
- when 'create'
239
- :green
240
- when 'update'
241
- :yellow
242
- when 'delete'
243
- :red
244
- when 'replace'
245
- :red
246
- when 'read'
247
- :cyan
248
- else
249
- :reset
250
- end
251
- end
252
-
253
- def symbol_for_action(action)
254
- case action
255
- when 'create'
256
- '+'
257
- when 'update'
258
- '~'
259
- when 'delete'
260
- '-'
261
- when 'replace'
262
- '±'
263
- when 'read'
264
- '>'
265
- else
266
- action
267
- end
268
- end
269
-
270
- def format_action(action)
271
- color = color_for_action(action)
272
- symbol = symbol_for_action(action)
273
- Paint[symbol, color]
274
- end
275
-
276
- def format_address(address)
277
- parts = address.split('.')
278
- parts.each_with_index do |part, index|
279
- parts[index] = Paint[part, :cyan] if index.odd?
53
+ puts
54
+ puts plan.summary
280
55
  end
281
- parts.join('.')
282
56
  end
283
57
  end
284
58
  end
@@ -6,13 +6,9 @@ module MuxTf
6
6
  extend PiotrbCliUtils::Util
7
7
 
8
8
  class << self
9
- # include CommandHelpers
10
-
11
9
  def pretty_plan(filename)
12
10
  pastel = Pastel.new
13
11
 
14
- plan_output = String.new
15
-
16
12
  phase = :init
17
13
 
18
14
  meta = {}
@@ -30,8 +26,7 @@ module MuxTf
30
26
 
31
27
  parser.state(:plan_error, /^Error: /, %i[refreshing refresh_done])
32
28
 
33
- status = tf_plan(out: filename, detailed_exitcode: true, compact_warnings: true) do |raw_line|
34
- plan_output << raw_line
29
+ status = tf_plan(out: filename, detailed_exitcode: true, compact_warnings: true) { |raw_line|
35
30
  parser.parse(raw_line.rstrip) do |state, line|
36
31
  case state
37
32
  when :none
@@ -42,19 +37,19 @@ module MuxTf
42
37
  end
43
38
  when :info
44
39
  if /Acquiring state lock. This may take a few moments.../.match?(line)
45
- log 'Acquiring state lock ...', depth: 2
40
+ log "Acquiring state lock ...", depth: 2
46
41
  else
47
42
  p [state, line]
48
43
  end
49
44
  when :error
50
- meta['error'] = 'lock'
45
+ meta["error"] = "lock"
51
46
  log Paint[line, :red], depth: 2
52
47
  when :plan_error
53
48
  if phase != :plan_error
54
49
  puts
55
50
  phase = :plan_error
56
51
  end
57
- meta['error'] = 'refresh'
52
+ meta["error"] = "refresh"
58
53
  log Paint[line, :red], depth: 2
59
54
  when :error_lock_info
60
55
  if line =~ /^ ([^ ]+):\s+([^ ].+)$/
@@ -64,9 +59,9 @@ module MuxTf
64
59
  when :refreshing
65
60
  if phase != :refreshing
66
61
  phase = :refreshing
67
- log 'Refreshing state ', depth: 2, newline: false
62
+ log "Refreshing state ", depth: 2, newline: false
68
63
  else
69
- print '.'
64
+ print "."
70
65
  end
71
66
  when :refresh_done
72
67
  if phase != :refresh_done
@@ -83,14 +78,25 @@ module MuxTf
83
78
  p [state, line]
84
79
  end
85
80
  end
86
- end
81
+ }
87
82
  [status.status, meta]
88
83
  end
89
84
 
90
- def process_upgrade
91
- pastel = Pastel.new
85
+ def init_status_to_remedies(status, meta)
86
+ remedies = Set.new
87
+ if status != 0
88
+ if meta[:need_reconfigure]
89
+ remedies << :reconfigure
90
+ else
91
+ p [status, meta]
92
+ remedies << :unknown
93
+ end
94
+ end
95
+ remedies
96
+ end
92
97
 
93
- plan_output = String.new
98
+ def run_tf_init(upgrade: nil, reconfigure: nil)
99
+ pastel = Pastel.new
94
100
 
95
101
  phase = :init
96
102
 
@@ -98,62 +104,104 @@ module MuxTf
98
104
 
99
105
  parser = StatefulParser.new(normalizer: pastel.method(:strip))
100
106
 
101
- parser.state(:modules, /^Upgrading modules\.\.\./)
102
- parser.state(:backend, /^Initializing the backend\.\.\./, [:modules])
107
+ parser.state(:modules_init, /^Initializing modules\.\.\./)
108
+ parser.state(:modules_upgrade, /^Upgrading modules\.\.\./)
109
+ parser.state(:backend, /^Initializing the backend\.\.\./, [:modules_init, :modules_upgrade])
103
110
  parser.state(:plugins, /^Initializing provider plugins\.\.\./, [:backend])
104
111
 
105
112
  parser.state(:plugin_warnings, /^$/, [:plugins])
113
+ parser.state(:backend_error, /Error:/, [:backend])
114
+
115
+ status = tf_init(upgrade: upgrade, reconfigure: reconfigure) { |raw_line|
116
+ stripped_line = pastel.strip(raw_line.rstrip)
106
117
 
107
- status = tf_init(upgrade: true, color: false) do |raw_line|
108
- plan_output << raw_line
109
118
  parser.parse(raw_line.rstrip) do |state, line|
110
119
  case state
111
- when :modules
120
+ when :modules_init
121
+ if phase != state
122
+ phase = state
123
+ log "Initializing modules ", depth: 1
124
+ next
125
+ end
126
+ case stripped_line
127
+ when ""
128
+ puts
129
+ else
130
+ p [state, stripped_line]
131
+ end
132
+ when :modules_upgrade
112
133
  if phase != state
113
134
  # first line
114
135
  phase = state
115
- log 'Upgrding modules ', depth: 1, newline: false
136
+ log "Upgrding modules ", depth: 1, newline: false
116
137
  next
117
138
  end
118
- case line
139
+ case stripped_line
119
140
  when /^- (?<module>[^ ]+) in (?<path>.+)$/
120
141
  # info = $~.named_captures
121
142
  # log "- #{info["module"]}", depth: 2
122
- print '.'
143
+ print "."
123
144
  when /^Downloading (?<repo>[^ ]+) (?<version>[^ ]+) for (?<module>[^ ]+)\.\.\./
124
145
  # info = $~.named_captures
125
146
  # log "Downloading #{info["module"]} from #{info["repo"]} @ #{info["version"]}"
126
- print 'D'
127
- when ''
147
+ print "D"
148
+ when ""
128
149
  puts
129
150
  else
130
- p [state, line]
151
+ p [state, stripped_line]
131
152
  end
132
153
  when :backend
133
154
  if phase != state
134
155
  # first line
135
156
  phase = state
136
- log 'Initializing the backend ', depth: 1, newline: false
157
+ log "Initializing the backend ", depth: 1, newline: false
137
158
  next
138
159
  end
139
- case line
140
- when ''
160
+ case stripped_line
161
+ when /^Successfully configured/
162
+ log line, depth: 2
163
+ when /unless the backend/
164
+ log line, depth: 2
165
+ when ""
141
166
  puts
142
167
  else
143
- p [state, line]
168
+ p [state, stripped_line]
169
+ end
170
+ when :backend_error
171
+ if raw_line.match "terraform init -reconfigure"
172
+ meta[:need_reconfigure] = true
173
+ log Paint["module needs to be reconfigured", :red], depth: 2
144
174
  end
145
175
  when :plugins
146
176
  if phase != state
147
177
  # first line
148
178
  phase = state
149
- log 'Initializing provider plugins ...', depth: 1
179
+ log "Initializing provider plugins ...", depth: 1
150
180
  next
151
181
  end
152
- case line
153
- when /^- Downloading plugin for provider "(?<provider>[^\"]+)" \((?<provider_path>[^\)]+)\) (?<version>.+)\.\.\.$/
182
+ case stripped_line
183
+ when /^- (?<module>.+) is built in to Terraform$/
154
184
  info = $LAST_MATCH_INFO.named_captures
155
- log "- #{info['provider']} #{info['version']}", depth: 2
156
- when '- Checking for available provider plugins...'
185
+ log "- [BUILTIN] #{info["module"]}", depth: 2
186
+ when /^- Finding (?<module>[^ ]+) versions matching "(?<version>.+)"\.\.\./
187
+ info = $LAST_MATCH_INFO.named_captures
188
+ log "- [FIND] #{info["module"]} matching #{info["version"].inspect}", depth: 2
189
+ when /^- Finding latest version of (?<module>.+)\.\.\.$/
190
+ info = $LAST_MATCH_INFO.named_captures
191
+ log "- [FIND] #{info["module"]}", depth: 2
192
+ when /^- Installing (?<module>[^ ]+) v(?<version>.+)\.\.\.$/
193
+ info = $LAST_MATCH_INFO.named_captures
194
+ log "- [INSTALLING] #{info["module"]} v#{info["version"]}", depth: 2
195
+ when /^- Installed (?<module>[^ ]+) v(?<version>.+) \(signed by( a)? (?<signed>.+)\)$/
196
+ info = $LAST_MATCH_INFO.named_captures
197
+ log "- [INSTALLED] #{info["module"]} v#{info["version"]} (#{info["signed"]})", depth: 2
198
+ when /^- Using previously-installed (?<module>[^ ]+) v(?<version>.+)$/
199
+ info = $LAST_MATCH_INFO.named_captures
200
+ log "- [USING] #{info["module"]} v#{info["version"]}", depth: 2
201
+ when /^- Downloading plugin for provider "(?<provider>[^"]+)" \((?<provider_path>[^)]+)\) (?<version>.+)\.\.\.$/
202
+ info = $LAST_MATCH_INFO.named_captures
203
+ log "- #{info["provider"]} #{info["version"]}", depth: 2
204
+ when "- Checking for available provider plugins..."
157
205
  # noop
158
206
  else
159
207
  p [state, line]
@@ -170,7 +218,7 @@ module MuxTf
170
218
  p [state, line]
171
219
  end
172
220
  end
173
- end
221
+ }
174
222
 
175
223
  [status.status, meta]
176
224
  end
@@ -178,20 +226,20 @@ module MuxTf
178
226
  def process_validation(info)
179
227
  remedies = Set.new
180
228
 
181
- if info['error_count'] > 0 || info['warning_count'] > 0
182
- log "Encountered #{Paint[info['error_count'], :red]} Errors and #{Paint[info['warning_count'], :yellow]} Warnings!", depth: 2
183
- info['diagnostics'].each do |dinfo|
184
- color = dinfo['severity'] == 'error' ? :red : :yellow
185
- log "#{Paint[dinfo['severity'].capitalize, color]}: #{dinfo['summary']}", depth: 3
186
- if dinfo['detail']&.include?('terraform init')
229
+ if info["error_count"] > 0 || info["warning_count"] > 0
230
+ log "Encountered #{Paint[info["error_count"], :red]} Errors and #{Paint[info["warning_count"], :yellow]} Warnings!", depth: 2
231
+ info["diagnostics"].each do |dinfo|
232
+ color = dinfo["severity"] == "error" ? :red : :yellow
233
+ log "#{Paint[dinfo["severity"].capitalize, color]}: #{dinfo["summary"]}", depth: 3
234
+ if dinfo["detail"]&.include?("terraform init")
187
235
  remedies << :init
188
236
  else
189
- log dinfo['detail'], depth: 4 if dinfo['detail']
190
- if dinfo['range']
191
- log format_validation_range(dinfo['range'], color), depth: 4
237
+ log dinfo["detail"], depth: 4 if dinfo["detail"]
238
+ if dinfo["range"]
239
+ log format_validation_range(dinfo["range"], color), depth: 4
192
240
  end
193
241
 
194
- remedies << :unknown if dinfo['severity'] == 'error'
242
+ remedies << :unknown if dinfo["severity"] == "error"
195
243
  end
196
244
  end
197
245
  end
@@ -214,17 +262,17 @@ module MuxTf
214
262
 
215
263
  context_lines = 3
216
264
 
217
- lines = range['start']['line']..range['end']['line']
218
- columns = range['start']['column']..range['end']['column']
265
+ lines = range["start"]["line"]..range["end"]["line"]
266
+ columns = range["start"]["column"]..range["end"]["column"]
219
267
 
220
268
  # on ../../../modules/pods/jane_pod/main.tf line 151, in module "jane":
221
269
  # 151: jane_resources_preset = var.jane_resources_presetx
222
270
  output = []
223
271
  lines_info = lines.size == 1 ? "#{lines.first}:#{columns.first}" : "#{lines.first}:#{columns.first} to #{lines.last}:#{columns.last}"
224
- output << "on: #{range['filename']} line#{lines.size > 1 ? 's' : ''}: #{lines_info}"
272
+ output << "on: #{range["filename"]} line#{lines.size > 1 ? "s" : ""}: #{lines_info}"
225
273
 
226
- if File.exist?(range['filename'])
227
- file_lines = File.read(range['filename']).split("\n")
274
+ if File.exist?(range["filename"])
275
+ file_lines = File.read(range["filename"]).split("\n")
228
276
  extract_range = ([lines.first - context_lines, 0].max)..([lines.last + context_lines, file_lines.length - 1].min)
229
277
  file_lines.each_with_index do |line, index|
230
278
  if extract_range.cover?(index + 1)
@@ -237,7 +285,7 @@ module MuxTf
237
285
  start_col = columns.last
238
286
  end
239
287
  painted_line = paint_line(line, color, start_col: start_col, end_col: end_col)
240
- output << "#{Paint['>', color]} #{index + 1}: #{painted_line}"
288
+ output << "#{Paint[">", color]} #{index + 1}: #{painted_line}"
241
289
  else
242
290
  output << " #{index + 1}: #{line}"
243
291
  end