kumogata2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,587 @@
1
+ class Kumogata2::Client
2
+ include Kumogata2::Logger::Helper
3
+
4
+ def initialize(options)
5
+ @options = options.kind_of?(Hashie::Mash) ? options : Hashie::Mash.new(options)
6
+ @client = Aws::CloudFormation::Client.new
7
+ @resource = Aws::CloudFormation::Resource.new(client: @client)
8
+ @plugin_by_ext = {}
9
+ end
10
+
11
+ def create(path_or_url, stack_name = nil)
12
+ validate_stack_name(stack_name) if stack_name
13
+ template = open_template(path_or_url)
14
+ update_deletion_policy(template, delete_stack: !stack_name)
15
+
16
+ outputs = create_stack(template, stack_name)
17
+
18
+ unless @options.detach?
19
+ post_process(path_or_url, outputs)
20
+ end
21
+ end
22
+
23
+ def update(path_or_url, stack_name)
24
+ validate_stack_name(stack_name)
25
+ template = open_template(path_or_url)
26
+ update_deletion_policy(template, update_metadate: true)
27
+
28
+ outputs = update_stack(template, stack_name)
29
+
30
+ unless @options.detach?
31
+ post_process(path_or_url, outputs)
32
+ end
33
+ end
34
+
35
+ def delete(stack_name)
36
+ validate_stack_name(stack_name)
37
+ @resource.stack(stack_name).stack_status
38
+
39
+ if @options.force? or agree("Are you sure you want to delete `#{stack_name}`? ".yellow)
40
+ delete_stack(stack_name)
41
+ end
42
+ end
43
+
44
+ def validate(path_or_url)
45
+ template = open_template(path_or_url)
46
+ validate_template(template)
47
+ end
48
+
49
+ def list(stack_name = nil)
50
+ validate_stack_name(stack_name) if stack_name
51
+ stacks = describe_stacks(stack_name)
52
+ JSON.pretty_generate(stacks).colorize_as(:json)
53
+ end
54
+
55
+ def export(stack_name)
56
+ validate_stack_name(stack_name)
57
+ template = export_template(stack_name)
58
+ convert0(template)
59
+ end
60
+
61
+ def convert(path_or_url)
62
+ template = open_template(path_or_url)
63
+ convert0(template)
64
+ end
65
+
66
+ def diff(path_or_url1, path_or_url2)
67
+ templates = [path_or_url1, path_or_url2].map do |path_or_url|
68
+ template = nil
69
+
70
+ if path_or_url =~ %r|\Astack://(.*)|
71
+ stack_name = $1 || ''
72
+ validate_stack_name(stack_name)
73
+ template = export_template(stack_name)
74
+ else
75
+ template = open_template(path_or_url)
76
+ end
77
+
78
+ JSON.pretty_generate(template)
79
+ end
80
+
81
+ diff_opts = @options.ignore_all_space? ? '-uw' : '-u'
82
+ opts = {:include_diff_info => true, :diff => diff_opts}
83
+ diff = Diffy::Diff.new(*templates, opts).to_s
84
+
85
+ diff.sub(/^(\e\[\d+m)?\-\-\-(\s+)(\S+)/m) { "#{$1}---#{$2}#{path_or_url1}"}
86
+ .sub(/^(\e\[\d+m)?\+\+\+(\s+)(\S+)/m) { "#{$1}+++#{$2}#{path_or_url2}"}
87
+ end
88
+
89
+ def dry_run(path_or_url, stack_name = nil)
90
+ validate_stack_name(stack_name) if stack_name
91
+ template = open_template(path_or_url)
92
+ update_deletion_policy(template, delete_stack: !stack_name)
93
+ changes = show_change_set(template, stack_name)
94
+ changes = JSON.pretty_generate(changes).colorize_as(:json) if changes
95
+ changes
96
+ end
97
+
98
+ def show_events(stack_name)
99
+ validate_stack_name(stack_name)
100
+ events = describe_events(stack_name)
101
+ JSON.pretty_generate(events).colorize_as(:json)
102
+ end
103
+
104
+ def show_outputs(stack_name)
105
+ validate_stack_name(stack_name)
106
+ outputs = describe_outputs(stack_name)
107
+ JSON.pretty_generate(outputs).colorize_as(:json)
108
+ end
109
+
110
+ def show_resources(stack_name)
111
+ validate_stack_name(stack_name)
112
+ resources = describe_resources(stack_name)
113
+ JSON.pretty_generate(resources).colorize_as(:json)
114
+ end
115
+
116
+ private
117
+
118
+ def create_stack(template, stack_name)
119
+ stack_will_be_deleted = !stack_name
120
+
121
+ unless stack_name
122
+ stack_name = random_stack_name
123
+ end
124
+
125
+ log(:info, "Creating stack: #{stack_name}", color: :cyan)
126
+
127
+ params = {
128
+ stack_name: stack_name,
129
+ template_body: template.to_json,
130
+ parameters: parameters_array,
131
+ }
132
+
133
+ set_api_params(params,
134
+ :disable_rollback,
135
+ :timeout_in_minutes,
136
+ :notification_arns,
137
+ :capabilities,
138
+ :resource_types,
139
+ :on_failure,
140
+ :stack_policy_body,
141
+ :stack_policy_url)
142
+
143
+ stack = @resource.create_stack(params)
144
+
145
+ return if @options.detach?
146
+
147
+ completed = wait(stack, 'CREATE_COMPLETE')
148
+
149
+ unless completed
150
+ raise_stack_error!(stack, 'Create failed')
151
+ end
152
+
153
+ outputs = outputs_for(stack)
154
+ summaries = resource_summaries_for(stack)
155
+
156
+ if stack_will_be_deleted
157
+ delete_stack(stack_name)
158
+ end
159
+
160
+ output_result(stack_name, outputs, summaries)
161
+
162
+ outputs
163
+ end
164
+
165
+ def update_stack(template, stack_name)
166
+ stack = @resource.stack(stack_name)
167
+ stack.stack_status
168
+
169
+ log(:info, "Updating stack: #{stack_name}", color: :green)
170
+
171
+ params = {
172
+ stack_name: stack_name,
173
+ template_body: template.to_json,
174
+ parameters: parameters_array,
175
+ }
176
+
177
+ set_api_params(params,
178
+ :use_previous_template,
179
+ :stack_policy_during_update_body,
180
+ :stack_policy_during_update_url,
181
+ :notification_arns,
182
+ :capabilities,
183
+ :resource_types,
184
+ :stack_policy_body,
185
+ :stack_policy_url)
186
+
187
+ event_log = create_event_log(stack)
188
+ stack.update(params)
189
+
190
+ return if @options.detach?
191
+
192
+ # XXX: Reacquire the stack
193
+ stack = @resource.stack(stack_name)
194
+ completed = wait(stack, 'UPDATE_COMPLETE', event_log)
195
+
196
+ unless completed
197
+ raise_stack_error!(stack, 'Update failed')
198
+ end
199
+
200
+ outputs = outputs_for(stack)
201
+ summaries = resource_summaries_for(stack)
202
+
203
+ output_result(stack_name, outputs, summaries)
204
+
205
+ outputs
206
+ end
207
+
208
+ def delete_stack(stack_name)
209
+ stack = @resource.stack(stack_name)
210
+ stack.stack_status
211
+
212
+ log(:info, "Deleting stack: #{stack_name}", color: :red)
213
+ event_log = create_event_log(stack)
214
+ stack.delete
215
+
216
+ return if @options.detach?
217
+
218
+ completed = false
219
+
220
+ begin
221
+ # XXX: Reacquire the stack
222
+ stack = @resource.stack(stack_name)
223
+ completed = wait(stack, 'DELETE_COMPLETE', event_log)
224
+ rescue Aws::CloudFormation::Errors::ValidationError
225
+ # Handle `Stack does not exist`
226
+ completed = true
227
+ end
228
+
229
+ unless completed
230
+ raise_stack_error!(stack, 'Delete failed')
231
+ end
232
+
233
+ log(:info, 'Success')
234
+ end
235
+
236
+ def validate_template(template)
237
+ @client.validate_template(template_body: template.to_json)
238
+ log(:info, 'Template validated successfully', color: :green)
239
+ end
240
+
241
+ def describe_stacks(stack_name)
242
+ params = {}
243
+ params[:stack_name] = stack_name if stack_name
244
+
245
+ @resource.stacks(params).map do |stack|
246
+ {
247
+ 'StackName' => stack.name,
248
+ 'CreationTime' => stack.creation_time,
249
+ 'StackStatus' => stack.stack_status,
250
+ 'Description' => stack.description,
251
+ }
252
+ end
253
+ end
254
+
255
+ def export_template(stack_name)
256
+ stack = @resource.stack(stack_name)
257
+ stack.stack_status
258
+ template = stack.client.get_template(stack_name: stack_name).template_body
259
+ JSON.parse(template)
260
+ end
261
+
262
+ def show_change_set(template, stack_name)
263
+ output = nil
264
+ change_set_name = [stack_name, SecureRandom.uuid].join('-')
265
+
266
+ log(:info, "Creating ChangeSet: #{change_set_name}", color: :cyan)
267
+
268
+ params = {
269
+ stack_name: stack_name,
270
+ change_set_name: change_set_name,
271
+ template_body: template.to_json,
272
+ parameters: parameters_array,
273
+ }
274
+
275
+ set_api_params(params,
276
+ :use_previous_template,
277
+ :notification_arns,
278
+ :capabilities,
279
+ :resource_types)
280
+
281
+ resp = @client.create_change_set(params)
282
+ change_set_arn = resp.id
283
+
284
+ completed, change_set = wait_change_set(change_set_arn, 'CREATE_COMPLETE')
285
+
286
+ if completed
287
+ output = changes_for(change_set)
288
+ else
289
+ log(:error, "Create ChangeSet failed: #{change_set.status_reason}", color: :red)
290
+ end
291
+
292
+ log(:info, "Deleting ChangeSet: #{change_set_name}", color: :red)
293
+
294
+ @client.delete_change_set(change_set_name: change_set_arn)
295
+
296
+ begin
297
+ completed, _ = wait_change_set(change_set_arn, 'DELETE_COMPLETE')
298
+ rescue Aws::CloudFormation::Errors::ChangeSetNotFound
299
+ # Handle `ChangeSet does not exist`
300
+ completed = true
301
+ end
302
+
303
+ unless completed
304
+ log(:error, "Delete ChangeSet failed: #{change_set.status_reason}", color: :red)
305
+ end
306
+
307
+ output
308
+ end
309
+
310
+ def describe_events(stack_name)
311
+ stack = @resource.stack(stack_name)
312
+ stack.stack_status
313
+ events_for(stack)
314
+ end
315
+
316
+ def describe_outputs(stack_name)
317
+ stack = @resource.stack(stack_name)
318
+ stack.stack_status
319
+ outputs_for(stack)
320
+ end
321
+
322
+ def describe_resources(stack_name)
323
+ stack = @resource.stack(stack_name)
324
+ stack.stack_status
325
+ resource_summaries_for(stack)
326
+ end
327
+
328
+ def convert0(template)
329
+ ext = @options.output_format || 'template'
330
+ plugin = find_or_create_plugin('xxx.' + ext)
331
+
332
+ if plugin
333
+ plugin.dump(template)
334
+ else
335
+ raise "Unknown format: #{ext}"
336
+ end
337
+ end
338
+
339
+ def open_template(path_or_url)
340
+ plugin = find_or_create_plugin(path_or_url)
341
+
342
+ if plugin
343
+ @options.path_or_url = path_or_url
344
+ plugin.parse(open(path_or_url, &:read))
345
+ else
346
+ raise "Unknown format: #{path_or_url}"
347
+ end
348
+ end
349
+
350
+ def find_or_create_plugin(path_or_url)
351
+ ext = File.extname(path_or_url).sub(/\A\./, '')
352
+
353
+ if @plugin_by_ext.has_key?(ext)
354
+ return @plugin_by_ext.fetch(ext)
355
+ end
356
+
357
+ plugin_class = Kumogata2::Plugin.find(ext)
358
+ plugin = plugin_class ? plugin_class.new(@options) : nil
359
+ @plugin_by_ext[ext] = plugin
360
+ end
361
+
362
+ def update_deletion_policy(template, options = {})
363
+ if options[:delete_stack] or @options.deletion_policy_retain?
364
+ template['Resources'].each do |k, v|
365
+ next if /\AAWS::CloudFormation::/ =~ v['Type']
366
+ v['DeletionPolicy'] ||= 'Retain'
367
+
368
+ if options[:update_metadate]
369
+ v['Metadata'] ||= {}
370
+ v['Metadata']['DeletionPolicyUpdateKeyForKumogata'] = "DeletionPolicyUpdateValueForKumogata#{Time.now.to_i}"
371
+ end
372
+ end
373
+ end
374
+ end
375
+
376
+ def validate_stack_name(stack_name)
377
+ unless /\A[a-zA-Z][-a-zA-Z0-9]*\Z/i =~ stack_name
378
+ raise "1 validation error detected: Value '#{stack_name}' at 'stackName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]*"
379
+ end
380
+ end
381
+
382
+ def parameters_array
383
+ @options.parameters.map do |key, value|
384
+ {parameter_key: key, parameter_value: value}
385
+ end
386
+ end
387
+
388
+ def set_api_params(params, *keys)
389
+ keys.each do |k|
390
+ opts[k] = @options[k] if @options[k]
391
+ end
392
+ end
393
+
394
+ def wait(stack, complete_status, event_log = {})
395
+ before_wait = proc do |attempts, response|
396
+ print_event_log(stack, event_log)
397
+ end
398
+
399
+ stack.wait_until(before_wait: before_wait, max_attempts: nil, delay: 1) do |s|
400
+ s.stack_status !~ /_IN_PROGRESS\z/
401
+ end
402
+
403
+ print_event_log(stack, event_log)
404
+
405
+ completed = (stack.stack_status == complete_status)
406
+ log(:info, completed ? 'Success' : 'Failure')
407
+
408
+ completed
409
+ end
410
+
411
+ def wait_change_set(change_set_name, complete_status)
412
+ change_set = nil
413
+
414
+ loop do
415
+ change_set = @client.describe_change_set(change_set_name: change_set_name)
416
+
417
+ if change_set.status !~ /(_PENDING|_IN_PROGRESS)\z/
418
+ break
419
+ end
420
+
421
+ sleep 1
422
+ end
423
+
424
+ completed = (change_set.status == complete_status)
425
+ [completed, change_set]
426
+ end
427
+
428
+ def print_event_log(stack, event_log)
429
+ events_for(stack).sort_by {|i| i['Timestamp'] }.each do |event|
430
+ event_id = event['EventId']
431
+
432
+ unless event_log[event_id]
433
+ event_log[event_id] = event
434
+
435
+ timestamp = event['Timestamp']
436
+ summary = {}
437
+
438
+ ['LogicalResourceId', 'ResourceStatus', 'ResourceStatusReason'].map do |k|
439
+ summary[k] = event[k]
440
+ end
441
+
442
+ puts [
443
+ timestamp.getlocal.strftime('%Y/%m/%d %H:%M:%S %Z'),
444
+ summary.to_json.colorize_as(:json),
445
+ ].join(': ')
446
+ end
447
+ end
448
+ end
449
+
450
+ def create_event_log(stack)
451
+ event_log = {}
452
+
453
+ events_for(stack).sort_by {|i| i['Timestamp'] }.each do |event|
454
+ event_id = event['EventId']
455
+ event_log[event_id] = event
456
+ end
457
+
458
+ return event_log
459
+ end
460
+
461
+ def events_for(stack)
462
+ stack.events.map do |event|
463
+ event_hash = {}
464
+
465
+ [
466
+ :event_id,
467
+ :logical_resource_id,
468
+ :physical_resource_id,
469
+ :resource_properties,
470
+ :resource_status,
471
+ :resource_status_reason,
472
+ :resource_type,
473
+ :stack_id,
474
+ :stack_name,
475
+ :timestamp,
476
+ ].each do |k|
477
+ event_hash[Kumogata2::Utils.camelize(k)] = event.send(k)
478
+ end
479
+
480
+ event_hash
481
+ end
482
+ end
483
+
484
+ def outputs_for(stack)
485
+ outputs_hash = {}
486
+
487
+ stack.outputs.each do |output|
488
+ outputs_hash[output.output_key] = output.output_value
489
+ end
490
+
491
+ outputs_hash
492
+ end
493
+
494
+ def resource_summaries_for(stack)
495
+ stack.resource_summaries.map do |summary|
496
+ summary_hash = {}
497
+
498
+ [
499
+ :logical_resource_id,
500
+ :physical_resource_id,
501
+ :resource_type,
502
+ :resource_status,
503
+ :resource_status_reason,
504
+ :last_updated_timestamp
505
+ ].each do |k|
506
+ summary_hash[Kumogata2::Utils.camelize(k)] = summary.send(k)
507
+ end
508
+
509
+ summary_hash
510
+ end
511
+ end
512
+
513
+ def changes_for(change_set)
514
+ change_set.changes.map do |change|
515
+ resource_change = change.resource_change
516
+ change_hash = {}
517
+
518
+ [
519
+ :action,
520
+ :logical_resource_id,
521
+ :physical_resource_id,
522
+ :resource_type,
523
+ ].each do |k|
524
+ change_hash[Kumogata2::Utils.camelize(k)] = resource_change[k]
525
+ end
526
+
527
+ change_hash['Details'] = resource_change.details.map do |detail|
528
+ {
529
+ attribute: detail.target.attribute,
530
+ name: detail.target.name,
531
+ }
532
+ end
533
+
534
+ change_hash
535
+ end
536
+ end
537
+
538
+ def output_result(stack_name, outputs, summaries)
539
+ puts <<-EOS
540
+
541
+ Stack Resource Summaries:
542
+ #{JSON.pretty_generate(summaries).colorize_as(:json)}
543
+
544
+ Outputs:
545
+ #{JSON.pretty_generate(outputs).colorize_as(:json)}
546
+ EOS
547
+
548
+ if @options.result_log?
549
+ puts <<-EOS
550
+
551
+ (Save to `#{@options.result_log}`)
552
+ EOS
553
+
554
+ open(@options.result_log, 'wb') do |f|
555
+ f.puts JSON.pretty_generate({
556
+ 'StackName' => stack_name,
557
+ 'StackResourceSummaries' => summaries,
558
+ 'Outputs' => outputs,
559
+ })
560
+ end
561
+ end
562
+ end
563
+
564
+ def post_process(path_or_url, outputs)
565
+ plugin = find_or_create_plugin(path_or_url)
566
+
567
+ if plugin and plugin.respond_to?(:post)
568
+ plugin.post(outputs)
569
+ end
570
+ end
571
+
572
+ def raise_stack_error!(stack, message)
573
+ errmsgs = [message]
574
+ errmsgs << stack.name
575
+ errmsgs << stack.stack_status_reason if stack.stack_status_reason
576
+ raise errmsgs.join(': ')
577
+ end
578
+
579
+ def random_stack_name
580
+ stack_name = ['kumogata']
581
+ user_host = Kumogata2::Utils.get_user_host
582
+ stack_name << user_host if user_host
583
+ stack_name << SecureRandom.uuid
584
+ stack_name = stack_name.join('-')
585
+ stack_name.gsub(/[^-a-zA-Z0-9]+/, '-').gsub(/-+/, '-')
586
+ end
587
+ end
@@ -0,0 +1,24 @@
1
+ {
2
+ constant: "\e[1;34m",
3
+ float: "\e[36m",
4
+ integer: "\e[36m",
5
+ keyword: "\e[1;31m",
6
+
7
+ key: {
8
+ self: "\e[1;34m",
9
+ char: "\e[1;34m",
10
+ delimiter: "\e[1;34m",
11
+ },
12
+
13
+ string: {
14
+ self: "\e[32m",
15
+ modifier: "\e[1;32m",
16
+ char: "\e[1;32m",
17
+ delimiter: "\e[1;32m",
18
+ escape: "\e[1;32m",
19
+ },
20
+
21
+ error: "\e[0m",
22
+ }.each do |key, value|
23
+ CodeRay::Encoders::Terminal::TOKEN_COLORS[key] = value
24
+ end
@@ -0,0 +1,36 @@
1
+ module Kumogata2::Ext
2
+ module StringExt
3
+ module ClassMethods
4
+ def colorize=(value)
5
+ @colorize = value
6
+ end
7
+
8
+ def colorize
9
+ @colorize
10
+ end
11
+ end # ClassMethods
12
+
13
+ Term::ANSIColor::Attribute.named_attributes.each do |attribute|
14
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
15
+ def #{attribute.name}
16
+ if String.colorize
17
+ Term::ANSIColor.send(#{attribute.name.inspect}, self)
18
+ else
19
+ self
20
+ end
21
+ end
22
+ EOS
23
+ end
24
+
25
+ def colorize_as(lang)
26
+ if String.colorize
27
+ CodeRay.scan(self, lang).terminal
28
+ else
29
+ self
30
+ end
31
+ end
32
+ end # StringExt
33
+ end # Kumogata2::Ext
34
+
35
+ String.include(Kumogata2::Ext::StringExt)
36
+ String.extend(Kumogata2::Ext::StringExt::ClassMethods)
@@ -0,0 +1,28 @@
1
+ class Kumogata2::Logger < ::Logger
2
+ include Singleton
3
+
4
+ def initialize
5
+ super($stdout)
6
+
7
+ self.formatter = proc do |severity, datetime, progname, msg|
8
+ "#{msg}\n"
9
+ end
10
+
11
+ self.level = Logger::INFO
12
+ end
13
+
14
+ def set_debug(value)
15
+ self.level = value ? Logger::DEBUG : Logger::INFO
16
+ end
17
+
18
+ module Helper
19
+ def log(level, message, log_options = {})
20
+ globa_options = @options || {}
21
+ message = "[#{level.to_s.upcase}] #{message}" unless level == :info
22
+ message = message.send(log_options[:color]) if log_options[:color]
23
+ logger = globa_options[:logger] || Kumogata2::Logger.instance
24
+ logger.send(level, message)
25
+ end
26
+ module_function :log
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ class Kumogata2::Plugin::JSON
2
+ Kumogata2::Plugin.register(:json, ['json', 'js', 'template'], self)
3
+
4
+ def initialize(options)
5
+ @options = options
6
+ end
7
+
8
+ def parse(str)
9
+ JSON.parse(str)
10
+ end
11
+
12
+ def dump(hash)
13
+ JSON.pretty_generate(hash).colorize_as(:json)
14
+ end
15
+ end