cf_deployer 1.5.0 → 1.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6f550eef2ccbd8df2bff3ad4441d228800bab756
4
- data.tar.gz: f3981f2ef12ab4338ddb5bec47fc31c6326e2582
3
+ metadata.gz: b15fbdcf8f040017be2e4f7fd2fca49e99327164
4
+ data.tar.gz: 6138254475f90d7983da1823754ace6134be9d85
5
5
  SHA512:
6
- metadata.gz: 0d09d5100924380dd8643c041bdd188b6d53fd3a2b408a4a7609ce785fc935149dc37a39d2b192c9003651cc695f1757c8014134bcbe930f679c4648691e3a78
7
- data.tar.gz: 93e9e12ece59c4a0a01508047be7da243977770b82ed8d1d844f1eea81c8b98b3875e942db5e88231ed4716db1004f8c87baca255fd9a61d3a02f24b9b79a0d6
6
+ metadata.gz: 209ff6fd2211ae9b37ef0592aaec30066139ee37742cc36bdb45593ad0bc8f4476393ccc76be3d04204b9021bef07621b703bdd09b65b85580f18827ff5ceb9a
7
+ data.tar.gz: 9d3a7676274ac56d77ef0d545f1dc2441bf9a84ce07bf50cae1aec809df349ff8a5f23202bfdabedf8064d08383ac191d879606c9a80b3586e91792f2819f91b
@@ -57,3 +57,9 @@ version 1.4.0
57
57
 
58
58
  version 1.5.0
59
59
  - Treat deployments that end in a rollback as a failure
