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,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'