eb_deployer_updated 0.8.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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +14 -0
  6. data/CHANGELOG.md +127 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +22 -0
  9. data/README.md +136 -0
  10. data/Rakefile +12 -0
  11. data/TODOS.md +11 -0
  12. data/bin/eb_deploy +13 -0
  13. data/eb_deployer.gemspec +22 -0
  14. data/lib/eb_deployer/application.rb +96 -0
  15. data/lib/eb_deployer/aws_driver/beanstalk.rb +158 -0
  16. data/lib/eb_deployer/aws_driver/cloud_formation_driver.rb +53 -0
  17. data/lib/eb_deployer/aws_driver/s3_driver.rb +35 -0
  18. data/lib/eb_deployer/aws_driver.rb +8 -0
  19. data/lib/eb_deployer/cf_event_source.rb +26 -0
  20. data/lib/eb_deployer/cloud_formation_provisioner.rb +120 -0
  21. data/lib/eb_deployer/component.rb +45 -0
  22. data/lib/eb_deployer/config_loader.rb +64 -0
  23. data/lib/eb_deployer/default_component.rb +32 -0
  24. data/lib/eb_deployer/default_config.rb +20 -0
  25. data/lib/eb_deployer/default_config.yml +159 -0
  26. data/lib/eb_deployer/deployment_strategy/blue_green.rb +79 -0
  27. data/lib/eb_deployer/deployment_strategy/blue_only.rb +45 -0
  28. data/lib/eb_deployer/deployment_strategy/inplace_update.rb +16 -0
  29. data/lib/eb_deployer/deployment_strategy.rb +20 -0
  30. data/lib/eb_deployer/eb_environment.rb +204 -0
  31. data/lib/eb_deployer/eb_event_source.rb +35 -0
  32. data/lib/eb_deployer/environment.rb +60 -0
  33. data/lib/eb_deployer/event_poller.rb +51 -0
  34. data/lib/eb_deployer/package.rb +39 -0
  35. data/lib/eb_deployer/resource_stacks.rb +20 -0
  36. data/lib/eb_deployer/smoke_test.rb +23 -0
  37. data/lib/eb_deployer/tasks.rb +45 -0
  38. data/lib/eb_deployer/throttling_handling.rb +17 -0
  39. data/lib/eb_deployer/utils.rb +33 -0
  40. data/lib/eb_deployer/version.rb +3 -0
  41. data/lib/eb_deployer/version_cleaner.rb +30 -0
  42. data/lib/eb_deployer.rb +339 -0
  43. data/lib/generators/eb_deployer/install/install_generator.rb +82 -0
  44. data/lib/generators/eb_deployer/install/templates/eb_deployer.rake +1 -0
  45. data/lib/generators/eb_deployer/install/templates/eb_deployer.yml.erb +181 -0
  46. data/lib/generators/eb_deployer/install/templates/ebextensions/01_postgres_packages.config +5 -0
  47. data/lib/generators/eb_deployer/install/templates/postgres_rds.json +88 -0
  48. data/test/aws_driver_stubs.rb +350 -0
  49. data/test/beanstalk_test.rb +23 -0
  50. data/test/blue_green_deploy_test.rb +114 -0
  51. data/test/blue_only_deploy_test.rb +78 -0
  52. data/test/cf_event_poller_test.rb +32 -0
  53. data/test/cloud_formation_provisioner_test.rb +47 -0
  54. data/test/config_loader_test.rb +205 -0
  55. data/test/deploy_test.rb +42 -0
  56. data/test/eb_environment_test.rb +120 -0
  57. data/test/eb_event_poller_test.rb +32 -0
  58. data/test/inplace_update_deploy_test.rb +110 -0
  59. data/test/multi_components_deploy_test.rb +164 -0
  60. data/test/rails_generators_test.rb +67 -0
  61. data/test/resources_deploy_test.rb +191 -0
  62. data/test/smoke_test_test.rb +23 -0
  63. data/test/template_deploy_test.rb +13 -0
  64. data/test/test_helper.rb +68 -0
  65. data/test/tier_setting_deploy_test.rb +24 -0
  66. data/test/versions_deploy_test.rb +120 -0
  67. metadata +194 -0
