jack_and_the_elastic_beanstalk 0.1.0.pre

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4578032c8d9235e27f4491ace04ec78f8929d63f
4
+ data.tar.gz: '06419fd23e051d5205a536a1c2e6f076819b881c'
5
+ SHA512:
6
+ metadata.gz: 9886c279514422cb879ca4c7c598cea9a868df7058efebebb9f775bdeb194993ff198bbc189f94345f025459a0ad9aa9ca9b89ff426b25e230f498d3d80f97f5
7
+ data.tar.gz: 1096c0027dfc9b5e5da7539b9f543bccfca76a8630ad9cdb2caf2ea677af075bba3cb58a12a5f6b207d1d1cea81a69ac7ab4cf07434a2e8c8b14c3ffa0ca660a
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.3
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.13.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jack_and_the_elastic_beanstalk.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # JackAndTheElasticBeanstalk
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/jack_and_the_elastic_beanstalk`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'jack_and_the_elastic_beanstalk'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install jack_and_the_elastic_beanstalk
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jack_and_the_elastic_beanstalk.
36
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "jack_and_the_elastic_beanstalk"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/jeb ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(__dir__, "../lib")
4
+ require "jack_and_the_elastic_beanstalk"
5
+
6
+ Dotenv.load
7
+ JackAndTheElasticBeanstalk::CLI.start(ARGV)
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jack_and_the_elastic_beanstalk/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jack_and_the_elastic_beanstalk"
8
+ spec.version = JackAndTheElasticBeanstalk::VERSION
9
+ spec.authors = ["Soutaro Matsumoto"]
10
+ spec.email = ["matsumoto@soutaro.com"]
11
+
12
+ spec.summary = %q{Jack and the Elastic Beanstalk.}
13
+ spec.description = %q{Jack and the Elastic Beanstalk.}
14
+ spec.homepage = "https://github.com/sideci/jack_and_the_elastic_beanstalk"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.13"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "minitest", "~> 5.0"
26
+
27
+ spec.add_runtime_dependency 'dotenv', "~> 2.1"
28
+ spec.add_runtime_dependency 'rainbow', '~> 2.1'
29
+ spec.add_runtime_dependency "aws-sdk", "~> 2.6"
30
+ spec.add_runtime_dependency "thor", "~> 0.19"
31
+ spec.add_runtime_dependency "parallel", "~> 1.10"
32
+ spec.add_runtime_dependency "activesupport", "~> 5.0"
33
+ spec.add_runtime_dependency "rubyzip", "~> 1.2"
34
+ end
@@ -0,0 +1,345 @@
1
+ module JackAndTheElasticBeanstalk
2
+ class CLI < Thor
3
+ no_commands do
4
+ def client
5
+ @client ||= Aws::ElasticBeanstalk::Client.new(region: config.region)
6
+ end
7
+
8
+ def runner
9
+ @runner ||= JackAndTheElasticBeanstalk::Runner.new(stdin: STDIN, stdout: STDOUT, stderr: STDERR, logger: logger)
10
+ end
11
+
12
+ def logger
13
+ @logger ||= Logger.new(STDERR).tap do |logger|
14
+ logger.level = Logger.const_get(options[:loglevel].upcase)
15
+ end
16
+ end
17
+
18
+ def eb
19
+ @eb ||= JackAndTheElasticBeanstalk::EB.new(application_name: config.app_name, logger: logger, client: client).tap do |eb|
20
+ eb.timeout = options[:timeout] * 60
21
+ end
22
+ end
23
+
24
+ def config
25
+ @config ||= JackAndTheElasticBeanstalk::Config.load(path: Pathname(options[:jack_dir]))
26
+ end
27
+
28
+ def service
29
+ @service ||= JackAndTheElasticBeanstalk::Service.new(source_dir: Pathname(options[:source_dir]), config: config, eb: eb, runner: runner, logger: logger)
30
+ end
31
+
32
+ def output_dir
33
+ Dir.mktmpdir do |dir|
34
+ yield Pathname(dir)
35
+ end
36
+ end
37
+
38
+ def try_process(name, is:)
39
+ if !is || is == name
40
+ yield(name)
41
+ end
42
+ end
43
+
44
+ def each_in_parallel(array)
45
+ Parallel.each_with_index(array, in_threads: array.size) do |a, index|
46
+ sleep index*5
47
+ yield a
48
+ end
49
+ end
50
+
51
+ def parse_env_args(args)
52
+ args.each.with_object({}) do |arg, hash|
53
+ k,v = arg.split("=", 2)
54
+ hash[k] = v
55
+ end
56
+ end
57
+ end
58
+
59
+ class_option :timeout, type: :numeric, default: 10, desc: "Minutes to timeout for each EB operation"
60
+ class_option :loglevel, type: :string, enum: ["info", "debug", "error"], default: "error", desc: "Loglevel"
61
+ class_option :jack_dir, type: :string, default: (Pathname.pwd + "jack").to_s, desc: "Directory to app.yml"
62
+ class_option :source_dir, type: :string, default: Pathname.pwd.to_s, desc: "Directory for source code"
63
+
64
+ desc "create CONFIGURATION GROUP ENV_VAR=VALUE...", "Create new group"
65
+ def create(configuration, group, *env_var_args)
66
+ processes = config.each_process(configuration).to_a
67
+ env_vars = parse_env_args(env_var_args)
68
+
69
+ output_dir do |base_path|
70
+ processes.each do |process, hash|
71
+ path = base_path + process
72
+ runner.stdout.puts "Staging for #{process}..."
73
+ service.stage(target_dir: path, process: process)
74
+ end
75
+
76
+ each_in_parallel(processes) do |process, hash|
77
+ runner.stdout.puts "Creating new environment for #{process}..."
78
+
79
+ path = base_path + process
80
+
81
+ service.eb_init(target_dir: path)
82
+ service.eb_create(target_dir: path, configuration: configuration, group: group, process: process, env_vars: env_vars)
83
+
84
+ if hash["type"] == "oneoff"
85
+ runner.stdout.puts "Scaling to 0 (#{process} is a oneoff process)"
86
+ env = service.each_environment(group: group).find {|_, p| p == process }.first
87
+ env.set_scale(0)
88
+ env.synchronize_update
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ desc "deploy GROUP", "Deploy to group"
95
+ def deploy(group)
96
+ envs = service.each_environment(group: group).to_a
97
+
98
+ prefix = Time.now.utc.iso8601
99
+
100
+ output_dir do |base_path|
101
+ archives = {}
102
+
103
+ envs.each do |_, process|
104
+ path = base_path + process
105
+ path.mkpath
106
+
107
+ runner.stdout.puts "Staging for #{process}..."
108
+ service.stage(target_dir: path, process: process)
109
+
110
+ name = "#{group}-#{prefix}-#{process}"
111
+ key = "#{config.app_name}/#{name}"
112
+
113
+ archive_path = base_path + "#{name}.zip"
114
+ service.archive(input_dir: path, output_path: archive_path)
115
+
116
+ archives[process] = [key, name, archive_path]
117
+ end
118
+
119
+ each_in_parallel(envs) do |_, process|
120
+ runner.stdout.puts "Deploying to #{process}..."
121
+
122
+ s3_key, label, archive_path = archives[process]
123
+
124
+ service.deploy(group: group,
125
+ process: process,
126
+ archive_path: archive_path,
127
+ s3_key: s3_key,
128
+ label: label)
129
+ end
130
+ end
131
+ end
132
+
133
+ desc "stage PROCESS OUTPUT_DIR", "Prepare application to deploy"
134
+ def stage(process, output_dir)
135
+ path = Pathname(output_dir)
136
+
137
+ if path.directory?
138
+ runner.stdout.puts "Deleting #{path}..."
139
+ path.rmtree
140
+ end
141
+
142
+ runner.stdout.puts "Staging for #{process} in #{path}..."
143
+
144
+ path.mkpath
145
+ service.eb_init target_dir: path
146
+ service.stage(target_dir: path, process: process)
147
+ end
148
+
149
+ desc "archive PROCESS OUTPUT_PATH", "Prepare application bundle at OUTPUT_PATH"
150
+ def archive(process, output_path)
151
+ zip_path = Pathname(output_path)
152
+
153
+ Dir.mktmpdir do |dir|
154
+ dir_path = Pathname(dir)
155
+
156
+ runner.stdout.puts "Staging for #{process}..."
157
+
158
+ dir_path.mkpath
159
+ service.stage(target_dir: dir_path, process: process)
160
+
161
+ runner.stdout.puts "Making application bundle to #{zip_path}..."
162
+
163
+ service.archive(input_dir: dir_path, output_path: zip_path)
164
+ end
165
+ end
166
+
167
+ desc "printenv GROUP [PROCESS]", "Print environment variables"
168
+ def printenv(group, process=nil)
169
+ service.each_environment(group: group) do |env, p|
170
+ try_process(p, is: process) do
171
+ puts "#{p} (#{env.environment_name}):"
172
+ env.env_vars.each do |key, value|
173
+ runner.stdout.puts " #{key}=#{value}"
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ desc "setenv GROUP PROCESS name=var name= ...", "Set environment variables"
180
+ def setenv(group, *args)
181
+ process = if args.first !~ /=/
182
+ args.shift
183
+ end
184
+
185
+ hash = parse_env_args(args)
186
+
187
+ logger.info("jeb::cli") { "Setting environment hash: #{hash.inspect}" }
188
+
189
+ envs = service.each_environment(group: group)
190
+ each_in_parallel(envs) do |env, p|
191
+ try_process(p, is: process) do
192
+ runner.stdout.puts "Updating #{p}'s environment variable..."
193
+ env.synchronize_update do
194
+ env.set_env_vars hash
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ desc "restart GROUP [PROCESS]", "Restart applications"
201
+ def restart(group, process=nil)
202
+ envs = service.each_environment(group: group)
203
+ each_in_parallel(envs) do |env, p|
204
+ try_process(p, is: process) do
205
+ runner.stdout.puts "Restarting #{p}..."
206
+ env.synchronize_update do
207
+ env.restart
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ desc "destroy GROUP [PROCESS]", "Terminate environments associated to GROUP"
214
+ def destroy(group, process=nil)
215
+ service.each_environment(group: group) do |env, p|
216
+ try_process p, is: process do
217
+ runner.stdout.puts "Destroying #{p}: #{env.environment_name}..."
218
+ env.destroy
219
+ end
220
+ end
221
+ end
222
+
223
+ desc "status GROUP [PROCESS]", "Print status of environments associated to GROUP"
224
+ def status(group, process=nil)
225
+ service.each_environment(group: group) do |env, p|
226
+ try_process p, is: process do
227
+ h = env.health
228
+ ih = h.instances_health
229
+ total = ih.no_data + ih.ok + ih.info + ih.warning + ih.degraded + ih.severe + ih.pending
230
+ runner.stdout.puts "#{p}: name=#{env.environment_name}, status=#{env.status}, health: #{h.health_status}, instances: #{total}, scale: #{env.scale}"
231
+ end
232
+ end
233
+ end
234
+
235
+ desc "exec GROUP command...", "Run oneoff command"
236
+ option :keep, type: :boolean, default: false, desc: "Keep started oneoff environment"
237
+ def exec(group, *command)
238
+ env = service.each_environment(group: group).find {|_, p| p == "oneoff" }&.first
239
+ if env
240
+ begin
241
+ env.synchronize_update do
242
+ runner.stdout.puts "Starting #{env.environment_name} for oneoff process..."
243
+ env.set_scale 1
244
+ end
245
+
246
+ output_dir do |path|
247
+ service.eb_init target_dir: path
248
+
249
+ runner.stdout.puts "Waiting for EB to complete deploy..."
250
+ sleep 30
251
+
252
+ start = Time.now
253
+
254
+ while true
255
+ dirs, _ = runner.capture3! "eb", "ssh", env.environment_name, "-c", "ls /var/app"
256
+
257
+ if dirs =~ /ondeck/
258
+ logger.info("jeb::cli") { "Waiting for deploy..." }
259
+ end
260
+ if dirs =~ /current/ && dirs !~ /ondeck/
261
+ break
262
+ end
263
+ if Time.now - start > options[:timeout]*60
264
+ raise "Timed out for waiting deploy..."
265
+ end
266
+
267
+ sleep 15
268
+ end
269
+
270
+ commandline = "cd /var/app/current && sudo -E -u webapp env PATH=$PATH #{command.join(' ')}"
271
+ out, err, status = runner.capture3 "eb", "ssh", env.environment_name, "-c", commandline
272
+
273
+ runner.stdout.print out
274
+ runner.stderr.print err
275
+
276
+ raise status.to_s unless status.success?
277
+ end
278
+ ensure
279
+ unless options[:keep]
280
+ env.synchronize_update do
281
+ runner.stdout.puts "Shutting down #{env.environment_name}..."
282
+ env.set_scale 0
283
+ end
284
+ end
285
+ end
286
+ else
287
+ runner.stdout.puts "Could not find environment associated to oneoff process..."
288
+ end
289
+ end
290
+
291
+ desc "scale GROUP PROCESS min max", "Scale instances"
292
+ def scale(group, process, min, max=min)
293
+ service.each_environment(group: group) do |env, p|
294
+ try_process p, is: process do
295
+ runner.stdout.puts "Scaling #{group} (#{env.environment_name}) to min=#{min}, max=#{max}..."
296
+ env.synchronize_update do
297
+ env.set_scale(min...max)
298
+ end
299
+ end
300
+ end
301
+ end
302
+
303
+ desc "list", "List groups"
304
+ def list
305
+ service.each_group do |group, envs|
306
+ runner.stdout.puts "#{group}: #{envs.map(&:environment_name).join(", ")}"
307
+ end
308
+ end
309
+
310
+ desc "synchronize", "Wait for update"
311
+ def synchronize(group)
312
+ service.each_environment(group: group) do |env, process|
313
+ if env.status != "Ready"
314
+ runner.stdout.puts "Waiting for #{process} (#{env.environment_name})..."
315
+ env.synchronize_update
316
+ end
317
+ end
318
+ end
319
+
320
+ desc "resources GROUP [PROCESS]", "Download resources associated to each environment"
321
+ def resources(group, process_name=nil)
322
+ resources = {}
323
+
324
+ service.each_environment(group: group) do |env, process|
325
+ try_process process, is: process_name do
326
+ ress = env.resources.environment_resources
327
+ resources[process] = {
328
+ environment_name: env.environment_name,
329
+ environment_id: env.environment_id,
330
+ auto_scaling_groups: ress.auto_scaling_groups.map(&:name),
331
+ instances: ress.instances.map(&:id),
332
+ launch_configurations: ress.launch_configurations.map(&:name),
333
+ load_balancers: ress.load_balancers.map(&:name),
334
+ queues: ress.queues.map(&:url).reject(&:empty?),
335
+ triggers: ress.triggers.map(&:name)
336
+ }
337
+ end
338
+ end
339
+
340
+ unless resources.empty?
341
+ runner.stdout.puts JSON.pretty_generate(resources)
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,71 @@
1
+ module JackAndTheElasticBeanstalk
2
+ class Config
3
+ attr_reader :app_hash
4
+ attr_reader :eb_configs
5
+
6
+ def initialize(app_hash:, eb_configs:)
7
+ @app_hash = app_hash
8
+ @eb_configs = eb_configs
9
+ end
10
+
11
+ def self.load(path:)
12
+ app_yml = path + "app.yml"
13
+ app_hash = YAML.load_file(app_yml.to_s)
14
+
15
+ eb_configs = path.children.each.with_object({}) do |file, acc|
16
+ relative_path = file.relative_path_from(path)
17
+ acc[relative_path] = file.read if file.extname == ".config"
18
+ end
19
+
20
+ Config.new(app_hash: app_hash, eb_configs: eb_configs)
21
+ end
22
+
23
+ def app_name
24
+ app_hash.dig("application", "name")
25
+ end
26
+
27
+ def region
28
+ app_hash.dig("application", "region")
29
+ end
30
+
31
+ def platform
32
+ app_hash.dig("application", "platform")
33
+ end
34
+
35
+ def configurations
36
+ app_hash["configurations"]
37
+ end
38
+
39
+ def s3_bucket
40
+ app_hash["s3_bucket"]
41
+ end
42
+
43
+ def processes(configuration)
44
+ configurations[configuration].select {|_, value| value["type"] }
45
+ end
46
+
47
+ def process_type(configuration, process)
48
+ processes(configuration)[process]["type"].to_s
49
+ end
50
+
51
+ def each_config
52
+ if block_given?
53
+ eb_configs.each do |path, content|
54
+ yield path, ERB.new(content).result
55
+ end
56
+ else
57
+ enum_for :each_config
58
+ end
59
+ end
60
+
61
+ def each_process(configuration)
62
+ if block_given?
63
+ processes(configuration).each do |key, hash|
64
+ yield key, hash
65
+ end
66
+ else
67
+ enum_for :each_process, configuration
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,240 @@
1
+ module JackAndTheElasticBeanstalk
2
+ class EB
3
+ attr_reader :application_name
4
+ attr_reader :logger
5
+ attr_reader :client
6
+ attr_accessor :timeout
7
+
8
+ def initialize(application_name:, logger:, client:)
9
+ @application_name = application_name
10
+ @logger = logger
11
+ @client = client
12
+ @env_stack = []
13
+ @timeout = 600
14
+ end
15
+
16
+ def environments
17
+ @environments = client.describe_environments(application_name: application_name, include_deleted: false).environments.map {|env|
18
+ Environment.new(application_name: application_name,
19
+ environment_name: env.environment_name,
20
+ logger: logger,
21
+ client: client).tap do |e|
22
+ e.timeout = timeout
23
+ e.data = env
24
+ end
25
+ }
26
+ end
27
+
28
+ def refresh
29
+ @environments = nil
30
+ end
31
+
32
+ def create_version(s3_bucket:, s3_key:, label:)
33
+ client.create_application_version(application_name: application_name,
34
+ description: label,
35
+ version_label: label,
36
+ source_bundle: {
37
+ s3_bucket: s3_bucket,
38
+ s3_key: s3_key,
39
+ },
40
+ process: true)
41
+ end
42
+
43
+ class Environment
44
+ attr_reader :application_name
45
+ attr_reader :logger
46
+ attr_reader :client
47
+ attr_reader :environment_name
48
+ attr_accessor :timeout
49
+
50
+ def initialize(application_name:, logger:, client:, environment_name:)
51
+ @application_name = application_name
52
+ @logger = logger
53
+ @client = client
54
+ @environment_name = environment_name
55
+ @timeout = 600
56
+ end
57
+
58
+ def refresh
59
+ @data = nil
60
+ @configuration_setting = nil
61
+ end
62
+
63
+ def data=(v)
64
+ @data = v
65
+ end
66
+
67
+ def data
68
+ @data ||= client.describe_environments(application_name: application_name).environments.find {|env|
69
+ env.environment_name == environment_name
70
+ }
71
+ end
72
+
73
+ def configuration_setting
74
+ @configuration_setting ||= client.describe_configuration_settings(application_name: application_name, environment_name: environment_name).configuration_settings.first
75
+ end
76
+
77
+ def env_vars
78
+ configuration_setting.option_settings.each.with_object({}) do |option, hash|
79
+ if option.namespace == "aws:elasticbeanstalk:application:environment"
80
+ hash[option.option_name] = option.value
81
+ end
82
+ end
83
+ end
84
+
85
+ def set_env_vars(env)
86
+ need_update = env.all? {|key, value|
87
+ if value
88
+ env_vars[key] == value
89
+ else
90
+ !env_vars.key?(key)
91
+ end
92
+ }
93
+
94
+ if need_update
95
+ logger.info("jeb::eb") { "Env vars looks like identical; skip" }
96
+ else
97
+ logger.info("jeb::eb") { "Updating environment variables" }
98
+
99
+ options_to_update = []
100
+ options_to_remove = []
101
+
102
+ env.each do |key, value|
103
+ if value
104
+ options_to_update << {
105
+ namespace: "aws:elasticbeanstalk:application:environment",
106
+ option_name: key.to_s,
107
+ value: value.to_s
108
+ }
109
+ else
110
+ options_to_remove << {
111
+ namespace: "aws:elasticbeanstalk:application:environment",
112
+ option_name: key.to_s
113
+ }
114
+ end
115
+ end
116
+
117
+ client.update_environment(application_name: application_name,
118
+ environment_name: environment_name,
119
+ option_settings: options_to_update,
120
+ options_to_remove: options_to_remove)
121
+
122
+ refresh
123
+ end
124
+ end
125
+
126
+ def synchronize_update(timeout: self.timeout)
127
+ logger.info("jeb::eb") { "Synchronizing update started... (timeout = #{timeout})" }
128
+
129
+ yield if block_given?
130
+
131
+ start = Time.now
132
+ wait = 1
133
+
134
+ while true
135
+ refresh
136
+ st = status
137
+
138
+ logger.info("jeb::eb") { "#{environment_name}:: status=#{st}" }
139
+
140
+ case st
141
+ when "Ready"
142
+ break
143
+ when "Updating", "Launching", "Aborting"
144
+ # ok
145
+ else
146
+ raise "Unexpected status: #{st}"
147
+ end
148
+
149
+ if Time.now - start > timeout
150
+ raise "Timeout exceeded"
151
+ end
152
+
153
+ sleep wait
154
+ wait = [wait*2, 30].min
155
+ end
156
+
157
+ logger.info("jeb::eb") { "Synchronized in #{(Time.now - start).to_i} seconds" }
158
+ end
159
+
160
+ def scale
161
+ option_settings = configuration_setting.option_settings
162
+
163
+ min = option_settings.find {|option| option.namespace == "aws:autoscaling:asg" && option.option_name == "MinSize" }.value.to_i
164
+ max = option_settings.find {|option| option.namespace == "aws:autoscaling:asg" && option.option_name == "MaxSize" }.value.to_i
165
+
166
+ min...max
167
+ end
168
+
169
+ def status
170
+ data.status
171
+ end
172
+
173
+ def set_scale(scale)
174
+ if scale.is_a?(Integer)
175
+ scale = scale...scale
176
+ end
177
+
178
+ if self.scale == scale
179
+ logger.info("jeb::eb") { "New scale is identical to current scale; skip" }
180
+ else
181
+ logger.info("jeb::eb") { "Scaling to #{scale}" }
182
+
183
+ client.update_environment(application_name: application_name,
184
+ environment_name: environment_name,
185
+ option_settings: [
186
+ {
187
+ namespace: "aws:autoscaling:asg",
188
+ option_name: "MinSize",
189
+ value: scale.begin.to_s
190
+ },
191
+ {
192
+ namespace: "aws:autoscaling:asg",
193
+ option_name: "MaxSize",
194
+ value: scale.end.to_s
195
+ }
196
+ ])
197
+
198
+ refresh
199
+ end
200
+ end
201
+
202
+ def environment_id
203
+ data.environment_id
204
+ end
205
+
206
+ def destroy
207
+ logger.info("jeb::eb") { "Terminating #{environment_name}..." }
208
+ client.terminate_environment(environment_id: environment_id)
209
+ end
210
+
211
+ def health
212
+ logger.info("jeb::eb") { "Downloading health data on #{environment_name}..." }
213
+
214
+ client.describe_environment_health(environment_id: environment_id, attribute_names: ["All"])
215
+ end
216
+
217
+ def resources
218
+ logger.info("jeb::eb") { "Downloading resources on #{environment_name}..."}
219
+
220
+ client.describe_environment_resources(environment_id: environment_id)
221
+ end
222
+
223
+ def restart
224
+ logger.info("jeb::eb") { "Restarting #{environment_name}..." }
225
+ client.restart_app_server(environment_id: environment_id)
226
+ end
227
+
228
+ def deploy(label:)
229
+ client.update_environment(environment_id: environment_id,
230
+ version_label: label)
231
+ end
232
+
233
+ def ensure_version!(expected_label:)
234
+ unless data.version_label == expected_label
235
+ raise "Unexpected version label: expected=#{expected_label}, actual=#{data.version_label}"
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,73 @@
1
+ module JackAndTheElasticBeanstalk
2
+ class Runner
3
+ attr_reader :stdin
4
+ attr_reader :stdout
5
+ attr_reader :stderr
6
+ attr_reader :logger
7
+
8
+ def initialize(stdin:, stdout:, stderr:, logger:)
9
+ @stdin = stdin
10
+ @stdout = stdout
11
+ @stderr = stderr
12
+ @paths = [Pathname.pwd]
13
+ @logger = logger
14
+ end
15
+
16
+ def paths
17
+ id = "#{inspect}:paths".to_sym
18
+
19
+ unless Thread.current[id]
20
+ Thread.current[id] = @paths.dup
21
+ end
22
+
23
+ Thread.current[id]
24
+ end
25
+
26
+ def chdir(dir)
27
+ paths.push dir
28
+ yield
29
+ ensure
30
+ paths.pop
31
+ end
32
+
33
+ def pwd
34
+ paths.last
35
+ end
36
+
37
+ def each_line(string, prefix: nil)
38
+ Array(string).flat_map {|s| s.split(/\n/) }.each do |line|
39
+ if prefix
40
+ yield "#{prefix}: #{line}"
41
+ else
42
+ yield line
43
+ end
44
+ end
45
+ end
46
+
47
+ def capture3(*commands, options: {}, env: {})
48
+ logger.debug("jeb") { commands.inspect }
49
+
50
+ Open3.capture3(env, *commands, { chdir: pwd.to_s }.merge(options)).tap do |out, err, status|
51
+ logger.debug("jeb") { status.inspect }
52
+
53
+ each_line(out, prefix: "stdout") do |line|
54
+ logger.debug("jeb") { line }
55
+ end
56
+
57
+ each_line(err, prefix: "stderr") do |line|
58
+ logger.debug("jeb") { line }
59
+ end
60
+ end
61
+ end
62
+
63
+ def capture3!(*commands, options: {}, env: {})
64
+ out, err, status = capture3(*commands, options: options, env: env)
65
+
66
+ unless status.success?
67
+ raise "Faiiled to execute command: #{commands.inspect}"
68
+ end
69
+
70
+ [out, err]
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,193 @@
1
+ module JackAndTheElasticBeanstalk
2
+ class Service
3
+ attr_reader :config
4
+ attr_reader :source_dir
5
+ attr_reader :eb
6
+ attr_reader :runner
7
+ attr_reader :logger
8
+
9
+ def initialize(config:, source_dir:, eb:, runner:, logger:)
10
+ @config = config
11
+ @source_dir = source_dir
12
+ @eb = eb
13
+ @runner = runner
14
+ @logger = logger
15
+ end
16
+
17
+ def eb_init(target_dir:)
18
+ runner.chdir target_dir do
19
+ runner.capture3!("eb", "init", config.app_name, "-r", config.region, "-p", config.platform)
20
+ end
21
+ end
22
+
23
+ def eb_deploy(target_dir:, group:, process:)
24
+ runner.chdir target_dir do
25
+ runner.capture3!("eb", "deploy", env_name(group: group, process: process), "--nohang")
26
+ end
27
+
28
+ env = eb.environments.find {|env| env.environment_name == env_name(group: group, process: process) }
29
+ env.synchronize_update
30
+ end
31
+
32
+ def eb_create(target_dir:, configuration:, group:, process:, env_vars:)
33
+ if eb.environments.none? {|env| env.environment_name == env_name(group: group, process: process) }
34
+ logger.info("jeb::service") { "Creating eb environment for #{process} from #{target_dir}..." }
35
+
36
+ commandline = ["eb", "create", env_name(group: group, process: process), "--nohang", "--scale", "1"]
37
+
38
+ hash = config.processes(configuration)[process]
39
+
40
+ commandline.concat(["-t", "worker"]) if config.process_type(configuration, process) == "worker"
41
+ commandline.concat(["--region", hash["region"] || config.region])
42
+ commandline.concat(["--platform", hash["platform"] || config.platform])
43
+ commandline.concat(["--instance_profile", hash["instance_profile"]]) if hash["instance_profile"]
44
+ commandline.concat(["--keyname", hash["keyname"]]) if hash["keyname"]
45
+ commandline.concat(["--instance_type", hash["instance_type"]]) if hash["instance_type"]
46
+ commandline.concat(["--service-role", hash["service_role"]]) if hash["service_role"]
47
+
48
+ if hash["tags"]&.any?
49
+ commandline.concat(["--tags", hash["tags"].each.with_object([]) do |(key, value), acc|
50
+ acc << "#{key}=#{value}"
51
+ end.concat(",")])
52
+ end
53
+
54
+ if hash["vpc"]
55
+ vpc = hash["vpc"]
56
+
57
+ commandline.concat(["--vpc", "--vpc.id", vpc["id"]])
58
+ commandline.concat(["--vpc.ec2subnets", vpc["ec2subnets"].join(",")]) if vpc["ec2subnets"]&.any?
59
+ commandline.concat(["--vpc.elbpublic"]) if vpc["elbpublic"]
60
+ commandline.concat(["--vpc.elbsubnets", vpc["elbsubnets"].join(",")]) if vpc["elbsubnets"]&.any?
61
+ commandline.concat(["--vpc.publicip"]) if vpc["publicip"]
62
+ commandline.concat(["--vpc.securitygroups", vpc["securitygroups"].join(",")]) if vpc["securitygroups"]&.any?
63
+ end
64
+
65
+ unless env_vars.empty?
66
+ vars = env_vars.to_a.map {|(k, v)| "#{k}=#{v}" }.join(",")
67
+ commandline.concat(["--envvars", vars])
68
+ end
69
+
70
+ runner.chdir target_dir do
71
+ runner.capture3!(*commandline)
72
+ end
73
+
74
+ eb.refresh
75
+ env = eb.environments.find {|env| env.environment_name == env_name(group: group, process: process) }
76
+ env.synchronize_update
77
+ else
78
+ logger.info("jeb::service") { "Environment #{env_name(group:group, process: process)} already exists..." }
79
+ end
80
+ end
81
+
82
+ def env_name(group:, process:)
83
+ "jeb-#{group}-#{process}"
84
+ end
85
+
86
+ def stage(target_dir:, process:)
87
+ logger.info("jeb::service") { "Staging files in #{target_dir} for #{process}" }
88
+
89
+ ENV["JEB_PROCESS"] = process
90
+
91
+ export_files(dest: target_dir)
92
+
93
+ eb_extensions = target_dir + ".ebextensions"
94
+ eb_extensions.mkpath
95
+ config.each_config do |path, content|
96
+ logger.debug("jeb::service") { "Writing #{path} ..." }
97
+ (eb_extensions + path).write content
98
+ end
99
+ ensure
100
+ ENV.delete("JEB_PROCESS")
101
+ end
102
+
103
+ def archive(input_dir:, output_path:)
104
+ paths = Pathname.glob(input_dir + "**/*", File::FNM_DOTMATCH).select(&:file?)
105
+
106
+ Zip::File.open(output_path.to_s, Zip::File::CREATE) do |zip|
107
+ paths.each do |path|
108
+ zip.add(path.relative_path_from(input_dir).to_s, path.to_s)
109
+ end
110
+ end
111
+ end
112
+
113
+ def export_files(dest:)
114
+ files = runner.chdir(source_dir) do
115
+ runner.capture3!("git", "ls-files", "-z").first.split("\x0")
116
+ end
117
+
118
+ files.each do |f|
119
+ logger.debug("jeb::service") { "Copying #{f} ..."}
120
+
121
+ source_path = source_dir + f
122
+ target_path = dest + f
123
+
124
+ unless target_path.parent.directory?
125
+ target_path.parent.mkpath
126
+ end
127
+
128
+ FileUtils.copy(source_path.to_s, target_path.to_s)
129
+ end
130
+ end
131
+
132
+ def upload(archive_path:, name:)
133
+ logger.debug("jeb::service") { "Uploading #{archive_path} to #{config.s3_bucket}/#{name}..." }
134
+ s3 = Aws::S3::Client.new(region: config.region)
135
+ archive_path.open do |io|
136
+ s3.put_object(bucket: config.s3_bucket, key: name, body: io)
137
+ end
138
+ end
139
+
140
+ def deploy(group:, process:, archive_path:, s3_key:, label:)
141
+ upload(archive_path: archive_path, name: s3_key)
142
+ eb.create_version(s3_bucket: config.s3_bucket, s3_key: s3_key, label: label)
143
+
144
+ eb.environments.find {|env| env.environment_name == env_name(group: group, process: process) }.try do |env|
145
+ env.synchronize_update do
146
+ env.deploy(label: label)
147
+ end
148
+
149
+ env.ensure_version!(expected_label: label)
150
+ end
151
+ end
152
+
153
+ def destroy(group:)
154
+ logger.info("jeb::service") { "Destroying #{group} ..." }
155
+
156
+ each_environment(group: group) do |env, _|
157
+ env.destroy
158
+ end
159
+ end
160
+
161
+ def each_environment(group:)
162
+ if block_given?
163
+ regexp = /\Ajeb-#{group}-([^\-]+)\Z/
164
+
165
+ eb.environments.each do |env|
166
+ if env.environment_name =~ regexp
167
+ yield env, $1
168
+ end
169
+ end
170
+ else
171
+ enum_for :each_environment, group: group
172
+ end
173
+ end
174
+
175
+ def each_group
176
+ if block_given?
177
+ regexp = /\Ajeb-(.+)-([^\-]+)\Z/
178
+
179
+ eb.environments.group_by {|env|
180
+ if env.environment_name =~ regexp
181
+ $1
182
+ end
183
+ }.each do |group, envs|
184
+ if group
185
+ yield group, envs
186
+ end
187
+ end
188
+ else
189
+ enum_for :each_group
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,3 @@
1
+ module JackAndTheElasticBeanstalk
2
+ VERSION = "0.1.0.pre"
3
+ end
@@ -0,0 +1,24 @@
1
+ require "jack_and_the_elastic_beanstalk/version"
2
+
3
+ require "optparse"
4
+ require "pathname"
5
+ require 'dotenv'
6
+ require "rainbow"
7
+ require "erb"
8
+ require "yaml"
9
+ require "open3"
10
+ require "logger"
11
+ require "tmpdir"
12
+ require "pp"
13
+ require "aws-sdk"
14
+ require "thor"
15
+ require "parallel"
16
+ require "active_support"
17
+ require "active_support/core_ext"
18
+ require "zip"
19
+
20
+ require "jack_and_the_elastic_beanstalk/eb"
21
+ require "jack_and_the_elastic_beanstalk/config"
22
+ require "jack_and_the_elastic_beanstalk/runner"
23
+ require "jack_and_the_elastic_beanstalk/service"
24
+ require "jack_and_the_elastic_beanstalk/cli"
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jack_and_the_elastic_beanstalk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre
5
+ platform: ruby
6
+ authors:
7
+ - Soutaro Matsumoto
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-02-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rainbow
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: aws-sdk
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.6'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: thor
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.19'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.19'
111
+ - !ruby/object:Gem::Dependency
112
+ name: parallel
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.10'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.10'
125
+ - !ruby/object:Gem::Dependency
126
+ name: activesupport
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '5.0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '5.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubyzip
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.2'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.2'
153
+ description: Jack and the Elastic Beanstalk.
154
+ email:
155
+ - matsumoto@soutaro.com
156
+ executables:
157
+ - jeb
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - ".gitignore"
162
+ - ".ruby-version"
163
+ - ".travis.yml"
164
+ - Gemfile
165
+ - README.md
166
+ - Rakefile
167
+ - bin/console
168
+ - bin/setup
169
+ - exe/jeb
170
+ - jack_and_the_elastic_beanstalk.gemspec
171
+ - lib/jack_and_the_elastic_beanstalk.rb
172
+ - lib/jack_and_the_elastic_beanstalk/cli.rb
173
+ - lib/jack_and_the_elastic_beanstalk/config.rb
174
+ - lib/jack_and_the_elastic_beanstalk/eb.rb
175
+ - lib/jack_and_the_elastic_beanstalk/runner.rb
176
+ - lib/jack_and_the_elastic_beanstalk/service.rb
177
+ - lib/jack_and_the_elastic_beanstalk/version.rb
178
+ homepage: https://github.com/sideci/jack_and_the_elastic_beanstalk
179
+ licenses: []
180
+ metadata: {}
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">"
193
+ - !ruby/object:Gem::Version
194
+ version: 1.3.1
195
+ requirements: []
196
+ rubyforge_project:
197
+ rubygems_version: 2.5.2
198
+ signing_key:
199
+ specification_version: 4
200
+ summary: Jack and the Elastic Beanstalk.
201
+ test_files: []