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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ class Environment
5
+ def initialize(options = {})
6
+ @environment = options.fetch(:environment, nil)
7
+ end
8
+
9
+ def environment
10
+ return @environment unless @environment.nil?
11
+
12
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
13
+ end
14
+
15
+ def database_url
16
+ ENV['DATABASE_URL']
17
+ end
18
+
19
+ def mongoid_configuration_path
20
+ root.join('config', 'mongoid.yml')
21
+ end
22
+
23
+ def database_configuration_path
24
+ root.join('config', 'database.yml')
25
+ end
26
+
27
+ def rabbitmq_configuration_path
28
+ root.join('config', 'rabbitmq.yml')
29
+ end
30
+
31
+ def orchestration_configuration_path
32
+ root.join('.orchestration.yml')
33
+ end
34
+
35
+ def application_name
36
+ Rails.application.class.parent.name.underscore
37
+ end
38
+
39
+ def settings
40
+ Settings.new(orchestration_configuration_path)
41
+ end
42
+
43
+ def root
44
+ Rails.root
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ class OrchestrationError < StandardError; end
5
+ class DatabaseConfigurationError < OrchestrationError; end
6
+ class MongoConfigurationError < OrchestrationError; end
7
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module FileHelpers
5
+ private
6
+
7
+ def template(template_name, environment = {})
8
+ Erubis::Eruby.new(read_template(template_name))
9
+ .result(environment)
10
+ end
11
+
12
+ def delete_and_inject_after(path, pattern, replacement)
13
+ return write_file(path, pattern + replacement) unless File.exist?(path)
14
+
15
+ input = File.read(path)
16
+ index = append_index(pattern, input)
17
+ output = input[0...index] + pattern + replacement
18
+
19
+ return @terminal.write(:identical, relative_path(path)) if input == output
20
+
21
+ update_file(path, output)
22
+ end
23
+
24
+ def append_index(pattern, input)
25
+ return 0 if input.empty?
26
+
27
+ index = input.index(pattern)
28
+ index.nil? ? (input.size + 1) : index
29
+ end
30
+
31
+ def relative_path(path)
32
+ path.relative_path_from(Rails.root).to_s
33
+ end
34
+
35
+ def write_file(path, content, options = {})
36
+ relpath = relative_path(path)
37
+ overwrite = options.fetch(:overwrite, true)
38
+ return @terminal.write(:skip, relpath) if File.exist?(path) && !overwrite
39
+
40
+ File.write(path, content)
41
+ @terminal.write(:create, relative_path(path))
42
+ end
43
+
44
+ def update_file(path, content)
45
+ File.write(path, content)
46
+ @terminal.write(:update, relative_path(path))
47
+ end
48
+
49
+ def append_file(path, content, echo: true)
50
+ return write_file(path, content) unless File.exist?(path)
51
+
52
+ File.write(path, content, File.size(path), mode: 'a')
53
+ @terminal.write(:update, relative_path(path)) if echo
54
+ end
55
+
56
+ def ensure_lines_in_file(path, lines)
57
+ updated = lines.map do |line|
58
+ ensure_line_in_file(path, line, echo: false)
59
+ end.compact
60
+ relpath = relative_path(path)
61
+
62
+ return @terminal.write(:update, relpath) if updated.any?
63
+
64
+ @terminal.write(:skip, relpath)
65
+ end
66
+
67
+ def ensure_line_in_file(path, line, echo: true)
68
+ return if line_in_file?(path, line)
69
+
70
+ append_file(path, "\n#{line.chomp}\n", echo: echo)
71
+ true
72
+ end
73
+
74
+ def line_in_file?(path, line)
75
+ return false unless File.exist?(path)
76
+
77
+ File.readlines(path).map(&:chomp).include?(line.chomp)
78
+ end
79
+
80
+ def templates_path
81
+ Orchestration.root.join('lib', 'orchestration', 'templates')
82
+ end
83
+
84
+ def read_template(template)
85
+ File.read(templates_path.join("#{template}.tt"))
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'tempfile'
5
+
6
+ module Orchestration
7
+ class InstallGenerator < Thor::Group
8
+ include FileHelpers
9
+
10
+ def initialize(*_args)
11
+ super
12
+ @env = Environment.new(environment: 'test')
13
+ @terminal ||= Terminal.new
14
+ end
15
+
16
+ def orchestration_configuration
17
+ path = @env.orchestration_configuration_path
18
+ settings = Settings.new(path)
19
+ docker_username(settings)
20
+ relpath = relative_path(path)
21
+ return @terminal.write(:create, relpath) unless settings.exist?
22
+ return @terminal.write(:update, relpath) if settings.dirty?
23
+
24
+ @terminal.write(:skip, relpath)
25
+ end
26
+
27
+ def makefile
28
+ environment = {
29
+ app_id: @env.application_name,
30
+ wait_commands: wait_commands
31
+ }
32
+ content = template('Makefile', environment)
33
+ path = @env.root.join('Makefile')
34
+ delete_and_inject_after(path, "\n#!!orchestration\n", content)
35
+ end
36
+
37
+ def dockerfile
38
+ docker_dir = Rails.root.join('docker')
39
+ path = docker_dir.join('Dockerfile')
40
+ content = template('Dockerfile', ruby_version: RUBY_VERSION)
41
+ FileUtils.mkdir(docker_dir) unless Dir.exist?(docker_dir)
42
+ write_file(path, content, overwrite: false)
43
+ end
44
+
45
+ def gitignore
46
+ path = Rails.root.join('.gitignore')
47
+ entries = [
48
+ 'docker/.build',
49
+ 'docker/Gemfile',
50
+ 'docker/Gemfile.lock',
51
+ 'docker/*.gemspec'
52
+ ].map { |entry| "#{entry} # Orchestration" }
53
+ ensure_lines_in_file(path, entries)
54
+ end
55
+
56
+ def docker_compose
57
+ path = Rails.root.join('docker-compose.yml')
58
+ return if File.exist?(path)
59
+
60
+ docker_compose = DockerCompose::Services.new(
61
+ database: configuration(:database),
62
+ mongo: configuration(:mongo),
63
+ rabbitmq: configuration(:rabbitmq)
64
+ )
65
+ write_file(path, docker_compose.structure.to_yaml)
66
+ end
67
+
68
+ private
69
+
70
+ def configuration(service)
71
+ # REVIEW: At the moment we only handle test dependencies - it would be
72
+ # nice to also handle development dependencies.
73
+ {
74
+ database: Services::Database::Configuration,
75
+ mongo: Services::Mongo::Configuration,
76
+ rabbitmq: Services::RabbitMQ::Configuration
77
+ }.fetch(service).new(@env)
78
+ end
79
+
80
+ def wait_commands
81
+ [
82
+ configuration(:database).settings.nil? ? nil : 'wait-database',
83
+ configuration(:mongo).settings.nil? ? nil : 'wait-mongo',
84
+ configuration(:rabbitmq).settings.nil? ? nil : 'wait-rabbitmq'
85
+ ].compact.join(' ')
86
+ end
87
+
88
+ def docker_username(settings)
89
+ return unless settings.get('docker.username').nil?
90
+
91
+ @terminal.write(:setup, I18n.t('orchestration.docker.username_request'))
92
+ settings.set('docker.username', @terminal.read('[username]:'))
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load 'tasks/orchestration.rake'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ class ServiceCheck
5
+ ATTEMPT_LIMIT = 10
6
+ RETRY_INTERVAL = 3 # seconds
7
+
8
+ def initialize(service, terminal, options = {})
9
+ @service = service
10
+ @service_name = service_name(service)
11
+ @terminal = terminal
12
+ @attempt_limit = options.fetch(:attempt_limit, ATTEMPT_LIMIT)
13
+ @retry_interval = options.fetch(:retry_interval, RETRY_INTERVAL)
14
+ @attempts = 0
15
+ @failure_callback = options.fetch(:failure_callback, nil)
16
+ end
17
+
18
+ def run
19
+ echo_start
20
+ success = attempt_connection
21
+ echo_ready if success
22
+ success
23
+ end
24
+
25
+ private
26
+
27
+ def attempt_connection
28
+ echo_waiting
29
+ @service.connect
30
+ true
31
+ rescue *@service.connection_errors => e
32
+ @attempts += 1
33
+ sleep @retry_interval
34
+ retry unless @attempts == @attempt_limit
35
+ echo_error(e)
36
+ echo_failure
37
+ false
38
+ end
39
+
40
+ def echo_start
41
+ @terminal.write(@service_name.to_sym, '', :status)
42
+ end
43
+
44
+ def echo_waiting
45
+ @terminal.write(
46
+ :waiting,
47
+ I18n.t(
48
+ "orchestration.#{@service_name}.waiting",
49
+ config: @service.configuration.friendly_config
50
+ )
51
+ )
52
+ end
53
+
54
+ def echo_ready
55
+ @terminal.write(
56
+ :ready,
57
+ I18n.t(
58
+ "orchestration.#{@service_name}.ready",
59
+ config: @service.configuration.friendly_config
60
+ )
61
+ )
62
+ end
63
+
64
+ def echo_failure
65
+ @terminal.write(
66
+ :failure,
67
+ I18n.t('orchestration.attempt_limit', limit: ATTEMPT_LIMIT)
68
+ )
69
+ end
70
+
71
+ def echo_error(error)
72
+ @terminal.write(:error, "[#{error.class.name}] #{error.message}")
73
+ end
74
+
75
+ def service_name(service)
76
+ # e.g.:
77
+ # Orchestration::Services::RabbitMQ::Healthcheck => 'rabbitmq'
78
+ service.class.name.split('::')[-2].downcase
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Database
6
+ module Adapters
7
+ class Mysql2
8
+ def credentials
9
+ {
10
+ 'username' => 'root',
11
+ 'password' => 'password',
12
+ 'database' => 'mysql'
13
+ }
14
+ end
15
+
16
+ def errors
17
+ [::Mysql2::Error]
18
+ end
19
+
20
+ def default_port
21
+ 3306
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Database
6
+ module Adapters
7
+ class Postgresql
8
+ def credentials
9
+ {
10
+ 'username' => 'postgres',
11
+ 'password' => 'password',
12
+ 'database' => 'postgres'
13
+ }
14
+ end
15
+
16
+ def errors
17
+ [PG::ConnectionBad]
18
+ end
19
+
20
+ def default_port
21
+ 5432
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Database
6
+ module Adapters
7
+ class Sqlite3
8
+ def credentials
9
+ {
10
+ 'username' => '',
11
+ 'password' => '',
12
+ 'database' => 'healthcheck'
13
+ }
14
+ end
15
+
16
+ def errors
17
+ []
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestration
4
+ module Services
5
+ module Database
6
+ module Adapters
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ require 'orchestration/services/database/adapters/mysql2'
13
+ require 'orchestration/services/database/adapters/postgresql'
14
+ require 'orchestration/services/database/adapters/sqlite3'