moonshot 0.7.6 → 0.7.7
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 +4 -4
- data/lib/moonshot/build_mechanism/github_release.rb +1 -1
- data/lib/moonshot/build_mechanism/script.rb +2 -4
- data/lib/moonshot/build_mechanism/travis_deploy.rb +43 -5
- data/lib/moonshot/cli.rb +2 -2
- data/lib/moonshot/controller.rb +10 -7
- data/lib/moonshot/controller_config.rb +4 -3
- data/lib/moonshot/deployment_mechanism/code_deploy.rb +85 -44
- data/lib/moonshot/shell.rb +1 -1
- data/lib/moonshot/ssh_command_builder.rb +32 -0
- data/lib/moonshot/ssh_config.rb +6 -0
- data/lib/moonshot/ssh_fork_executor.rb +20 -0
- data/lib/moonshot/ssh_target_selector.rb +30 -0
- data/lib/moonshot/stack.rb +0 -43
- data/lib/moonshot/stack_asg_printer.rb +2 -2
- data/lib/moonshot/stack_config.rb +0 -5
- data/lib/moonshot/tools/asg_rollout/asg.rb +164 -0
- data/lib/moonshot/tools/asg_rollout/asg_instance.rb +16 -0
- data/lib/moonshot/tools/asg_rollout/hook_exec_environment.rb +43 -0
- data/lib/moonshot/tools/asg_rollout/instance_health.rb +36 -0
- data/lib/moonshot/tools/asg_rollout.rb +170 -0
- data/lib/moonshot/tools/asg_rollout_config.rb +39 -0
- data/lib/moonshot.rb +4 -1
- data/lib/plugins/backup.rb +2 -2
- metadata +34 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a022cf1a54f893264b6ee7487303d79b8c29fb44
|
4
|
+
data.tar.gz: 7425456a02ec0451c431c6dc1863616fae9200e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8857003d9882c8fa70b559ba182887af90c5bd0b6cf2a62bc60e6bd4109c4e26e1f4ab6fd74bbc5764286cb592a1f70347659f0dfc79b8e13df104cc8356bc40
|
7
|
+
data.tar.gz: 36b9bb90372c8ae2bacf1253061d243c2333f57095fb49f2741de6d2bab602bfaed19652b2bcb86a22df67dc8e45af1ea583b0a760105dc8474c40a570dfa538
|
@@ -118,7 +118,7 @@ module Moonshot::BuildMechanism
|
|
118
118
|
end
|
119
119
|
|
120
120
|
def hub_create_release(semver, commitish, changelog_entry)
|
121
|
-
return if hub_release_exists(semver
|
121
|
+
return if hub_release_exists(semver)
|
122
122
|
|
123
123
|
message = "#{semver}\n\n#{changelog_entry}"
|
124
124
|
cmd = "hub release create #{semver} --commitish=#{commitish}"
|
@@ -47,7 +47,7 @@ class Moonshot::BuildMechanism::Script
|
|
47
47
|
|
48
48
|
private
|
49
49
|
|
50
|
-
def run_script(step, env: {})
|
50
|
+
def run_script(step, env: {})
|
51
51
|
popen2e(env, @script) do |_, out, wait|
|
52
52
|
output = []
|
53
53
|
|
@@ -66,9 +66,7 @@ class Moonshot::BuildMechanism::Script
|
|
66
66
|
end
|
67
67
|
unless result.exitstatus == 0
|
68
68
|
ilog.error "Build script failed with exit status #{result.exitstatus}!"
|
69
|
-
ilog.error
|
70
|
-
output.pop(10).each { |l| ilog.error l }
|
71
|
-
|
69
|
+
ilog.error output.join("\n")
|
72
70
|
step.failure "Build script #{@script} failed with exit status #{result.exitstatus}!"
|
73
71
|
end
|
74
72
|
end
|
@@ -1,4 +1,7 @@
|
|
1
1
|
require 'moonshot/shell'
|
2
|
+
require 'travis'
|
3
|
+
require 'travis/pro'
|
4
|
+
require 'travis/client/auto_login'
|
2
5
|
|
3
6
|
module Moonshot::BuildMechanism
|
4
7
|
# This simply waits for Travis-CI to finish building a job matching the
|
@@ -12,9 +15,13 @@ module Moonshot::BuildMechanism
|
|
12
15
|
|
13
16
|
attr_reader :output_file
|
14
17
|
|
15
|
-
def initialize(slug, pro: false)
|
18
|
+
def initialize(slug, pro: false, timeout: 900)
|
16
19
|
@slug = slug
|
20
|
+
@pro = pro
|
21
|
+
@timeout = timeout
|
22
|
+
|
17
23
|
@endpoint = pro ? '--pro' : '--org'
|
24
|
+
@travis_base = @pro ? Travis::Pro : Travis
|
18
25
|
@cli_args = "-r #{@slug} #{@endpoint}"
|
19
26
|
end
|
20
27
|
|
@@ -32,6 +39,18 @@ module Moonshot::BuildMechanism
|
|
32
39
|
|
33
40
|
private
|
34
41
|
|
42
|
+
# Authenticates with the proper travis service.
|
43
|
+
def authenticate
|
44
|
+
Travis::Client::AutoLogin.new(@travis_base).authenticate
|
45
|
+
end
|
46
|
+
|
47
|
+
# Retrieves the travis repository.
|
48
|
+
#
|
49
|
+
# @return [Travis::Client::Repository]
|
50
|
+
def repo
|
51
|
+
@repo ||= @travis_base::Repository.find(@slug)
|
52
|
+
end
|
53
|
+
|
35
54
|
def find_build_and_job(version)
|
36
55
|
job_number = nil
|
37
56
|
ilog.start_threaded('Find Travis CI build') do |step|
|
@@ -78,16 +97,35 @@ module Moonshot::BuildMechanism
|
|
78
97
|
job_number
|
79
98
|
end
|
80
99
|
|
100
|
+
# Waits for a job to complete, within the defined timeout.
|
101
|
+
#
|
102
|
+
# @param job_number [String] The job number to wait for.
|
81
103
|
def wait_for_job(job_number)
|
82
|
-
|
83
|
-
|
84
|
-
|
104
|
+
authenticate
|
105
|
+
|
106
|
+
# Wait for the job to complete or hit the timeout.
|
107
|
+
start = Time.new
|
108
|
+
job = repo.job(job_number)
|
109
|
+
ilog.start_threaded("Waiting for job #{job_number} to complete.") do |s|
|
110
|
+
while !job.finished? && Time.new - start < @timeout
|
111
|
+
s.continue("Job status: #{job.state}")
|
112
|
+
sleep 10
|
113
|
+
job.reload
|
114
|
+
end
|
115
|
+
|
116
|
+
if job.finished?
|
117
|
+
s.success
|
118
|
+
else
|
119
|
+
s.failure("Job #{job_number} did not complete within time limit of " \
|
120
|
+
"#{@timeout} seconds")
|
121
|
+
end
|
122
|
+
end
|
85
123
|
end
|
86
124
|
|
87
125
|
def check_build(version)
|
88
126
|
cmd = "bundle exec travis show #{@cli_args} #{version}"
|
89
127
|
sh_step(cmd) do |step, out|
|
90
|
-
raise "Build didn't pass.\n#{
|
128
|
+
raise "Build didn't pass.\n#{out}" \
|
91
129
|
if out =~ /^#(\d+\.\d+) (?!passed).+BUILD=1.+/
|
92
130
|
|
93
131
|
step.success("Travis CI build for #{version} passed.")
|
data/lib/moonshot/cli.rb
CHANGED
@@ -110,8 +110,8 @@ module Moonshot
|
|
110
110
|
config.parameter_strategy = parameter_strategy_factory(parameter_strategy) \
|
111
111
|
unless parameter_strategy.nil?
|
112
112
|
|
113
|
-
config.ssh_user = options[:user]
|
114
|
-
config.ssh_identity_file = options[:identity_file]
|
113
|
+
config.ssh_config.ssh_user = options[:user]
|
114
|
+
config.ssh_config.ssh_identity_file = options[:identity_file]
|
115
115
|
config.ssh_instance = options[:instance]
|
116
116
|
config.ssh_command = options[:command]
|
117
117
|
config.ssh_auto_scaling_group_name = options[:auto_scaling_group]
|
data/lib/moonshot/controller.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require_relative 'ssh_target_selector'
|
2
|
+
require_relative 'ssh_command_builder'
|
3
|
+
|
1
4
|
module Moonshot
|
2
5
|
# The Controller coordinates and performs all Moonshot actions.
|
3
6
|
class Controller # rubocop:disable ClassLength
|
@@ -82,8 +85,13 @@ module Moonshot
|
|
82
85
|
|
83
86
|
def ssh
|
84
87
|
run_plugins(:pre_ssh)
|
85
|
-
|
86
|
-
|
88
|
+
@config.ssh_instance ||= SSHTargetSelector.new(
|
89
|
+
stack, asg_name: @config.ssh_auto_scaling_group_name).choose!
|
90
|
+
cb = SSHCommandBuilder.new(@config.ssh_config, @config.ssh_instance)
|
91
|
+
result = cb.build(@config.ssh_command)
|
92
|
+
|
93
|
+
puts "Opening SSH connection to #{@config.ssh_instance} (#{result.ip})..."
|
94
|
+
exec(result.cmd)
|
87
95
|
end
|
88
96
|
|
89
97
|
def stack
|
@@ -94,11 +102,6 @@ module Moonshot
|
|
94
102
|
config.parent_stacks = @config.parent_stacks
|
95
103
|
config.show_all_events = @config.show_all_stack_events
|
96
104
|
config.parameter_strategy = @config.parameter_strategy
|
97
|
-
config.ssh_user = @config.ssh_user
|
98
|
-
config.ssh_identity_file = @config.ssh_identity_file
|
99
|
-
config.ssh_instance = @config.ssh_instance
|
100
|
-
config.ssh_command = @config.ssh_command
|
101
|
-
config.ssh_auto_scaling_group_name = @config.ssh_auto_scaling_group_name
|
102
105
|
end
|
103
106
|
end
|
104
107
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'default_strategy'
|
2
|
+
require_relative 'ssh_config'
|
2
3
|
|
3
4
|
module Moonshot
|
4
5
|
# Holds configuration for Moonshot::Controller
|
@@ -15,11 +16,10 @@ module Moonshot
|
|
15
16
|
attr_accessor :plugins
|
16
17
|
attr_accessor :show_all_stack_events
|
17
18
|
attr_accessor :parameter_strategy
|
18
|
-
attr_accessor :
|
19
|
-
attr_accessor :ssh_identity_file
|
20
|
-
attr_accessor :ssh_user
|
19
|
+
attr_accessor :ssh_config
|
21
20
|
attr_accessor :ssh_command
|
22
21
|
attr_accessor :ssh_auto_scaling_group_name
|
22
|
+
attr_accessor :ssh_instance
|
23
23
|
|
24
24
|
def initialize
|
25
25
|
@auto_prefix_stack = true
|
@@ -29,6 +29,7 @@ module Moonshot
|
|
29
29
|
@plugins = []
|
30
30
|
@show_all_stack_events = false
|
31
31
|
@parameter_strategy = Moonshot::ParameterStrategy::DefaultStrategy.new
|
32
|
+
@ssh_config = SSHConfig.new
|
32
33
|
end
|
33
34
|
end
|
34
35
|
end
|
@@ -13,20 +13,33 @@ class Moonshot::DeploymentMechanism::CodeDeploy # rubocop:disable ClassLength
|
|
13
13
|
include Moonshot::CredsHelper
|
14
14
|
include Moonshot::DoctorHelper
|
15
15
|
|
16
|
-
# @param asg [String]
|
16
|
+
# @param asg [Array, String]
|
17
17
|
# The logical name of the AutoScalingGroup to create and manage a Deployment
|
18
18
|
# Group for in CodeDeploy.
|
19
19
|
# @param role [String]
|
20
20
|
# IAM role with AWSCodeDeployRole policy. CodeDeployRole is considered as
|
21
21
|
# default role if its not specified.
|
22
22
|
# @param app_name [String, nil] (nil)
|
23
|
-
# The name of the CodeDeploy Application
|
24
|
-
#
|
25
|
-
#
|
26
|
-
|
27
|
-
|
23
|
+
# The name of the CodeDeploy Application. By default, this is the same as
|
24
|
+
# the stack name, and probably what you want. If you have multiple
|
25
|
+
# deployments in a single Stack, they must have unique names.
|
26
|
+
# @param group_name [String, nil] (nil)
|
27
|
+
# The name of the CodeDeploy Deployment Group. By default, this is the same
|
28
|
+
# as app_name.
|
29
|
+
# @param config_name [String]
|
30
|
+
# Name of the Deployment Config to use for CodeDeploy, By default we use
|
31
|
+
# CodeDeployDefault.OneAtATime.
|
32
|
+
def initialize(
|
33
|
+
asg: [],
|
34
|
+
role: 'CodeDeployRole',
|
35
|
+
app_name: nil,
|
36
|
+
group_name: nil,
|
37
|
+
config_name: 'CodeDeployDefault.OneAtATime')
|
38
|
+
@asg_logical_ids = asg.is_a?(Array) ? asg : [asg]
|
28
39
|
@app_name = app_name
|
40
|
+
@group_name = group_name
|
29
41
|
@codedeploy_role = role
|
42
|
+
@codedeploy_config = config_name
|
30
43
|
end
|
31
44
|
|
32
45
|
def post_create_hook
|
@@ -56,9 +69,9 @@ class Moonshot::DeploymentMechanism::CodeDeploy # rubocop:disable ClassLength
|
|
56
69
|
ilog.start_threaded 'Creating Deployment' do |s|
|
57
70
|
res = cd_client.create_deployment(
|
58
71
|
application_name: app_name,
|
59
|
-
deployment_group_name:
|
72
|
+
deployment_group_name: group_name,
|
60
73
|
revision: revision_for_artifact_repo(artifact_repo, version_name),
|
61
|
-
deployment_config_name:
|
74
|
+
deployment_config_name: @codedeploy_config,
|
62
75
|
description: "Deploying version #{version_name}"
|
63
76
|
)
|
64
77
|
deployment_id = res.deployment_id
|
@@ -80,12 +93,18 @@ class Moonshot::DeploymentMechanism::CodeDeploy # rubocop:disable ClassLength
|
|
80
93
|
|
81
94
|
private
|
82
95
|
|
83
|
-
# By default, use the stack name as the application
|
84
|
-
#
|
96
|
+
# By default, use the stack name as the application name, unless one has been
|
97
|
+
# provided.
|
85
98
|
def app_name
|
86
99
|
@app_name || stack.name
|
87
100
|
end
|
88
101
|
|
102
|
+
# By default, use the stack name as the deployment group name, unless one has
|
103
|
+
# been provided.
|
104
|
+
def group_name
|
105
|
+
@group_name || stack.name
|
106
|
+
end
|
107
|
+
|
89
108
|
def pretty_app_name
|
90
109
|
"CodeDeploy Application #{app_name.blue}"
|
91
110
|
end
|
@@ -131,27 +150,35 @@ class Moonshot::DeploymentMechanism::CodeDeploy # rubocop:disable ClassLength
|
|
131
150
|
end
|
132
151
|
end
|
133
152
|
|
134
|
-
def
|
135
|
-
@
|
153
|
+
def auto_scaling_groups
|
154
|
+
@auto_scaling_groups ||= load_auto_scaling_groups
|
136
155
|
end
|
137
156
|
|
138
|
-
def
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
157
|
+
def load_auto_scaling_groups
|
158
|
+
autoscaling_groups = []
|
159
|
+
@asg_logical_ids.each do |asg_logical_id|
|
160
|
+
asg_name = stack.physical_id_for(asg_logical_id)
|
161
|
+
unless asg_name
|
162
|
+
raise Thor::Error, "Could not find #{asg_logical_id} resource in Stack."
|
163
|
+
end
|
143
164
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
165
|
+
groups = as_client.describe_auto_scaling_groups(
|
166
|
+
auto_scaling_group_names: [asg_name])
|
167
|
+
if groups.auto_scaling_groups.empty?
|
168
|
+
raise Thor::Error, "Could not find ASG #{asg_name}."
|
169
|
+
end
|
149
170
|
|
150
|
-
|
171
|
+
autoscaling_groups.push(groups.auto_scaling_groups.first)
|
172
|
+
end
|
173
|
+
autoscaling_groups
|
151
174
|
end
|
152
175
|
|
153
|
-
def
|
154
|
-
|
176
|
+
def asg_names
|
177
|
+
names = []
|
178
|
+
auto_scaling_groups.each do |auto_scaling_group|
|
179
|
+
names.push(auto_scaling_group.auto_scaling_group_name)
|
180
|
+
end
|
181
|
+
names
|
155
182
|
end
|
156
183
|
|
157
184
|
def application_exists?
|
@@ -163,13 +190,13 @@ class Moonshot::DeploymentMechanism::CodeDeploy # rubocop:disable ClassLength
|
|
163
190
|
|
164
191
|
def deployment_group
|
165
192
|
cd_client.get_deployment_group(
|
166
|
-
application_name: app_name, deployment_group_name:
|
193
|
+
application_name: app_name, deployment_group_name: group_name)
|
167
194
|
.deployment_group_info
|
168
195
|
end
|
169
196
|
|
170
197
|
def deployment_group_exists?
|
171
198
|
cd_client.get_deployment_group(
|
172
|
-
application_name: app_name, deployment_group_name:
|
199
|
+
application_name: app_name, deployment_group_name: group_name)
|
173
200
|
true
|
174
201
|
rescue Aws::CodeDeploy::Errors::ApplicationDoesNotExistException,
|
175
202
|
Aws::CodeDeploy::Errors::DeploymentGroupDoesNotExistException
|
@@ -178,9 +205,15 @@ class Moonshot::DeploymentMechanism::CodeDeploy # rubocop:disable ClassLength
|
|
178
205
|
|
179
206
|
def deployment_group_ok?
|
180
207
|
return false unless deployment_group_exists?
|
181
|
-
|
182
|
-
return false unless
|
183
|
-
|
208
|
+
asgs = deployment_group.auto_scaling_groups
|
209
|
+
return false unless asgs
|
210
|
+
return false unless asgs.count == auto_scaling_groups.count
|
211
|
+
asgs.each do |asg|
|
212
|
+
if (auto_scaling_groups.find_index { |a| a.auto_scaling_group_name == asg.name }).nil?
|
213
|
+
return false
|
214
|
+
end
|
215
|
+
end
|
216
|
+
true
|
184
217
|
end
|
185
218
|
|
186
219
|
def role
|
@@ -193,7 +226,7 @@ class Moonshot::DeploymentMechanism::CodeDeploy # rubocop:disable ClassLength
|
|
193
226
|
ilog.start "Deleting #{pretty_deploy_group}." do |s|
|
194
227
|
cd_client.delete_deployment_group(
|
195
228
|
application_name: app_name,
|
196
|
-
deployment_group_name:
|
229
|
+
deployment_group_name: group_name)
|
197
230
|
s.success
|
198
231
|
end
|
199
232
|
end
|
@@ -201,22 +234,28 @@ class Moonshot::DeploymentMechanism::CodeDeploy # rubocop:disable ClassLength
|
|
201
234
|
def create_deployment_group
|
202
235
|
cd_client.create_deployment_group(
|
203
236
|
application_name: app_name,
|
204
|
-
deployment_group_name:
|
237
|
+
deployment_group_name: group_name,
|
205
238
|
service_role_arn: role.arn,
|
206
|
-
auto_scaling_groups:
|
239
|
+
auto_scaling_groups: asg_names)
|
207
240
|
end
|
208
241
|
|
209
242
|
def wait_for_asg_capacity
|
210
|
-
ilog.start 'Waiting for AutoScaling Group to reach capacity...' do |s|
|
243
|
+
ilog.start 'Waiting for AutoScaling Group(s) to reach capacity...' do |s|
|
211
244
|
loop do
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
245
|
+
asgs_at_capacity = 0
|
246
|
+
asgs = load_auto_scaling_groups
|
247
|
+
asgs.each do |asg|
|
248
|
+
count = asg.instances.count { |i| i.lifecycle_state == 'InService' }
|
249
|
+
if asg.desired_capacity == count
|
250
|
+
asgs_at_capacity += 1
|
251
|
+
s.continue "#{asg.auto_scaling_group_name} DesiredCapacity is #{asg.desired_capacity}, currently #{count} instance(s) are InService." # rubocop:disable LineLength
|
252
|
+
end
|
253
|
+
end
|
254
|
+
break if asgs.count == asgs_at_capacity
|
216
255
|
sleep 5
|
217
256
|
end
|
218
257
|
|
219
|
-
s.success 'AutoScaling Group up to capacity!'
|
258
|
+
s.success 'AutoScaling Group(s) up to capacity!'
|
220
259
|
end
|
221
260
|
end
|
222
261
|
|
@@ -239,7 +278,7 @@ class Moonshot::DeploymentMechanism::CodeDeploy # rubocop:disable ClassLength
|
|
239
278
|
end
|
240
279
|
end
|
241
280
|
|
242
|
-
def handle_deployment_failure(deployment_id)
|
281
|
+
def handle_deployment_failure(deployment_id)
|
243
282
|
instances = cd_client.list_deployment_instances(deployment_id: deployment_id)
|
244
283
|
.instances_list.map do |instance_id|
|
245
284
|
cd_client.get_deployment_instance(deployment_id: deployment_id,
|
@@ -298,10 +337,12 @@ http://docs.aws.amazon.com/codedeploy/latest/userguide/how-to-create-service-rol
|
|
298
337
|
end
|
299
338
|
|
300
339
|
def doctor_check_auto_scaling_resource_defined
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
340
|
+
@asg_logical_ids.each do |asg_logical_id|
|
341
|
+
if stack.template.resource_names.include?(asg_logical_id)
|
342
|
+
success("Resource '#{asg_logical_id}' exists in the CloudFormation template.") # rubocop:disable LineLength
|
343
|
+
else
|
344
|
+
critical("Resource '#{asg_logical_id}' does not exist in the CloudFormation template!") # rubocop:disable LineLength
|
345
|
+
end
|
305
346
|
end
|
306
347
|
end
|
307
348
|
end
|
data/lib/moonshot/shell.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
module Moonshot::Shell
|
3
3
|
# Run a command, returning stdout. Stderr is suppressed unless the command
|
4
4
|
# returns non-zero.
|
5
|
-
def sh_out(cmd, fail: true, stdin: '')
|
5
|
+
def sh_out(cmd, fail: true, stdin: '')
|
6
6
|
r_in, w_in = IO.pipe
|
7
7
|
r_out, w_out = IO.pipe
|
8
8
|
r_err, w_err = IO.pipe
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module Moonshot
|
4
|
+
# Create an ssh command from configuration.
|
5
|
+
class SSHCommandBuilder
|
6
|
+
Result = Struct.new(:cmd, :ip)
|
7
|
+
|
8
|
+
def initialize(ssh_config, instance_id)
|
9
|
+
@config = ssh_config
|
10
|
+
@instance_id = instance_id
|
11
|
+
end
|
12
|
+
|
13
|
+
def build(command = nil)
|
14
|
+
cmd = ['ssh', '-t']
|
15
|
+
cmd << "-i #{@config.ssh_identity_file}" if @config.ssh_identity_file
|
16
|
+
cmd << "-l #{@config.ssh_user}" if @config.ssh_user
|
17
|
+
cmd << instance_ip
|
18
|
+
cmd << Shellwords.escape(command) if command
|
19
|
+
Result.new(cmd.join(' '), instance_ip)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def instance_ip
|
25
|
+
@instance_ip ||= Aws::EC2::Client.new
|
26
|
+
.describe_instances(instance_ids: [@instance_id])
|
27
|
+
.reservations.first.instances.first.public_ip_address
|
28
|
+
rescue
|
29
|
+
raise "Failed to determine public IP address for instance #{@instance_id}!"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Moonshot
|
4
|
+
# Run an SSH command via fork/exec.
|
5
|
+
class SSHForkExecutor
|
6
|
+
Result = Struct.new(:output, :exitstatus)
|
7
|
+
|
8
|
+
def run(cmd)
|
9
|
+
output = StringIO.new
|
10
|
+
|
11
|
+
exit_status = nil
|
12
|
+
Open3.popen3(cmd) do |_, stdout, _, wt|
|
13
|
+
output << stdout.read until stdout.eof?
|
14
|
+
exit_status = wt.value.exitstatus
|
15
|
+
end
|
16
|
+
|
17
|
+
Result.new(output.string.chomp, exit_status)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Moonshot
|
2
|
+
# Choose a publically accessible instance to run commands on, given a Moonshot::Stack.
|
3
|
+
class SSHTargetSelector
|
4
|
+
def initialize(stack, asg_name: nil)
|
5
|
+
@asg_name = asg_name
|
6
|
+
@stack = stack
|
7
|
+
end
|
8
|
+
|
9
|
+
def choose!
|
10
|
+
groups = @stack.resources_of_type('AWS::AutoScaling::AutoScalingGroup')
|
11
|
+
|
12
|
+
asg = if groups.count == 1
|
13
|
+
groups.first
|
14
|
+
elsif asgs.count > 1
|
15
|
+
unless @asg_name
|
16
|
+
raise 'Multiple Auto Scaling Groups found in the stack. Please specify which '\
|
17
|
+
'one to SSH into using the --auto-scaling-group (-g) option.'
|
18
|
+
end
|
19
|
+
groups.detect { |x| x.logical_resource_id == @config.ssh_auto_scaling_group_name }
|
20
|
+
end
|
21
|
+
raise 'Failed to find the Auto Scaling Group.' unless asg
|
22
|
+
|
23
|
+
Aws::AutoScaling::Client.new.describe_auto_scaling_groups(
|
24
|
+
auto_scaling_group_names: [asg.physical_resource_id]
|
25
|
+
).auto_scaling_groups.first.instances.map(&:instance_id).first
|
26
|
+
rescue
|
27
|
+
raise 'Failed to find instances in the Auto Scaling Group!'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/moonshot/stack.rb
CHANGED
@@ -93,18 +93,6 @@ module Moonshot
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
|
-
def ssh
|
97
|
-
box_id = @config.ssh_instance || instances.sort.first
|
98
|
-
box_ip = instance_ip(box_id)
|
99
|
-
cmd = ['ssh', '-t']
|
100
|
-
cmd << "-i #{@config.ssh_identity_file}" if @config.ssh_identity_file
|
101
|
-
cmd << "-l #{@config.ssh_user}" if @config.ssh_user
|
102
|
-
cmd << box_ip
|
103
|
-
cmd << @config.ssh_command if @config.ssh_command
|
104
|
-
puts "Opening SSH connection to #{box_id} (#{box_ip})..."
|
105
|
-
exec(cmd.join(' '))
|
106
|
-
end
|
107
|
-
|
108
96
|
def parameters
|
109
97
|
get_stack(@name)
|
110
98
|
.parameters
|
@@ -188,37 +176,6 @@ module Moonshot
|
|
188
176
|
|
189
177
|
private
|
190
178
|
|
191
|
-
def asgs
|
192
|
-
resources_of_type('AWS::AutoScaling::AutoScalingGroup')
|
193
|
-
end
|
194
|
-
|
195
|
-
def instance_ip(instance_id)
|
196
|
-
Aws::EC2::Client.new.describe_instances(instance_ids: [instance_id])
|
197
|
-
.reservations.first.instances.first.public_ip_address
|
198
|
-
rescue
|
199
|
-
raise "Failed to determine public IP address for instance #{instance_id}."
|
200
|
-
end
|
201
|
-
|
202
|
-
def instances # rubocop:disable Metrics/AbcSize
|
203
|
-
groups = asgs
|
204
|
-
asg = if groups.count == 1
|
205
|
-
groups.first
|
206
|
-
elsif asgs.count > 1
|
207
|
-
unless @config.ssh_auto_scaling_group_name
|
208
|
-
raise 'Multiple Auto Scaling Groups found in the stack. Please specify which '\
|
209
|
-
'one to SSH into using the --auto-scaling-group (-g) option.'
|
210
|
-
end
|
211
|
-
groups.detect { |x| x.logical_resource_id == @config.ssh_auto_scaling_group_name }
|
212
|
-
end
|
213
|
-
raise 'Failed to find the Auto Scaling Group.' unless asg
|
214
|
-
|
215
|
-
Aws::AutoScaling::Client.new.describe_auto_scaling_groups(
|
216
|
-
auto_scaling_group_names: [asg.physical_resource_id]
|
217
|
-
).auto_scaling_groups.first.instances.map(&:instance_id)
|
218
|
-
rescue
|
219
|
-
raise 'Failed to find instances in the Auto Scaling Group.'
|
220
|
-
end
|
221
|
-
|
222
179
|
def stack_name
|
223
180
|
"CloudFormation Stack #{@name.blue}"
|
224
181
|
end
|
@@ -5,7 +5,7 @@ require 'ruby-duration'
|
|
5
5
|
module Moonshot
|
6
6
|
# Display information about the AutoScaling Groups, associated ELBs, and
|
7
7
|
# managed instances to the user.
|
8
|
-
class StackASGPrinter
|
8
|
+
class StackASGPrinter
|
9
9
|
include CredsHelper
|
10
10
|
|
11
11
|
def initialize(stack, table)
|
@@ -79,7 +79,7 @@ module Moonshot
|
|
79
79
|
data
|
80
80
|
end
|
81
81
|
|
82
|
-
def add_asg_info(table, asg_info)
|
82
|
+
def add_asg_info(table, asg_info)
|
83
83
|
name = asg_info.auto_scaling_group_name.blue
|
84
84
|
table.add_line "Name: #{name}"
|
85
85
|
|
@@ -4,11 +4,6 @@ module Moonshot
|
|
4
4
|
attr_accessor :parent_stacks
|
5
5
|
attr_accessor :show_all_events
|
6
6
|
attr_accessor :parameter_strategy
|
7
|
-
attr_accessor :ssh_instance
|
8
|
-
attr_accessor :ssh_identity_file
|
9
|
-
attr_accessor :ssh_user
|
10
|
-
attr_accessor :ssh_command
|
11
|
-
attr_accessor :ssh_auto_scaling_group_name
|
12
7
|
|
13
8
|
def initialize
|
14
9
|
@parent_stacks = []
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require_relative 'asg_instance'
|
2
|
+
require_relative 'instance_health'
|
3
|
+
|
4
|
+
module Moonshot
|
5
|
+
module Tools
|
6
|
+
class ASGRollout
|
7
|
+
# Abstration layer with AWS Auto Scaling Groups, for the Rollout tool.
|
8
|
+
class ASG
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@last_seen_ids = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def current_max_and_desired
|
17
|
+
asg = load_asg
|
18
|
+
|
19
|
+
[asg.max_size, asg.desired_capacity]
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_max_and_desired(max, desired)
|
23
|
+
autoscaling.update_auto_scaling_group(
|
24
|
+
auto_scaling_group_name: @name,
|
25
|
+
max_size: max,
|
26
|
+
desired_capacity: desired)
|
27
|
+
end
|
28
|
+
|
29
|
+
def non_conforming_instances
|
30
|
+
asg = load_asg
|
31
|
+
|
32
|
+
asg.instances
|
33
|
+
.select { |i| i.launch_configuration_name != asg.launch_configuration_name }
|
34
|
+
.map(&:instance_id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def wait_for_new_instance
|
38
|
+
# Query the ASG until an instance appears which is not in
|
39
|
+
# @last_seen_ids, then add it to @last_seen_ids and return
|
40
|
+
# it.
|
41
|
+
previous_ids = @last_seen_ids
|
42
|
+
|
43
|
+
loop do
|
44
|
+
load_asg
|
45
|
+
|
46
|
+
new_ids = @last_seen_ids - previous_ids
|
47
|
+
previous_ids = @last_seen_ids
|
48
|
+
|
49
|
+
unless new_ids.empty?
|
50
|
+
@last_seen_ids << new_ids.first
|
51
|
+
return new_ids.first
|
52
|
+
end
|
53
|
+
|
54
|
+
sleep 3
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def detach_instance(id, decrement:)
|
59
|
+
# Store the current instance IDs for the next call to wait_for_new_instance.
|
60
|
+
load_asg
|
61
|
+
|
62
|
+
resp = autoscaling.detach_instances(
|
63
|
+
auto_scaling_group_name: @name,
|
64
|
+
instance_ids: [id],
|
65
|
+
should_decrement_desired_capacity: decrement)
|
66
|
+
|
67
|
+
activity = resp.activities.first
|
68
|
+
unless activity
|
69
|
+
raise 'Did not receive Activity from DetachInstances call!'
|
70
|
+
end
|
71
|
+
|
72
|
+
# Wait for the detach activity to complete:
|
73
|
+
loop do
|
74
|
+
resp = autoscaling.describe_scaling_activities(
|
75
|
+
auto_scaling_group_name: @name)
|
76
|
+
|
77
|
+
current_status = resp.activities
|
78
|
+
.find { |a| a.activity_id == activity.activity_id }
|
79
|
+
.status_code
|
80
|
+
|
81
|
+
case current_status
|
82
|
+
when 'Failed', 'Cancelled'
|
83
|
+
raise 'Detachment did not complete successfully!'
|
84
|
+
when 'Successful'
|
85
|
+
return
|
86
|
+
end
|
87
|
+
|
88
|
+
sleep 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def instance_health(id)
|
93
|
+
elb_status = nil
|
94
|
+
elb_status = elb_instance_state(id) if elb_name
|
95
|
+
|
96
|
+
InstanceHealth.new(asg_instance_state(id), elb_status)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def asg_instance_state(id)
|
102
|
+
resp = autoscaling.describe_auto_scaling_instances(
|
103
|
+
instance_ids: [id])
|
104
|
+
|
105
|
+
instance_info = resp.auto_scaling_instances.first
|
106
|
+
return 'Missing' unless instance_info
|
107
|
+
|
108
|
+
instance_info.lifecycle_state
|
109
|
+
end
|
110
|
+
|
111
|
+
def elb_instance_state(id)
|
112
|
+
resp = loadbalancing.describe_instance_health(
|
113
|
+
load_balancer_name: elb_name,
|
114
|
+
instances: [
|
115
|
+
{ instance_id: id }
|
116
|
+
])
|
117
|
+
|
118
|
+
instance_info = resp.instance_states.first
|
119
|
+
unless instance_info
|
120
|
+
raise "Failed to call DescribeInstanceHealth for #{id}!"
|
121
|
+
end
|
122
|
+
|
123
|
+
instance_info.state
|
124
|
+
rescue Aws::ElasticLoadBalancing::Errors::InvalidInstance
|
125
|
+
# We expect the instance to be in an ELB, eventually.
|
126
|
+
'Missing'
|
127
|
+
end
|
128
|
+
|
129
|
+
def autoscaling
|
130
|
+
@autoscaling ||= Aws::AutoScaling::Client.new
|
131
|
+
end
|
132
|
+
|
133
|
+
def loadbalancing
|
134
|
+
@loadbalancing ||= Aws::ElasticLoadBalancing::Client.new
|
135
|
+
end
|
136
|
+
|
137
|
+
def load_asg
|
138
|
+
resp = autoscaling.describe_auto_scaling_groups(
|
139
|
+
auto_scaling_group_names: [@name])
|
140
|
+
|
141
|
+
if resp.auto_scaling_groups.empty?
|
142
|
+
raise "Failed to call DescribeAutoScalingGroups for #{@name}!"
|
143
|
+
end
|
144
|
+
|
145
|
+
asg = resp.auto_scaling_groups.first
|
146
|
+
@last_seen_ids = asg.instances.map(&:instance_id)
|
147
|
+
|
148
|
+
asg
|
149
|
+
end
|
150
|
+
|
151
|
+
def elb_name
|
152
|
+
return @elb_name if @elb_name
|
153
|
+
|
154
|
+
asg = load_asg
|
155
|
+
if asg.load_balancer_names.size > 1
|
156
|
+
raise 'ASGRollout does not support configurations with multiple ELBs!'
|
157
|
+
end
|
158
|
+
|
159
|
+
@elb_name ||= asg.load_balancer_names.first
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Moonshot
|
2
|
+
module Tools
|
3
|
+
class ASGRollout
|
4
|
+
# Provides an abstraction around an Auto Scaling Group's
|
5
|
+
# relationship with an instance.
|
6
|
+
class ASGInstance
|
7
|
+
attr_reader :asg_name, :id
|
8
|
+
|
9
|
+
def initialize(asg_name, id, _config)
|
10
|
+
@asg_name = asg_name
|
11
|
+
@instance_id = id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'moonshot/ssh_fork_executor'
|
2
|
+
|
3
|
+
module Moonshot
|
4
|
+
module Tools
|
5
|
+
class ASGRollout
|
6
|
+
# This object is passed into hooks defined in the ASGRollout
|
7
|
+
# process, to give them access to instances and logging
|
8
|
+
# facilities.
|
9
|
+
class HookExecEnvironment
|
10
|
+
attr_reader :instance_id
|
11
|
+
|
12
|
+
def initialize(config, instance_id)
|
13
|
+
@ilog = config.interactive_logger
|
14
|
+
@command_builder = Moonshot::SSHCommandBuilder.new(config.ssh_config, instance_id)
|
15
|
+
@instance_id = instance_id
|
16
|
+
end
|
17
|
+
|
18
|
+
def exec(cmd)
|
19
|
+
cb = @command_builder.build(cmd)
|
20
|
+
fe = SSHForkExecutor.new
|
21
|
+
fe.run(cb.cmd)
|
22
|
+
end
|
23
|
+
|
24
|
+
def ec2
|
25
|
+
Aws::EC2::Client.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def ec2_instance
|
29
|
+
res = Aws::EC2::Resource.new(client: ec2)
|
30
|
+
res.instance(@instance_id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def debug(msg)
|
34
|
+
@ilog.debug(msg)
|
35
|
+
end
|
36
|
+
|
37
|
+
def info(msg)
|
38
|
+
@ilog.info(msg)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Moonshot
|
2
|
+
module Tools
|
3
|
+
class ASGRollout
|
4
|
+
class InstanceHealth # rubocop:disable Documentation
|
5
|
+
attr_reader :asg_status, :elb_status
|
6
|
+
|
7
|
+
VALID_ASG_IN_SERVICE_STATES = ['InService'].freeze
|
8
|
+
VALID_ELB_IN_SERVICE_STATES = [nil, 'InService'].freeze
|
9
|
+
|
10
|
+
VALID_ASG_OUT_OF_SERVICE_STATES = [nil, 'Missing', 'Detached'].freeze
|
11
|
+
VALID_ELB_OUT_OF_SERVICE_STATES = [nil, 'Missing', 'OutOfService'].freeze
|
12
|
+
|
13
|
+
def initialize(asg_status, elb_status)
|
14
|
+
@asg_status = asg_status
|
15
|
+
@elb_status = elb_status
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
result = "ASG:#{@asg_status}"
|
20
|
+
result << "/ELB:#{@elb_status}" if @elb_status
|
21
|
+
result
|
22
|
+
end
|
23
|
+
|
24
|
+
def in_service?
|
25
|
+
VALID_ASG_IN_SERVICE_STATES.include?(@asg_status) &&
|
26
|
+
VALID_ELB_IN_SERVICE_STATES.include?(@elb_status)
|
27
|
+
end
|
28
|
+
|
29
|
+
def out_of_service?
|
30
|
+
VALID_ASG_OUT_OF_SERVICE_STATES.include?(@asg_status) &&
|
31
|
+
VALID_ELB_OUT_OF_SERVICE_STATES.include?(@elb_status)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require_relative 'asg_rollout_config'
|
2
|
+
require_relative 'asg_rollout/asg'
|
3
|
+
require_relative 'asg_rollout/hook_exec_environment'
|
4
|
+
|
5
|
+
module Moonshot
|
6
|
+
module Tools
|
7
|
+
class ASGRollout # rubocop:disable Documentation
|
8
|
+
attr_accessor :config
|
9
|
+
|
10
|
+
def initialize(controller:, logical_id:)
|
11
|
+
@config = ASGRolloutConfig.new
|
12
|
+
@controller = controller
|
13
|
+
@logical_id = logical_id
|
14
|
+
yield @config if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def run!
|
18
|
+
increase_max_and_desired
|
19
|
+
new_instance = wait_for_new_instance
|
20
|
+
wait_for_in_service(new_instance)
|
21
|
+
|
22
|
+
targets = asg.non_conforming_instances
|
23
|
+
last_instance = targets.last
|
24
|
+
|
25
|
+
targets.each do |instance|
|
26
|
+
run_pre_detach(instance) if @config.pre_detach
|
27
|
+
detach(instance, decrement: instance == last_instance)
|
28
|
+
wait_for_out_of_service(instance)
|
29
|
+
|
30
|
+
unless instance == last_instance
|
31
|
+
new_instance = wait_for_new_instance
|
32
|
+
wait_for_in_service(new_instance)
|
33
|
+
end
|
34
|
+
|
35
|
+
wait_for_terminate_when_hook(instance) if @config.terminate_when
|
36
|
+
terminate(instance)
|
37
|
+
end
|
38
|
+
ensure
|
39
|
+
log.start_threaded 'Restoring MaxSize/DesiredCapacity values to normal...' do |s|
|
40
|
+
asg.set_max_and_desired(@max, @desired)
|
41
|
+
|
42
|
+
s.success 'Restored MaxSize/DesiredCapacity values to normal!'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def increase_max_and_desired
|
49
|
+
log.start_threaded 'Increasing MaxSize/DesiredCapacity by 1.' do |s|
|
50
|
+
@max, @desired = asg.current_max_and_desired
|
51
|
+
|
52
|
+
asg.set_max_and_desired(@max + 1, @desired + 1)
|
53
|
+
s.success 'Increased MaxSize/DesiredCapacity by 1.'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def wait_for_new_instance
|
58
|
+
new_instance = nil
|
59
|
+
log.start_threaded 'Waiting for a new instance to join Auto Scaling Group...' do |s|
|
60
|
+
new_instance = asg.wait_for_new_instance
|
61
|
+
s.success "A wild #{new_instance.blue} appears!"
|
62
|
+
end
|
63
|
+
new_instance
|
64
|
+
end
|
65
|
+
|
66
|
+
def wait_for_in_service(new_instance)
|
67
|
+
log.start_threaded "Waiting for #{new_instance.blue} to be InService..." do |s|
|
68
|
+
instance_health = nil
|
69
|
+
|
70
|
+
loop do
|
71
|
+
instance_health = asg.instance_health(new_instance)
|
72
|
+
break if instance_health.in_service?
|
73
|
+
|
74
|
+
s.continue "Instance #{new_instance.blue} is #{instance_health}..."
|
75
|
+
|
76
|
+
sleep @config.instance_health_delay
|
77
|
+
end
|
78
|
+
|
79
|
+
s.success "Instance #{new_instance.blue} is #{instance_health}!"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def run_pre_detach(instance)
|
84
|
+
if @config.pre_detach
|
85
|
+
log.start_threaded "Running PreDetach hook on #{instance.blue}..." do |s|
|
86
|
+
he = HookExecEnvironment.new(@controller.config, instance)
|
87
|
+
if false == @config.pre_detach.call(he)
|
88
|
+
s.failure "PreDetach hook failed for #{instance.blue}!"
|
89
|
+
raise "PreDetach hook failed for #{instance.blue}!"
|
90
|
+
end
|
91
|
+
|
92
|
+
s.success "PreDetach hook complete for #{instance.blue}!"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def detach(instance, decrement:)
|
98
|
+
log.start_threaded "Detaching instance #{instance.blue}..." do |s|
|
99
|
+
asg.detach_instance(instance, decrement: decrement)
|
100
|
+
|
101
|
+
if decrement
|
102
|
+
s.success "Detached instance #{instance.blue}, and decremented DesiredCapacity."
|
103
|
+
else
|
104
|
+
s.success "Detached instance #{instance.blue}."
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def wait_for_out_of_service(instance)
|
110
|
+
log.start_threaded "Waiting for #{instance.blue} to be OutOfService..." do |s|
|
111
|
+
instance_health = nil
|
112
|
+
|
113
|
+
loop do
|
114
|
+
instance_health = asg.instance_health(instance)
|
115
|
+
break if instance_health.out_of_service?
|
116
|
+
|
117
|
+
s.continue "Instance #{instance.blue} is #{instance_health}..."
|
118
|
+
|
119
|
+
sleep @config.instance_health_delay
|
120
|
+
end
|
121
|
+
|
122
|
+
s.success "Instance #{instance.blue} is #{instance_health}!"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def wait_for_terminate_when_hook(instance)
|
127
|
+
log.start_threaded "Waiting for TerminateWhen hook for #{instance.blue}..." do |s|
|
128
|
+
start = Time.now.to_f
|
129
|
+
he = HookExecEnvironment.new(@controller.config, instance)
|
130
|
+
timeout = @config.terminate_when_timeout
|
131
|
+
|
132
|
+
loop do
|
133
|
+
break if @config.terminate_when.call(he)
|
134
|
+
sleep @config.terminate_when_delay
|
135
|
+
|
136
|
+
if Time.now.to_f - start > timeout
|
137
|
+
s.failure "TerminateWhen for #{instance.blue} did not complete in #{timeout} seconds!"
|
138
|
+
raise "TerminateWhen for #{instance.blue} did not complete in #{timeout} seconds!"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
s.success "Completed TerminateWhen check for #{instance.blue}!"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def terminate(instance)
|
147
|
+
log.start_threaded "Terminating #{instance.blue}..." do |s|
|
148
|
+
he = HookExecEnvironment.new(@controller.config, instance)
|
149
|
+
@config.terminate.call(he)
|
150
|
+
s.success "Terminated #{instance.blue}!"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def asg
|
155
|
+
return @asg if @asg
|
156
|
+
|
157
|
+
asg_name = @controller.stack.physical_id_for(@logical_id)
|
158
|
+
unless asg_name
|
159
|
+
raise "Could not find Auto Scaling Group #{@logical_id}!"
|
160
|
+
end
|
161
|
+
|
162
|
+
@asg ||= ASGRollout::ASG.new(asg_name)
|
163
|
+
end
|
164
|
+
|
165
|
+
def log
|
166
|
+
@controller.config.interactive_logger
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Moonshot
|
2
|
+
module Tools
|
3
|
+
class ASGRolloutConfig # rubocop:disable Documentation
|
4
|
+
attr_reader :pre_detach, :terminate_when, :terminate_when_timeout, :terminate
|
5
|
+
attr_accessor :terminate_when_delay, :instance_health_delay
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@instance_health_delay = 2
|
9
|
+
@terminate_when_delay = 1
|
10
|
+
@terminate_when_timeout = 300
|
11
|
+
@terminate = proc do |h|
|
12
|
+
h.ec2_instance.terminate
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def pre_detach=(value)
|
17
|
+
raise ArgumentError, 'pre_detach must be callable' unless value.respond_to?(:call)
|
18
|
+
|
19
|
+
@pre_detach = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def terminate_when=(value)
|
23
|
+
raise ArgumentError, 'terminate_when must be callable' unless value.respond_to?(:call)
|
24
|
+
|
25
|
+
@terminate_when = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def terminate_when_timeout=(value)
|
29
|
+
@terminate_when_timeout = Float(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
def terminate=(value)
|
33
|
+
raise ArgumentError, 'terminate must be callable' unless value.respond_to?(:call)
|
34
|
+
|
35
|
+
@terminate = value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/moonshot.rb
CHANGED
@@ -39,7 +39,10 @@ end
|
|
39
39
|
'build_mechanism/github_release',
|
40
40
|
'build_mechanism/travis_deploy',
|
41
41
|
'build_mechanism/version_proxy',
|
42
|
-
'deployment_mechanism/code_deploy'
|
42
|
+
'deployment_mechanism/code_deploy',
|
43
|
+
|
44
|
+
# Core Tools
|
45
|
+
'tools/asg_rollout'
|
43
46
|
].each { |f| require_relative "moonshot/#{f}" }
|
44
47
|
|
45
48
|
# Bundled plugins
|
data/lib/plugins/backup.rb
CHANGED
@@ -5,7 +5,7 @@ require_relative '../moonshot/creds_helper'
|
|
5
5
|
module Moonshot
|
6
6
|
module Plugins
|
7
7
|
# Moonshot plugin class for deflating and uploading files on given hooks
|
8
|
-
class Backup
|
8
|
+
class Backup
|
9
9
|
include Moonshot::CredsHelper
|
10
10
|
|
11
11
|
attr_accessor :bucket,
|
@@ -42,7 +42,7 @@ module Moonshot
|
|
42
42
|
# to an S3 bucket.
|
43
43
|
#
|
44
44
|
# @param resources [Resources] injected Moonshot resources
|
45
|
-
def backup(resources)
|
45
|
+
def backup(resources)
|
46
46
|
raise ArgumentError if resources.nil?
|
47
47
|
|
48
48
|
@app_name = resources.stack.app_name
|
metadata
CHANGED
@@ -1,20 +1,23 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: moonshot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cloud Engineering <engineering@acquia.com>
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
- - ">="
|
18
21
|
- !ruby/object:Gem::Version
|
19
22
|
version: 2.2.0
|
20
23
|
type: :runtime
|
@@ -22,6 +25,9 @@ dependencies:
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
27
|
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.0'
|
30
|
+
- - ">="
|
25
31
|
- !ruby/object:Gem::Version
|
26
32
|
version: 2.2.0
|
27
33
|
- !ruby/object:Gem::Dependency
|
@@ -58,14 +64,14 @@ dependencies:
|
|
58
64
|
requirements:
|
59
65
|
- - "~>"
|
60
66
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0.1.
|
67
|
+
version: 0.1.2
|
62
68
|
type: :runtime
|
63
69
|
prerelease: false
|
64
70
|
version_requirements: !ruby/object:Gem::Requirement
|
65
71
|
requirements:
|
66
72
|
- - "~>"
|
67
73
|
- !ruby/object:Gem::Version
|
68
|
-
version: 0.1.
|
74
|
+
version: 0.1.2
|
69
75
|
- !ruby/object:Gem::Dependency
|
70
76
|
name: rotp
|
71
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -136,6 +142,20 @@ dependencies:
|
|
136
142
|
- - ">="
|
137
143
|
- !ruby/object:Gem::Version
|
138
144
|
version: '0'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: travis
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
type: :runtime
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
139
159
|
- !ruby/object:Gem::Dependency
|
140
160
|
name: vandamme
|
141
161
|
requirement: !ruby/object:Gem::Requirement
|
@@ -218,6 +238,10 @@ files:
|
|
218
238
|
- lib/moonshot/resources.rb
|
219
239
|
- lib/moonshot/resources_helper.rb
|
220
240
|
- lib/moonshot/shell.rb
|
241
|
+
- lib/moonshot/ssh_command_builder.rb
|
242
|
+
- lib/moonshot/ssh_config.rb
|
243
|
+
- lib/moonshot/ssh_fork_executor.rb
|
244
|
+
- lib/moonshot/ssh_target_selector.rb
|
221
245
|
- lib/moonshot/stack.rb
|
222
246
|
- lib/moonshot/stack_asg_printer.rb
|
223
247
|
- lib/moonshot/stack_config.rb
|
@@ -226,6 +250,12 @@ files:
|
|
226
250
|
- lib/moonshot/stack_output_printer.rb
|
227
251
|
- lib/moonshot/stack_parameter_printer.rb
|
228
252
|
- lib/moonshot/stack_template.rb
|
253
|
+
- lib/moonshot/tools/asg_rollout.rb
|
254
|
+
- lib/moonshot/tools/asg_rollout/asg.rb
|
255
|
+
- lib/moonshot/tools/asg_rollout/asg_instance.rb
|
256
|
+
- lib/moonshot/tools/asg_rollout/hook_exec_environment.rb
|
257
|
+
- lib/moonshot/tools/asg_rollout/instance_health.rb
|
258
|
+
- lib/moonshot/tools/asg_rollout_config.rb
|
229
259
|
- lib/moonshot/unicode_table.rb
|
230
260
|
- lib/plugins/backup.rb
|
231
261
|
homepage: https://github.com/acquia/moonshot
|