mux_tf 0.2.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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