capistrano-asg-rolling 0.3.0 → 0.4.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
  SHA256:
3
- metadata.gz: 8df47c0498d2086c65ea0b92eb4ac4fbcce15f253e6752ad1cbe1fedcf58abe5
4
- data.tar.gz: 1ec79f6ab87ce44a220e003ec47dd9cc35843f824dc389756c69c4e0c4418265
3
+ metadata.gz: 517d95aa2350605f6a9fb472c3c5b293916946747936f2850219f65f6eb4b439
4
+ data.tar.gz: 159c94f7e0c371d0e7d1877fbc73ad40e923fc6e8c24e22ab586b7c603c98999
5
5
  SHA512:
6
- metadata.gz: 70325e34622519ef8b91cc6d324ca0e22ebd61f7c69ce0011c1f0b958845ecacd8a0fbeba0985ca192c83b323e8a307dfb067271b2db0ade59332e7205062126
7
- data.tar.gz: 9fa3da49333f7721986593de03f764d4bb75809ce2401b654de45548610d5b73c65357ccb88205d7516c55cf91d7191bef03c8dc1def11f091762cbd9e4cc33d
6
+ metadata.gz: 628c4200fb0e4e81370829d700bc0206a81390ad4392d97459c20f8c358a72a1882bcacd829cb69dcb816f9c2f5ed39ef5f8c05c35e7abc7231defa41fce98c0
7
+ data.tar.gz: 688a2886933cb90fd2781946e09fd54bd8d72fbbe450256fdd8c16302465007652c34194047a9a49e87226a7a0f823aeba6f5ddc9f210102edf1e6622afa495c
data/.rubocop.yml CHANGED
@@ -21,6 +21,9 @@ Metrics/AbcSize:
21
21
  Metrics/BlockLength:
22
22
  Enabled: false
23
23
 
24
+ Metrics/ClassLength:
25
+ Enabled: false
26
+
24
27
  Metrics/MethodLength:
25
28
  Enabled: false
26
29
 
data/Gemfile CHANGED
@@ -7,7 +7,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
7
7
  # Specify your gem's dependencies in capistrano-asg-rolling.gemspec
8
8
  gemspec
9
9
 
10
- gem 'rubocop', '~> 1.52.0'
11
- gem 'rubocop-performance', '~> 1.18.0'
10
+ gem 'rubocop', '~> 1.56.3'
11
+ gem 'rubocop-performance', '~> 1.19.0'
12
12
  gem 'rubocop-rake', '~> 0.6.0'
13
- gem 'rubocop-rspec', '~> 2.22.0'
13
+ gem 'rubocop-rspec', '~> 2.24.0'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capistrano-asg-rolling (0.3.0)
4
+ capistrano-asg-rolling (0.4.0)
5
5
  aws-sdk-autoscaling (~> 1, >= 1.67.0)
6
6
  aws-sdk-ec2 (~> 1)
7
7
  capistrano (~> 3)
@@ -10,26 +10,27 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- addressable (2.8.4)
13
+ addressable (2.8.5)
14
14
  public_suffix (>= 2.0.2, < 6.0)
15
- airbrussh (1.4.1)
15
+ airbrussh (1.5.0)
16
16
  sshkit (>= 1.6.1, != 1.7.0)
17
17
  ast (2.4.2)
18
18
  aws-eventstream (1.2.0)
19
- aws-partitions (1.774.0)
20
- aws-sdk-autoscaling (1.90.0)
21
- aws-sdk-core (~> 3, >= 3.174.0)
19
+ aws-partitions (1.830.0)
20
+ aws-sdk-autoscaling (1.98.0)
21
+ aws-sdk-core (~> 3, >= 3.184.0)
22
22
  aws-sigv4 (~> 1.1)
23
- aws-sdk-core (3.174.0)
23
+ aws-sdk-core (3.184.0)
24
24
  aws-eventstream (~> 1, >= 1.0.2)
25
25
  aws-partitions (~> 1, >= 1.651.0)
