jack_and_the_elastic_beanstalk 0.1.0.pre

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