@@ -0,0 +1,45 @@
1
+ module EbDeployer
2
+ module DeploymentStrategy
3
+ class BlueOnly
4
+ def initialize(component)
5
+ @component = component
6
+ end
7
+
8
+ def test_compatibility(env_create_options)
9
+ tier = env_create_options[:tier]
10
+ if tier && tier.downcase == 'worker'
11
+ raise "Blue only deployment is not supported for Worker tier"
12
+ end
13
+ end
14
+
15
+ def deploy(version_label, env_settings, inactive_settings=[])
16
+ if !ebenvs.any?(&method(:active_ebenv?))
17
+ ebenv('a', @component.cname_prefix).
18
+ deploy(version_label, env_settings)
19
+ return
20
+ end
21
+
22
+ inactive_ebenv = ebenvs.reject(&method(:active_ebenv?)).first
23
+
24
+ inactive_ebenv.deploy(version_label, env_settings)
25
+ end
26
+
27
+ private
28
+ def active_ebenv?(ebenv)
29
+ ebenv.cname_prefix == @component.cname_prefix
30
+ end
31
+
32
+ def ebenvs
33
+ [ebenv('a'), ebenv('b')]
34
+ end
35
+
36
+ def ebenv(suffix, cname_prefix=nil)
37
+ @component.new_eb_env(suffix, cname_prefix || inactive_cname_prefix)
38
+ end
39
+
40
+ def inactive_cname_prefix
41
+ "#{@component.cname_prefix}-inactive"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ module EbDeployer
2
+ module DeploymentStrategy
3
+ class InplaceUpdate
4
+ def initialize(component)
5
+ @component = component
6
+ end
7
+
8
+ def test_compatibility(env_create_opts)
9
+ end
10
+
11
+ def deploy(version_label, env_settings, inactive_settings)
12
+ @component.new_eb_env.deploy(version_label, env_settings)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ require 'eb_deployer/deployment_strategy/inplace_update'
2
+ require 'eb_deployer/deployment_strategy/blue_green'
3
+ require 'eb_deployer/deployment_strategy/blue_only'
4
+
5
+ module EbDeployer
6
+ module DeploymentStrategy
7
+ def self.create(component, strategy_name)
8
+ case strategy_name.to_s
9
+ when 'inplace_update', 'inplace-update'
10
+ InplaceUpdate.new(component)
11
+ when 'blue_green', 'blue-green'
12
+ BlueGreen.new(component)
13
+ when 'blue_only', 'blue-only'
14
+ BlueOnly.new(component)
15
+ else
16
+ raise 'strategy_name: ' + strategy_name.to_s + ' not supported'
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,204 @@
1
+ module EbDeployer
2
+ class EbEnvironment
3
+ include Utils
4
+
5
+ attr_reader :app, :name
6
+ attr_writer :event_poller
7
+
8
+ def self.unique_ebenv_name(env_name, app_name)
9
+ raise "Environment name #{env_name} is too long, it must be under 32 chars" if env_name.size > 32
10
+ digest = Digest::SHA1.hexdigest(app_name + '-' + env_name)[0..6]
11
+ "#{env_name}-#{digest}"
12
+ end
13
+
14
+ def initialize(app, name, eb_driver, creation_opts={})
15
+ @app = app
16
+ @name = self.class.unique_ebenv_name(name, app)
17
+ @bs = eb_driver
18
+ @creation_opts = default_create_options.merge(reject_nil(creation_opts))
19
+ @accepted_healthy_states = @creation_opts[:accepted_healthy_states]
20
+ @event_poller = nil
21
+ end
22
+
23
+ def deploy(version_label, settings={})
24
+ terminate if @creation_opts[:phoenix_mode]
25
+
26
+ if @bs.environment_exists?(@app, @name)
27
+ update_eb_env(settings, version_label)
28
+ else
29
+ create_eb_env(settings, version_label)
30
+ end
31
+
32
+ smoke_test
33
+ wait_for_env_become_healthy
34
+ end
35
+
36
+ def apply_settings(settings)
37
+ raise "Env #{self.name} not exists for applying settings" unless @bs.environment_exists?(@app, @name)
38
+ wait_for_env_status_to_be_ready
39
+ with_polling_events(/Successfully deployed new configuration to environment/i) do
40
+ @bs.update_environment_settings(@app, @name, settings)
41
+ end
42
+ end
43
+
44
+ def cname_prefix
45
+ @bs.environment_cname_prefix(@app, @name)
46
+ end
47
+
48
+ def swap_cname_with(another)
49
+ log("Swap CNAME with env #{another.name}")
50
+ with_polling_events(/Completed swapping CNAMEs for environments/i) do
51
+ @bs.environment_swap_cname(self.app, self.name, another.name)
52
+ end
53
+ end
54
+
55
+ def log(msg)
56
+ puts "[#{Time.now.utc}][environment:#{@name}] #{msg}"
57
+ end
58
+
59
+ def terminate
60
+ if @bs.environment_exists?(@app, @name)
61
+ with_polling_events(/terminateEnvironment completed successfully/i) do
62
+ @bs.delete_environment(@app, @name)
63
+ end
64
+ end
65
+ end
66
+
67
+ def health_state
68
+ @bs.environment_health_state(@app, @name)
69
+ end
70
+
71
+ private
72
+
73
+ def configured_tier
74
+ @creation_opts[:tier]
75
+ end
76
+
77
+ def template_name
78
+ @creation_opts[:template_name]
79
+ end
80
+
81
+ def has_cname?
82
+ !configured_tier || configured_tier.downcase == 'webserver'
83
+ end
84
+
85
+ def configured_cname_prefix
86
+ @creation_opts[:cname_prefix]
87
+ end
88
+
89
+ def create_eb_env(settings, version_label)
90
+ solution_stack = @creation_opts[:solution_stack]
91
+ tags = convert_tags_hash_to_array(@creation_opts.delete(:tags))
92
+ validate_solutions_stack(solution_stack)
93
+ with_polling_events(/Successfully launched environment/i) do
94
+ @bs.create_environment(@app,
95
+ @name,
96
+ solution_stack,
97
+ has_cname? ? configured_cname_prefix : nil,
98
+ version_label,
99
+ configured_tier,
100
+ tags,
101
+ settings,
102
+ template_name)
103
+ end
104
+ end
105
+
106
+ def update_eb_env(settings, version_label)
107
+ with_polling_events(/Successfully deployed new configuration to environment/i) do
108
+ @bs.update_environment(@app,
109
+ @name,
110
+ version_label,
111
+ configured_tier,
112
+ settings,
113
+ template_name)
114
+ end
115
+ end
116
+
117
+ def validate_solutions_stack(stack_name)
118
+ names = @bs.list_solution_stack_names
119
+ raise "'#{stack_name}' is not a valid solution stack name, available solution stack names are: #{names.join(', ')}" unless names.include?(stack_name)
120
+ end
121
+
122
+ def smoke_test
123
+ host_name = @bs.environment_cname(@app, @name)
124
+ SmokeTest.new(@creation_opts[:smoke_test]).run(host_name, self)
125
+ end
126
+
127
+ def with_polling_events(terminate_pattern, &block)
128
+ anchor = event_poller.get_anchor
129
+ yield
130
+ event_poller.poll(anchor) do |event|
131
+ if event[:message] =~ /Failed to deploy application/
132
+ raise event[:message]
133
+ end
134
+
135
+ if event[:message] =~ /Command failed on instance/
136
+ raise "Elasticbeanstalk instance provision failed (maybe a problem with your .ebextension files). The original message: #{event[:message]}"
137
+ end
138
+
139
+ if event[:message] =~ /complete, but with errors/
140
+ raise event[:message]
141
+ end
142
+
143
+ if event[:message] =~ /However, there were issues during launch\. See event log for details\./
144
+ raise "Environment launched, but with errors. The original message: #{event[:message]}"
145
+ end
146
+
147
+ log_event(event)
148
+ break if event[:message] =~ terminate_pattern
149
+ end
150
+ end
151
+
152
+ def convert_tags_hash_to_array tags
153
+ tags ||= {}
154
+ tags.inject([]) do |arr, (k, v)|
155
+ arr << {:key => k, :value => v}
156
+ arr
157
+ end
158
+ end
159
+
160
+ def wait_for_env_status_to_be_ready
161
+ Timeout.timeout(600) do
162
+ current_status = @bs.environment_status(@app, @name)
163
+
164
+ while current_status.downcase != 'ready'
165
+ log("Environment status: #{current_status}")
166
+ sleep 15
167
+ current_status = @bs.environment_status(@app, @name)
168
+ end
169
+
170
+ log("Environment status: #{current_status}")
171
+ end
172
+ end
173
+
174
+ def wait_for_env_become_healthy
175
+ Timeout.timeout(600) do
176
+ current_health_status = @bs.environment_health_state(@app, @name)
177
+ while !@accepted_healthy_states.include?(current_health_status)
178
+ log("health status: #{current_health_status}")
179
+ sleep 15
180
+ current_health_status = @bs.environment_health_state(@app, @name)
181
+ end
182
+
183
+ log("health status: #{current_health_status}")
184
+ end
185
+ end
186
+
187
+ def event_poller
188
+ @event_poller || EventPoller.new(EbEventSource.new(@app, @name, @bs))
189
+ end
190
+
191
+ def default_create_options
192
+ {
193
+ :solution_stack => "64bit Amazon Linux 2014.09 v1.1.0 running Tomcat 7 Java 7",
194
+ :smoke_test => Proc.new {},
195
+ :tier => 'WebServer',
196
+ :accepted_healthy_states => ['Green']
197
+ }
198
+ end
199
+
200
+ def log_event(event)
201
+ puts "[#{event[:event_date]}][environment:#{@name}] #{event[:message]}"
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,35 @@
1
+ module EbDeployer
2
+ class EbEventSource
3
+ def initialize(app, env, eb_driver)
4
+ @app, @env, @eb_driver = app, env, eb_driver
5
+ end
6
+
7
+ def get_anchor
8
+ events, _ = fetch_events_from_eb(:max_records => 1)
9
+ events.first
10
+ end
11
+
12
+ def fetch_events(from_anchor, &block)
13
+ options = {}
14
+ if from_anchor && from_anchor[:event_date]
15
+ options[:start_time] = from_anchor[:event_date].iso8601
16
+ end
17
+ events, next_token = fetch_events_from_eb(options)
18
+ should_continue = yield(events)
19
+ fetch_next(next_token, &block) if next_token && should_continue
20
+ end
21
+
22
+ private
23
+
24
+ def fetch_next(next_token, &block)
25
+ events, next_token = fetch_events_from_eb(:next_token => next_token)
26
+ should_continue = yield(events)
27
+ fetch_next(next_token, &block) if next_token && should_continue
28
+ end
29
+
30
+ def fetch_events_from_eb(options)
31
+ @eb_driver.fetch_events(@app, @env, options)
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,60 @@
1
+ module EbDeployer
2
+ class Environment
3
+ include Utils
4
+ attr_accessor :creation_opts, :strategy_name
5
+
6
+ attr_writer :resource_stacks, :settings, :inactive_settings, :component_under_deploy
7
+
8
+ attr_reader :name
9
+
10
+ def initialize(app, name, stack_name, eb_driver, &block)
11
+ @app = app
12
+ @name = name
13
+ @stack_name = stack_name
14
+ @eb_driver = eb_driver
15
+ @creation_opts = {}
16
+ @settings = []
17
+ @inactive_settings = []
18
+ @strategy_name = :blue_green
19
+ @components = nil
20
+ yield(self) if block_given?
21
+ unless @components
22
+ @components = [DefaultComponent.new(self, @creation_opts, @strategy_name, @eb_driver)]
23
+ end
24
+ end
25
+
26
+ def app_name
27
+ @app.name
28
+ end
29
+
30
+ def deploy(version_label)
31
+ resource_settings = @resource_stacks.provision(@stack_name)
32
+ components_to_deploy.each do |component|
33
+ component.deploy(version_label, @settings + resource_settings, @inactive_settings)
34
+ end
35
+ end
36
+
37
+ def components=(components_attrs)
38
+ return unless components_attrs
39
+ @components = components_attrs.map do |attrs|
40
+ attrs = symbolize_keys(attrs)
41
+ Component.new(attrs.delete(:name), self, attrs, @eb_driver)
42
+ end
43
+ end
44
+
45
+ private
46
+ def components_to_deploy
47
+ if @component_under_deploy
48
+ component = component_named(@component_under_deploy)
49
+ raise "'#{@component_under_deploy}' is not in the configuration. Available components are #{@components.map(&:name) }" unless component
50
+ [component]
51
+ else
52
+ @components
53
+ end
54
+ end
55
+
56
+ def component_named(name)
57
+ @components.detect { |c| c.name == name }
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,51 @@
1
+ module EbDeployer
2
+ class EventPoller
3
+ include Utils
4
+ POLL_INTERVAL = 15
5
+
6
+ def initialize(event_source)
7
+ @event_source = event_source
8
+ end
9
+
10
+ def get_anchor
11
+ @event_source.get_anchor
12
+ end
13
+
14
+ def poll(from_anchor, &block)
15
+ handled = Set.new
16
+ loop do
17
+ @event_source.fetch_events(from_anchor) do |events|
18
+ # events from event source is latest first order
19
+ to_be_handled = []
20
+ reached_anchor = false
21
+
22
+ events.each do |event|
23
+ if digest(event) == digest(from_anchor)
24
+ reached_anchor = true
25
+ end
26
+
27
+ if !handled.include?(digest(event)) && !reached_anchor
28
+ to_be_handled << event
29
+ end
30
+ end
31
+
32
+ to_be_handled.reverse.each do |event|
33
+ yield(event)
34
+ handled << digest(event)
35
+ end
36
+
37
+ !reached_anchor
38
+ end
39
+ sleep POLL_INTERVAL
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def digest(event)
46
+ return nil unless event
47
+ event = event.to_h if event.respond_to?(:to_h)
48
+ event.to_json
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ module EbDeployer
2
+ class Package
3
+ def initialize(file, bucket_name, s3_driver)
4
+ @file, @bucket_name = file, bucket_name
5
+ @s3 = s3_driver
6
+ end
7
+
8
+ def upload
9
+ ensure_bucket(@bucket_name)
10
+ upload_if_not_exists(@file, @bucket_name)
11
+ end
12
+
13
+ def source_bundle
14
+ { :s3_bucket => @bucket_name, :s3_key => s3_path }
15
+ end
16
+
17
+ private
18
+
19
+ def s3_path
20
+ @_s3_path ||= Digest::MD5.file(@file).hexdigest + "-" + File.basename(@file)
21
+ end
22
+
23
+ def ensure_bucket(bucket_name)
24
+ @s3.create_bucket(@bucket_name) unless @s3.bucket_exists?(@bucket_name)
25
+ end
26
+
27
+ def upload_if_not_exists(file, bucket_name)
28
+ if @s3.object_length(@bucket_name, s3_path) != File.size(file)
29
+ log("start uploading to s3 bucket #{@bucket_name}...")
30
+ @s3.upload_file(@bucket_name, s3_path, file)
31
+ log("uploading finished")
32
+ end
33
+ end
34
+
35
+ def log(message)
36
+ puts "[#{Time.now.utc}][package:#{File.basename(@file)}] #{message}"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ module EbDeployer
2
+ class ResourceStacks
3
+ def initialize(resources, cf_driver, skip_provision, tags)
4
+ @resources = resources
5
+ @cf_driver = cf_driver
6
+ @skip_provision = skip_provision
7
+ @tags = (tags || {}).map { |k, v| { key: k, value: v } }
8
+ end
9
+
10
+ def provision(stack_name)
11
+ provisioner = CloudFormationProvisioner.new(stack_name, @cf_driver)
12
+ if @resources
13
+ provisioner.provision(@resources, @tags) unless @skip_provision
14
+ provisioner.transform_outputs(@resources)
15
+ else
16
+ []
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module EbDeployer
2
+ class SmokeTest
3
+ def initialize(test_body)
4
+ @test_body = test_body
5
+ end
6
+
7
+ def run(host_name, logger=nil)
8
+ return unless @test_body
9
+ logger.log("running smoke test for #{host_name}...") if logger
10
+
11
+ case @test_body
12
+ when Proc
13
+ @test_body.call(host_name)
14
+ when String
15
+ eval(@test_body, binding)
16
+ else
17
+ raise "smoke test can only be a string to evaluate or a proc object such as lambda"
18
+ end
19
+
20
+ logger.log("smoke test succeeded.") if logger
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ require 'fileutils'
2
+ require 'open3'
3
+
4
+ namespace :eb do
5
+ def eb_deployer_env
6
+ ENV['EB_DEPLOYER_ENV'] || 'dev'
7
+ end
8
+
9
+ def eb_deployer_package
10
+ name = File.basename(Dir.pwd).downcase.gsub(/[^0-9a-z]/, '-').gsub(/--/, '-')
11
+ "tmp/#{name}.zip"
12
+ end
13
+
14
+ def eb_package_files
15
+ ignore_file = File.join(Dir.pwd, ".ebdeployerignore")
16
+ ignore_patterns = File.exists?(ignore_file) ? File.readlines(ignore_file).map(&:strip) : []
17
+ `git ls-files`.lines.reject { |f| ignore_patterns.any? { |p| File.fnmatch(p, f.strip) } }
18
+ end
19
+
20
+ desc "Remove the package file we generated."
21
+ task :clean do
22
+ sh "rm -rf #{eb_deployer_package}"
23
+ end
24
+
25
+ desc "Build package for eb_deployer to deploy to a Ruby environment in tmp directory. It zips all file list by 'git ls-files'"
26
+ task :package => [:clean] do
27
+ package = eb_deployer_package
28
+ FileUtils.mkdir_p(File.dirname(package))
29
+ Open3.popen2("zip #{package} -@") do |i, o, t|
30
+ i.write(eb_package_files.join)
31
+ i.close
32
+ puts o.read
33
+ end
34
+ end
35
+
36
+ desc "Deploy package we built in tmp directory. default to dev environment, specify environment variable EB_DEPLOYER_ENV to override, for example: EB_DEPLOYER_ENV=production rake eb:deploy."
37
+ task :deploy => [:package] do
38
+ sh "eb_deploy -p #{eb_deployer_package} -e #{eb_deployer_env}"
39
+ end
40
+
41
+ desc "Destroy Elastic Beanstalk environments. It won't destroy resources defined in eb_deployer.yml. Default to dev environment, specify EB_DEPLOYER_ENV to override."
42
+ task :destroy do
43
+ sh "eb_deploy -d -e #{eb_deployer_env}"
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ module EbDeployer
2
+ class ThrottlingHandling
3
+ include Utils
4
+
5
+ def initialize(delegatee, throttling_error)
6
+ @delegatee = delegatee
7
+ @throttling_error = throttling_error
8
+ end
9
+
10
+ def method_missing(method, *args, &block)
11
+ super unless @delegatee.respond_to?(method)
12
+ backoff(@throttling_error) do
13
+ @delegatee.send(method, *args, &block)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ module EbDeployer
2
+ module Utils
3
+ BACKOFF_INITIAL_SLEEP = 1
4
+
5
+ # A util deal with throttling exceptions
6
+ # example:
7
+ # backoff(Aws::EC2::Errors::RequestLimitExceeded) do
8
+ # ...
9
+ # end
10
+ def backoff(error_class, retry_limit=9, &block)
11
+ next_sleep = BACKOFF_INITIAL_SLEEP
12
+ begin
13
+ yield
14
+ rescue error_class
15
+ raise if retry_limit == 0
16
+ sleep(next_sleep)
17
+ next_sleep *= 2
18
+ retry_limit -= 1
19
+ retry
20
+ end
21
+ end
22
+
23
+ # convert top level key in a hash to symbol
24
+ def symbolize_keys(hash)
25
+ hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
26
+ end
27
+
28
+
29
+ def reject_nil(hash)
30
+ hash.reject{| k, v| v.nil?}
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module EbDeployer
2
+ VERSION = "0.8.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ module EbDeployer
2
+ class VersionCleaner
3
+ def initialize(app, number_to_keep)
4
+ @app = app
5
+ @number_to_keep = number_to_keep
6
+ end
7
+
8
+ def clean(version_prefix = "")
9
+ if @number_to_keep > 0
10
+ versions_to_remove = versions_to_clean(version_prefix)
11
+ @app.remove(versions_to_remove, true)
12
+ end
13
+ end
14
+
15
+ private
16
+ def versions_to_clean(version_prefix = "")
17
+ all_versions = @app.versions.select do |apv|
18
+ apv[:version].start_with?(version_prefix)
19
+ end
20
+
21
+ all_versions.sort! { |x, y| y[:date_updated] <=> x[:date_updated] }
22
+ all_versions.slice!(range_to_keep)
23
+ all_versions.map { |apv| apv[:version] }
24
+ end
25
+
26
+ def range_to_keep
27
+ (0..(@number_to_keep-1))
28
+ end
29
+ end
30
+ end