26
26
  aws-sigv4 (~> 1.5)
27
27
  jmespath (~> 1, >= 1.6.1)
28
- aws-sdk-ec2 (1.381.0)
29
- aws-sdk-core (~> 3, >= 3.174.0)
28
+ aws-sdk-ec2 (1.410.0)
29
+ aws-sdk-core (~> 3, >= 3.184.0)
30
30
  aws-sigv4 (~> 1.1)
31
- aws-sigv4 (1.5.2)
31
+ aws-sigv4 (1.6.0)
32
32
  aws-eventstream (~> 1, >= 1.0.2)
33
+ base64 (0.1.1)
33
34
  capistrano (3.17.3)
34
35
  airbrussh (>= 1.0.0)
35
36
  i18n
@@ -44,17 +45,20 @@ GEM
44
45
  concurrent-ruby (~> 1.0)
45
46
  jmespath (1.6.2)
46
47
  json (2.6.3)
48
+ language_server-protocol (3.17.0.3)
47
49
  net-scp (4.0.0)
48
50
  net-ssh (>= 2.6.5, < 8.0.0)
49
- net-ssh (7.1.0)
51
+ net-ssh (7.2.0)
50
52
  parallel (1.23.0)
51
- parser (3.2.2.1)
53
+ parser (3.2.2.3)
52
54
  ast (~> 2.4.1)
53
- public_suffix (5.0.1)
55
+ racc
56
+ public_suffix (5.0.3)
57
+ racc (1.7.1)
54
58
  rainbow (3.1.1)
55
59
  rake (13.0.6)
56
- regexp_parser (2.8.0)
57
- rexml (3.2.5)
60
+ regexp_parser (2.8.1)
61
+ rexml (3.2.6)
58
62
  rspec (3.12.0)
59
63
  rspec-core (~> 3.12.0)
60
64
  rspec-expectations (~> 3.12.0)
@@ -64,41 +68,43 @@ GEM
64
68
  rspec-expectations (3.12.3)
65
69
  diff-lcs (>= 1.2.0, < 2.0)
66
70
  rspec-support (~> 3.12.0)
67
- rspec-mocks (3.12.5)
71
+ rspec-mocks (3.12.6)
68
72
  diff-lcs (>= 1.2.0, < 2.0)
69
73
  rspec-support (~> 3.12.0)
70
- rspec-support (3.12.0)
71
- rubocop (1.52.0)
74
+ rspec-support (3.12.1)
75
+ rubocop (1.56.4)
76
+ base64 (~> 0.1.1)
72
77
  json (~> 2.3)
78
+ language_server-protocol (>= 3.17.0)
73
79
  parallel (~> 1.10)
74
- parser (>= 3.2.0.0)
80
+ parser (>= 3.2.2.3)
75
81
  rainbow (>= 2.2.2, < 4.0)
76
82
  regexp_parser (>= 1.8, < 3.0)
77
83
  rexml (>= 3.2.5, < 4.0)
78
- rubocop-ast (>= 1.28.0, < 2.0)
84
+ rubocop-ast (>= 1.28.1, < 2.0)
79
85
  ruby-progressbar (~> 1.7)
80
86
  unicode-display_width (>= 2.4.0, < 3.0)
81
87
  rubocop-ast (1.29.0)
82
88
  parser (>= 3.2.1.0)
83
- rubocop-capybara (2.18.0)
89
+ rubocop-capybara (2.19.0)
84
90
  rubocop (~> 1.41)
85
- rubocop-factory_bot (2.23.1)
91
+ rubocop-factory_bot (2.24.0)
86
92
  rubocop (~> 1.33)
87
- rubocop-performance (1.18.0)
93
+ rubocop-performance (1.19.1)
88
94
  rubocop (>= 1.7.0, < 2.0)
89
95
  rubocop-ast (>= 0.4.0)
90
96
  rubocop-rake (0.6.0)
91
97
  rubocop (~> 1.0)
92
- rubocop-rspec (2.22.0)
98
+ rubocop-rspec (2.24.1)
93
99
  rubocop (~> 1.33)
