orchestration 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|