moonshot 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/moonshot/artifact_repository/s3_bucket.rb +60 -0
- data/lib/moonshot/artifact_repository/s3_bucket_via_github_releases.rb +89 -0
- data/lib/moonshot/build_mechanism/github_release.rb +148 -0
- data/lib/moonshot/build_mechanism/script.rb +84 -0
- data/lib/moonshot/build_mechanism/travis_deploy.rb +70 -0
- data/lib/moonshot/build_mechanism/version_proxy.rb +55 -0
- data/lib/moonshot/cli.rb +146 -0
- data/lib/moonshot/controller.rb +151 -0
- data/lib/moonshot/controller_config.rb +25 -0
- data/lib/moonshot/creds_helper.rb +28 -0
- data/lib/moonshot/deployment_mechanism/code_deploy.rb +303 -0
- data/lib/moonshot/doctor_helper.rb +57 -0
- data/lib/moonshot/environment_parser.rb +32 -0
- data/lib/moonshot/interactive_logger_proxy.rb +49 -0
- data/lib/moonshot/resources.rb +13 -0
- data/lib/moonshot/resources_helper.rb +24 -0
- data/lib/moonshot/shell.rb +52 -0
- data/lib/moonshot/stack.rb +345 -0
- data/lib/moonshot/stack_asg_printer.rb +151 -0
- data/lib/moonshot/stack_config.rb +12 -0
- data/lib/moonshot/stack_events_poller.rb +56 -0
- data/lib/moonshot/stack_lister.rb +20 -0
- data/lib/moonshot/stack_output_printer.rb +16 -0
- data/lib/moonshot/stack_parameter_printer.rb +73 -0
- data/lib/moonshot/stack_template.rb +35 -0
- data/lib/moonshot/unicode_table.rb +63 -0
- data/lib/moonshot.rb +41 -0
- metadata +239 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
# Mixin providing the Thor::Shell methods and other shell execution helpers.
|
2
|
+
module Moonshot::Shell
|
3
|
+
# Run a command, returning stdout. Stderr is suppressed unless the command
|
4
|
+
# returns non-zero.
|
5
|
+
def sh_out(cmd, fail: true, stdin: '') # rubocop:disable AbcSize
|
6
|
+
r_in, w_in = IO.pipe
|
7
|
+
r_out, w_out = IO.pipe
|
8
|
+
r_err, w_err = IO.pipe
|
9
|
+
w_in.write(stdin)
|
10
|
+
w_in.close
|
11
|
+
pid = Process.spawn(cmd, in: r_in, out: w_out, err: w_err)
|
12
|
+
Process.wait(pid)
|
13
|
+
|
14
|
+
r_in.close
|
15
|
+
w_out.close
|
16
|
+
w_err.close
|
17
|
+
stdout = r_out.read
|
18
|
+
r_out.close
|
19
|
+
stderr = r_err.read
|
20
|
+
r_err.close
|
21
|
+
|
22
|
+
if fail && $CHILD_STATUS.exitstatus != 0
|
23
|
+
raise "`#{cmd}` exited #{$CHILD_STATUS.exitstatus}\n" \
|
24
|
+
"stdout:\n" \
|
25
|
+
"#{stdout}\n" \
|
26
|
+
"stderr:\n" \
|
27
|
+
"#{stderr}\n"
|
28
|
+
end
|
29
|
+
stdout
|
30
|
+
end
|
31
|
+
module_function :sh_out
|
32
|
+
|
33
|
+
def shell
|
34
|
+
@thor_shell ||= Thor::Base.shell.new
|
35
|
+
end
|
36
|
+
|
37
|
+
Thor::Shell::Basic.public_instance_methods(false).each do |meth|
|
38
|
+
define_method(meth) { |*args| shell.public_send(meth, *args) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def sh_step(cmd, args = {})
|
42
|
+
msg = args.delete(:msg) || cmd
|
43
|
+
if msg.length > (terminal_width - 18)
|
44
|
+
msg = "#{msg[0..(terminal_width - 22)]}..."
|
45
|
+
end
|
46
|
+
ilog.start_threaded(msg) do |step|
|
47
|
+
out = sh_out(cmd, args)
|
48
|
+
yield step, out if block_given?
|
49
|
+
step.success
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,345 @@
|
|
1
|
+
require_relative 'creds_helper'
|
2
|
+
require_relative 'doctor_helper'
|
3
|
+
|
4
|
+
require_relative 'stack_template'
|
5
|
+
require_relative 'stack_parameter_printer'
|
6
|
+
require_relative 'stack_output_printer'
|
7
|
+
require_relative 'stack_asg_printer'
|
8
|
+
require_relative 'unicode_table'
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
module Moonshot
|
12
|
+
# The Stack wraps all CloudFormation actions performed by Moonshot. It
|
13
|
+
# stores the state of the active stack running on AWS, but contains a
|
14
|
+
# reference to the StackTemplate that would be applied with an update
|
15
|
+
# action.
|
16
|
+
class Stack # rubocop:disable ClassLength
|
17
|
+
include CredsHelper
|
18
|
+
include DoctorHelper
|
19
|
+
|
20
|
+
attr_reader :app_name
|
21
|
+
attr_reader :name
|
22
|
+
|
23
|
+
# TODO: Refactor more of these parameters into the config object.
|
24
|
+
def initialize(name, app_name:, log:, ilog:, config: StackConfig.new)
|
25
|
+
@name = name
|
26
|
+
@app_name = app_name
|
27
|
+
@log = log
|
28
|
+
@ilog = ilog
|
29
|
+
@config = config
|
30
|
+
yield @config if block_given?
|
31
|
+
end
|
32
|
+
|
33
|
+
def create
|
34
|
+
import_parent_parameters
|
35
|
+
|
36
|
+
should_wait = true
|
37
|
+
@ilog.start "Creating #{stack_name}." do |s|
|
38
|
+
if stack_exists?
|
39
|
+
s.success "#{stack_name} already exists."
|
40
|
+
should_wait = false
|
41
|
+
else
|
42
|
+
create_stack
|
43
|
+
s.success "Created #{stack_name}."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
should_wait ? wait_for_stack_state(:stack_create_complete, 'created') : true
|
48
|
+
end
|
49
|
+
|
50
|
+
def update
|
51
|
+
raise Thor::Error, "No stack found #{@name.blue}!" unless stack_exists?
|
52
|
+
|
53
|
+
should_wait = true
|
54
|
+
@ilog.start "Updating #{stack_name}." do |s|
|
55
|
+
if update_stack
|
56
|
+
s.success "Initiated update for #{stack_name}."
|
57
|
+
else
|
58
|
+
s.success 'No Stack update required.'
|
59
|
+
should_wait = false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
should_wait ? wait_for_stack_state(:stack_update_complete, 'updated') : true
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete
|
67
|
+
should_wait = true
|
68
|
+
@ilog.start "Deleting #{stack_name}." do |s|
|
69
|
+
if stack_exists?
|
70
|
+
cf_client.delete_stack(stack_name: @name)
|
71
|
+
s.success "Initiated deletion of #{stack_name}."
|
72
|
+
else
|
73
|
+
s.success "#{stack_name} does not exist."
|
74
|
+
should_wait = false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
should_wait ? wait_for_stack_state(:stack_delete_complete, 'deleted') : true
|
79
|
+
end
|
80
|
+
|
81
|
+
def status
|
82
|
+
if exists?
|
83
|
+
puts "#{stack_name} exists."
|
84
|
+
t = UnicodeTable.new('')
|
85
|
+
StackParameterPrinter.new(self, t).print
|
86
|
+
StackOutputPrinter.new(self, t).print
|
87
|
+
StackASGPrinter.new(self, t).print
|
88
|
+
t.draw_children
|
89
|
+
else
|
90
|
+
puts "#{stack_name} does NOT exist."
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def parameters
|
95
|
+
get_stack(@name)
|
96
|
+
.parameters
|
97
|
+
.map { |p| [p.parameter_key, p.parameter_value] }
|
98
|
+
.to_h
|
99
|
+
end
|
100
|
+
|
101
|
+
def outputs
|
102
|
+
get_stack(@name)
|
103
|
+
.outputs
|
104
|
+
.map { |o| [o.output_key, o.output_value] }
|
105
|
+
.to_h
|
106
|
+
end
|
107
|
+
|
108
|
+
def exists?
|
109
|
+
cf_client.describe_stacks(stack_name: @name)
|
110
|
+
true
|
111
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
112
|
+
false
|
113
|
+
end
|
114
|
+
alias stack_exists? exists?
|
115
|
+
|
116
|
+
def resource_summaries
|
117
|
+
cf_client.list_stack_resources(stack_name: @name).stack_resource_summaries
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [String, nil]
|
121
|
+
def physical_id_for(logical_id)
|
122
|
+
resource_summary = resource_summaries.find do |r|
|
123
|
+
r.logical_resource_id == logical_id
|
124
|
+
end
|
125
|
+
resource_summary.physical_resource_id if resource_summary
|
126
|
+
end
|
127
|
+
|
128
|
+
# @return [Array<Aws::CloudFormation::Types::StackResourceSummary>]
|
129
|
+
def resources_of_type(type)
|
130
|
+
resource_summaries.select do |r|
|
131
|
+
r.resource_type == type
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Build a hash of overrides that would be applied to this stack by an
|
136
|
+
# update.
|
137
|
+
def overrides
|
138
|
+
if File.exist?(parameters_file)
|
139
|
+
YAML.load_file(parameters_file)
|
140
|
+
else
|
141
|
+
{}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Return a Hash of the default values defined in the stack template.
|
146
|
+
def default_values
|
147
|
+
h = {}
|
148
|
+
JSON.parse(template.body).fetch('Parameters', {}).map do |k, v|
|
149
|
+
h[k] = v['Default']
|
150
|
+
end
|
151
|
+
h
|
152
|
+
end
|
153
|
+
|
154
|
+
def template
|
155
|
+
@template ||= StackTemplate.new(template_file, log: @log)
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [String] the path to the template file.
|
159
|
+
def template_file
|
160
|
+
File.join(Dir.pwd, 'cloud_formation', "#{@app_name}.json")
|
161
|
+
end
|
162
|
+
|
163
|
+
# @return [String] the path to the parameters file.
|
164
|
+
def parameters_file
|
165
|
+
File.join(Dir.pwd, 'cloud_formation', 'parameters', "#{@name}.yml")
|
166
|
+
end
|
167
|
+
|
168
|
+
def add_parameter_overrides(hash)
|
169
|
+
new_overrides = hash.merge(overrides)
|
170
|
+
File.open(parameters_file, 'w') do |f|
|
171
|
+
YAML.dump(new_overrides, f)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def stack_name
|
178
|
+
"CloudFormation Stack #{@name.blue}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def load_parameters_file
|
182
|
+
@ilog.msg "Loading stack parameters file '#{parameters_file}'."
|
183
|
+
result = stack_parameter_overrides
|
184
|
+
|
185
|
+
if result.empty?
|
186
|
+
@ilog.msg "No parameters file for #{@name.blue}, using defaults."
|
187
|
+
return result
|
188
|
+
end
|
189
|
+
|
190
|
+
@ilog.msg 'Setting stack parameter overrides:'
|
191
|
+
result.each do |e|
|
192
|
+
@ilog.msg " #{e[:parameter_key]}: #{e[:parameter_value]}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def stack_parameter_overrides
|
197
|
+
overrides.map do |k, v|
|
198
|
+
{ parameter_key: k, parameter_value: v.to_s }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def stack_parameters
|
203
|
+
@stack_parameters ||= JSON.parse(template.body).fetch('Parameters', {}).keys
|
204
|
+
end
|
205
|
+
|
206
|
+
def import_parent_parameters
|
207
|
+
add_parameter_overrides(parent_stack_outputs)
|
208
|
+
end
|
209
|
+
|
210
|
+
# Return a Hash of parent stack outputs that match parameter names for this
|
211
|
+
# stack.
|
212
|
+
def parent_stack_outputs
|
213
|
+
result = {}
|
214
|
+
|
215
|
+
@config.parent_stacks.each do |stack_name|
|
216
|
+
resp = cf_client.describe_stacks(stack_name: stack_name)
|
217
|
+
raise "Parent Stack #{stack_name} not found!" unless resp.stacks.size == 1
|
218
|
+
|
219
|
+
# If there is an input parameters matching a stack output, pass it.
|
220
|
+
resp.stacks[0].outputs.each do |output|
|
221
|
+
if stack_parameters.include?(output.output_key)
|
222
|
+
result[output.output_key] = output.output_value
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
result
|
228
|
+
end
|
229
|
+
|
230
|
+
# @return [Aws::CloudFormation::Types::Stack]
|
231
|
+
def get_stack(name)
|
232
|
+
stacks = cf_client.describe_stacks(stack_name: name).stacks
|
233
|
+
raise Thor::Error, "Could not describe stack: #{name}" if stacks.empty?
|
234
|
+
|
235
|
+
stacks.first
|
236
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
237
|
+
raise Thor::Error, "Could not describe stack: #{name}"
|
238
|
+
end
|
239
|
+
|
240
|
+
def create_stack
|
241
|
+
cf_client.create_stack(
|
242
|
+
stack_name: @name,
|
243
|
+
template_body: template.body,
|
244
|
+
capabilities: ['CAPABILITY_IAM'],
|
245
|
+
parameters: load_parameters_file,
|
246
|
+
tags: [
|
247
|
+
{ key: 'ah_stage', value: @name }
|
248
|
+
]
|
249
|
+
)
|
250
|
+
rescue Aws::CloudFormation::Errors::AccessDenied
|
251
|
+
raise Thor::Error, 'You are not authorized to perform create_stack calls.'
|
252
|
+
end
|
253
|
+
|
254
|
+
# @return [Boolean]
|
255
|
+
# true if a stack update was required and initiated, false otherwise.
|
256
|
+
def update_stack
|
257
|
+
cf_client.update_stack(
|
258
|
+
stack_name: @name,
|
259
|
+
template_body: template.body,
|
260
|
+
capabilities: ['CAPABILITY_IAM'],
|
261
|
+
parameters: stack_parameter_overrides
|
262
|
+
)
|
263
|
+
true
|
264
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
265
|
+
raise Thor::Error, e.message unless
|
266
|
+
e.message == 'No updates are to be performed.'
|
267
|
+
false
|
268
|
+
end
|
269
|
+
|
270
|
+
# TODO: Refactor this into it's own class.
|
271
|
+
def wait_for_stack_state(wait_target, past_tense_verb) # rubocop:disable AbcSize
|
272
|
+
result = true
|
273
|
+
|
274
|
+
stack_id = get_stack(@name).stack_id
|
275
|
+
|
276
|
+
events = StackEventsPoller.new(cf_client, stack_id)
|
277
|
+
events.show_only_errors unless @config.show_all_events
|
278
|
+
|
279
|
+
@ilog.start_threaded "Waiting for #{stack_name} to be successfully #{past_tense_verb}." do |s|
|
280
|
+
begin
|
281
|
+
cf_client.wait_until(wait_target, stack_name: stack_id) do |w|
|
282
|
+
w.delay = 10
|
283
|
+
w.max_attempts = 180 # 30 minutes.
|
284
|
+
w.before_wait do |attempt, resp|
|
285
|
+
begin
|
286
|
+
events.latest_events.each { |e| @ilog.error(format_event(e)) }
|
287
|
+
# rubocop:disable Lint/HandleExceptions
|
288
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
289
|
+
# Do nothing. The above event logging block may result in
|
290
|
+
# a ValidationError while waiting for a stack to delete.
|
291
|
+
end
|
292
|
+
# rubocop:enable Lint/HandleExceptions
|
293
|
+
|
294
|
+
if attempt == w.max_attempts - 1
|
295
|
+
s.failure "#{stack_name} was not #{past_tense_verb} after 30 minutes."
|
296
|
+
result = false
|
297
|
+
|
298
|
+
# We don't want the interactive logger to catch an exception.
|
299
|
+
throw :success
|
300
|
+
end
|
301
|
+
s.continue "Waiting for CloudFormation Stack to be successfully #{past_tense_verb}, current status '#{resp.stacks.first.stack_status}'." # rubocop:disable LineLength
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
s.success "#{stack_name} successfully #{past_tense_verb}." if result
|
306
|
+
rescue Aws::Waiters::Errors::FailureStateError
|
307
|
+
result = false
|
308
|
+
s.failure "#{stack_name} failed to update."
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
result
|
313
|
+
end
|
314
|
+
|
315
|
+
def format_event(event)
|
316
|
+
str = case event.resource_status
|
317
|
+
when /FAILED/
|
318
|
+
event.resource_status.red
|
319
|
+
when /IN_PROGRESS/
|
320
|
+
event.resource_status.yellow
|
321
|
+
else
|
322
|
+
event.resource_status.green
|
323
|
+
end
|
324
|
+
str << " #{event.logical_resource_id}"
|
325
|
+
str << " #{event.resource_status_reason.light_black}" if event.resource_status_reason
|
326
|
+
|
327
|
+
str
|
328
|
+
end
|
329
|
+
|
330
|
+
def doctor_check_template_exists
|
331
|
+
if File.exist?(template_file)
|
332
|
+
success "CloudFormation template found at '#{template_file}'."
|
333
|
+
else
|
334
|
+
critical "CloudFormation template not found at '#{template_file}'!"
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def doctor_check_template_against_aws
|
339
|
+
cf_client.validate_template(template_body: template.body)
|
340
|
+
success('CloudFormation template is valid.')
|
341
|
+
rescue => e
|
342
|
+
critical('Invalid CloudFormation template!', e.message)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'colorize'
|
3
|
+
require 'ruby-duration'
|
4
|
+
|
5
|
+
module Moonshot
|
6
|
+
# Display information about the AutoScaling Groups, associated ELBs, and
|
7
|
+
# managed instances to the user.
|
8
|
+
class StackASGPrinter # rubocop:disable ClassLength
|
9
|
+
include CredsHelper
|
10
|
+
|
11
|
+
def initialize(stack, table)
|
12
|
+
@stack = stack
|
13
|
+
@table = table
|
14
|
+
end
|
15
|
+
|
16
|
+
def print
|
17
|
+
asgs.each do |asg|
|
18
|
+
asg_info = as_client.describe_auto_scaling_groups(
|
19
|
+
auto_scaling_group_names: [asg.physical_resource_id])
|
20
|
+
.auto_scaling_groups.first
|
21
|
+
t_asg_info = @table.add_leaf("ASG: #{asg.logical_resource_id}")
|
22
|
+
|
23
|
+
add_asg_info(t_asg_info, asg_info)
|
24
|
+
instances_leaf = t_asg_info.add_leaf('Instances')
|
25
|
+
|
26
|
+
if asg_info.instances.empty?
|
27
|
+
instances_leaf.add_line('There are no instances in this Auto-Scaling Group.')
|
28
|
+
else
|
29
|
+
instances_leaf.add_table(create_instance_table(asg_info))
|
30
|
+
end
|
31
|
+
|
32
|
+
add_recent_activity_leaf(t_asg_info, asg.physical_resource_id)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def asgs
|
39
|
+
@stack.resources_of_type('AWS::AutoScaling::AutoScalingGroup')
|
40
|
+
end
|
41
|
+
|
42
|
+
def status_with_color(status)
|
43
|
+
case status
|
44
|
+
when 'Successful'
|
45
|
+
status.green
|
46
|
+
when 'Failed'
|
47
|
+
status.red
|
48
|
+
else
|
49
|
+
status.yellow
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def lifecycle_color(lifecycle)
|
54
|
+
case lifecycle
|
55
|
+
when 'InService'
|
56
|
+
lifecycle.green
|
57
|
+
else
|
58
|
+
lifecycle.red
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def health_color(health)
|
63
|
+
case health
|
64
|
+
when 'Healthy'
|
65
|
+
health.green
|
66
|
+
else
|
67
|
+
health.red
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get additional information about instances not returned by the ASG API.
|
72
|
+
def get_addl_info(instance_ids)
|
73
|
+
resp = ec2_client.describe_instances(instance_ids: instance_ids)
|
74
|
+
|
75
|
+
data = {}
|
76
|
+
resp.reservations.map(&:instances).flatten.each do |instance|
|
77
|
+
data[instance.instance_id] = instance
|
78
|
+
end
|
79
|
+
data
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_asg_info(table, asg_info) # rubocop:disable AbcSize
|
83
|
+
name = asg_info.auto_scaling_group_name.blue
|
84
|
+
table.add_line "Name: #{name}"
|
85
|
+
|
86
|
+
hc = asg_info.health_check_type.blue
|
87
|
+
gp = (asg_info.health_check_grace_period.to_s << 's').blue
|
88
|
+
table.add_line "Using #{hc} health checks, with a #{gp} health check grace period." # rubocop:disable LineLength
|
89
|
+
|
90
|
+
dc = asg_info.desired_capacity.to_s.blue
|
91
|
+
min = asg_info.min_size.to_s.blue
|
92
|
+
max = asg_info.max_size.to_s.blue
|
93
|
+
table.add_line "Desired Capacity is #{dc} (Min: #{min}, Max: #{max})."
|
94
|
+
|
95
|
+
lbs = asg_info.load_balancer_names
|
96
|
+
table.add_line "Has #{lbs.count.to_s.blue} Load Balancer(s): #{lbs.map(&:blue).join(', ')}" # rubocop:disable LineLength
|
97
|
+
end
|
98
|
+
|
99
|
+
def create_instance_table(asg_info)
|
100
|
+
current_lc = asg_info.launch_configuration_name
|
101
|
+
ec2_info = get_addl_info(asg_info.instances.map(&:instance_id))
|
102
|
+
asg_info.instances.map do |asg_instance|
|
103
|
+
row = instance_row(asg_instance,
|
104
|
+
ec2_info[asg_instance.instance_id])
|
105
|
+
row << if current_lc == asg_instance.launch_configuration_name
|
106
|
+
'(launch config up to date)'.green
|
107
|
+
else
|
108
|
+
'(launch config out of date)'.red
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def instance_row(asg_instance, ec2_instance)
|
114
|
+
[
|
115
|
+
asg_instance.instance_id,
|
116
|
+
# @todo What about ASGs with only private IPs?
|
117
|
+
ec2_instance.public_ip_address,
|
118
|
+
lifecycle_color(asg_instance.lifecycle_state),
|
119
|
+
health_color(asg_instance.health_status),
|
120
|
+
uptime_format(ec2_instance.launch_time)
|
121
|
+
]
|
122
|
+
end
|
123
|
+
|
124
|
+
def uptime_format(launch_time)
|
125
|
+
# %td is "total days", instead of counting up again to weeks.
|
126
|
+
Duration.new(Time.now.to_i - launch_time.to_i)
|
127
|
+
.format('%tdd %hh %mm %ss')
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_recent_activity_leaf(table, asg_name)
|
131
|
+
recent = table.add_leaf('Recent Activity')
|
132
|
+
resp = as_client.describe_scaling_activities(
|
133
|
+
auto_scaling_group_name: asg_name).activities
|
134
|
+
|
135
|
+
rows = resp.sort_by(&:start_time).reverse.first(10).map do |activity|
|
136
|
+
row_for_activity(activity)
|
137
|
+
end
|
138
|
+
|
139
|
+
recent.add_table(rows)
|
140
|
+
end
|
141
|
+
|
142
|
+
def row_for_activity(activity)
|
143
|
+
[
|
144
|
+
activity.start_time.to_s.light_black,
|
145
|
+
activity.description,
|
146
|
+
status_with_color(activity.status_code),
|
147
|
+
activity.progress.to_s << '%'
|
148
|
+
]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Moonshot
|
2
|
+
# The StackEventsPoller queries DescribeStackEvents every time #latest_events
|
3
|
+
# is invoked, filtering out events that have already been returned. It can
|
4
|
+
# also, optionally, filter all non-error events (@see #show_errors_only).
|
5
|
+
class StackEventsPoller
|
6
|
+
def initialize(cf_client, stack_name)
|
7
|
+
@cf_client = cf_client
|
8
|
+
@stack_name = stack_name
|
9
|
+
|
10
|
+
# Start showing events from now.
|
11
|
+
@last_time = Time.now
|
12
|
+
end
|
13
|
+
|
14
|
+
def show_only_errors
|
15
|
+
@errors_only = true
|
16
|
+
end
|
17
|
+
|
18
|
+
# Build a list of events that have occurred since the last call to this
|
19
|
+
# method.
|
20
|
+
#
|
21
|
+
# @return [Array<Aws::CloudFormation::Event>]
|
22
|
+
def latest_events
|
23
|
+
events = get_stack_events.select do |event|
|
24
|
+
event.timestamp > @last_time
|
25
|
+
end
|
26
|
+
|
27
|
+
@last_time = Time.now
|
28
|
+
|
29
|
+
filter_events(events.sort_by(&:timestamp))
|
30
|
+
end
|
31
|
+
|
32
|
+
def filter_events(events)
|
33
|
+
if @errors_only
|
34
|
+
events.select do |event|
|
35
|
+
%w(CREATE_FAILED UPDATE_FAILED DELETE_FAILED).include?(event.resource_status)
|
36
|
+
end
|
37
|
+
else
|
38
|
+
events
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_stack_events(token = nil)
|
43
|
+
opts = {
|
44
|
+
stack_name: @stack_name
|
45
|
+
}
|
46
|
+
|
47
|
+
opts[:next_token] = token if token
|
48
|
+
|
49
|
+
result = @cf_client.describe_stack_events(**opts)
|
50
|
+
events = result.stack_events
|
51
|
+
events += get_stack_events(result.next_token) if result.next_token
|
52
|
+
|
53
|
+
events
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Moonshot
|
2
|
+
# The StackLister is world renoun for it's ability to list stacks.
|
3
|
+
class StackLister
|
4
|
+
include CredsHelper
|
5
|
+
|
6
|
+
def initialize(app_name, log:)
|
7
|
+
@app_name = app_name
|
8
|
+
@log = log
|
9
|
+
end
|
10
|
+
|
11
|
+
def list
|
12
|
+
all_stacks = cf_client.describe_stacks.stacks
|
13
|
+
app_stacks = all_stacks.reject { |s| s.stack_name !~ /^#{@app_name}/ }
|
14
|
+
|
15
|
+
app_stacks.each do |stack|
|
16
|
+
puts stack.stack_name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Moonshot
|
2
|
+
# Display the stack outputs to the user.
|
3
|
+
class StackOutputPrinter
|
4
|
+
def initialize(stack, table)
|
5
|
+
@stack = stack
|
6
|
+
@table = table
|
7
|
+
end
|
8
|
+
|
9
|
+
def print
|
10
|
+
o_table = @table.add_leaf('Stack Outputs')
|
11
|
+
@stack.outputs.each do |k, v|
|
12
|
+
o_table.add_line("#{k}: #{v}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|