moonshot 0.7.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.
- 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
|