orchestration 0.2.2 → 0.2.3

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +1 -1
  4. data/TODO +11 -5
  5. data/config/locales/en.yml +13 -1
  6. data/lib/Rakefile +5 -0
  7. data/lib/orchestration/docker_compose/application_service.rb +20 -22
  8. data/lib/orchestration/docker_compose/mongo_service.rb +17 -8
  9. data/lib/orchestration/docker_compose/nginx_proxy_service.rb +19 -0
  10. data/lib/orchestration/docker_compose/services.rb +4 -2
  11. data/lib/orchestration/docker_compose.rb +1 -0
  12. data/lib/orchestration/environment.rb +11 -1
  13. data/lib/orchestration/errors.rb +2 -0
  14. data/lib/orchestration/file_helpers.rb +3 -3
  15. data/lib/orchestration/install_generator.rb +29 -8
  16. data/lib/orchestration/service_check.rb +1 -1
  17. data/lib/orchestration/services/application/configuration.rb +29 -2
  18. data/lib/orchestration/services/application/healthcheck.rb +37 -0
  19. data/lib/orchestration/services/application.rb +3 -0
  20. data/lib/orchestration/services/configuration_base.rb +44 -0
  21. data/lib/orchestration/services/database/configuration.rb +19 -21
  22. data/lib/orchestration/services/database/healthcheck.rb +2 -0
  23. data/lib/orchestration/services/healthcheck_base.rb +38 -0
  24. data/lib/orchestration/services/mongo/configuration.rb +4 -25
  25. data/lib/orchestration/services/mongo/healthcheck.rb +2 -8
  26. data/lib/orchestration/services/nginx_proxy/configuration.rb +21 -0
  27. data/lib/orchestration/services/nginx_proxy/healthcheck.rb +23 -0
  28. data/lib/orchestration/services/nginx_proxy.rb +11 -0
  29. data/lib/orchestration/services/rabbitmq/configuration.rb +4 -5
  30. data/lib/orchestration/services/rabbitmq/healthcheck.rb +4 -3
  31. data/lib/orchestration/services.rb +4 -0
  32. data/lib/orchestration/templates/{Dockerfile.tt → Dockerfile.erb} +5 -2
  33. data/lib/orchestration/templates/{Makefile.tt → Makefile.erb} +30 -14
  34. data/lib/orchestration/templates/entrypoint.sh.erb +6 -0
  35. data/lib/orchestration/templates/unicorn.rb.erb +17 -0
  36. data/lib/orchestration/version.rb +1 -1
  37. data/lib/orchestration.rb +4 -1
  38. data/lib/tasks/orchestration.rake +15 -1
  39. data/orchestration.gemspec +3 -0
  40. metadata +56 -5
  41. data/lib/orchestration/healthcheck_base.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f8ceff8fc2dd990496a6de51dddca57caa2e7000
4
- data.tar.gz: dd0507265efb67c67ea5164cb2a7ff8b22675f0d
3
+ metadata.gz: 1ff223f91a5979874db88e7e55a788b9c7a645f5
4
+ data.tar.gz: d910c778ab54470515efdfa2f514853a91f5f0ea
5
5
  SHA512:
6
- metadata.gz: 6f254e30b288a9d20aba6b55abd7fc01fd8973690bb31f430904f870a40088a6b0b7b4c5f7677c81c1cf02c6ed99c2fc8552a3a2ead211ed3bf877757dccea80
7
- data.tar.gz: fc397b5f3341a7f6b2eda9e3231159c5fa5460a9feb3b5059472c5ea21de94796a3f55a4b7ad18571ef6a99eeb1ba3c3896efa7868024ccc577679777ae621fb
6
+ metadata.gz: e03236d51bfd3476b89cbe27a3811f2e4455c1c50b741901bd98a9db539cd029df39d8bdd0547228279b532252a1a484e7716f37c0f86a82c162847ff48a293a
7
+ data.tar.gz: 1538b05bdf17e1ecb6478992aff82ededd06cbfcf23b87dec959f2a8afc66cc408f5171de36539d9ba148b0b66372e57126c7f7023c0b135fbbefff881b6853d
data/.gitignore CHANGED
@@ -21,5 +21,6 @@ spec/dummy/.gitignore
21
21
  spec/dummy/Makefile