94
100
  rubocop-capybara (~> 2.17)
95
101
  rubocop-factory_bot (~> 2.22)
96
102
  ruby-progressbar (1.13.0)
97
- sshkit (1.21.4)
103
+ sshkit (1.21.5)
98
104
  net-scp (>= 1.1.2)
99
105
  net-ssh (>= 2.8.0)
100
106
  unicode-display_width (2.4.2)
101
- webmock (3.18.1)
107
+ webmock (3.19.1)
102
108
  addressable (>= 2.8.0)
103
109
  crack (>= 0.3.2)
104
110
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -111,10 +117,10 @@ DEPENDENCIES
111
117
  capistrano-asg-rolling!
112
118
  rake (~> 13.0)
113
119
  rspec (~> 3.0)
114
- rubocop (~> 1.52.0)
115
- rubocop-performance (~> 1.18.0)
120
+ rubocop (~> 1.56.3)
121
+ rubocop-performance (~> 1.19.0)
116
122
  rubocop-rake (~> 0.6.0)
117
- rubocop-rspec (~> 2.22.0)
123
+ rubocop-rspec (~> 2.24.0)
118
124
  webmock (~> 3.11)
119
125
 
120
126
  BUNDLED WITH
data/README.md CHANGED
@@ -101,6 +101,14 @@ When launching an Instance, you can override any settings defined in the Launch
101
101
  set :asg_rolling_instance_overrides, { instance_type: 'c5.large' }
102
102
  ```
103
103
 
104
+ You can make Capistrano wait until the instances in the autoscaling group have completed refreshing with:
105
+
106
+ ```ruby
107
+ # config/deploy.rb
108
+ set :asg_wait_for_instance_refresh, true
109
+ set :asg_instance_refresh_polling_interval, 30 # default
110
+ ```
111
+
104
112
  ## Usage
105
113
 
106
114
  Specify the Auto Scaling Groups with the keyword `autoscale` instead of using the `server` keyword in Capistrano's stage configuration. Provide the name of the Auto Scaling Group and any properties you want to pass to the server:
@@ -141,7 +149,7 @@ autoscale 'web-autoscale-group', user: 'deployer', healthy_percentage: 75 # ov
141
149
 
142
150
  The rolling configuration of the stage has a side-effect: any Capistrano tasks you run, will also launch instances per Auto Scaling Group.
143
151
 
144
- For example the command: `cap production rails:console`, will launch a new instance and run `rails:console` and that instance. While that can be useful, you often just want to run the task on the primary server. A solution is to create two stages with different rolling configurations, for example:
152
+ For example the command: `cap production rails:console`, will launch a new instance and run `rails:console` on that instance. While that can be useful, you often just want to run the task on the primary server. A solution is to create two stages with different rolling configurations, for example:
145
153
 
146
154
  ```ruby
147
155
  # config/deploy/production.rb
@@ -12,7 +12,9 @@ module Capistrano
12
12
  LIFECYCLE_STATE_IN_SERVICE = 'InService'
13
13
  LIFECYCLE_STATE_STANDBY = 'Standby'
14
14
 
15
- attr_reader :name, :properties
15
+ COMPLETED_REFRESH_STATUSES = %w[Successful Failed Cancelled RollbackSuccessful RollbackFailed].freeze
16
+
17
+ attr_reader :name, :properties, :refresh_id
16
18
 
17
19
  def initialize(name, properties = {})
18
20
  @name = name
@@ -45,7 +47,7 @@ module Capistrano
45
47
  end
46
48
 
47
49
  def start_instance_refresh(launch_template)
48
- aws_autoscaling_client.start_instance_refresh(
50
+ @refresh_id = aws_autoscaling_client.start_instance_refresh(
49
51
  auto_scaling_group_name: name,
50
52
  strategy: 'Rolling',
51
53
  desired_configuration: {
@@ -59,11 +61,26 @@ module Capistrano
59
61
  min_healthy_percentage: healthy_percentage,
60
62
  skip_matching: true
61
63
  }
62
- )
64
+ ).instance_refresh_id
63
65
  rescue Aws::AutoScaling::Errors::InstanceRefreshInProgress => e
