cf_deployer 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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