22
22
  spec/dummy/docker/*
23
23
  spec/dummy/docker-compose.yml
24
+ spec/dummy/config/unicorn.rb
24
25
 
25
26
  .byebug_history
data/README.md CHANGED
@@ -18,7 +18,7 @@ Containers are automatically created for the following dependencies:
18
18
  Add this line to your application's Gemfile:
19
19
 
20
20
  ```ruby
21
- gem 'orchestration', '~> 0.2.2'
21
+ gem 'orchestration', '~> 0.2.3'
22
22
  ```
23
23
 
24
24
  And then build your bundle:
data/TODO CHANGED
@@ -1,9 +1,15 @@
1
1
  Be aware of yarn and include build steps in Dockerfile if present. Use dashboard
2
2
  front end as a reference.
3
3
 
4
- Create a production docker-compose.yml and set:
5
- - RAILS_ENV
6
- - SECRET_KEY_BASE
7
- - DATABASE_URL (from config/database.yml)
8
-
9
4
  Provide volumes for databases and mount appropriate directories for adapter
5
+
6
+ Refactor docker-compose services - these really belong in
7
+ lib/orchestration/services/<service-name>/docker_compose.rb
8
+
9
+ Add a note to README to handle `RAILS_LOG_TO_STDOUT` in development -
10
+ unfortunately Rails ignores this value in development mode. (It's configured in
11
+ config/environments/production.rb)
12
+
13
+ Standardise on log formats - by policy or recommendation ?
14
+
15
+ Redis support
@@ -2,15 +2,27 @@ en:
2
2
  orchestration:
3
3
  unknown_scheme: "Unrecognised database adapter for URL scheme: %{scheme}"
4
4
  attempt_limit: "Unable to reconnect after %{limit} attempts. Aborting."
5
+
6
+ application:
7
+ waiting: "Waiting for application: %{config}"
8
+ ready: "Application is ready."
9
+ connection_error: "Error attempting to connect to application: received status code %{code}"
10
+
5
11
  database:
6
12
  waiting: "Waiting for database: %{config}"
7
13
  ready: "Database is ready."
8
- missing_default: "database.yml must define a `default` section."
14
+
9
15
  mongo:
10
16
  waiting: "Waiting for Mongo: %{config}"
11
17
  ready: "Mongo is ready."
18
+
19
+ nginxproxy:
20
+ waiting: "Waiting for Nginx proxy: %{config}"
21
+ ready: "Nginx proxy is ready."
22
+
12
23
  rabbitmq:
13
24
  waiting: "Waiting for RabbitMQ: %{config}"
14
25
  ready: "RabbitMQ is ready."
26
+
15
27
  docker:
16
28
  username_request: "Enter your Docker registry username (used for tagging images)"
data/lib/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'orchestration'
4
+
5
+ import Orchestration.root.join('lib', 'tasks', 'orchestration.rake')
@@ -3,48 +3,46 @@
3
3
  module Orchestration
4
4
  module DockerCompose
5
5
  class ApplicationService
6
- PORT = 3000
7
-
8
6
  def initialize(config)
9
7
  @config = config
10
- @env = config.environment
11
8
  end
12
9
 
13
10
  def definition
14
11
  {
15
12
  'image' => image,
13
+ 'entrypoint' => '/entrypoint.sh',
14
+ 'command' => %w[
15
+ bundle exec unicorn -c /application/config/unicorn.rb
16
+ ],
16
17
  'environment' => environment,
17
- 'ports' => ["#{PORT}:#{PORT}"]
18
+ 'expose' => [8080]
18
19
  }
19
20
  end
20
21
 
21
22
  private
22
23
 
23
24
  def image
24
- "#{@env.settings.get('docker.username')}/#{@env.application_name}"
25
+ "#{@config.docker_username}/#{@config.application_name}"
25
26
  end
26
27
 
27
28
  def environment
28
29
  {
29
- # `nil` values will inherit from environment or `.env` file.
30
- 'RAILS_ENV' => nil,
31
- 'SECRET_KEY_BASE' => nil,
32
- 'DATABASE_URL' => database_url,
33
- 'RAILS_LOG_TO_STDOUT' => '1'
34
- }
30
+ 'DATABASE_URL' => @config.database_url,
31
+ 'RAILS_LOG_TO_STDOUT' => '1',
32
+ 'UNICORN_PRELOAD_APP' => '1',
33
+ 'UNICORN_TIMEOUT' => '60',
34
+ 'UNICORN_WORKER_PROCESSES' => '8',
35
+ 'VIRTUAL_PORT' => '8080',
36
+ 'VIRTUAL_HOST' => 'localhost'
37
+ }.merge(inherited_environment)
35
38
  end
36
39
 
37
- def database_url
38
- settings = @config.database_settings
39
- return nil if settings.fetch('adapter') == 'sqlite3'
40
-
41
- scheme = settings.fetch('scheme')
42
- database = settings.fetch('database')
43
- username = settings.fetch('username')
44
- password = settings.fetch('password')
45
- port = DatabaseService::PORT
46
-
47
- "#{scheme}://#{username}:#{password}@database:#{port}/#{database}"
40
+ def inherited_environment
41
+ {
42
+ 'HOST_UID' => nil,
43
+ 'RAILS_ENV' => nil,
44
+ 'SECRET_KEY_BASE' => nil
45
+ }
48
46
  end
49
47
  end
50
48
  end
@@ -10,18 +10,27 @@ module Orchestration
10
10
  def definition
11
11
  return nil if @config.settings.nil?
12
12
 
13
- # REVIEW: If the host application defines multiple mongo hosts then we
14
- # create one service instance and point them all at the same service.
15
- # Instead we should probably create a separate service for each.
16
- ports = @config.ports.map do |port|
17
- "#{port}:#{Orchestration::Services::Mongo::PORT}"
18
- end
19
-
20
13
  {
21
14
  'image' => 'library/mongo',
22
- 'ports' => ports
15
+ 'ports' => ["#{local_port}:#{remote_port}"]
23
16
  }
24
17
  end
18
+
19
+ private
20
+
21
+ def local_port
22
+ _host, _, port = @config.settings
23
+ .fetch('clients')
24
+ .fetch('default')
25
+ .fetch('hosts')
26
+ .first
27
+ .partition(':')
28
+ port.empty? ? remote_port : port
29
+ end
30
+
31
+ def remote_port
32
+ Orchestration::Services::Mongo::PORT
33
+ end
25
34
  end
26
35
  end
27
36
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module DockerCompose
5
+ class NginxProxyService
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def definition
11
+ {
12
+ 'image' => 'jwilder/nginx-proxy',
13
+ 'ports' => %w[3000:80],
14
+ 'volumes' => ['/var/run/docker.sock:/tmp/docker.sock:ro']
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -8,7 +8,8 @@ module Orchestration
8
8
  'application' => options.fetch(:application, nil),
9
9
  'database' => options.fetch(:database, nil),
10
10
  'mongo' => options.fetch(:mongo, nil),
11
- 'rabbitmq' => options.fetch(:rabbitmq, nil)
11
+ 'rabbitmq' => options.fetch(:rabbitmq, nil),
12
+ 'nginx-proxy' => options.fetch(:nginx_proxy, nil)
12
13
  }
13
14
  end
14
15
 
@@ -31,7 +32,8 @@ module Orchestration
31
32
  { name: 'application', class: ApplicationService },
32
33
  { name: 'database', class: DatabaseService },
33
34
  { name: 'mongo', class: MongoService },
34
- { name: 'rabbitmq', class: RabbitMQService }
35
+ { name: 'rabbitmq', class: RabbitMQService },
36
+ { name: 'nginx-proxy', class: NginxProxyService }
35
37
  ]
36
38
  end
37
39
 
@@ -10,4 +10,5 @@ require 'orchestration/docker_compose/services'
10
10
  require 'orchestration/docker_compose/application_service'
11
11
  require 'orchestration/docker_compose/database_service'
12
12
  require 'orchestration/docker_compose/mongo_service'
13
+ require 'orchestration/docker_compose/nginx_proxy_service'
13
14
  require 'orchestration/docker_compose/rabbitmq_service'
@@ -32,6 +32,14 @@ module Orchestration
32
32
  root.join('.orchestration.yml')
33
33
  end
34
34
 
35
+ def docker_compose_configuration_path
36
+ root.join('docker-compose.yml')
37
+ end
38
+
39
+ def docker_compose_config
40
+ YAML.safe_load(File.read(docker_compose_configuration_path))
41
+ end
42
+
35
43
  def application_name
36
44
  Rails.application.class.parent.name.underscore
37
45
  end
@@ -41,7 +49,9 @@ module Orchestration
41
49
  end
42
50
 
43
51
  def root
44
- Rails.root
52
+ return Rails.root if defined?(Rails) && Rails.root
53
+
54
+ Pathname.new(Dir.pwd)
45
55
  end
46
56
  end
47
57
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Orchestration
4
4
  class OrchestrationError < StandardError; end
5
+
6
+ class ApplicationConnectionError < OrchestrationError; end
5
7
  class DatabaseConfigurationError < OrchestrationError; end
6
8
  class MongoConfigurationError < OrchestrationError; end
7
9
  end
@@ -4,9 +4,9 @@ module Orchestration
4
4
  module FileHelpers
5
5
  private
6
6
 
7
- def template(template_name, environment = {})
7
+ def template(template_name, context = {})
8
8
  Erubis::Eruby.new(read_template(template_name))
9
- .result(environment)
9
+ .result(context)
10
10
  end
11
11
 
12
12
  def delete_and_inject_after(path, pattern, replacement)
@@ -82,7 +82,7 @@ module Orchestration
82
82
  end
83
83
 
84
84
  def read_template(template)
85
- File.read(templates_path.join("#{template}.tt"))
85
+ File.read(templates_path.join("#{template}.erb"))
86
86
  end
87
87
  end
88
88
  end
@@ -35,15 +35,19 @@ module Orchestration
35
35
  end
36
36
 
37
37
  def dockerfile
38
- docker_dir = Rails.root.join('docker')
39
- path = docker_dir.join('Dockerfile')
40
38
  content = template('Dockerfile', ruby_version: RUBY_VERSION)
41
- FileUtils.mkdir(docker_dir) unless Dir.exist?(docker_dir)
39
+ write_file(docker_dir.join('Dockerfile'), content, overwrite: false)
40
+ end
41
+
42
+ def entrypoint
43
+ content = template('entrypoint.sh')
44
+ path = docker_dir.join('entrypoint.sh')
42
45
  write_file(path, content, overwrite: false)
46
+ FileUtils.chmod('a+x', path)
43
47
  end
44
48
 
45
49
  def gitignore
46
- path = Rails.root.join('.gitignore')
50
+ path = @env.root.join('.gitignore')
47
51
  entries = [
48
52
  'docker/.build',
49
53
  'docker/Gemfile',
@@ -54,18 +58,25 @@ module Orchestration
54
58
  end
55
59
 
56
60
  def docker_compose
57
- path = Rails.root.join('docker-compose.yml')
61
+ path = @env.root.join('docker-compose.yml')
58
62
  return if File.exist?(path)
59
63
 
60
64
  docker_compose = DockerCompose::Services.new(
61
65
  application: configuration(:application),
62
66
  database: configuration(:database),
63
67
  mongo: configuration(:mongo),
64
- rabbitmq: configuration(:rabbitmq)
68
+ rabbitmq: configuration(:rabbitmq),
69
+ nginx_proxy: configuration(:nginx_proxy)
65
70
  )
66
71
  write_file(path, docker_compose.structure.to_yaml)
67
72
  end
68
73
 
74
+ def unicorn
75
+ content = template('unicorn.rb')
76
+ path = @env.root.join('config', 'unicorn.rb')
77
+ write_file(path, content, overwrite: false)
78
+ end
79
+
69
80
  private
70
81
 
71
82
  def configuration(service)
@@ -73,7 +84,8 @@ module Orchestration
73
84
  application: Services::Application::Configuration,
74
85
  database: Services::Database::Configuration,
75
86
  mongo: Services::Mongo::Configuration,
76
- rabbitmq: Services::RabbitMQ::Configuration
87
+ rabbitmq: Services::RabbitMQ::Configuration,
88
+ nginx_proxy: Services::NginxProxy::Configuration
77
89
  }.fetch(service).new(@env)
78
90
  end
79
91
 
@@ -81,7 +93,9 @@ module Orchestration
81
93
  [
82
94
  configuration(:database).settings.nil? ? nil : 'wait-database',
83
95
  configuration(:mongo).settings.nil? ? nil : 'wait-mongo',
84
- configuration(:rabbitmq).settings.nil? ? nil : 'wait-rabbitmq'
96
+ configuration(:rabbitmq).settings.nil? ? nil : 'wait-rabbitmq',
97
+ 'wait-nginx-proxy',
98
+ 'wait-application'
85
99
  ].compact.join(' ')
86
100
  end
87
101
 
@@ -91,5 +105,12 @@ module Orchestration
91
105
  @terminal.write(:setup, I18n.t('orchestration.docker.username_request'))
92
106
  settings.set('docker.username', @terminal.read('[username]:'))
93
107
  end
108
+
109
+ def docker_dir
110
+ path = @env.root.join('docker')
111
+ FileUtils.mkdir(path) unless Dir.exist?(path)
112
+
113
+ path
114
+ end
94
115
  end
95
116
  end
@@ -64,7 +64,7 @@ module Orchestration
64
64
  def echo_failure
65
65
  @terminal.write(
66
66
  :failure,
67
- I18n.t('orchestration.attempt_limit', limit: ATTEMPT_LIMIT)
67
+ I18n.t('orchestration.attempt_limit', limit: @attempt_limit)
68
68
  )
69
69
  end
70
70
 
@@ -4,17 +4,44 @@ module Orchestration
4
4
  module Services
5
5
  module Application
6
6
  class Configuration
7
+ include ConfigurationBase
8
+
9
+ self.service_name = 'application'
10
+
7
11
  def initialize(env)
8
12
  @env = env
13
+ @settings = {} # Included for interface consistency; currently unused.
14
+ end
15
+
16
+ def docker_username
17
+ @env.settings.get('docker.username')
18
+ end
19
+
20
+ def application_name
21
+ @env.application_name
9
22
  end
10
23
 
11
- def environment
12
- @env
24
+ def friendly_config
25
+ "[#{application_name}] #{host}:#{local_port}"
13
26
  end
14
27
 
15
28
  def database_settings
16
29
  Database::Configuration.new(@env).settings
17
30
  end
31
+
32
+ def database_url
33
+ settings = database_settings
34
+ return nil if settings.fetch('adapter') == 'sqlite3'
35
+
36
+ scheme = settings.fetch('scheme')
37
+ database = settings.fetch('database')
38
+ username = settings.fetch('username')
39
+ password = settings.fetch('password')
40
+ port = settings.fetch('port')
41
+ host = Database::Configuration.service_name
42
+
43
+ "#{scheme}://#{username}:#{password}@#{host}:#{port}/#{database}"
44
+ end
18
45
  end
19
46
  end
20
47
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Application
6
+ class Healthcheck
7
+ include HealthcheckBase
8
+
9
+ def initialize(env)
10
+ @configuration = Configuration.new(env)
11
+ end
12
+
13
+ def connect
14
+ response = Net::HTTP.get_response(
15
+ URI("http://localhost:#{@configuration.local_port}")
16
+ )
17
+ connection_error(response.code) if connection_error?(response.code)
18
+ end
19
+
20
+ def connection_errors
21
+ [Errno::ECONNREFUSED, ApplicationConnectionError]
22
+ end
23
+
24
+ private
25
+
26
+ def connection_error(code)
27
+ raise ApplicationConnectionError,
28
+ I18n.t('orchestration.application.connection_error', code: code)
29
+ end
30
+
31
+ def connection_error?(code)
32
+ %w[502 503 500].include?(code)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -7,4 +7,7 @@ module Orchestration
7
7
  end
8
8
  end
9
9
 
10
+ require 'net/http'
11
+
10
12
  require 'orchestration/services/application/configuration'
13
+ require 'orchestration/services/application/healthcheck'
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module ConfigurationBase
6
+ attr_reader :settings
7
+
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def service_name=(val)
14
+ @service_name = val
15
+ end
16
+
17
+ def service_name
18
+ return @service_name unless @service_name.nil?
19
+
20
+ raise ArgumentError,
21
+ "Must call `self.name=` on #{self.class.service_name}"
22
+ end
23
+ end
24
+
25
+ def host
26
+ 'localhost'
27
+ end
28
+
29
+ def local_port
30
+ name = self.class.service_name
31
+ key = name == 'application' ? 'nginx-proxy' : name
32
+
33
+ @env.docker_compose_config
34
+ .fetch('services')
35
+ .fetch(key)
36
+ .fetch('ports')
37
+ .first
38
+ .partition(':')
39
+ .first
40
+ .to_i
41
+ end
42
+ end
43
+ end
44
+ end
@@ -4,7 +4,11 @@ module Orchestration
4
4
  module Services
5
5
  module Database
6
6
  class Configuration
7
- attr_reader :adapter, :settings
7
+ include ConfigurationBase
8
+
9
+ self.service_name = 'database'
10
+
11
+ attr_reader :adapter
8
12
 
9
13
  def initialize(env)
10
14
  @env = env
@@ -13,28 +17,24 @@ module Orchestration
13
17
  return unless defined?(ActiveRecord)
14
18
  return unless File.exist?(@env.database_configuration_path)
15
19
 
20
+ @environments = parse(File.read(@env.database_configuration_path))
16
21
  setup
17
22
  end
18
23
 
19
24
  def friendly_config
20
- adapter = @settings['adapter']
21
- host = @settings['host']
22
- port = @settings['port']
25
+ adapter = @settings.fetch('adapter')
23
26
  return "[#{adapter}]" if adapter == 'sqlite3'
24
- return "[#{adapter}] #{host}" unless port.present?
25
27
 
26
- "[#{adapter}] #{host}:#{port}"
28
+ "[#{adapter}] #{host}:#{local_port}"
27
29
  end
28
30
 
29
31
  private
30
32
 
31
33
  def setup
32
- environments = parse(File.read(@env.database_configuration_path))
33
- base = base_config(environments)
34
34
  @adapter = adapter_object(base['adapter'])
35
35
  @settings = base.merge(@adapter.credentials)
36
36
  .merge('scheme' => scheme_name(base['adapter']))
37
- @settings.merge!(default_port) unless @settings.key?('port')
37
+ @settings.merge!(default_port) if @settings['port'].nil?
38
38
  end
39
39
 
40
40
  def parse(content)
@@ -57,15 +57,18 @@ module Orchestration
57
57
  }.fetch(name).new
58
58
  end
59
59
 
60
- def base_config(environments)
61
- missing_default unless environments.key?('default')
60
+ def environment
61
+ @environments[@env.environment]
62
+ end
62
63
 
63
- host = url_config['host'] || environments[@env.environment]['host']
64
+ def base
65
+ environment.merge(url_config).merge('host' => host)
66
+ end
64
67
 
65
- environments
66
- .fetch('default')
67
- .merge(url_config)
68
- .merge('host' => host)
68
+ def host
69
+ return nil if @adapter.is_a?(adapters::Sqlite3)
70
+
71
+ super
69
72
  end
70
73
 
71
74
  def adapters
@@ -109,11 +112,6 @@ module Orchestration
109
112
  Hash[URI.decode_www_form(uri.query)]
110
113
  end
111
114
 
112
- def missing_default
113
- raise DatabaseConfigurationError,
114
- I18n.t('orchestration.database.missing_default')
115
- end
116
-
117
115
  def adapter_mapping
118
116
  {
119
117
  'mysql' => 'mysql2',
@@ -6,6 +6,8 @@ module Orchestration
6
6
  class Healthcheck
7
7
  include HealthcheckBase
8
8
 
9
+ dependencies 'active_record'
10
+
9
11
  def initialize(env)
10
12
  @configuration = Configuration.new(env)
11
13
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module HealthcheckBase
6
+ attr_reader :configuration
7
+
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def start(env = nil, terminal = nil, options = {})
14
+ load_dependencies
15
+ exit_on_error = options.fetch(:exit_on_error, true)
16
+ options.delete(:exit_on_error)
17
+ env ||= Environment.new
18
+ terminal ||= Terminal.new
19
+ check = ServiceCheck.new(new(env), terminal, options)
20
+
21
+ exit 1 if !check.run && exit_on_error
22
+ end
23
+
24
+ def dependencies(*args)
25
+ @dependencies = args
26
+ end
27
+
28
+ private
29
+
30
+ def load_dependencies
31
+ return if @dependencies.nil?
32
+
33
+ @dependencies.map { |dependency| require dependency }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -4,7 +4,9 @@ module Orchestration
4
4
  module Services
5
5
  module Mongo
6
6
  class Configuration
7
- attr_reader :settings
7
+ include ConfigurationBase
8
+
9
+ self.service_name = 'mongo'
8
10
 
9
11
  def initialize(env)
10
12
  @env = env
@@ -15,16 +17,8 @@ module Orchestration
15
17
  @settings = config.fetch(@env.environment)
16
18
  end
17
19
 
18
- def ports
19
- hosts_and_ports.map { |_host, port| port }
20
- end
21
-
22
20
  def friendly_config
23
- hosts_string = hosts_and_ports.map do |host, port|
24
- "#{host}:#{port}"
25
- end.join(', ')
26
-
27
- "[mongoid] #{hosts_string}"
21
+ "[mongoid] #{host}:#{local_port}"
28
22
  end
29
23
 
30
24
  private
@@ -34,21 +28,6 @@ module Orchestration
34
28
  File.read(@env.mongoid_configuration_path), [], [], true
35
29
  )
36
30
  end
37
-
38
- def clients
39
- # 'sessions' and 'clients' are synonymous but vary between versions of
40
- # Mongoid: https://github.com/mongoid/mongoid/commit/657650bc4befa001c0f66e8788e9df6a1af37e84
41
- key = @settings.key?('sessions') ? 'sessions' : 'clients'
42
-
43
- @settings.fetch(key)
44
- end
45
-
46
- def hosts_and_ports
47
- clients.fetch('default').fetch('hosts').map do |host_string|
48
- host, _, port = host_string.partition(':')
49
- [host, (port.empty? ? PORT : port)]
50
- end
51
- end
52
31
  end
53
32
  end
54
33
  end
@@ -6,6 +6,8 @@ module Orchestration
6
6
  class Healthcheck
7
7
  include HealthcheckBase
8
8
 
9
+ dependencies 'mongoid'
10
+
9
11
  def initialize(env)
10
12
  @configuration = Configuration.new(env)
11
13
  end
@@ -20,14 +22,6 @@ module Orchestration
20
22
  Mongoid.load_configuration(@configuration.settings)
21
23
  !Mongoid.default_client.database_names.empty?
22
24
  end
23
-
24
- private
25
-
26
- def clients
27
- return Mongoid.sessions if Mongoid.respond_to?(:sessions)
28
-
29
- Mongoid.clients
30
- end
31
25
  end
32
26
  end
33
27
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module NginxProxy
6
+ class Configuration
7
+ include ConfigurationBase
8
+
9
+ self.service_name = 'nginx-proxy'
10
+
11
+ def initialize(env)
12
+ @env = env
13
+ end
14
+
15
+ def friendly_config
16
+ "[nginx-proxy] #{host}:#{local_port}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module NginxProxy
6
+ class Healthcheck
7
+ include HealthcheckBase
8
+
9
+ def initialize(env)
10
+ @configuration = Configuration.new(env)
11
+ end
12
+
13
+ def connect
14
+ Net::HTTP.start('localhost', 3000)
15
+ end
16
+
17
+ def connection_errors
18
+ [Errno::ECONNREFUSED]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module NginxProxy
6
+ end
7
+ end
8
+ end
9
+
10
+ require 'orchestration/services/nginx_proxy/configuration'
11
+ require 'orchestration/services/nginx_proxy/healthcheck'
@@ -4,7 +4,9 @@ module Orchestration
4
4
  module Services
5
5
  module RabbitMQ
6
6
  class Configuration
7
- attr_reader :settings
7
+ include ConfigurationBase
8
+
9
+ self.service_name = 'rabbitmq'
8
10
 
9
11
  def initialize(env)
10
12
  @env = env
@@ -17,10 +19,7 @@ module Orchestration
17
19
  end
18
20
 
19
21
  def friendly_config
20
- host = @settings.fetch('host')
21
- port = @settings.fetch('port')
22
-
23
- "[bunny] amqp://#{host}:#{port}"
22
+ "[bunny] amqp://#{host}:#{local_port}"
24
23
  end
25
24
 
26
25
  private
@@ -6,6 +6,8 @@ module Orchestration
6
6
  class Healthcheck
7
7
  include HealthcheckBase
8
8
 
9
+ dependencies 'bunny'
10
+
9
11
  def initialize(env)
10
12
  @configuration = Configuration.new(env)
11
13
  end
@@ -18,10 +20,9 @@ module Orchestration
18
20
  end
19
21
 
20
22
  def connect
21
- host = @configuration.settings.fetch('host')
22
- port = @configuration.settings.fetch('port')
23
+ port = @configuration.local_port
23
24
 
24
- connection = Bunny.new("amqp://#{host}:#{port}", log_file: devnull)
25
+ connection = Bunny.new("amqp://localhost:#{port}", log_file: devnull)
25
26
  connection.start
26
27
  connection.stop
27
28
  end
@@ -5,7 +5,11 @@ module Orchestration
5
5
  end
6
6
  end
7
7
 
8
+ require 'orchestration/services/configuration_base'
9
+ require 'orchestration/services/healthcheck_base'
10
+
8
11
  require 'orchestration/services/application'
9
12
  require 'orchestration/services/database'
10
13
  require 'orchestration/services/mongo'
14
+ require 'orchestration/services/nginx_proxy'
11
15
  require 'orchestration/services/rabbitmq'
@@ -1,11 +1,14 @@
1
1
  FROM ruby:<%= ruby_version %>
2
2
  ARG BUNDLE_BITBUCKET__ORG
3
+ ARG BUNDLE_GITHUB__COM
3
4
  RUN apt-get update \
4
- && apt-get install -y node.js \
5
+ && apt-get install -y node.js gosu \
6
+ && rm -rf /var/lib/apt/lists/* \
5
7
  && gem install bundler \
8
+ && gem install unicorn \
6
9
  && mkdir /application
7
10
  WORKDIR /application
8
11
  COPY Gemfile Gemfile.lock ./
9
12
  RUN bundle install --deployment
13
+ COPY entrypoint.sh /
10
14
  ADD .build/context.tar.gz .
11
- CMD ["bundle", "exec", "rails", "server", "-p", "3000", "-b", "0.0.0.0"]
@@ -10,21 +10,27 @@
10
10
  # bundle exec rubocop
11
11
  # yarn run eslint app/javascript
12
12
  #
13
- .PHONY: docker docker-build docker-push wait-database start
13
+ .PHONY: start stop migrate docker build push start <%= wait_commands %>
14
14
 
15
15
  ### Container management commands ###
16
16
 
17
+ COMPOSE:=HOST_UID=$(shell id -u) docker-compose
18
+
17
19
  start:
18
- docker-compose up -d
20
+ @echo "Starting containers..."
21
+ @${COMPOSE} up -d --scale application=$${INSTANCES:-1}
22
+ @make wait
19
23
 
20
24
  stop:
21
- docker-compose down
25
+ @echo "Stopping containers..."
26
+ @${COMPOSE} down
27
+ @echo "All containers stopped."
22
28
 
23
29
  ### Database utility commands ###
24
30
 
25
31
  migrate: wait-database
26
32
  @echo "Running migrations..."
27
- @docker-compose run application bundle exec rake db:migrate
33
+ @${COMPOSE} run application bundle exec rake db:migrate
28
34
  @echo "Migrations complete."
29
35
 
30
36
  ### Service healthcheck commands ###
@@ -32,30 +38,40 @@ migrate: wait-database
32
38
  wait: <%= wait_commands %>
33
39
  @echo "All Containers ready."
34
40
 
41
+ wait-application:
42
+ @bin/rake orchestration:application:wait
43
+
35
44
  wait-database:
36
- @bin/rake orchestration:db:wait
45
+ @bin/rake orchestration:database:wait
37
46
 
38
47
  wait-mongo:
39
48
  @bin/rake orchestration:mongo:wait
40
49
 
50
+ wait-nginx-proxy:
51
+ @bin/rake orchestration:nginx_proxy:wait
52
+
41
53
  wait-rabbitmq:
42
54
  @bin/rake orchestration:rabbitmq:wait
43
55
 
44
56
  ### Docker build commands ###
45
57
 
46
- docker: docker-build docker-push
58
+ docker: build push
47
59
 
48
- docker-build:
49
- mkdir -p ./docker/.build
50
- git show master:./Gemfile > ./docker/Gemfile
51
- git show master:./Gemfile.lock > ./docker/Gemfile.lock
52
- git archive --format tar.gz -o docker/.build/context.tar.gz master
53
- docker build --build-arg BUNDLE_GITHUB__COM \
60
+ build:
61
+ @echo "Preparing build."
62
+ @mkdir -p ./docker/.build
63
+ @git show master:./Gemfile > ./docker/Gemfile
64
+ @git show master:./Gemfile.lock > ./docker/Gemfile.lock
65
+ @echo "Building..."
66
+ @git archive --format tar.gz -o docker/.build/context.tar.gz master
67
+ @docker build \
68
+ --build-arg BUNDLE_GITHUB__COM \
54
69
  --build-arg BUNDLE_BITBUCKET__ORG \
55
70
  -t $(shell bin/rake orchestration:docker:username)/<%= app_id %> \
56
71
  -t $(shell bin/rake orchestration:docker:username)/<%= app_id %>:$(shell git rev-parse --short --verify master) \
57
72
  ./docker/
73
+ @echo "Build complete."
58
74
 
59
- docker-push: VERSION := $(shell git rev-parse --short --verify master)
60
- docker-push:
75
+ push: VERSION := $(shell git rev-parse --short --verify master)
76
+ push:
61
77
  docker push $(shell bin/rake orchestration:docker:username)/<%= app_id %>:${VERSION}
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ set -u
3
+ useradd -u ${HOST_UID} -m -o owner
4
+ mkdir -p /application/tmp/pids
5
+ chown -R owner:owner /application/tmp /application/log /application/db
6
+ exec gosu owner "$@"
@@ -0,0 +1,17 @@
1
+ listen '0.0.0.0:8080', :tcp_nopush => true
2
+
3
+ pid '/application/tmp/pids/unicorn.pid'
4
+
5
+ preload_app ENV.fetch('UNICORN_PRELOAD_APP', '1') == '1'
6
+
7
+ timeout ENV.fetch('UNICORN_TIMEOUT', 60).to_i
8
+
9
+ worker_processes ENV.fetch('UNICORN_WORKER_PROCESSES', 8).to_i
10
+
11
+ before_fork do |_server, _worker|
12
+ ActiveRecord::Base.connection.disconnect! if defined?(ActiveRecord)
13
+ end
14
+
15
+ after_fork do |_server, _worker|
16
+ ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Orchestration
4
- VERSION = '0.2.2'
4
+ VERSION = '0.2.3'
5
5
  end
data/lib/orchestration.rb CHANGED
@@ -12,7 +12,6 @@ require 'orchestration/docker_compose'
12
12
  require 'orchestration/environment'
13
13
  require 'orchestration/errors'
14
14
  require 'orchestration/file_helpers'
15
- require 'orchestration/healthcheck_base'
16
15
  require 'orchestration/install_generator'
17
16
  require 'orchestration/railtie'
18
17
  require 'orchestration/service_check'
@@ -25,4 +24,8 @@ module Orchestration
25
24
  def self.root
26
25
  Pathname.new(File.dirname(__dir__))
27
26
  end
27
+
28
+ def self.rakefile
29
+ root.join('lib', 'Rakefile')
30
+ end
28
31
  end
@@ -8,7 +8,14 @@ namespace :orchestration do
8
8
  Orchestration::InstallGenerator.start
9
9
  end
10
10
 
11
- namespace :db do
11
+ namespace :application do
12
+ desc 'Wait for application to become available'
13
+ task :wait do
14
+ Orchestration::Services::Application::Healthcheck.start
15
+ end
16
+ end
17
+
18
+ namespace :database do
12
19
  desc 'Wait for database to become available'
13
20
  task :wait do
14
21
  Orchestration::Services::Database::Healthcheck.start
@@ -22,6 +29,13 @@ namespace :orchestration do
22
29
  end
23
30
  end
24
31
 
32
+ namespace :nginx_proxy do
33
+ desc 'Wait for Nginx proxy to become available'
34
+ task :wait do
35
+ Orchestration::Services::NginxProxy::Healthcheck.start
36
+ end
37
+ end
38
+
25
39
  namespace :rabbitmq do
26
40
  desc 'Wait for database to become available'
27
41
  task :wait do
@@ -27,12 +27,15 @@ Gem::Specification.new do |spec|
27
27
  spec.add_runtime_dependency 'colorize', '~> 0.8.1'
28
28
  spec.add_runtime_dependency 'erubis', '~> 2.7'
29
29
  spec.add_runtime_dependency 'i18n', '>= 0.5'
30
+ spec.add_runtime_dependency 'unicorn'
30
31
 
31
32
  spec.add_development_dependency 'activerecord', '~> 5.2'
32
33
  spec.add_development_dependency 'bundler', '~> 1.16'
33
34
  spec.add_development_dependency 'bunny', '~> 2.12'
34
35
  spec.add_development_dependency 'byebug', '~> 10.0'
35
36
  spec.add_development_dependency 'mongoid', '~> 7.0'
37
+ spec.add_development_dependency 'mysql2', '~> 0.5.2'
38
+ spec.add_development_dependency 'pg', '~> 1.1'
36
39
  spec.add_development_dependency 'rails', '~> 5.2'
37
40
  spec.add_development_dependency 'rake', '~> 10.0'
38
41
  spec.add_development_dependency 'rspec', '~> 3.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: orchestration
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-30 00:00:00.000000000 Z
11
+ date: 2018-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: unicorn
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: activerecord
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,34 @@ dependencies:
122
136
  - - "~>"
123
137
  - !ruby/object:Gem::Version
124
138
  version: '7.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: mysql2
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.5.2
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.5.2
153
+ - !ruby/object:Gem::Dependency
154
+ name: pg
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '1.1'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1.1'
125
167
  - !ruby/object:Gem::Dependency
126
168
  name: rails
127
169
  requirement: !ruby/object:Gem::Requirement
@@ -245,23 +287,26 @@ files:
245
287
  - bin/rubocop
246
288
  - bin/setup
247
289
  - config/locales/en.yml
290
+ - lib/Rakefile
248
291
  - lib/orchestration.rb
249
292
  - lib/orchestration/docker_compose.rb
250
293
  - lib/orchestration/docker_compose/application_service.rb
251
294
  - lib/orchestration/docker_compose/database_service.rb
252
295
  - lib/orchestration/docker_compose/mongo_service.rb
296
+ - lib/orchestration/docker_compose/nginx_proxy_service.rb
253
297
  - lib/orchestration/docker_compose/rabbitmq_service.rb
254
298
  - lib/orchestration/docker_compose/services.rb
255
299
  - lib/orchestration/environment.rb
256
300
  - lib/orchestration/errors.rb
257
301
  - lib/orchestration/file_helpers.rb
258
- - lib/orchestration/healthcheck_base.rb
259
302
  - lib/orchestration/install_generator.rb
260
303
  - lib/orchestration/railtie.rb
261
304
  - lib/orchestration/service_check.rb
262
305
  - lib/orchestration/services.rb
263
306
  - lib/orchestration/services/application.rb
264
307
  - lib/orchestration/services/application/configuration.rb
308
+ - lib/orchestration/services/application/healthcheck.rb
309
+ - lib/orchestration/services/configuration_base.rb
265
310
  - lib/orchestration/services/database.rb
266
311
  - lib/orchestration/services/database/adapters.rb
267
312
  - lib/orchestration/services/database/adapters/mysql2.rb
@@ -269,15 +314,21 @@ files:
269
314
  - lib/orchestration/services/database/adapters/sqlite3.rb
270
315
  - lib/orchestration/services/database/configuration.rb
271
316
  - lib/orchestration/services/database/healthcheck.rb
317
+ - lib/orchestration/services/healthcheck_base.rb
272
318
  - lib/orchestration/services/mongo.rb
273
319
  - lib/orchestration/services/mongo/configuration.rb
274
320
  - lib/orchestration/services/mongo/healthcheck.rb
321
+ - lib/orchestration/services/nginx_proxy.rb
322
+ - lib/orchestration/services/nginx_proxy/configuration.rb
323
+ - lib/orchestration/services/nginx_proxy/healthcheck.rb
275
324
  - lib/orchestration/services/rabbitmq.rb
276
325
  - lib/orchestration/services/rabbitmq/configuration.rb
277
326
  - lib/orchestration/services/rabbitmq/healthcheck.rb
278
327
  - lib/orchestration/settings.rb
279
- - lib/orchestration/templates/Dockerfile.tt
280
- - lib/orchestration/templates/Makefile.tt
328
+ - lib/orchestration/templates/Dockerfile.erb
329
+ - lib/orchestration/templates/Makefile.erb
330
+ - lib/orchestration/templates/entrypoint.sh.erb
331
+ - lib/orchestration/templates/unicorn.rb.erb
281
332
  - lib/orchestration/terminal.rb
282
333
  - lib/orchestration/version.rb
283
334
  - lib/tasks/orchestration.rake
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Orchestration
4
- module HealthcheckBase
5
- attr_reader :configuration
6
-
7
- def self.included(base)
8
- base.extend(ClassMethods)
9
- end
10
-
11
- module ClassMethods
12
- def start(env = nil, terminal = nil)
13
- env ||= Environment.new
14
- terminal ||= Terminal.new
15
- check = ServiceCheck.new(new(env), terminal)
16
-
17
- exit 1 unless check.run
18
- end
19
- end
20
- end
21
- end