64
66
  raise Capistrano::ASG::Rolling::InstanceRefreshFailed, e
65
67
  end
66
68
 
69
+ InstanceRefreshStatus = Struct.new(:status, :percentage_complete) do
70
+ def completed?
71
+ COMPLETED_REFRESH_STATUSES.include?(status)
72
+ end
73
+ end
74
+
75
+ def latest_instance_refresh
76
+ instance_refresh = most_recent_instance_refresh
77
+ status = instance_refresh&.dig(:status)
78
+ percentage_complete = instance_refresh&.dig(:percentage_complete)
79
+ return nil if status.nil?
80
+
81
+ InstanceRefreshStatus.new(status, percentage_complete)
82
+ end
83
+
67
84
  # Returns instances with lifecycle state "InService" for this Auto Scaling Group.
68
85
  def instances
69
86
  instance_ids = aws_autoscaling_group.instances.select { |i| i.lifecycle_state == LIFECYCLE_STATE_IN_SERVICE }.map(&:instance_id)
@@ -106,6 +123,16 @@ module Capistrano
106
123
 
107
124
  private
108
125
 
126
+ def most_recent_instance_refresh
127
+ parameters = {
128
+ auto_scaling_group_name: name,
129
+ max_records: 1
130
+ }
131
+ parameters[:instance_refresh_ids] = [@refresh_id] if @refresh_id
132
+ refresh = aws_autoscaling_client.describe_instance_refreshes(parameters).to_h
133
+ refresh[:instance_refreshes].first
134
+ end
135
+
109
136
  def aws_autoscaling_group
110
137
  @aws_autoscaling_group ||= ::Aws::AutoScaling::AutoScalingGroup.new(name: name, client: aws_autoscaling_client)
111
138
  end
@@ -71,6 +71,14 @@ module Capistrano
71
71
  def rolling_update?
72
72
  fetch(:asg_rolling_update)
73
73
  end
74
+
75
+ def wait_for_instance_refresh?
76
+ fetch(:asg_wait_for_instance_refresh, false)
77
+ end
78
+
79
+ def instance_refresh_polling_interval
80
+ fetch(:asg_instance_refresh_polling_interval, 30)
81
+ end
74
82
  end
75
83
  end
76
84
  end
@@ -15,6 +15,16 @@ module Capistrano
15
15
 
16
16
  class InstanceRefreshFailed < Capistrano::ASG::Rolling::Exception
17
17
  end
18
+
19
+ # Exception when instance terminate fails.
20
+ class InstanceTerminateFailed < Capistrano::ASG::Rolling::Exception
21
+ attr_reader :instance
22
+
23
+ def initialize(instance, exception)
24
+ @instance = instance
25
+ super(exception)
26
+ end
27
+ end
18
28
  end
19
29
  end
20
30
  end
@@ -91,6 +91,8 @@ module Capistrano
91
91
  aws_ec2_client.wait_until(:instance_terminated, instance_ids: [id]) if wait
92
92
 
93
93
  @terminated = true
94
+ rescue Aws::EC2::Errors::ServiceError => e
95
+ raise Capistrano::ASG::Rolling::InstanceTerminateFailed.new(self, e)
94
96
  end
95
97
 
96
98
  def create_ami(name: nil, description: nil, tags: nil)
@@ -13,6 +13,10 @@ module Capistrano
13
13
  $stdout.puts format_text(text)
14
14
  end
15
15
 
16
+ def warning(text)
17
+ $stdout.puts format_text("WARNING: #{text}")
18
+ end
19
+
16
20
  def error(text)
17
21
  $stderr.puts format_text(text, color: :red) # rubocop:disable Style/StderrPuts
18
22
  end
@@ -28,6 +28,7 @@ module Capistrano
28
28
 
29
29
  after 'rolling:update', 'rolling:cleanup'
