orchestration 0.1.0 → 0.2.0

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