awx 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,282 @@
1
+ module App
2
+
3
+ class Replacer
4
+
5
+ REGEXP_MATCHER = /\$\{\{[^}]+\}\}/
6
+ REGEXP_MULTILINE = /^[A-Za-z0-9]+:\s*\|$/
7
+ REGEXP_MODIFIER = /^\$\{\{awx-(for|end).*\}\}$/
8
+ TYPE_YML = 'yml'
9
+ VALID_MODIFIERS = %w(base64encode)
10
+ OP_SCAN = 'scan'
11
+ OP_REPLACE = 'replace'
12
+ SSH_USERS = 'SSHUsers'
13
+
14
+ # Return Array of Hashes containing all matchers.
15
+ # Use this for validation.
16
+ # @return Array
17
+ def self.scan_string(line)
18
+ matchers = {}
19
+ errors = []
20
+ x, matchers, errors = process_matchers(line, matchers, errors, OP_SCAN) if line =~ REGEXP_MATCHER
21
+ return {
22
+ :matchers => matchers,
23
+ :errors => errors
24
+ }
25
+ end
26
+
27
+ # Return Array of Hashes containing all matchers.
28
+ # Use this for validation.
29
+ # @return Hash
30
+ def self.scan_file(path_and_file, operation = OP_SCAN, data: nil, new_lines: nil)
31
+ raise RuntimeError, "File does not exist: #{path_and_file}" unless Blufin::Files::file_exists(path_and_file)
32
+ raise RuntimeError, "Cannot call scan_file without new_lines being an Array when operation is #{OP_REPLACE}." if operation == OP_REPLACE && !new_lines.is_a?(Array)
33
+ matchers = {}
34
+ errors = []
35
+ multiline = nil
36
+ awx_for = nil
37
+ line_count = 0
38
+ Blufin::Files::read_file(path_and_file).each do |line|
39
+ next if line =~ /^\s*#/ && line !~ /^\s*#cloud-config/ # Skip comments (but not #cloud-config).
40
+ line = line.gsub("\n", '')
41
+ line_count += 1
42
+ # This handles multi-line block (IE -> content: | ) which basically gets skipped.
43
+ # If you want to make replace work within comments, you will need to write more code (or possibly just add a flag?).
44
+ if line.strip =~ REGEXP_MULTILINE
45
+ # Stores the number of spaces from start.
46
+ multiline = line[/\A */].size
47
+ awx_for[:lines] << line if awx_for.is_a?(Hash)
48
+ new_lines << line if operation == OP_REPLACE && awx_for.nil?
49
+ next
50
+ elsif !multiline.nil?
51
+ whitespace_from_start = line[/\A */].size
52
+ # If white-space from beginning is same or higher, we're probably still in a multi-line.
53
+ if line.strip == '' || whitespace_from_start > multiline
54
+ awx_for[:lines] << line if awx_for.is_a?(Hash)
55
+ new_lines << line if operation == OP_REPLACE && awx_for.nil?
56
+ next
57
+ end
58
+ # Otherwise, break-out.
59
+ multiline = nil
60
+ end
61
+ # This handles ${{awx-[modifier]}} tags.
62
+ if line.strip =~ REGEXP_MODIFIER
63
+ awx_modifier = line.strip.gsub(/^\$\{\{awx-/, '').gsub(/\}\}$/, '')
64
+ # Handle end-tag first.
65
+ if awx_modifier == 'end'
66
+ if awx_for.is_a?(Hash)
67
+ if operation == OP_SCAN
68
+ # Handle matchers.
69
+ if awx_for[:matchers].any?
70
+ awx_for[:matchers].each do |k, v|
71
+ if k != awx_for[:item]
72
+ matchers[k] = [] unless matchers.has_key?(k)
73
+ matchers[k].push(*v)
74
+ end
75
+
76
+ end
77
+ end
78
+ # Handle errors.
79
+ errors.push(*awx_for[:errors]) if awx_for[:errors].any?
80
+ else
81
+ ms = awx_for[:item_source].split(':')
82
+ raise RuntimeError, "Invalid key: #{ms[0]}" unless data.has_key?(ms[0])
83
+ raise RuntimeError, "Invalid key: #{ms[0]}.#{ms[1]}" unless data[ms[0]].has_key?(ms[1])
84
+ is = data[ms[0]][ms[1]]
85
+ awx_for_data = data
86
+ if is.is_a?(Array)
87
+ if ms[1] == SSH_USERS
88
+ # SSH Users get handled differently.
89
+ is.each do |ssh_key_file|
90
+ raise RuntimeError, "File not found: #{ssh_key_file}" unless Blufin::Files::file_exists(ssh_key_file)
91
+ contents = []
92
+ Blufin::Files::read_file(ssh_key_file).each do |c|
93
+ next if c.strip == ''
94
+ contents << c
95
+ end
96
+ awx_for_data[awx_for[:item]] = {
97
+ 'name' => Blufin::Files::extract_file_name(ssh_key_file).gsub(/\.pub$/i, '').gsub(/\./, '-'),
98
+ 'pub_key' => contents.join("\n")
99
+ }
100
+ new_lines = process_awx_for_loop(awx_for, awx_for_data, new_lines)
101
+ end
102
+ else
103
+
104
+ # TODO - Finish this.
105
+ raise RuntimeError, 'Not yet implemented!'
106
+
107
+ end
108
+ else
109
+
110
+ # TODO - Finish this.
111
+ raise RuntimeError, 'Not yet implemented!'
112
+
113
+ end
114
+ end
115
+ awx_for = nil
116
+ end
117
+ next
118
+ end
119
+ # If a tag is already open, return an error (or throw one).
120
+ unless awx_for.nil?
121
+ tag_open_error = "Detected #{line.strip} tag even though another one is already open (Line: #{line_count})."
122
+ if operation == OP_SCAN
123
+ errors << tag_open_error
124
+ next
125
+ else
126
+ raise RuntimeError, tag_open_error
127
+ end
128
+ end
129
+ # Hits here at the start of a for loop (IE: ${{awx-for(user in Parameters:SSHUsers)}})
130
+ if awx_modifier =~ /for\([A-Za-z0-9]+\s*in\s*[A-Za-z0-9]+:[A-Za-z0-9]+\)/
131
+ ams = awx_modifier.strip.gsub(/^for\(/, '').gsub(/\)$/, '')
132
+ ams = ams.split(' ')
133
+ awx_for = {
134
+ :lines => [],
135
+ :item => ams[0],
136
+ :item_source => ams[2],
137
+ :matchers => {},
138
+ :errors => []
139
+ }
140
+ line, matchers, errors = process_matchers("${{#{ams[2]}}}", matchers, errors, OP_SCAN, data: data, file: path_and_file)
141
+ next
142
+ else
143
+ raise RuntimeError, "Unsupported awx modifier: #{awx_modifier}"
144
+ end
145
+ end
146
+ # If we're in a for loop, start buffering content.
147
+ if awx_for.is_a?(Hash)
148
+ if operation == OP_SCAN
149
+ line, awx_for[:matchers], awx_for[:errors] = process_matchers(line, awx_for[:matchers], awx_for[:errors], operation, data: data, file: path_and_file)
150
+ end
151
+ awx_for[:lines] << line
152
+ next
153
+ end
154
+ # This just processes a regular line.
155
+ line, matchers, errors = process_matchers(line, matchers, errors, operation, data: data, file: path_and_file) if line =~ REGEXP_MATCHER
156
+ new_lines << line if operation == OP_REPLACE
157
+ end
158
+ if operation == OP_SCAN
159
+ return {
160
+ :matchers => matchers,
161
+ :errors => errors
162
+ }
163
+ else
164
+ return new_lines
165
+ end
166
+ end
167
+
168
+ # Takes a string, replaces all the matchers and returns string.
169
+ # @return string
170
+ def self.replace_string(line, data)
171
+ return nil if line.nil?
172
+ raise RuntimeError, "Expected String, instead got #{line.class}" unless line.is_a?(String)
173
+ raise RuntimeError, "Expected Hash, instead got #{data.class}" unless data.is_a?(Hash)
174
+ line, x, y = process_matchers(line, {}, [], OP_REPLACE, data: data)
175
+ return line
176
+ end
177
+
178
+ # Takes file path and returns array of lines (that can then be used to write same/new file).
179
+ # @return Array (of file lines)
180
+ def self.replace_yml(path_and_file, data)
181
+ raise RuntimeError, "Expected String, instead got #{path_and_file.class}" unless path_and_file.is_a?(String)
182
+ raise RuntimeError, "Expected Hash, instead got #{data.class}" unless data.is_a?(Hash)
183
+ extension = Blufin::Files::extract_file_name(path_and_file).split('.')
184
+ extension = extension[extension.length - 1].downcase
185
+ raise RuntimeError, "Expected YML file, instead got: #{path_and_file}" unless %w(yml yaml).include?(extension)
186
+ new_lines = []
187
+ scan_file(path_and_file, OP_REPLACE, data: data, new_lines: new_lines)
188
+ new_lines
189
+ end
190
+
191
+ private
192
+
193
+ # Process matchers. Either replaces line or populates Hash.
194
+ # @return multiple
195
+ def self.process_matchers(line, matchers, errors, operation, data: nil, file: nil)
196
+ matches = line.scan(REGEXP_MATCHER)
197
+ matches.each do |match|
198
+ ms = match.gsub(/^\$\{\{/, '').gsub(/\}\}$/, '')
199
+ ms = ms.split(':')
200
+ if ms.length <= 1 || ms.length >= 4
201
+ if operation == OP_SCAN
202
+ errors << match
203
+ next
204
+ else
205
+ raise RuntimeError, "Invalid matcher: #{match}"
206
+ end
207
+ end
208
+ matchers[ms[0]] = [] unless matchers.has_key?(ms[0])
209
+ if operation == OP_SCAN
210
+ matchers[ms[0]] << {:key => ms[1]} if ms.length == 2
211
+ if ms.length == 3
212
+ unless VALID_MODIFIERS.include?(ms[2])
213
+ errors << match
214
+ next
215
+ end
216
+ matchers[ms[0]] << {:key => ms[1], :modifier => ms[2]}
217
+ end
218
+ else
219
+ if ms[0] == 'file'
220
+ raise RuntimeError, "Cannot process file: #{ms[1]} because containing file path was not passed. This is an unsupported edge-case." if file.nil?
221
+ file_path = "#{Blufin::Files::extract_path_name(file)}/#{ms[1]}"
222
+ file_content = replace_yml(file_path, data)
223
+ if ms.length == 3
224
+ case ms[2]
225
+ when 'base64encode'
226
+ tmp_file = "/tmp/base64encode-#{Blufin::Strings::random_string(4)}.txt"
227
+ Blufin::Terminal::execute_proc("Base64 Encoding raw file: \x1B[38;5;240m#{tmp_file}\x1B[0m", Proc.new {
228
+ Blufin::Files::write_file(tmp_file, file_content)
229
+ b64_cmd = Blufin::Tools::value_based_on_os(mac: "openssl base64 -in #{tmp_file} | tr -d '\n'", linux: "base64 -w0 #{tmp_file}")
230
+ result = Blufin::Terminal::command_capture(b64_cmd, nil, nil, nil)[0]
231
+ result = result.split("\n").join('') # Removes line breaks, just in case :)
232
+ file_content = result
233
+ })
234
+ else
235
+ raise RuntimeError, "Invalid modifier: #{ms[2]}"
236
+ end
237
+ end
238
+ file_content = file_content.join('') if file_content.is_a?(Array)
239
+ line = line.gsub(/\$\{\{#{ms[0]}:#{ms[1]}\}\}/, file_content) if ms.length == 2
240
+ line = line.gsub(/\$\{\{#{ms[0]}:#{ms[1]}:#{ms[2]}\}\}/, file_content) if ms.length == 3
241
+ elsif ms[0] == AppCommand::CloudFormationCreate::STACK
242
+ stack = ms[1]
243
+ Blufin::Terminal::error("Found: #{Blufin::Terminal::format_invalid("${{Stack:#{stack}}}")} on a line that does support this property.", ["Expected: #{Blufin::Terminal::format_highlight("TemplateURL: ${{Stack:#{stack}}}")}", " Got: #{Blufin::Terminal::format_invalid(line.strip)}"], true) unless line =~ /TemplateURL:\s*\$\{\{Stack:#{stack.gsub('/', '\/')}\}\}/
244
+ raise RuntimeError, "data is missing key: #{AppCommand::CloudFormationCreate::STACK}" unless data.has_key?(AppCommand::CloudFormationCreate::STACK)
245
+ raise RuntimeError, "data[Stack] is missing key: #{stack}" unless data[AppCommand::CloudFormationCreate::STACK].has_key?(stack)
246
+ line = line.gsub( /\$\{\{Stack:#{stack.gsub('/', '\/')}\}\}/, data[AppCommand::CloudFormationCreate::STACK][stack][:s3_url])
247
+ else
248
+ raise RuntimeError, "Invalid key: #{ms[0]}" unless data.has_key?(ms[0])
249
+ raise RuntimeError, "Invalid key: #{ms[0]}.#{ms[1]}" unless data[ms[0]].has_key?(ms[1])
250
+ line = line.gsub(/\$\{\{#{ms[0]}:#{ms[1]}\}\}/, data[ms[0]][ms[1]])
251
+ end
252
+ end
253
+ end
254
+ return line, matchers, errors
255
+ end
256
+
257
+ # Processes the awx-for content.
258
+ # @return Array
259
+ def self.process_awx_for_loop(awx_for, awx_for_data, new_lines)
260
+ ml = nil
261
+ awx_for[:lines].each do |line|
262
+ if line.strip =~ REGEXP_MULTILINE
263
+ ml = line[/\A */].size
264
+ new_lines << line
265
+ next
266
+ elsif !ml.nil?
267
+ whitespace_from_start = line[/\A */].size
268
+ if line.strip == '' || whitespace_from_start > ml
269
+ new_lines << line
270
+ next
271
+ end
272
+ ml = nil
273
+ end
274
+ line, x, y = process_matchers(line, {}, [], OP_REPLACE, data: awx_for_data)
275
+ new_lines << line
276
+ end
277
+ new_lines
278
+ end
279
+
280
+ end
281
+
282
+ end