30
30
  after 'rolling:create_ami', 'rolling:cleanup'
31
+ after 'rolling:update', 'rolling:instance_refresh_status'
31
32
 
32
33
  # Register an exit hook to do some cleanup when Capistrano
33
34
  # terminates without calling our after cleanup hook.
@@ -52,6 +53,8 @@ module Capistrano
52
53
 
53
54
  logger.info 'Terminating instance(s)...'
54
55
  instances.terminate
56
+ rescue Capistrano::ASG::Rolling::InstanceTerminateFailed => e
57
+ logger.warning "Failed to terminate Instance **#{e.instance.id}**: #{e.message}"
55
58
  end
56
59
  end
57
60
  end
@@ -3,7 +3,7 @@
3
3
  module Capistrano
4
4
  module ASG
5
5
  module Rolling
6
- VERSION = '0.3.0'
6
+ VERSION = '0.4.0'
7
7
  end
8
8
  end
9
9
  end
@@ -21,7 +21,7 @@ namespace :rolling do
21
21
  else
22
22
  logger.info "Auto Scaling Group: **#{group.name}**, standard deployment strategy."
23
23
 
24
- group.instances.each_with_index do |instance, index| # rubocop:disable Lint/ShadowingOuterLocalVariable
24
+ group.instances.each_with_index do |instance, index|
25
25
  if index.zero? && group.properties.key?(:primary_roles)
26
26
  server_properties = group.properties.dup
27
27
  server_properties[:roles] = server_properties.delete(:primary_roles)
@@ -86,7 +86,7 @@ namespace :rolling do
86
86
  deleted = deleted_amis.include?(ami)
87
87
 
88
88
  if !exists && !deleted
89
- logger.info("WARNING: AMI **#{ami.id}** does not exist for Launch Template **#{version.name}** version **#{version.version}**.")
89
+ logger.warning("AMI **#{ami.id}** does not exist for Launch Template **#{version.name}** version **#{version.version}**.")
90
90
  next
91
91
  end
92
92
 
@@ -109,7 +109,11 @@ namespace :rolling do
109
109
  instances = config.instances.auto_terminate
110
110
  if instances.any?
111
111
  logger.info 'Terminating instance(s)...'
112
- instances.terminate
112
+ begin
113
+ instances.terminate
114
+ rescue Capistrano::ASG::Rolling::InstanceTerminateFailed => e
115
+ logger.warning "Failed to terminate Instance **#{e.instance.id}**: #{e.message}"
116
+ end
113
117
  end
114
118
  end
115
119
 
@@ -168,4 +172,30 @@ namespace :rolling do
168
172
  end
169
173
  end
170
174
  end
175
+
176
+ desc 'Get status of instance refresh'
177
+ task :instance_refresh_status do
178
+ return unless config.wait_for_instance_refresh?
179
+
180
+ groups = config.autoscale_groups.to_h { |group| [group.name, group] }
181
+
182
+ while groups.any?
183
+ groups.each do |name, group|
184
+ refresh = group.latest_instance_refresh
185
+ if refresh.nil? || refresh.completed?
186
+ logger.info "Auto Scaling Group: **#{name}**, completed with status '#{refresh.status}'." if refresh.completed?
187
+ groups.delete(name)
188
+ elsif !refresh.percentage_complete.nil?
189
+ logger.info "Auto Scaling Group: **#{name}**, #{refresh.percentage_complete}% completed, status '#{refresh.status}'."
190
+ else
191
+ logger.info "Auto Scaling Group: **#{name}**, status '#{refresh.status}'."
192
+ end
193
+ end
194
+ next if groups.empty?
195
+
196
+ wait_for = config.instance_refresh_polling_interval
197
+ logger.info "Instance refresh(es) not completed, waiting #{wait_for} seconds..."
198
+ sleep wait_for
199
+ end
200
+ end
171
201
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capistrano-asg-rolling
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kentaa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-05 00:00:00.000000000 Z
11
+ date: 2023-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler