orchestration 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +14 -0
  3. data/.rubocop.yml +14 -0
  4. data/Gemfile +4 -2
  5. data/Makefile +5 -0
  6. data/README.md +82 -16
  7. data/Rakefile +5 -3
  8. data/TODO +2 -0
  9. data/bin/rspec +29 -0
  10. data/bin/rubocop +29 -0
  11. data/config/locales/en.yml +16 -0
  12. data/lib/orchestration/docker_compose/database_service.rb +51 -0
  13. data/lib/orchestration/docker_compose/mongo_service.rb +27 -0
  14. data/lib/orchestration/docker_compose/rabbitmq_service.rb +23 -0
  15. data/lib/orchestration/docker_compose/services.rb +48 -0
  16. data/lib/orchestration/docker_compose.rb +11 -0
  17. data/lib/orchestration/environment.rb +47 -0
  18. data/lib/orchestration/errors.rb +7 -0
  19. data/lib/orchestration/file_helpers.rb +88 -0
  20. data/lib/orchestration/healthcheck_base.rb +21 -0
  21. data/lib/orchestration/install_generator.rb +95 -0
  22. data/lib/orchestration/railtie.rb +9 -0
  23. data/lib/orchestration/service_check.rb +81 -0
  24. data/lib/orchestration/services/database/adapters/mysql2.rb +27 -0
  25. data/lib/orchestration/services/database/adapters/postgresql.rb +27 -0
  26. data/lib/orchestration/services/database/adapters/sqlite3.rb +23 -0
  27. data/lib/orchestration/services/database/adapters.rb +14 -0
  28. data/lib/orchestration/services/database/configuration.rb +114 -0
  29. data/lib/orchestration/services/database/healthcheck.rb +30 -0
  30. data/lib/orchestration/services/database.rb +15 -0
  31. data/lib/orchestration/services/mongo/configuration.rb +55 -0
  32. data/lib/orchestration/services/mongo/healthcheck.rb +34 -0
  33. data/lib/orchestration/services/mongo.rb +12 -0
  34. data/lib/orchestration/services/rabbitmq/configuration.rb +36 -0
  35. data/lib/orchestration/services/rabbitmq/healthcheck.rb +37 -0
  36. data/lib/orchestration/services/rabbitmq.rb +12 -0
  37. data/lib/orchestration/services.rb +10 -0
  38. data/lib/orchestration/settings.rb +43 -0
  39. data/lib/orchestration/templates/Dockerfile.tt +11 -0
  40. data/lib/orchestration/templates/Makefile.tt +53 -0
  41. data/lib/orchestration/terminal.rb +44 -0
  42. data/lib/orchestration/version.rb +3 -1
  43. data/lib/orchestration.rb +25 -2
  44. data/lib/tasks/orchestration.rake +39 -0
  45. data/orchestration.gemspec +34 -17
  46. metadata +213 -7
  47. data/CODE_OF_CONDUCT.md +0 -74
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Database
6
+ class Configuration
7
+ attr_reader :adapter, :settings
8
+
9
+ def initialize(env)
10
+ @env = env
11
+ @adapter = nil
12
+ @settings = nil
13
+ return unless defined?(ActiveRecord)
14
+ return unless File.exist?(@env.database_configuration_path)
15
+
16
+ setup
17
+ end
18
+
19
+ def friendly_config
20
+ adapter = @settings['adapter']
21
+ host = @settings['host']
22
+ port = @settings['port']
23
+ return "[#{adapter}]" if adapter == 'sqlite3'
24
+ return "[#{adapter}] #{host}" unless port.present?
25
+
26
+ "[#{adapter}] #{host}:#{port}"
27
+ end
28
+
29
+ private
30
+
31
+ def setup
32
+ environments = parse(File.read(@env.database_configuration_path))
33
+ base = base_config(environments)
34
+ @adapter = adapter_object(base['adapter'])
35
+ @settings = base.merge(@adapter.credentials)
36
+ @settings.merge!(default_port) unless @settings.key?('port')
37
+ end
38
+
39
+ def parse(content)
40
+ yaml(erb(content))
41
+ end
42
+
43
+ def erb(content)
44
+ ERB.new(content).result
45
+ end
46
+
47
+ def yaml(content)
48
+ YAML.safe_load(content, [], [], true) # true: Allow aliases
49
+ end
50
+
51
+ def adapter_object(name)
52
+ {
53
+ 'mysql2' => adapters::Mysql2,
54
+ 'postgresql' => adapters::Postgresql,
55
+ 'sqlite3' => adapters::Sqlite3
56
+ }.fetch(name).new
57
+ end
58
+
59
+ def base_config(environments)
60
+ missing_default unless environments.key?('default')
61
+
62
+ host = url_config['host'] || environments[@env.environment]['host']
63
+
64
+ environments
65
+ .fetch('default')
66
+ .merge(url_config)
67
+ .merge('host' => host)
68
+ end
69
+
70
+ def adapters
71
+ Orchestration::Services::Database::Adapters
72
+ end
73
+
74
+ def default_port
75
+ return {} if @adapter.is_a?(adapters::Sqlite3)
76
+
77
+ { 'port' => @adapter.default_port }
78
+ end
79
+
80
+ def url_config
81
+ return {} if @env.database_url.nil?
82
+
83
+ uri = URI.parse(@env.database_url)
84
+
85
+ {
86
+ 'host' => uri.hostname,
87
+ 'adapter' => adapter_name_from_scheme(uri.scheme),
88
+ 'port' => uri.port
89
+ }.merge(query_params(uri))
90
+ end
91
+
92
+ def adapter_name_from_scheme(scheme)
93
+ return 'mysql2' if scheme == 'mysql'
94
+ return 'postgresql' if scheme == 'postgres'
95
+ return 'sqlite3' if scheme == 'sqlite3'
96
+
97
+ raise ArgumentError,
98
+ I18n.t('orchestration.unknown_scheme', scheme: scheme)
99
+ end
100
+
101
+ def query_params(uri)
102
+ return {} if uri.query.nil?
103
+
104
+ Hash[URI.decode_www_form(uri.query)]
105
+ end
106
+
107
+ def missing_default
108
+ raise DatabaseConfigurationError,
109
+ I18n.t('orchestration.database.missing_default')
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Database
6
+ class Healthcheck
7
+ include HealthcheckBase
8
+
9
+ def initialize(env)
10
+ @configuration = Configuration.new(env)
11
+ end
12
+
13
+ def connect
14
+ ActiveRecord::Base.establish_connection(@configuration.settings)
15
+ ActiveRecord::Base.connection
16
+ end
17
+
18
+ def connection_errors
19
+ [ActiveRecord::ConnectionNotEstablished].concat(adapter_errors)
20
+ end
21
+
22
+ private
23
+
24
+ def adapter_errors
25
+ @configuration.adapter.errors
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Database
6
+ end
7
+ end
8
+ end
9
+
10
+ require 'erb'
11
+ require 'uri'
12
+
13
+ require 'orchestration/services/database/adapters'
14
+ require 'orchestration/services/database/configuration'
15
+ require 'orchestration/services/database/healthcheck'
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Mongo
6
+ class Configuration
7
+ attr_reader :settings
8
+
9
+ def initialize(env)
10
+ @env = env
11
+ @settings = nil
12
+ return unless defined?(Mongoid)
13
+ return unless File.exist?(@env.mongoid_configuration_path)
14
+
15
+ @settings = config.fetch(@env.environment)
16
+ end
17
+
18
+ def ports
19
+ hosts_and_ports.map { |_host, port| port }
20
+ end
21
+
22
+ def friendly_config
23
+ hosts_string = hosts_and_ports.map do |host, port|
24
+ "#{host}:#{port}"
25
+ end.join(', ')
26
+
27
+ "[mongoid] #{hosts_string}"
28
+ end
29
+
30
+ private
31
+
32
+ def config
33
+ YAML.safe_load(
34
+ File.read(@env.mongoid_configuration_path), [], [], true
35
+ )
36
+ 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
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Mongo
6
+ class Healthcheck
7
+ include HealthcheckBase
8
+
9
+ def initialize(env)
10
+ @configuration = Configuration.new(env)
11
+ end
12
+
13
+ def connection_errors
14
+ [::Mongo::Error::NoServerAvailable]
15
+ end
16
+
17
+ def connect
18
+ # REVIEW: For some reason this is extremely slow. Worth trying
19
+ # to see if there's a faster way to fail.
20
+ Mongoid.load_configuration(@configuration.settings)
21
+ !Mongoid.default_client.database_names.empty?
22
+ end
23
+
24
+ private
25
+
26
+ def clients
27
+ return Mongoid.sessions if Mongoid.respond_to?(:sessions)
28
+
29
+ Mongoid.clients
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Mongo
6
+ PORT = 27_017
7
+ end
8
+ end
9
+ end
10
+
11
+ require 'orchestration/services/mongo/configuration'
12
+ require 'orchestration/services/mongo/healthcheck'
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module RabbitMQ
6
+ class Configuration
7
+ attr_reader :settings
8
+
9
+ def initialize(env)
10
+ @env = env
11
+ @settings = nil
12
+ return unless defined?(RabbitMQ)
13
+ return unless File.exist?(@env.rabbitmq_configuration_path)
14
+
15
+ @settings = config.fetch(@env.environment)
16
+ @settings.merge!('port' => PORT) unless @settings.key?('port')
17
+ end
18
+
19
+ def friendly_config
20
+ host = @settings.fetch('host')
21
+ port = @settings.fetch('port')
22
+
23
+ "[bunny] amqp://#{host}:#{port}"
24
+ end
25
+
26
+ private
27
+
28
+ def config
29
+ YAML.safe_load(
30
+ File.read(@env.rabbitmq_configuration_path), [], [], true
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module RabbitMQ
6
+ class Healthcheck
7
+ include HealthcheckBase
8
+
9
+ def initialize(env)
10
+ @configuration = Configuration.new(env)
11
+ end
12
+
13
+ def connection_errors
14
+ [
15
+ Bunny::TCPConnectionFailedForAllHosts,
16
+ AMQ::Protocol::EmptyResponseError
17
+ ]
18
+ end
19
+
20
+ def connect
21
+ host = @configuration.settings.fetch('host')
22
+ port = @configuration.settings.fetch('port')
23
+
24
+ connection = Bunny.new("amqp://#{host}:#{port}", log_file: devnull)
25
+ connection.start
26
+ connection.stop
27
+ end
28
+
29
+ private
30
+
31
+ def devnull
32
+ File.open(File::NULL, 'w')
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module RabbitMQ
6
+ PORT = 5672
7
+ end
8
+ end
9
+ end
10
+
11
+ require 'orchestration/services/rabbitmq/configuration'
12
+ require 'orchestration/services/rabbitmq/healthcheck'
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ end
6
+ end
7
+
8
+ require 'orchestration/services/database'
9
+ require 'orchestration/services/mongo'
10
+ require 'orchestration/services/rabbitmq'
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ class Settings
5
+ def initialize(path)
6
+ @path = path
7
+ @dirty = false
8
+ @exist = File.exist?(path)
9
+ end
10
+
11
+ def get(identifier)
12
+ identifier.to_s.split('.').reduce(config) do |result, key|
13
+ (result || {}).fetch(key)
14
+ end
15
+ rescue KeyError
16
+ nil
17
+ end
18
+
19
+ def set(identifier, val)
20
+ *keys, setting_key = identifier.to_s.split('.')
21
+ new_config = config || {}
22
+ parent = keys.reduce(new_config) { |result, key| result[key] ||= {} }
23
+ parent[setting_key] = val
24
+ @dirty ||= config != new_config
25
+ File.write(@path, new_config.to_yaml)
26
+ end
27
+
28
+ def dirty?
29
+ @dirty
30
+ end
31
+
32
+ def exist?
33
+ @exist
34
+ end
35
+
36
+ private
37
+
38
+ def config
39
+ File.write(@path, {}.to_yaml) unless File.exist?(@path)
40
+ YAML.safe_load(File.read(@path))
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,11 @@
1
+ FROM ruby:<%= ruby_version %>
2
+ ARG BUNDLE_BITBUCKET__ORG
3
+ RUN apt-get update \
4
+ && apt-get install -y node.js \
5
+ && gem install bundler \
6
+ && mkdir /application
7
+ WORKDIR /application
8
+ COPY Gemfile Gemfile.lock ./
9
+ RUN bundle install --deployment --without development test
10
+ ADD .build/context.tar.gz .
11
+ CMD ["bundle", "exec", "rails", "server", "-p", "3000"]
@@ -0,0 +1,53 @@
1
+
2
+ # Do not edit this file below this point. Any changes will be overwritten.
3
+ #
4
+ # Example `test` command which will start and wait for all services before
5
+ # running tests:
6
+ #
7
+ # test: start wait
8
+ # bundle exec rspec
9
+ # yarn test app/javascript
10
+ # bundle exec rubocop
11
+ # yarn run eslint app/javascript
12
+ #
13
+ .PHONY: docker docker-build docker-push wait-database start
14
+
15
+ ### Container management commands ###
16
+
17
+ start:
18
+ docker-compose up -d
19
+
20
+ stop:
21
+ docker-compose down
22
+
23
+ ### Service healthcheck commands ###
24
+
25
+ wait: <%= wait_commands %>
26
+ @echo "All Containers ready."
27
+
28
+ wait-database:
29
+ @RAILS_ENV=test bundle exec rake orchestration:db:wait
30
+
31
+ wait-mongo:
32
+ @RAILS_ENV=test bundle exec rake orchestration:mongo:wait
33
+
34
+ wait-rabbitmq:
35
+ @RAILS_ENV=test bundle exec rake orchestration:rabbitmq:wait
36
+
37
+ ### Docker build commands ###
38
+
39
+ docker: docker-build docker-push
40
+
41
+ docker-build:
42
+ mkdir -p ./docker/.build
43
+ git show master:./Gemfile > ./docker/Gemfile
44
+ git show master:./Gemfile.lock > ./docker/Gemfile.lock
45
+ git archive --format tar.gz -o docker/.build/context.tar.gz master
46
+ docker build --build-arg BUNDLE_GITHUB__COM \
47
+ --build-arg BUNDLE_BITBUCKET__ORG \
48
+ -t $(shell bundle exec rake orchestration:docker:username)/<%= app_id %>:$(shell git rev-parse --short --verify master) \
49
+ ./docker/
50
+
51
+ docker-push: VERSION := $(shell git rev-parse --short --verify master)
52
+ docker-push:
53
+ docker push $(shell bundle exec rake orchestration:docker:username)/<%= app_id %>:${VERSION}