60
+
61
+ version 1.6.0
62
+ - Improve warm up for AutoScalingGroup based deployments (See: https://github.com/manheim/cf_deployer/issues/32)
63
+ - Automatically rollback on failures for AutoScalingGroup based deployments (See: https://github.com/manheim/cf_deployer/issues/39)
64
+ - Improve error message when no stack is exists on ASG-based switch (See: https://github.com/manheim/cf_deployer/issues/50)
65
+ - Reduce AWS calls during healthy_instance_count (See: https://github.com/manheim/cf_deployer/issues/48)
data/Rakefile CHANGED
@@ -22,7 +22,12 @@ namespace :spec do
22
22
  t.pattern = 'spec/functional/**/*_spec.rb'
23
23
  end
24
24
 
25
- task :all => [:unit, :functional]
25
+ RSpec::Core::RakeTask.new(:aws_call_count) do |t|
26
+ t.rspec_opts = RSPEC_OPTS
27
+ t.pattern = 'spec/aws_call_count/**/*_spec.rb'
28
+ end
29
+
30
+ task :all => [:unit, :functional, :aws_call_count]
26
31
  end
27
32
 
28
33
  task :default => 'spec:all'
@@ -18,6 +18,8 @@ Gem::Specification.new do |gem|
18
18
  gem.add_development_dependency 'pry', '~> 0.10.1'
19
19
  gem.add_development_dependency 'rspec', '2.14.1'
20
20
  gem.add_development_dependency 'rake', '~> 10.3.0'
21
+ gem.add_development_dependency 'webmock', '~> 2.1.0'
22
+ gem.add_development_dependency 'vcr', '~> 2.9.3'
21
23
 
22
24
  gem.files = `git ls-files`.split($\).reject {|f| f =~ /^samples\// }
23
25
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -0,0 +1,243 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://autoscaling.us-east-1.amazonaws.com/
6
+ response:
7
+ status:
8
+ code: 200
9
+ message: OK
10
+ body:
11
+ encoding: US-ASCII
12
+ string: ! "<DescribeAutoScalingGroupsResponse xmlns=\"http://autoscaling.amazonaws.com/doc/2011-01-01/\">\n
13
+ \ <DescribeAutoScalingGroupsResult>\n <AutoScalingGroups>\n <member>\n
14
+ \ <HealthCheckType>ELB</HealthCheckType>\n <LoadBalancerNames>\n
15
+ \ <member>myELB</member>\n </LoadBalancerNames>\n
16
+ \ <Instances>\n <member>\n <LaunchConfigurationName>myLaunchConfig</LaunchConfigurationName>\n
17
+ \ <LifecycleState>InService</LifecycleState>\n <InstanceId>instance1</InstanceId>\n
18
+ \ <HealthStatus>Healthy</HealthStatus>\n <ProtectedFromScaleIn>false</ProtectedFromScaleIn>\n
19
+ \ <AvailabilityZone>us-east-1d</AvailabilityZone>\n </member>\n
20
+ \ <member>\n <LaunchConfigurationName>myLaunchConfig</LaunchConfigurationName>\n
21
+ \ <LifecycleState>InService</LifecycleState>\n <InstanceId>instance2</InstanceId>\n
22
+ \ <HealthStatus>Healthy</HealthStatus>\n <ProtectedFromScaleIn>false</ProtectedFromScaleIn>\n
23
+ \ <AvailabilityZone>us-east-1c</AvailabilityZone>\n </member>\n
24
+ \ <member>\n <LaunchConfigurationName>myLaunchConfig</LaunchConfigurationName>\n
25
+ \ <LifecycleState>InService</LifecycleState>\n <InstanceId>instance3</InstanceId>\n
26
+ \ <HealthStatus>Healthy</HealthStatus>\n <ProtectedFromScaleIn>false</ProtectedFromScaleIn>\n
27
+ \ <AvailabilityZone>us-east-1e</AvailabilityZone>\n </member>\n
28
+ \ <member>\n <LaunchConfigurationName>myLaunchConfig</LaunchConfigurationName>\n
29
+ \ <LifecycleState>InService</LifecycleState>\n <InstanceId>instance4</InstanceId>\n
30
+ \ <HealthStatus>Healthy</HealthStatus>\n <ProtectedFromScaleIn>false</ProtectedFromScaleIn>\n
31
+ \ <AvailabilityZone>us-east-1e</AvailabilityZone>\n </member>\n
32
+ \ </Instances>\n <TerminationPolicies>\n <member>Default</member>\n
33
+ \ </TerminationPolicies>\n <DefaultCooldown>300</DefaultCooldown>\n
34
+ \ <AutoScalingGroupARN>arn:aws:autoscaling:us-east-1:12345:autoScalingGroup:12345:autoScalingGroupName/myASG</AutoScalingGroupARN>\n
35
+ \ <EnabledMetrics/>\n <MaxSize>5</MaxSize>\n <AvailabilityZones>\n
36
+ \ <member>us-east-1c</member>\n <member>us-east-1d</member>\n
37
+ \ <member>us-east-1e</member>\n </AvailabilityZones>\n <TargetGroupARNs/>\n
38
+ \ <Tags/>\n
39
+ \ <LaunchConfigurationName>myLaunchConfig</LaunchConfigurationName>\n
40
+ \ <AutoScalingGroupName>myASG</AutoScalingGroupName>\n
41
+ \ <HealthCheckGracePeriod>600</HealthCheckGracePeriod>\n <NewInstancesProtectedFromScaleIn>false</NewInstancesProtectedFromScaleIn>\n
42
+ \ <CreatedTime>2016-09-23T03:52:21.733Z</CreatedTime>\n <MinSize>1</MinSize>\n
43
+ \ <SuspendedProcesses/>\n <DesiredCapacity>4</DesiredCapacity>\n
44
+ \ <VPCZoneIdentifier>subnet</VPCZoneIdentifier>\n
45
+ \ </member>\n </AutoScalingGroups>\n </DescribeAutoScalingGroupsResult>\n
46
+ \ <ResponseMetadata>\n <RequestId>12345</RequestId>\n
47
+ \ </ResponseMetadata>\n</DescribeAutoScalingGroupsResponse>\n"
48
+ http_version:
49
+ recorded_at: Tue, 27 Sep 2016 05:47:40 GMT
50
+ - request:
51
+ method: post
52
+ uri: https://autoscaling.us-east-1.amazonaws.com/
53
+ response:
54
+ status:
55
+ code: 200
56
+ message: OK
57
+ body:
58
+ encoding: US-ASCII
59
+ string: ! "<DescribeAutoScalingInstancesResponse xmlns=\"http://autoscaling.amazonaws.com/doc/2011-01-01/\">\n
60
+ \ <DescribeAutoScalingInstancesResult>\n <AutoScalingInstances>\n <member>\n
61
+ \ <LaunchConfigurationName>myLaunchConfig</LaunchConfigurationName>\n
62
+ \ <LifecycleState>InService</LifecycleState>\n <AutoScalingGroupName></AutoScalingGroupName>\n
63
+ \ <InstanceId>instance1</InstanceId>\n <HealthStatus>HEALTHY</HealthStatus>\n
64
+ \ <ProtectedFromScaleIn>false</ProtectedFromScaleIn>\n <AvailabilityZone>us-east-1d</AvailabilityZone>\n
65
+ \ </member>\n </AutoScalingInstances>\n </DescribeAutoScalingInstancesResult>\n
66
+ \ <ResponseMetadata>\n <RequestId>12346</RequestId>\n
67
+ \ </ResponseMetadata>\n</DescribeAutoScalingInstancesResponse>\n"
68
+ http_version:
69
+ recorded_at: Tue, 27 Sep 2016 05:47:40 GMT
70
+ - request:
71
+ method: post
72
+ uri: https://elasticloadbalancing.us-east-1.amazonaws.com/
73
+ body:
74
+ encoding: UTF-8
75
+ string: Action=DescribeInstanceHealth&LoadBalancerName=myELB&Timestamp=2016-09-27T05%3A47%3A41Z&Version=2012-06-01
76
+ response:
77
+ status:
78
+ code: 200
79
+ message: OK
80
+ body:
81
+ encoding: US-ASCII
82
+ string: ! "<DescribeInstanceHealthResponse xmlns=\"http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/\">\n
83
+ \ <DescribeInstanceHealthResult>\n <InstanceStates>\n <member>\n <Description>N/A</Description>\n
84
+ \ <InstanceId>instance1</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
85
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
86
+ \ <InstanceId>instance2</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
87
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
88
+ \ <InstanceId>instance3</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
89
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
90
+ \ <InstanceId>instance4</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
91
+ \ <State>InService</State>\n </member>\n </InstanceStates>\n
92
+ \ </DescribeInstanceHealthResult>\n <ResponseMetadata>\n <RequestId>12347</RequestId>\n
93
+ \ </ResponseMetadata>\n</DescribeInstanceHealthResponse>\n"
94
+ http_version:
95
+ recorded_at: Tue, 27 Sep 2016 05:47:41 GMT
96
+ - request:
97
+ method: post
98
+ uri: https://autoscaling.us-east-1.amazonaws.com/
99
+ body:
100
+ encoding: UTF-8
101
+ string: Action=DescribeAutoScalingInstances&InstanceIds.member.1=instance2&Timestamp=2016-09-27T05%3A47%3A41Z&Version=2011-01-01
102
+ response:
103
+ status:
104
+ code: 200
105
+ message: OK
106
+ body:
107
+ encoding: US-ASCII
108
+ string: ! "<DescribeAutoScalingInstancesResponse xmlns=\"http://autoscaling.amazonaws.com/doc/2011-01-01/\">\n
109
+ \ <DescribeAutoScalingInstancesResult>\n <AutoScalingInstances>\n <member>\n
110
+ \ <LaunchConfigurationName>myLaunchConfig</LaunchConfigurationName>\n
111
+ \ <LifecycleState>InService</LifecycleState>\n <AutoScalingGroupName>myASG</AutoScalingGroupName>\n
112
+ \ <InstanceId>instance2</InstanceId>\n <HealthStatus>HEALTHY</HealthStatus>\n
113
+ \ <ProtectedFromScaleIn>false</ProtectedFromScaleIn>\n <AvailabilityZone>us-east-1c</AvailabilityZone>\n
114
+ \ </member>\n </AutoScalingInstances>\n </DescribeAutoScalingInstancesResult>\n
115
+ \ <ResponseMetadata>\n <RequestId>12348</RequestId>\n
116
+ \ </ResponseMetadata>\n</DescribeAutoScalingInstancesResponse>\n"
117
+ http_version:
118
+ recorded_at: Tue, 27 Sep 2016 05:47:42 GMT
119
+ - request:
120
+ method: post
121
+ uri: https://elasticloadbalancing.us-east-1.amazonaws.com/
122
+ body:
123
+ encoding: UTF-8
124
+ string: Action=DescribeInstanceHealth&LoadBalancerName=myELB&Timestamp=2016-09-27T05%3A47%3A42Z&Version=2012-06-01
125
+ response:
126
+ status:
127
+ code: 200
128
+ message: OK
129
+ body:
130
+ encoding: US-ASCII
131
+ string: ! "<DescribeInstanceHealthResponse xmlns=\"http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/\">\n
132
+ \ <DescribeInstanceHealthResult>\n <InstanceStates>\n <member>\n <Description>N/A</Description>\n
133
+ \ <InstanceId>instance1</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
134
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
135
+ \ <InstanceId>instance2</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
136
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
137
+ \ <InstanceId>instance3</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
138
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
139
+ \ <InstanceId>instance4</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
140
+ \ <State>InService</State>\n </member>\n </InstanceStates>\n
141
+ \ </DescribeInstanceHealthResult>\n <ResponseMetadata>\n <RequestId>12349</RequestId>\n
142
+ \ </ResponseMetadata>\n</DescribeInstanceHealthResponse>\n"
143
+ http_version:
144
+ recorded_at: Tue, 27 Sep 2016 05:47:42 GMT
145
+ - request:
146
+ method: post
147
+ uri: https://autoscaling.us-east-1.amazonaws.com/
148
+ body:
149
+ encoding: UTF-8
150
+ string: Action=DescribeAutoScalingInstances&InstanceIds.member.1=instance3&Timestamp=2016-09-27T05%3A47%3A42Z&Version=2011-01-01
151
+ response:
152
+ status:
153
+ code: 200
154
+ message: OK
155
+ body:
156
+ encoding: US-ASCII
157
+ string: ! "<DescribeAutoScalingInstancesResponse xmlns=\"http://autoscaling.amazonaws.com/doc/2011-01-01/\">\n
158
+ \ <DescribeAutoScalingInstancesResult>\n <AutoScalingInstances>\n <member>\n
159
+ \ <LaunchConfigurationName>myLaunchConfig</LaunchConfigurationName>\n
160
+ \ <LifecycleState>InService</LifecycleState>\n <AutoScalingGroupName>myASG</AutoScalingGroupName>\n
161
+ \ <InstanceId>instance3</InstanceId>\n <HealthStatus>HEALTHY</HealthStatus>\n
162
+ \ <ProtectedFromScaleIn>false</ProtectedFromScaleIn>\n <AvailabilityZone>us-east-1e</AvailabilityZone>\n
163
+ \ </member>\n </AutoScalingInstances>\n </DescribeAutoScalingInstancesResult>\n
164
+ \ <ResponseMetadata>\n <RequestId>12350</RequestId>\n
165
+ \ </ResponseMetadata>\n</DescribeAutoScalingInstancesResponse>\n"
166
+ http_version:
167
+ recorded_at: Tue, 27 Sep 2016 05:47:42 GMT
168
+ - request:
169
+ method: post
170
+ uri: https://elasticloadbalancing.us-east-1.amazonaws.com/
171
+ body:
172
+ encoding: UTF-8
173
+ string: Action=DescribeInstanceHealth&LoadBalancerName=myELB&Timestamp=2016-09-27T05%3A47%3A42Z&Version=2012-06-01
174
+ response:
175
+ status:
176
+ code: 200
177
+ message: OK
178
+ body:
179
+ encoding: US-ASCII
180
+ string: ! "<DescribeInstanceHealthResponse xmlns=\"http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/\">\n
181
+ \ <DescribeInstanceHealthResult>\n <InstanceStates>\n <member>\n <Description>N/A</Description>\n
182
+ \ <InstanceId>instance1</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
183
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
184
+ \ <InstanceId>instance2</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
185
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
186
+ \ <InstanceId>instance3</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
187
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
188
+ \ <InstanceId>instance4</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
189
+ \ <State>InService</State>\n </member>\n </InstanceStates>\n
190
+ \ </DescribeInstanceHealthResult>\n <ResponseMetadata>\n <RequestId>12351</RequestId>\n
191
+ \ </ResponseMetadata>\n</DescribeInstanceHealthResponse>\n"
192
+ http_version:
193
+ recorded_at: Tue, 27 Sep 2016 05:47:42 GMT
194
+ - request:
195
+ method: post
196
+ uri: https://autoscaling.us-east-1.amazonaws.com/
197
+ body:
198
+ encoding: UTF-8
199
+ string: Action=DescribeAutoScalingInstances&InstanceIds.member.1=instance4&Timestamp=2016-09-27T05%3A47%3A42Z&Version=2011-01-01
200
+ response:
201
+ status:
202
+ code: 200
203
+ message: OK
204
+ body:
205
+ encoding: US-ASCII
206
+ string: ! "<DescribeAutoScalingInstancesResponse xmlns=\"http://autoscaling.amazonaws.com/doc/2011-01-01/\">\n
207
+ \ <DescribeAutoScalingInstancesResult>\n <AutoScalingInstances>\n <member>\n
208
+ \ <LaunchConfigurationName>myLaunchConfig</LaunchConfigurationName>\n
209
+ \ <LifecycleState>InService</LifecycleState>\n <AutoScalingGroupName>myASG</AutoScalingGroupName>\n
210
+ \ <InstanceId>instance4</InstanceId>\n <HealthStatus>HEALTHY</HealthStatus>\n
211
+ \ <ProtectedFromScaleIn>false</ProtectedFromScaleIn>\n <AvailabilityZone>us-east-1e</AvailabilityZone>\n
212
+ \ </member>\n </AutoScalingInstances>\n </DescribeAutoScalingInstancesResult>\n
213
+ \ <ResponseMetadata>\n <RequestId>12352</RequestId>\n
214
+ \ </ResponseMetadata>\n</DescribeAutoScalingInstancesResponse>\n"
215
+ http_version:
216
+ recorded_at: Tue, 27 Sep 2016 05:47:42 GMT
217
+ - request:
218
+ method: post
219
+ uri: https://elasticloadbalancing.us-east-1.amazonaws.com/
220
+ body:
221
+ encoding: UTF-8
222
+ string: Action=DescribeInstanceHealth&LoadBalancerName=myELB&Timestamp=2016-09-27T05%3A47%3A42Z&Version=2012-06-01
223
+ response:
224
+ status:
225
+ code: 200
226
+ message: OK
227
+ body:
228
+ encoding: US-ASCII
229
+ string: ! "<DescribeInstanceHealthResponse xmlns=\"http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/\">\n
230
+ \ <DescribeInstanceHealthResult>\n <InstanceStates>\n <member>\n <Description>N/A</Description>\n
231
+ \ <InstanceId>instance1</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
232
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
233
+ \ <InstanceId>instance2</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
234
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
235
+ \ <InstanceId>instance3</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
236
+ \ <State>InService</State>\n </member>\n <member>\n <Description>N/A</Description>\n
237
+ \ <InstanceId>instance4</InstanceId>\n <ReasonCode>N/A</ReasonCode>\n
238
+ \ <State>InService</State>\n </member>\n </InstanceStates>\n
239
+ \ </DescribeInstanceHealthResult>\n <ResponseMetadata>\n <RequestId>12353</RequestId>\n
240
+ \ </ResponseMetadata>\n</DescribeInstanceHealthResponse>\n"
241
+ http_version:
242
+ recorded_at: Tue, 27 Sep 2016 05:47:42 GMT
243
+ recorded_with: VCR 2.9.3
@@ -2,12 +2,25 @@ module CfDeployer
2
2
  module DeploymentStrategy
3
3
  class AutoScalingGroupSwap < BlueGreen
4
4
 
5
+ def cool_inactive_on_failure
6
+ yield
7
+ rescue => e
8
+ if both_stacks_active?
9
+ Log.error "Deployment failed - #{e.message} - and both stacks are active. Cooling down failed stack. Look into the failure, and try your deployment again."
10
+ cool_down(inactive_stack)
11
+ end
12
+
13
+ raise e
14
+ end
15
+
5
16
  def deploy
6
17
  check_blue_green_not_both_active 'Deployment'
7
18
  Log.info "Found active stack #{active_stack.name}" if active_stack
8
19
  delete_stack inactive_stack
9
- create_inactive_stack
10
- swap_group
20
+ cool_inactive_on_failure do
21
+ create_inactive_stack
22
+ swap_group
23
+ end
11
24
  run_hook(:'after-swap')
12
25
  Log.info "Active stack has been set to #{inactive_stack.name}"
13
26
  delete_stack(active_stack) if active_stack && !keep_previous_stack
@@ -22,8 +35,8 @@ module CfDeployer
22
35
 
23
36
  def switch
24
37
  check_blue_green_not_both_active 'Switch'
25
- raise ApplicationError.new('Only one color stack exists, cannot switch to a non-existent version!') unless both_stacks_exist?
26
- swap_group true
38
+ raise ApplicationError.new('Both stacks must exist to switch.') unless both_stacks_exist?
39
+ cool_inactive_on_failure { swap_group true }
27
40
  end
28
41
 
29
42
  private
@@ -35,7 +48,7 @@ module CfDeployer
35
48
 
36
49
  def swap_group is_switching_to_cooled = false
37
50
  is_switching_to_cooled ? warm_up_cooled_stack : warm_up_inactive_stack
38
- cool_down_active_stack if active_stack && (is_switching_to_cooled || keep_previous_stack)
51
+ cool_down(active_stack) if active_stack && (is_switching_to_cooled || keep_previous_stack)
39
52
  end
40
53
 
41
54
  def keep_previous_stack
@@ -56,8 +69,8 @@ module CfDeployer
56
69
  warm_up_stack(inactive_stack, active_stack, true)
57
70
  end
58
71
 
59
- def cool_down_active_stack
60
- get_active_asgs(active_stack).each do |id|
72
+ def cool_down stack
73
+ get_active_asgs(stack).each do |id|
61
74
  asg_driver(id).cool_down
62
75
  end
63
76
  end
@@ -3,7 +3,7 @@ module CfDeployer
3
3
  class AutoScalingGroup
4
4
  extend Forwardable
5
5
 
6
- def_delegators :aws_group, :auto_scaling_instances, :ec2_instances, :load_balancers
6
+ def_delegators :aws_group, :auto_scaling_instances, :ec2_instances, :load_balancers, :desired_capacity
7
7
 
8
8
  attr_reader :group_name, :group
9
9
 
@@ -23,7 +23,7 @@ module CfDeployer
23
23
 
24
24
  CfDeployer::Driver::DryRun.guard "Skipping ASG warmup" do
25
25
  aws_group.set_desired_capacity desired
26
- wait_for_healthy_instance desired
26
+ wait_for_desired_capacity
27
27
  end
28
28
  end
29
29
 
@@ -50,37 +50,54 @@ module CfDeployer
50
50
  instance_info
51
51
  end
52
52
 
53
- private
54
-
55
- def healthy_instance_count
56
- begin
57
- count = auto_scaling_instances.count do |instance|
58
- instance.health_status == 'HEALTHY' && (load_balancers.empty? || instance_in_service?( instance.ec2_instance ))
53
+ def wait_for_desired_capacity
54
+ Timeout::timeout(@timeout){
55
+ until desired_capacity_reached?
56
+ sleep 15
59
57
  end
60
- Log.info "Healthy instance count: #{count}"
61
- count
62
- rescue => e
63
- Log.info "Unable to determine healthy instance count due to error: #{e.message}"
64
- -1
65
- end
58
+ }
66
59
  end
67
60
 
68
- def instance_in_service? instance
69
- load_balancers.all? do |load_balancer|
70
- load_balancer.instances.health.any? do |health|
71
- health[:instance] == instance ? health[:state] == 'InService' : false
72
- end
61
+ def desired_capacity_reached?
62
+ healthy_instance_count >= desired_capacity
63
+ end
64
+
65
+ def healthy_instance_ids
66
+ instances = auto_scaling_instances.select do |instance|
67
+ 'HEALTHY'.casecmp(instance.health_status) == 0
73
68
  end
69
+ instances.map(&:id)
74
70
  end
75
71
 
76
- def wait_for_healthy_instance number
77
- Timeout::timeout(@timeout){
78
- while healthy_instance_count != number
79
- sleep 15
80
- end
81
- }
72
+ def in_service_instance_ids
73
+ elbs = load_balancers
74
+ return [] if elbs.empty?
75
+
76
+ ids = elbs.collect(&:instances)
77
+ .collect(&:health)
78
+ .to_a
79
+ .collect { |elb_healths|
80
+ elb_healths.select { |health| health[:state] == 'InService' }
81
+ .map { |health| health[:instance].id }
82
+ }
83
+
84
+ ids.inject(:&)
82
85
  end
83
86
 
87
+ def healthy_instance_count
88
+ AWS.memoize do
89
+ instances = healthy_instance_ids
90
+ instances &= in_service_instance_ids unless load_balancers.empty?
91
+ Log.info "Healthy instance count: #{instances.count}"
92
+ instances.count
93
+ end
94
+ rescue => e
95
+ Log.error "Unable to determine healthy instance count due to error: #{e.message}"
96
+ -1
97
+ end
98
+
99
+ private
100
+
84
101
  def aws_group
85
102
  @my_group ||= AWS::AutoScaling.new.groups[group_name]
86
103
  end
@@ -11,6 +11,10 @@ module CfDeployer
11
11
  log.info message
12
12
  end
13
13
 
14
+ def self.error(message)
15
+ log.error message
16
+ end
17
+
14
18
  def self.log
15
19
  return @log if @log
16
20
  @log = Logger.new('cf_deployer')
@@ -1,3 +1,3 @@
1
1
  module CfDeployer
2
- VERSION = "1.5.0"
2
+ VERSION = "1.6.0"
3
3
  end
@@ -0,0 +1,19 @@
1
+ require 'aws_call_count_spec_helper'
2
+
3
+ describe CfDeployer::Driver::AutoScalingGroup do
4
+ it 'makes the minimum number of calls to AWS when there are 4 instances in the ASG' do
5
+ asg = 'myASG'
6
+
7
+ override_aws_environment(AWS_REGION: 'us-east-1') do
8
+ logs = nil
9
+ allow(CfDeployer::Log).to receive(:error) { |message| logs = message }
10
+ driver = CfDeployer::Driver::AutoScalingGroup.new asg
11
+ VCR.use_cassette("aws_call_count/driver/auto_scaling_group/healthy_instance_count") do
12
+ expect(driver.send(:healthy_instance_count)).to equal(4), "Logs: #{logs}"
13
+ end
14
+
15
+ expect(WebMock).to have_requested(:post, "https://autoscaling.us-east-1.amazonaws.com/").times(1)
16
+ expect(WebMock).to have_requested(:post, "https://elasticloadbalancing.us-east-1.amazonaws.com/").times(1)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'webmock/rspec'
3
+ require 'vcr'
4
+
5
+ VCR.configure do |config|
6
+ config.cassette_library_dir = "fixtures/vcr_cassettes"
7
+ config.hook_into :webmock
8
+ end
9
+
10
+ def override_aws_environment options = {}
11
+ options[:AWS_REGION] ||= 'us-east-1'
12
+ options[:AWS_ACCESS_KEY_ID] ||= 'someId'
13
+ options[:AWS_SECRET_ACCESS_KEY] ||= 'secretKey'
14
+
15
+ override_environment_variables(options) { yield }
16
+ end
17
+
18
+ def override_environment_variables options = {}
19
+ previous_values = override_previous_values(options)
20
+
21
+ yield
22
+
23
+ restore_values(previous_values)
24
+ end
25
+
26
+ def override_previous_values options = {}
27
+ previous_values = options.inject([]) do |memo, (key,value)|
28
+ memo << { key: key.to_s, value: value, existed: ENV.has_key?(key.to_s), old_value: ENV[key.to_s] }
29
+ ENV[key.to_s] = value
30
+ memo
31
+ end
32
+ end
33
+
34
+ def restore_values previous_values = []
35
+ previous_values.each do |value|
36
+ value[:existed] ? ENV[value[:key]] = value[:old_value] : ENV.delete(value[:key])
37
+ end
38
+ end
@@ -3,7 +3,20 @@ Dir.glob("#{File.dirname File.absolute_path(__FILE__)}/fakes/*.rb") { |file| req
3
3
 
4
4
  CfDeployer::Log.log.outputters = nil
5
5
 
6
+ RSPEC_LOG = Logger.new(STDOUT)
7
+ RSPEC_LOG.level = Logger::WARN
8
+
9
+ if ENV['DEBUG']
10
+ RSPEC_LOG.level = Logger::DEBUG
11
+ AWS.config :logger => RSPEC_LOG
12
+ end
13
+
6
14
  def puts *args
7
15
 
8
16
  end
9
17
 
18
+ def ignore_errors
19
+ yield
20
+ rescue => e
21
+ RSPEC_LOG.debug "Intentionally ignoring error: #{e.message}"
22
+ end
@@ -127,6 +127,114 @@ describe 'Auto Scaling Group Swap Deployment Strategy' do
127
127
  end
128
128
  end
129
129
 
130
+ context 'on deployment failure' do
131
+ context 'in stack creation' do
132
+ context 'and only one stack is active' do
133
+ it 'should not cool down the only available stack' do
134
+ strategy = create_strategy(blue: :active)
135
+ error = RuntimeError.new("Error before inactive_stack became active")
136
+
137
+ expect(strategy).to receive(:create_inactive_stack).and_raise(error)
138
+ expect(strategy).to_not receive(:cool_down)
139
+
140
+ ignore_errors { strategy.deploy }
141
+ end
142
+
143
+ it 'should report the deployment as a failure' do
144
+ strategy = create_strategy(blue: :active)
145
+ error = RuntimeError.new("Error before inactive_stack became active")
146
+
147
+ expect(strategy).to receive(:create_inactive_stack).and_raise(error)
148
+ expect { strategy.deploy }.to raise_error(error)
149
+ end
150
+ end
151
+
152
+ context 'and both stacks are active' do
153
+ it 'should cool down inactive stack' do
154
+ strategy = create_strategy(blue: :active)
155
+ inactive_stack = strategy.send(:green_stack)
156
+
157
+ expect(strategy).to receive(:create_inactive_stack) do
158
+ activate_stack(inactive_stack)
159
+ raise RuntimeError.new("Error after inactive_stack became active")
160
+ end
161
+
162
+ expect(strategy).to receive(:cool_down).with(inactive_stack)
163
+ ignore_errors { strategy.deploy }
164
+ end
165
+
166
+ it 'should report the deployment as a failure' do
167
+ strategy = create_strategy(blue: :active)
168
+ inactive_stack = strategy.send(:green_stack)
169
+ error = RuntimeError.new("Error after inactive_stack became active")
170
+
171
+ expect(strategy).to receive(:create_inactive_stack) do
172
+ activate_stack(inactive_stack)
173
+ raise error
174
+ end
175
+
176
+ allow(strategy).to receive(:cool_down)
177
+ expect { strategy.deploy }.to raise_error(error)
178
+ end
179
+ end
180
+ end
181
+
182
+ context 'in asg swap' do
183
+ # This shouldn't be possible - a stack normally becomes active at creation
184
+ context 'and only one stack is active' do
185
+ it 'should not cool down the only available stack' do
186
+ strategy = create_strategy(blue: :active)
187
+ error = RuntimeError.new("Error during swap")
188
+
189
+ # The stack would normally be active after create_inactive_stack
190
+ allow(strategy).to receive(:create_inactive_stack)
191
+ expect(strategy).to receive(:swap_group).and_raise(error)
192
+ expect(strategy).to_not receive(:cool_down)
193
+
194
+ ignore_errors { strategy.deploy }
195
+ end
196
+
197
+ it 'should report the deployment as a failure' do
198
+ strategy = create_strategy(blue: :active)
199
+ error = RuntimeError.new("Error during swap")
200
+
201
+ # The stack would normally be active after create_inactive_stack
202
+ allow(strategy).to receive(:create_inactive_stack)
203
+ expect(strategy).to receive(:swap_group).and_raise(error)
204
+ expect(strategy).to_not receive(:cool_down)
205
+
206
+ expect { strategy.deploy }.to raise_error(error)
207
+ end
208
+ end
209
+
210
+ context 'and both stacks are active' do
211
+ it 'should cool down inactive stack' do
212
+ strategy = create_strategy(blue: :active)
213
+ inactive_stack = strategy.send(:green_stack)
214
+ error = RuntimeError.new("Error during swap")
215
+
216
+ allow(strategy).to receive(:create_inactive_stack) { activate_stack(inactive_stack) }
217
+ expect(strategy).to receive(:swap_group).and_raise(error)
218
+ expect(strategy).to receive(:cool_down).with(inactive_stack)
219
+
220
+ ignore_errors { strategy.deploy }
221
+ end
222
+
223
+ it 'should report the deployment as a failure' do
224
+ strategy = create_strategy(blue: :active)
225
+ inactive_stack = strategy.send(:green_stack)
226
+ error = RuntimeError.new("Error during swap")
227
+
228
+ allow(strategy).to receive(:create_inactive_stack) { activate_stack(inactive_stack) }
229
+ expect(strategy).to receive(:swap_group).and_raise(error)
230
+ expect(strategy).to receive(:cool_down).with(inactive_stack)
231
+
232
+ expect { strategy.deploy }.to raise_error(error)
233
+ end
234
+ end
235
+ end
236
+ end
237
+
130
238
  context 'has active group' do
131
239
  it 'should deploy blue stack if green stack is active' do
132
240
  blue_stack.live!
@@ -306,62 +414,169 @@ describe 'Auto Scaling Group Swap Deployment Strategy' do
306
414
  describe '#switch' do
307
415
  context 'both stacks are active' do
308
416
  it 'should raise an error' do
309
- green_stack.live!
310
- blue_stack.live!
311
- allow(CfDeployer::Driver::AutoScalingGroup).to receive(:new).with('greenASG') { green_asg_driver }
312
- allow(CfDeployer::Driver::AutoScalingGroup).to receive(:new).with('blueASG') { blue_asg_driver }
313
- allow(green_asg_driver).to receive(:describe) { {desired: 1, min: 1, max: 3 } }
314
- allow(blue_asg_driver).to receive(:describe) { {desired: 1, min: 1, max: 3 } }
417
+ strategy = create_strategy(blue: :active, green: :active)
418
+ error = 'Found both auto-scaling-groups, ["greenASG", "blueASG"], in green and blue stacks are active. Switch aborted!'
315
419
 
316
- strategy = CfDeployer::DeploymentStrategy.create(app, env, component, context)
317
- expect{strategy.switch}.to raise_error 'Found both auto-scaling-groups, ["greenASG", "blueASG"], in green and blue stacks are active. Switch aborted!'
420
+ expect { strategy.switch }.to raise_error(error)
318
421
  end
319
422
  end
320
423
 
321
424
  context 'both stacks do not exist' do
322
- it 'should raise an error' do
323
- green_stack.live!
324
- blue_stack.die!
325
- allow(CfDeployer::Driver::AutoScalingGroup).to receive(:new).with('greenASG') { green_asg_driver }
326
- allow(CfDeployer::Driver::AutoScalingGroup).to receive(:new).with('blueASG') { blue_asg_driver }
327
- allow(green_asg_driver).to receive(:describe) { {desired: 1, min: 1, max: 3 } }
425
+ let(:error) { 'Both stacks must exist to switch.' }
426
+
427
+ context '(only green exists)' do
428
+ it 'should raise an error' do
429
+ strategy = create_strategy(green: :active, blue: :dead)
430
+
431
+ expect { strategy.switch }.to raise_error(error)
432
+ end
433
+ end
434
+
435
+ context '(only blue exists)' do
436
+ it 'should raise an error' do
437
+ strategy = create_strategy(green: :dead, blue: :active)
438
+
439
+ expect { strategy.switch }.to raise_error(error)
440
+ end
441
+ end
328
442
 
329
- strategy = CfDeployer::DeploymentStrategy.create(app, env, component, context)
330
- expect{ strategy.switch }.to raise_error 'Only one color stack exists, cannot switch to a non-existent version!'
443
+ context '(no stack exists)' do
444
+ it 'should raise an error' do
445
+ strategy = create_strategy(green: :dead, blue: :dead)
446
+
447
+ expect { strategy.switch }.to raise_error(error)
448
+ end
331
449
  end
332
450
  end
333
451
 
334
452
  context 'green stack is active' do
453
+ let(:strategy) { create_strategy(green: :active, blue: :inactive) }
454
+
335
455
  it 'should warm up blue stack and cool down green stack' do
336
- green_stack.live!
337
- blue_stack.live!
338
- allow(CfDeployer::Driver::AutoScalingGroup).to receive(:new).with('blueASG') { blue_asg_driver }
339
- allow(CfDeployer::Driver::AutoScalingGroup).to receive(:new).with('greenASG') { green_asg_driver }
340
- options = {desired: 5, min: 3, max: 7}
341
- allow(green_asg_driver).to receive(:describe) {options}
342
- allow(blue_asg_driver).to receive(:describe) {{desired: 0, min: 0, max: 0}}
456
+ active_stack = strategy.send(:green_stack)
457
+ inactive_stack = strategy.send(:blue_stack)
458
+
459
+ expect(strategy).to receive(:warm_up_stack).with(inactive_stack, active_stack, true)
460
+ expect(strategy).to receive(:cool_down).with(active_stack)
461
+
462
+ strategy.switch
463
+ end
464
+
465
+ context 'swap fails' do
466
+ context 'before blue stack becomes active' do
467
+ let(:error) { 'Error before inactive stack becomes active' }
468
+
469
+ it 'does not cool down any stack' do
470
+ expect(strategy).to receive(:warm_up_stack).and_raise(error)
471
+ expect(strategy).to_not receive(:cool_down)
472
+
473
+ ignore_errors { strategy.switch }
474
+ end
343
475
 
344
- expect(blue_asg_driver).to receive(:warm_up_cooled_group).with(options)
345
- expect(green_asg_driver).to receive(:cool_down)
476
+ it 'reports the switch as a failure' do
477
+ expect(strategy).to receive(:warm_up_stack).and_raise(error)
478
+ allow(strategy).to receive(:cool_down)
479
+
480
+ expect { strategy.switch }.to raise_error(error)
481
+ end
482
+ end
483
+
484
+ context 'after both stacks became active' do
485
+ let(:error) { 'Error after inactive stack becomes active ' }
486
+
487
+ it 'cools down the blue stack' do
488
+ expect(strategy).to receive(:warm_up_stack) do
489
+ expect(strategy).to receive(:both_stacks_active?).and_return(true)
490
+ raise error
491
+ end
492
+
493
+ inactive_stack = strategy.send(:blue_stack)
494
+ expect(strategy).to receive(:cool_down).with(inactive_stack)
495
+
496
+ ignore_errors { strategy.switch }
497
+ end
498
+
499
+ it 'reports the switch as a failure' do
500
+ expect(strategy).to receive(:warm_up_stack) do
501
+ expect(strategy).to receive(:both_stacks_active?).and_return(true)
502
+ raise error
503
+ end
504
+
505
+ expect { strategy.switch }.to raise_error(error)
506
+ end
507
+ end
508
+ end
509
+ end
510
+
511
+ context 'blue stack is active' do
512
+ let(:strategy) { create_strategy(green: :inactive, blue: :active) }
513
+
514
+ it 'should warm up green stack and cool down blue stack' do
515
+ active_stack = strategy.send(:blue_stack)
516
+ inactive_stack = strategy.send(:green_stack)
517
+
518
+ expect(strategy).to receive(:warm_up_stack).with(inactive_stack, active_stack, true)
519
+ expect(strategy).to receive(:cool_down).with(active_stack)
346
520
 
347
- strategy = CfDeployer::DeploymentStrategy.create(app, env, component, context)
348
521
  strategy.switch
349
522
  end
523
+
524
+ context 'swap fails' do
525
+ context 'before green stack becomes active' do
526
+ let(:error) { 'Error before inactive stack becomes active' }
527
+
528
+ it 'does not cool down any stack' do
529
+ expect(strategy).to receive(:warm_up_stack).and_raise(error)
530
+ expect(strategy).to_not receive(:cool_down)
531
+
532
+ ignore_errors { strategy.switch }
533
+ end
534
+
535
+ it 'reports the switch as a failure' do
536
+ expect(strategy).to receive(:warm_up_stack).and_raise(error)
537
+ allow(strategy).to receive(:cool_down)
538
+
539
+ expect { strategy.switch }.to raise_error(error)
540
+ end
541
+ end
542
+
543
+ context 'after both stacks become active' do
544
+ let(:error) { 'Error after inactive stack becomes active ' }
545
+
546
+ it 'cools down the green stack' do
547
+ expect(strategy).to receive(:warm_up_stack) do
548
+ expect(strategy).to receive(:both_stacks_active?).and_return(true)
549
+ raise error
550
+ end
551
+
552
+ inactive_stack = strategy.send(:green_stack)
553
+ expect(strategy).to receive(:cool_down).with(inactive_stack)
554
+
555
+ ignore_errors { strategy.switch }
556
+ end
557
+
558
+ it 'reports the switch as a failure' do
559
+ expect(strategy).to receive(:warm_up_stack) do
560
+ expect(strategy).to receive(:both_stacks_active?).and_return(true)
561
+ raise error
562
+ end
563
+
564
+ expect { strategy.switch }.to raise_error(error)
565
+ end
566
+ end
567
+ end
350
568
  end
351
569
  end
352
570
 
353
- context '#cool_down_active_stack' do
571
+ context '#cool_down' do
354
572
  it 'should cool down only those ASGs which actually exist' do
355
573
  blue_stack.live!
356
- green_stack.die!
357
- allow(CfDeployer::Driver::AutoScalingGroup).to receive(:new).with('greenASG') { green_asg_driver }
358
574
  allow(CfDeployer::Driver::AutoScalingGroup).to receive(:new).with('blueASG') { blue_asg_driver }
359
- allow(green_asg_driver).to receive(:describe) { {desired: 0, min: 0, max: 0 } }
360
575
  allow(blue_asg_driver).to receive(:describe) { {desired: 1, min: 1, max: 3 } }
361
576
 
362
577
  strategy = CfDeployer::DeploymentStrategy.create(app, env, component, context)
363
578
  expect(blue_asg_driver).to receive(:cool_down)
364
- strategy.send(:cool_down_active_stack)
579
+ strategy.send(:cool_down, blue_stack)
365
580
  end
366
581
  end
367
582
 
@@ -488,4 +703,66 @@ describe 'Auto Scaling Group Swap Deployment Strategy' do
488
703
  asg_swap.send(:stack_active?, blue_stack).should be(true)
489
704
  end
490
705
  end
706
+
707
+ def default_options
708
+ {
709
+ app_name: 'app',
710
+ environment: 'environment',
711
+ component: 'component',
712
+ context: {
713
+ :'deployment-strategy' => 'auto-scaling-group-swap',
714
+ :settings => {
715
+ :'auto-scaling-group-name-output' => ['AutoScalingGroupID']
716
+ }
717
+ }
718
+ }
719
+ end
720
+
721
+ def create_strategy original_options = {}
722
+ options = default_options.merge(original_options)
723
+
724
+ create_stack(:blue, options.delete(:blue) || :dead, options)
725
+ create_stack(:green, options.delete(:green) || :dead, options)
726
+
727
+ CfDeployer::DeploymentStrategy.create(options[:app_name], options[:environment], options[:component], options[:context])
728
+ end
729
+
730
+ def create_stack color, status = :active, original_options = {}
731
+ options = default_options.merge(original_options)
732
+
733
+ stack = Fakes::Stack.new(name: color.to_s, outputs: {'web-elb-name' => "#{color}-elb"}, parameters: { name: color.to_s})
734
+
735
+ stack_color_name = (color.to_s == 'green' ? 'G' : 'B')
736
+ stack_name = "#{options[:app_name]}-#{options[:environment]}-#{options[:component]}-#{stack_color_name}"
737
+ allow(CfDeployer::Stack).to receive(:new).with(stack_name, options[:component], options[:context]).and_return(stack)
738
+
739
+ stack.tap do
740
+ case status
741
+ when :active; activate_stack(stack)
742
+ when :inactive; activate_stack(stack, { desired: 0, max: 0, min: 0 })
743
+ when :dead; kill_stack(stack)
744
+ else raise "Trying to create stack with unknown status; #{status}"
745
+ end
746
+ end
747
+ end
748
+
749
+ def activate_stack stack, instances = {}
750
+ stack.live!
751
+
752
+ allow(stack).to receive(:output).with('AutoScalingGroupID').and_return("#{stack.name}ASG")
753
+ allow(stack).to receive(:find_output).with('AutoScalingGroupID').and_return("#{stack.name}ASG")
754
+ allow(stack).to receive(:resource_statuses).and_return(asg_ids("#{stack.name}ASG"))
755
+
756
+ asg_driver = double("#{stack.name}_asg_driver")
757
+ instances[:desired] ||= 2
758
+ instances[:min] ||= 1
759
+ instances[:max] ||= 5
760
+
761
+ allow(asg_driver).to receive(:describe).and_return(instances)
762
+ allow(CfDeployer::Driver::AutoScalingGroup).to receive(:new).with("#{stack.name}ASG") { asg_driver }
763
+ end
764
+
765
+ def kill_stack stack
766
+ stack.die!
767
+ end
491
768
  end
@@ -27,79 +27,152 @@ describe 'Autoscaling group driver' do
27
27
 
28
28
  describe '#warm_up' do
29
29
  it 'should warm up the group to the desired size' do
30
- expect(group).to receive(:auto_scaling_instances){[instance1, instance2]}
31
30
  expect(group).to receive(:set_desired_capacity).with(2)
31
+ expect(@driver).to receive(:wait_for_desired_capacity)
32
32
  @driver.warm_up 2
33
33
  end
34
34
 
35
35
  it 'should wait for the warm up of the group even if desired is the same as the minimum' do
36
- expect(group).to receive(:auto_scaling_instances){[instance2]}
37
36
  expect(group).to receive(:set_desired_capacity).with(1)
37
+ expect(@driver).to receive(:wait_for_desired_capacity)
38
38
  @driver.warm_up 1
39
39
  end
40
40
 
41
41
  it 'should ignore warming up if desired number is less than min size of the group' do
42
42
  expect(group).not_to receive(:set_desired_capacity)
43
+ expect(@driver).not_to receive(:wait_for_desired_capacity)
43
44
  @driver.warm_up 0
44
45
  end
45
46
 
46
47
  it 'should warm up to maximum if desired number is greater than maximum size of group' do
47
- expect(group).to receive(:auto_scaling_instances){[instance1, instance2, instance3, instance4]}
48
48
  expect(group).to receive(:set_desired_capacity).with(4)
49
+ expect(@driver).to receive(:wait_for_desired_capacity)
49
50
  @driver.warm_up 5
50
51
  end
51
52
  end
52
53
 
53
- describe '#healthy_instance_count' do
54
- it 'should respond with the number of instances that are HEALTHY' do
55
- instance5 = double('instance1', :health_status => 'UNHEALTHY')
56
- allow(group).to receive(:auto_scaling_instances){[instance1, instance2, instance3, instance4, instance5]}
57
- expect(@driver.send(:healthy_instance_count)).to eql 4
54
+ describe '#healthy_instance_ids' do
55
+ it 'returns the ids of all instances that are healthy' do
56
+ instance1 = double('instance1', :health_status => 'HEALTHY', id: 'instance1')
57
+ instance2 = double('instance2', :health_status => 'HEALTHY', id: 'instance2')
58
+ instance3 = double('instance3', :health_status => 'UNHEALTHY', id: 'instance3')
59
+ instance4 = double('instance4', :health_status => 'HEALTHY', id: 'instance4')
60
+ allow(group).to receive(:auto_scaling_instances){[instance1, instance2, instance3, instance4]}
61
+
62
+ expect(@driver.healthy_instance_ids).to eql ['instance1', 'instance2', 'instance4']
58
63
  end
59
64
 
60
- it 'health check should be resilient against intermittent errors' do
61
- instance5 = double('instance5')
62
- expect(instance5).to receive(:health_status).and_raise(StandardError)
63
- allow(group).to receive(:auto_scaling_instances){ [ instance5 ] }
64
- expect(@driver.send(:healthy_instance_count)).to eql -1
65
+ it 'returns the ids of all instances that are healthy (case insensitive)' do
66
+ instance1 = double('instance1', :health_status => 'HealThy', id: 'instance1')
67
+ allow(group).to receive(:auto_scaling_instances){[instance1]}
68
+
69
+ expect(@driver.healthy_instance_ids).to eql ['instance1']
65
70
  end
71
+ end
66
72
 
67
- context 'when an elb is associated with the auto scaling group' do
68
- it 'should not include instances that are HEALTHY but not associated with the elb' do
69
- instance_collection = double('instance_collection', :health => [{:instance => ec2_instance1, :state => 'InService'}])
70
- load_balancer = double('load_balancer', :instances => instance_collection)
71
- allow(group).to receive(:load_balancers) { [load_balancer] }
72
- allow(group).to receive(:auto_scaling_instances) { [instance1, instance2] }
73
+ describe '#in_service_instance_ids' do
74
+ context 'when there are no load balancers' do
75
+ it 'returns no ids' do
76
+ allow(group).to receive(:load_balancers).and_return([])
73
77
 
74
- expect(@driver.send(:healthy_instance_count)).to eql 1
78
+ expect(@driver.in_service_instance_ids).to eq []
75
79
  end
80
+ end
76
81
 
77
- it 'should only include instances registered with an elb that are InService' do
78
- allow(group).to receive(:auto_scaling_instances) { [instance1, instance2, instance3] }
79
- instance_collection = double('instance_collection', :health => [{:instance => ec2_instance1, :state => 'InService'},
80
- {:instance => ec2_instance2, :state => 'OutOfService'},
81
- {:instance => ec2_instance3, :state => 'OutOfService'}])
82
- load_balancer = double('load_balancer', :instances => instance_collection)
83
- allow(group).to receive(:load_balancers) { [load_balancer] }
82
+ context 'when there is only 1 elb' do
83
+ it 'returns the ids of all instances that are in service' do
84
+ health1 = { state: 'InService', instance: double('i1', id: 'instance1') }
85
+ health2 = { state: 'OutOfService', instance: double('i2', id: 'instance2') }
86
+ health3 = { state: 'InService', instance: double('i3', id: 'instance3') }
87
+ health4 = { state: 'InService', instance: double('i4', id: 'instance4') }
84
88
 
85
- expect(@driver.send(:healthy_instance_count)).to eql 1
89
+ instance_collection = double('instance_collection', health: [health1, health2, health3, health4])
90
+ elb = double('elb', instances: instance_collection)
91
+ allow(group).to receive(:load_balancers).and_return([ elb ])
92
+
93
+ expect(@driver.in_service_instance_ids).to eql ['instance1', 'instance3', 'instance4']
86
94
  end
87
95
  end
88
96
 
89
- context 'when there are multiple elbs for an auto scaling group' do
90
- it 'should not include instances that are not registered with all load balancers' do
91
- instance_collection1 = double('instance_collection1', :health => [{:instance => ec2_instance1, :state => 'InService'}])
92
- instance_collection2 = double('instance_collection2', :health => [])
93
- load_balancer1 = double('load_balancer1', :instances => instance_collection1)
94
- load_balancer2 = double('load_balancer2', :instances => instance_collection2)
95
- allow(group).to receive(:load_balancers) { [load_balancer1, load_balancer2] }
96
- allow(group).to receive(:auto_scaling_instances) { [instance1] }
97
+ context 'when there are multiple elbs' do
98
+ it 'returns only the ids of instances that are in all ELBs' do
99
+ health1 = { state: 'InService', instance: double('i1', id: 'instance1') }
100
+ health2 = { state: 'InService', instance: double('i2', id: 'instance2') }
101
+ health3 = { state: 'InService', instance: double('i3', id: 'instance3') }
102
+ health4 = { state: 'InService', instance: double('i4', id: 'instance4') }
103
+ health5 = { state: 'InService', instance: double('i5', id: 'instance5') }
104
+
105
+ instance_collection1 = double('instance_collection1', health: [health1, health2, health3])
106
+ instance_collection2 = double('instance_collection2', health: [health2, health3, health4])
107
+ instance_collection3 = double('instance_collection3', health: [health2, health3, health5])
108
+ elb1 = double('elb1', instances: instance_collection1)
109
+ elb2 = double('elb2', instances: instance_collection2)
110
+ elb3 = double('elb3', instances: instance_collection3)
111
+ allow(group).to receive(:load_balancers).and_return([ elb1, elb2, elb3 ])
112
+
113
+ # Only instance 2 and 3 are associated with all ELB's
114
+ expect(@driver.in_service_instance_ids).to eql ['instance2', 'instance3']
115
+ end
116
+
117
+ it 'returns only the ids instances that are InService in all ELBs' do
118
+ health11 = { state: 'OutOfService', instance: double('i1', id: 'instance1') }
119
+ health12 = { state: 'InService', instance: double('i2', id: 'instance2') }
120
+ health13 = { state: 'InService', instance: double('i3', id: 'instance3') }
121
+
122
+ health21 = { state: 'InService', instance: double('i1', id: 'instance1') }
123
+ health22 = { state: 'InService', instance: double('i2', id: 'instance2') }
124
+ health23 = { state: 'OutOfService', instance: double('i3', id: 'instance3') }
125
+
126
+ health31 = { state: 'InService', instance: double('i1', id: 'instance1') }
127
+ health32 = { state: 'InService', instance: double('i2', id: 'instance2') }
128
+ health33 = { state: 'InService', instance: double('i3', id: 'instance3') }
129
+
130
+ instance_collection1 = double('instance_collection1', health: [health11, health12, health13])
131
+ instance_collection2 = double('instance_collection2', health: [health21, health22, health23])
132
+ instance_collection3 = double('instance_collection3', health: [health31, health32, health33])
97
133
 
98
- expect(@driver.send(:healthy_instance_count)).to eql 0
134
+ elb1 = double('elb1', instances: instance_collection1)
135
+ elb2 = double('elb2', instances: instance_collection2)
136
+ elb3 = double('elb3', instances: instance_collection3)
137
+
138
+ allow(group).to receive(:load_balancers).and_return([ elb1, elb2, elb3 ])
139
+
140
+ # Only instance 2 is InService across all ELB's
141
+ expect(@driver.in_service_instance_ids).to eql ['instance2']
99
142
  end
100
143
  end
101
144
  end
102
145
 
146
+ describe '#healthy_instance_count' do
147
+ context 'when there are no load balancers' do
148
+ it 'should return the number of healthy instances' do
149
+ healthy_instance_ids = ['1', '3', '4', '5']
150
+ allow(@driver).to receive(:load_balancers).and_return([])
151
+ expect(@driver).to receive(:healthy_instance_ids).and_return(healthy_instance_ids)
152
+
153
+ expect(@driver.healthy_instance_count).to eql(healthy_instance_ids.count)
154
+ end
155
+ end
156
+
157
+ context 'when load balancers exist' do
158
+ it 'should return the number of instances that are both healthy, and in service' do
159
+ healthy_instance_ids = ['1', '3', '4', '5']
160
+ in_service_instance_ids = ['3', '4']
161
+ allow(@driver).to receive(:load_balancers).and_return(double('elb', empty?: false))
162
+ expect(@driver).to receive(:healthy_instance_ids).and_return(healthy_instance_ids)
163
+ expect(@driver).to receive(:in_service_instance_ids).and_return(in_service_instance_ids)
164
+
165
+ # Only instances 3 and 4 are both healthy and in service
166
+ expect(@driver.healthy_instance_count).to eql(2)
167
+ end
168
+ end
169
+
170
+ it 'health check should be resilient against intermittent errors' do
171
+ expect(@driver).to receive(:healthy_instance_ids).and_raise("Some error")
172
+ expect(@driver.healthy_instance_count).to eql -1
173
+ end
174
+ end
175
+
103
176
  describe '#cool_down' do
104
177
  it 'should cool down group' do
105
178
  expect(group).to receive(:update).with({min_size: 0, max_size: 0})
@@ -113,7 +186,7 @@ describe 'Autoscaling group driver' do
113
186
  hash = {:max => 5, :min => 2, :desired => 3}
114
187
  allow(group).to receive(:auto_scaling_instances){[instance1, instance2, instance3]}
115
188
  expect(group).to receive(:update).with({:min_size => 2, :max_size => 5})
116
- expect(group).to receive(:set_desired_capacity).with(3)
189
+ expect(@driver).to receive(:warm_up).with(3)
117
190
  @driver.warm_up_cooled_group hash
118
191
  end
119
192
  end
@@ -131,4 +204,47 @@ describe 'Autoscaling group driver' do
131
204
  expect(@driver.instance_statuses).to eq( { 'i-abcd1234' => returned_status } )
132
205
  end
133
206
  end
207
+
208
+ describe '#wait_for_desired_capacity' do
209
+ it 'completes if desired capacity reached' do
210
+ expect(@driver).to receive(:desired_capacity_reached?).and_return(true)
211
+
212
+ @driver.wait_for_desired_capacity
213
+ end
214
+
215
+ it 'times out if desired capacity is not reached' do
216
+ expect(@driver).to receive(:desired_capacity_reached?).and_return(false)
217
+
218
+ expect { @driver.wait_for_desired_capacity }.to raise_error(Timeout::Error)
219
+ end
220
+ end
221
+
222
+ describe '#desired_capacity_reached?' do
223
+ it 'returns true if healthy instance count matches desired capacity' do
224
+ expected_number = 5
225
+
226
+ expect(group).to receive(:desired_capacity).and_return(expected_number)
227
+ expect(@driver).to receive(:healthy_instance_count).and_return(expected_number)
228
+
229
+ expect(@driver.desired_capacity_reached?).to be_true
230
+ end
231
+
232
+ it 'returns false if healthy instance count is less than desired capacity' do
233
+ expected_number = 5
234
+
235
+ expect(group).to receive(:desired_capacity).and_return(expected_number)
236
+ expect(@driver).to receive(:healthy_instance_count).and_return(expected_number - 1)
237
+
238
+ expect(@driver.desired_capacity_reached?).to be_false
239
+ end
240
+
241
+ it 'returns true if healthy instance count is more than desired capacity' do
242
+ expected_number = 5
243
+
244
+ expect(group).to receive(:desired_capacity).and_return(expected_number)
245
+ expect(@driver).to receive(:healthy_instance_count).and_return(expected_number + 1)
246
+
247
+ expect(@driver.desired_capacity_reached?).to be_true
248
+ end
249
+ end
134
250
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cf_deployer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jame Brechtel
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2016-07-11 00:00:00.000000000 Z
14
+ date: 2016-10-04 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: aws-sdk
@@ -139,6 +139,34 @@ dependencies:
139
139
  - - ~>
140
140
  - !ruby/object:Gem::Version
141
141
  version: 10.3.0
142
+ - !ruby/object:Gem::Dependency
143
+ name: webmock
144
+ requirement: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ~>
147
+ - !ruby/object:Gem::Version
148
+ version: 2.1.0
149
+ type: :development
150
+ prerelease: false
151
+ version_requirements: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ~>
154
+ - !ruby/object:Gem::Version
155
+ version: 2.1.0
156
+ - !ruby/object:Gem::Dependency
157
+ name: vcr
158
+ requirement: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ~>
161
+ - !ruby/object:Gem::Version
162
+ version: 2.9.3
163
+ type: :development
164
+ prerelease: false
165
+ version_requirements: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ~>
168
+ - !ruby/object:Gem::Version
169
+ version: 2.9.3
142
170
  description: For automatic blue green deployment flow on CloudFormation.
143
171
  email:
144
172
  - jbrechtel@gmail.com
@@ -162,6 +190,7 @@ files:
162
190
  - Rakefile
163
191
  - bin/cf_deploy
164
192
  - cf_deployer.gemspec
193
+ - fixtures/vcr_cassettes/aws_call_count/driver/auto_scaling_group/healthy_instance_count.yml
165
194
  - lib/cf_deployer.rb
166
195
  - lib/cf_deployer/application.rb
167
196
  - lib/cf_deployer/application_error.rb
@@ -188,6 +217,8 @@ files:
188
217
  - lib/cf_deployer/stack.rb
189
218
  - lib/cf_deployer/status_presenter.rb
190
219
  - lib/cf_deployer/version.rb
220
+ - spec/aws_call_count/driver/auto_scaling_group_spec.rb
221
+ - spec/aws_call_count_spec_helper.rb
191
222
  - spec/fakes/instance.rb
192
223
  - spec/fakes/route53_client.rb
193
224
  - spec/fakes/stack.rb
@@ -240,6 +271,8 @@ specification_version: 4
240
271
  summary: Support multiple components deployment using CloudFormation templates with
241
272
  multiple blue green strategies.
242
273
  test_files:
274
+ - spec/aws_call_count/driver/auto_scaling_group_spec.rb
275
+ - spec/aws_call_count_spec_helper.rb
243
276
  - spec/fakes/instance.rb
244
277
  - spec/fakes/route53_client.rb
245
278
  - spec/fakes/stack.rb