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.
- checksums.yaml +4 -4
- data/.gitignore +14 -0
- data/.rubocop.yml +14 -0
- data/Gemfile +4 -2
- data/Makefile +5 -0
- data/README.md +82 -16
- data/Rakefile +5 -3
- data/TODO +2 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/config/locales/en.yml +16 -0
- data/lib/orchestration/docker_compose/database_service.rb +51 -0
- data/lib/orchestration/docker_compose/mongo_service.rb +27 -0
- data/lib/orchestration/docker_compose/rabbitmq_service.rb +23 -0
- data/lib/orchestration/docker_compose/services.rb +48 -0
- data/lib/orchestration/docker_compose.rb +11 -0
- data/lib/orchestration/environment.rb +47 -0
- data/lib/orchestration/errors.rb +7 -0
- data/lib/orchestration/file_helpers.rb +88 -0
- data/lib/orchestration/healthcheck_base.rb +21 -0
- data/lib/orchestration/install_generator.rb +95 -0
- data/lib/orchestration/railtie.rb +9 -0
- data/lib/orchestration/service_check.rb +81 -0
- data/lib/orchestration/services/database/adapters/mysql2.rb +27 -0
- data/lib/orchestration/services/database/adapters/postgresql.rb +27 -0
- data/lib/orchestration/services/database/adapters/sqlite3.rb +23 -0
- data/lib/orchestration/services/database/adapters.rb +14 -0
- data/lib/orchestration/services/database/configuration.rb +114 -0
- data/lib/orchestration/services/database/healthcheck.rb +30 -0
- data/lib/orchestration/services/database.rb +15 -0
- data/lib/orchestration/services/mongo/configuration.rb +55 -0
- data/lib/orchestration/services/mongo/healthcheck.rb +34 -0
- data/lib/orchestration/services/mongo.rb +12 -0
- data/lib/orchestration/services/rabbitmq/configuration.rb +36 -0
- data/lib/orchestration/services/rabbitmq/healthcheck.rb +37 -0
- data/lib/orchestration/services/rabbitmq.rb +12 -0
- data/lib/orchestration/services.rb +10 -0
- data/lib/orchestration/settings.rb +43 -0
- data/lib/orchestration/templates/Dockerfile.tt +11 -0
- data/lib/orchestration/templates/Makefile.tt +53 -0
- data/lib/orchestration/terminal.rb +44 -0
- data/lib/orchestration/version.rb +3 -1
- data/lib/orchestration.rb +25 -2
- data/lib/tasks/orchestration.rake +39 -0
- data/orchestration.gemspec +34 -17
- metadata +213 -7
- 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,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,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'
|