tomo 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +221 -0
- data/exe/tomo +4 -0
- data/lib/tomo/cli/command.rb +36 -0
- data/lib/tomo/cli/common_options.rb +48 -0
- data/lib/tomo/cli/completions.rb +70 -0
- data/lib/tomo/cli/deploy_options.rb +59 -0
- data/lib/tomo/cli/error.rb +16 -0
- data/lib/tomo/cli/interrupted_error.rb +9 -0
- data/lib/tomo/cli/options.rb +38 -0
- data/lib/tomo/cli/parser.rb +92 -0
- data/lib/tomo/cli/project_options.rb +47 -0
- data/lib/tomo/cli/rules/argument.rb +42 -0
- data/lib/tomo/cli/rules/switch.rb +43 -0
- data/lib/tomo/cli/rules/value_switch.rb +58 -0
- data/lib/tomo/cli/rules.rb +98 -0
- data/lib/tomo/cli/rules_evaluator.rb +71 -0
- data/lib/tomo/cli/state.rb +29 -0
- data/lib/tomo/cli/usage.rb +42 -0
- data/lib/tomo/cli.rb +75 -0
- data/lib/tomo/colors.rb +46 -0
- data/lib/tomo/commands/completion_script.rb +46 -0
- data/lib/tomo/commands/default.rb +72 -0
- data/lib/tomo/commands/deploy.rb +67 -0
- data/lib/tomo/commands/help.rb +9 -0
- data/lib/tomo/commands/init.rb +92 -0
- data/lib/tomo/commands/run.rb +76 -0
- data/lib/tomo/commands/setup.rb +54 -0
- data/lib/tomo/commands/tasks.rb +32 -0
- data/lib/tomo/commands/version.rb +23 -0
- data/lib/tomo/commands.rb +13 -0
- data/lib/tomo/configuration/dsl/batch_block.rb +17 -0
- data/lib/tomo/configuration/dsl/config_file.rb +39 -0
- data/lib/tomo/configuration/dsl/environment_block.rb +13 -0
- data/lib/tomo/configuration/dsl/error_formatter.rb +75 -0
- data/lib/tomo/configuration/dsl/hosts_and_settings.rb +24 -0
- data/lib/tomo/configuration/dsl/tasks_block.rb +24 -0
- data/lib/tomo/configuration/dsl.rb +12 -0
- data/lib/tomo/configuration/environment.rb +12 -0
- data/lib/tomo/configuration/glob.rb +26 -0
- data/lib/tomo/configuration/plugin_file_not_found_error.rb +14 -0
- data/lib/tomo/configuration/plugin_resolver.rb +63 -0
- data/lib/tomo/configuration/plugins_registry/file_resolver.rb +43 -0
- data/lib/tomo/configuration/plugins_registry/gem_resolver.rb +63 -0
- data/lib/tomo/configuration/plugins_registry.rb +67 -0
- data/lib/tomo/configuration/project_not_found_error.rb +28 -0
- data/lib/tomo/configuration/role_based_task_filter.rb +42 -0
- data/lib/tomo/configuration/unknown_environment_error.rb +46 -0
- data/lib/tomo/configuration/unknown_plugin_error.rb +28 -0
- data/lib/tomo/configuration/unspecified_environment_error.rb +28 -0
- data/lib/tomo/configuration.rb +124 -0
- data/lib/tomo/console/key_reader.rb +51 -0
- data/lib/tomo/console/menu.rb +109 -0
- data/lib/tomo/console.rb +33 -0
- data/lib/tomo/error/suggestions.rb +44 -0
- data/lib/tomo/error.rb +22 -0
- data/lib/tomo/host.rb +57 -0
- data/lib/tomo/logger/tagged_io.rb +38 -0
- data/lib/tomo/logger.rb +70 -0
- data/lib/tomo/path.rb +19 -0
- data/lib/tomo/paths.rb +36 -0
- data/lib/tomo/plugin/bundler/helpers.rb +14 -0
- data/lib/tomo/plugin/bundler/tasks.rb +57 -0
- data/lib/tomo/plugin/bundler.rb +17 -0
- data/lib/tomo/plugin/core/helpers.rb +65 -0
- data/lib/tomo/plugin/core/tasks.rb +138 -0
- data/lib/tomo/plugin/core.rb +31 -0
- data/lib/tomo/plugin/env/tasks.rb +113 -0
- data/lib/tomo/plugin/env.rb +13 -0
- data/lib/tomo/plugin/git/helpers.rb +11 -0
- data/lib/tomo/plugin/git/tasks.rb +78 -0
- data/lib/tomo/plugin/git.rb +19 -0
- data/lib/tomo/plugin/nvm/tasks.rb +61 -0
- data/lib/tomo/plugin/nvm.rb +14 -0
- data/lib/tomo/plugin/puma/tasks.rb +38 -0
- data/lib/tomo/plugin/puma.rb +12 -0
- data/lib/tomo/plugin/rails/helpers.rb +20 -0
- data/lib/tomo/plugin/rails/tasks.rb +79 -0
- data/lib/tomo/plugin/rails.rb +11 -0
- data/lib/tomo/plugin/rbenv/tasks.rb +55 -0
- data/lib/tomo/plugin/rbenv.rb +12 -0
- data/lib/tomo/plugin/testing.rb +16 -0
- data/lib/tomo/plugin.rb +4 -0
- data/lib/tomo/plugin_dsl.rb +23 -0
- data/lib/tomo/remote.rb +55 -0
- data/lib/tomo/result.rb +28 -0
- data/lib/tomo/runtime/concurrent_ruby_load_error.rb +26 -0
- data/lib/tomo/runtime/concurrent_ruby_thread_pool.rb +50 -0
- data/lib/tomo/runtime/context.rb +21 -0
- data/lib/tomo/runtime/current.rb +41 -0
- data/lib/tomo/runtime/execution_plan.rb +107 -0
- data/lib/tomo/runtime/host_execution_step.rb +49 -0
- data/lib/tomo/runtime/inline_thread_pool.rb +27 -0
- data/lib/tomo/runtime/privileged_task.rb +6 -0
- data/lib/tomo/runtime/settings_interpolation.rb +55 -0
- data/lib/tomo/runtime/settings_required_error.rb +33 -0
- data/lib/tomo/runtime/task_aborted_error.rb +15 -0
- data/lib/tomo/runtime/task_runner.rb +56 -0
- data/lib/tomo/runtime/unknown_task_error.rb +23 -0
- data/lib/tomo/runtime.rb +82 -0
- data/lib/tomo/script.rb +44 -0
- data/lib/tomo/shell_builder.rb +108 -0
- data/lib/tomo/ssh/child_process.rb +64 -0
- data/lib/tomo/ssh/connection.rb +82 -0
- data/lib/tomo/ssh/connection_error.rb +16 -0
- data/lib/tomo/ssh/connection_validator.rb +87 -0
- data/lib/tomo/ssh/error.rb +11 -0
- data/lib/tomo/ssh/executable_error.rb +21 -0
- data/lib/tomo/ssh/options.rb +67 -0
- data/lib/tomo/ssh/permission_error.rb +18 -0
- data/lib/tomo/ssh/script_error.rb +23 -0
- data/lib/tomo/ssh/unknown_error.rb +13 -0
- data/lib/tomo/ssh/unsupported_version_error.rb +15 -0
- data/lib/tomo/ssh.rb +36 -0
- data/lib/tomo/task_library.rb +51 -0
- data/lib/tomo/templates/config.rb.erb +66 -0
- data/lib/tomo/testing/Dockerfile +10 -0
- data/lib/tomo/testing/connection.rb +34 -0
- data/lib/tomo/testing/docker_image.rb +115 -0
- data/lib/tomo/testing/docker_plugin_tester.rb +39 -0
- data/lib/tomo/testing/host_extensions.rb +27 -0
- data/lib/tomo/testing/local.rb +75 -0
- data/lib/tomo/testing/mock_plugin_tester.rb +26 -0
- data/lib/tomo/testing/mocked_exec_error.rb +6 -0
- data/lib/tomo/testing/plugin_tester.rb +49 -0
- data/lib/tomo/testing/remote_extensions.rb +10 -0
- data/lib/tomo/testing/ssh_extensions.rb +13 -0
- data/lib/tomo/testing/tomo_test_ed25519 +7 -0
- data/lib/tomo/testing/tomo_test_ed25519.pub +1 -0
- data/lib/tomo/testing/ubuntu_setup.sh +33 -0
- data/lib/tomo/testing.rb +39 -0
- data/lib/tomo/version.rb +3 -0
- data/lib/tomo.rb +45 -0
- metadata +308 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require "forwardable"
|
|
2
|
+
|
|
3
|
+
module Tomo
|
|
4
|
+
class Runtime
|
|
5
|
+
class ExecutionPlan
|
|
6
|
+
extend Forwardable
|
|
7
|
+
|
|
8
|
+
def_delegators :@task_runner, :paths, :settings
|
|
9
|
+
|
|
10
|
+
attr_reader :applicable_hosts
|
|
11
|
+
|
|
12
|
+
def initialize(tasks:, hosts:, task_filter:, task_runner:)
|
|
13
|
+
@hosts = hosts
|
|
14
|
+
@tasks = tasks
|
|
15
|
+
@task_runner = task_runner
|
|
16
|
+
@plan = build_plan(task_filter)
|
|
17
|
+
@applicable_hosts = gather_applicable_hosts
|
|
18
|
+
@thread_pool = build_thread_pool
|
|
19
|
+
freeze
|
|
20
|
+
validate_tasks!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def applicable_hosts_sentence
|
|
24
|
+
return "no hosts" if applicable_hosts.empty?
|
|
25
|
+
|
|
26
|
+
case applicable_hosts.length
|
|
27
|
+
when 1 then applicable_hosts.first.to_s
|
|
28
|
+
when 2 then applicable_hosts.map(&:to_s).join(" and ")
|
|
29
|
+
else
|
|
30
|
+
"#{applicable_hosts.first} and "\
|
|
31
|
+
"#{applicable_hosts.length - 1} other hosts"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def execute
|
|
36
|
+
open_connections do |remotes|
|
|
37
|
+
plan.each do |steps|
|
|
38
|
+
steps.each do |step|
|
|
39
|
+
step.execute(thread_pool: thread_pool, remotes: remotes)
|
|
40
|
+
end
|
|
41
|
+
thread_pool.run_to_completion
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :tasks, :hosts, :plan, :task_runner, :thread_pool
|
|
50
|
+
|
|
51
|
+
def validate_tasks!
|
|
52
|
+
plan.each do |steps|
|
|
53
|
+
steps.each do |step|
|
|
54
|
+
step.applicable_tasks.each do |task|
|
|
55
|
+
task_runner.validate_task!(task)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def open_connections
|
|
62
|
+
remotes = applicable_hosts.each_with_object({}) do |host, opened|
|
|
63
|
+
thread_pool.post(host) do |thr_host|
|
|
64
|
+
opened[thr_host] = task_runner.connect(thr_host)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
thread_pool.run_to_completion
|
|
68
|
+
yield(remotes)
|
|
69
|
+
ensure
|
|
70
|
+
(remotes || {}).values.each(&:close)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_plan(task_filter)
|
|
74
|
+
tasks.each_with_object([]) do |task, result|
|
|
75
|
+
steps = hosts.map do |host|
|
|
76
|
+
HostExecutionStep.new(
|
|
77
|
+
tasks: task, host: host,
|
|
78
|
+
task_filter: task_filter, task_runner: task_runner
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
steps.reject!(&:empty?)
|
|
82
|
+
result << steps unless steps.empty?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def gather_applicable_hosts
|
|
87
|
+
plan.each_with_object([]) do |steps, result|
|
|
88
|
+
steps.each do |step|
|
|
89
|
+
result.push(*step.applicable_hosts)
|
|
90
|
+
end
|
|
91
|
+
end.uniq
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_thread_pool
|
|
95
|
+
if plan.map(&:length).max.to_i > 1
|
|
96
|
+
ConcurrentRubyThreadPool.new(concurrency)
|
|
97
|
+
else
|
|
98
|
+
InlineThreadPool.new
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def concurrency
|
|
103
|
+
[settings[:concurrency].to_i, 1].max
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Runtime
|
|
3
|
+
class HostExecutionStep
|
|
4
|
+
attr_reader :applicable_hosts, :applicable_tasks
|
|
5
|
+
|
|
6
|
+
def initialize(tasks:, host:, task_filter:, task_runner:)
|
|
7
|
+
tasks = Array(tasks).flatten
|
|
8
|
+
@host = host
|
|
9
|
+
@task_runner = task_runner
|
|
10
|
+
@applicable_tasks = task_filter.filter(tasks, host: @host).freeze
|
|
11
|
+
@applicable_hosts = compute_applicable_hosts
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def empty?
|
|
16
|
+
applicable_tasks.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute(thread_pool:, remotes:)
|
|
20
|
+
return if applicable_tasks.empty?
|
|
21
|
+
|
|
22
|
+
thread_pool.post do
|
|
23
|
+
applicable_tasks.each do |task|
|
|
24
|
+
break if thread_pool.failure?
|
|
25
|
+
|
|
26
|
+
task_host = task.is_a?(PrivilegedTask) ? host.as_privileged : host
|
|
27
|
+
remote = remotes[task_host]
|
|
28
|
+
task_runner.run(task: task, remote: remote)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :host, :task_runner
|
|
36
|
+
|
|
37
|
+
def compute_applicable_hosts
|
|
38
|
+
priv_tasks, normal_tasks = applicable_tasks.partition do |task|
|
|
39
|
+
task.is_a?(PrivilegedTask)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
hosts = []
|
|
43
|
+
hosts << host if normal_tasks.any?
|
|
44
|
+
hosts << host.as_privileged if priv_tasks.any?
|
|
45
|
+
hosts.uniq.freeze
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Runtime
|
|
3
|
+
class InlineThreadPool
|
|
4
|
+
def post(*args)
|
|
5
|
+
return if failure?
|
|
6
|
+
|
|
7
|
+
yield(*args)
|
|
8
|
+
nil
|
|
9
|
+
rescue StandardError => e
|
|
10
|
+
self.failure = e
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run_to_completion
|
|
15
|
+
raise failure if failure?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def failure?
|
|
19
|
+
!!failure
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_accessor :failure
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Runtime
|
|
3
|
+
class SettingsInterpolation
|
|
4
|
+
def self.interpolate(settings)
|
|
5
|
+
new(settings).call
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize(settings)
|
|
9
|
+
@settings = symbolize(settings)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
hash = Hash[settings.keys.map { |name| [name, fetch(name)] }]
|
|
14
|
+
dump_settings(hash) if Tomo.debug?
|
|
15
|
+
hash
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :settings
|
|
21
|
+
|
|
22
|
+
def fetch(name, stack=[])
|
|
23
|
+
raise_circular_dependency_error(name, stack) if stack.include?(name)
|
|
24
|
+
value = settings.fetch(name)
|
|
25
|
+
return value unless value.is_a?(String)
|
|
26
|
+
|
|
27
|
+
value.gsub(/%<(\w+)>/) do
|
|
28
|
+
fetch(Regexp.last_match[1].to_sym, stack + [name])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def raise_circular_dependency_error(name, stack)
|
|
33
|
+
dependencies = [*stack, name].join(" -> ")
|
|
34
|
+
raise "Circular dependency detected in settings: #{dependencies}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def symbolize(hash)
|
|
38
|
+
hash.each_with_object({}) do |(key, value), symbolized|
|
|
39
|
+
symbolized[key.to_sym] = value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dump_settings(hash)
|
|
44
|
+
key_len = hash.keys.map(&:to_s).map(&:length).max
|
|
45
|
+
dump = "Settings: {\n"
|
|
46
|
+
hash.to_a.sort_by(&:first).each do |key, value|
|
|
47
|
+
justified_key = "#{key}:".ljust(key_len + 1)
|
|
48
|
+
dump << " #{justified_key} #{value.inspect},\n"
|
|
49
|
+
end
|
|
50
|
+
dump << "}"
|
|
51
|
+
Tomo.logger.debug(dump)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Runtime
|
|
3
|
+
class SettingsRequiredError < Tomo::Error
|
|
4
|
+
attr_accessor :command_name, :settings, :task
|
|
5
|
+
|
|
6
|
+
def to_console
|
|
7
|
+
<<~ERROR
|
|
8
|
+
The #{yellow(task)} task requires #{settings_sentence}
|
|
9
|
+
|
|
10
|
+
Settings can be specified in #{blue(DEFAULT_CONFIG_PATH)}, or by running tomo
|
|
11
|
+
with the #{blue('-s')} option. For example:
|
|
12
|
+
|
|
13
|
+
#{blue("tomo -s #{settings.first}=foo")}
|
|
14
|
+
|
|
15
|
+
You can also use environment variables:
|
|
16
|
+
|
|
17
|
+
#{blue("TOMO_#{settings.first.upcase}=foo tomo #{command_name}")}
|
|
18
|
+
ERROR
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def settings_sentence
|
|
24
|
+
if settings.length == 1
|
|
25
|
+
return "a value for the #{yellow(settings.first.to_s)} setting."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sentence = "values for these settings:\n\n "
|
|
29
|
+
sentence << settings.map { |s| yellow(s.to_s) }.join("\n ")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Runtime
|
|
3
|
+
class TaskRunner
|
|
4
|
+
extend Forwardable
|
|
5
|
+
|
|
6
|
+
def_delegators :@context, :paths, :settings
|
|
7
|
+
attr_reader :context
|
|
8
|
+
|
|
9
|
+
def initialize(plugins_registry:, settings:)
|
|
10
|
+
interpolated_settings = SettingsInterpolation.interpolate(
|
|
11
|
+
plugins_registry.settings.merge(settings)
|
|
12
|
+
)
|
|
13
|
+
@helper_modules = plugins_registry.helper_modules.freeze
|
|
14
|
+
@context = Context.new(interpolated_settings)
|
|
15
|
+
@tasks_by_name = plugins_registry.bind_tasks(context).freeze
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate_task!(name)
|
|
20
|
+
return if tasks_by_name.key?(name)
|
|
21
|
+
|
|
22
|
+
UnknownTaskError.raise_with(
|
|
23
|
+
name,
|
|
24
|
+
unknown_task: name,
|
|
25
|
+
known_tasks: tasks_by_name.keys
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run(task:, remote:)
|
|
30
|
+
validate_task!(task)
|
|
31
|
+
Current.with(task: task, remote: remote) do
|
|
32
|
+
Tomo.logger.task_start(task)
|
|
33
|
+
tasks_by_name[task].call
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def connect(host)
|
|
38
|
+
Current.with(host: host) do
|
|
39
|
+
conn = SSH.connect(host: host, options: SSH::Options.new(settings))
|
|
40
|
+
remote = Remote.new(conn, context, helper_modules)
|
|
41
|
+
return remote unless block_given?
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
return yield(remote)
|
|
45
|
+
ensure
|
|
46
|
+
remote&.close if block_given?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
attr_reader :helper_modules, :tasks_by_name
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Runtime
|
|
3
|
+
class UnknownTaskError < Error
|
|
4
|
+
attr_accessor :unknown_task, :known_tasks
|
|
5
|
+
|
|
6
|
+
def to_console
|
|
7
|
+
error = <<~ERROR
|
|
8
|
+
#{yellow(unknown_task)} is not a recognized task.
|
|
9
|
+
To see a list of all available tasks, run #{blue('tomo tasks')}.
|
|
10
|
+
ERROR
|
|
11
|
+
|
|
12
|
+
# TODO: suggest "did you forget to add the <abc> plugin?"
|
|
13
|
+
|
|
14
|
+
sugg = Error::Suggestions.new(
|
|
15
|
+
dictionary: known_tasks,
|
|
16
|
+
word: unknown_task
|
|
17
|
+
)
|
|
18
|
+
error << sugg.to_console if sugg.any?
|
|
19
|
+
error
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/tomo/runtime.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Tomo
|
|
4
|
+
class Runtime
|
|
5
|
+
autoload :ConcurrentRubyLoadError, "tomo/runtime/concurrent_ruby_load_error"
|
|
6
|
+
autoload :ConcurrentRubyThreadPool,
|
|
7
|
+
"tomo/runtime/concurrent_ruby_thread_pool"
|
|
8
|
+
autoload :Context, "tomo/runtime/context"
|
|
9
|
+
autoload :Current, "tomo/runtime/current"
|
|
10
|
+
autoload :ExecutionPlan, "tomo/runtime/execution_plan"
|
|
11
|
+
autoload :HostExecutionStep, "tomo/runtime/host_execution_step"
|
|
12
|
+
autoload :InlineThreadPool, "tomo/runtime/inline_thread_pool"
|
|
13
|
+
autoload :PrivilegedTask, "tomo/runtime/privileged_task"
|
|
14
|
+
autoload :SettingsInterpolation, "tomo/runtime/settings_interpolation"
|
|
15
|
+
autoload :SettingsRequiredError, "tomo/runtime/settings_required_error"
|
|
16
|
+
autoload :TaskAbortedError, "tomo/runtime/task_aborted_error"
|
|
17
|
+
autoload :TaskRunner, "tomo/runtime/task_runner"
|
|
18
|
+
autoload :UnknownTaskError, "tomo/runtime/unknown_task_error"
|
|
19
|
+
|
|
20
|
+
attr_reader :tasks
|
|
21
|
+
|
|
22
|
+
def initialize(deploy_tasks:, setup_tasks:, hosts:, task_filter:,
|
|
23
|
+
settings:, plugins_registry:)
|
|
24
|
+
@deploy_tasks = deploy_tasks.freeze
|
|
25
|
+
@setup_tasks = setup_tasks.freeze
|
|
26
|
+
@hosts = hosts.freeze
|
|
27
|
+
@task_filter = task_filter.freeze
|
|
28
|
+
@settings = settings
|
|
29
|
+
@plugins_registry = plugins_registry
|
|
30
|
+
@tasks = plugins_registry.task_names
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def deploy!
|
|
35
|
+
execution_plan_for(deploy_tasks, release: :new).execute
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def setup!
|
|
39
|
+
execution_plan_for(setup_tasks, release: :tmp).execute
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run!(task, *args, privileged: false)
|
|
43
|
+
task = task.dup.extend(PrivilegedTask) if privileged
|
|
44
|
+
execution_plan_for([task], release: :current, args: args).execute
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def execution_plan_for(tasks, release: :current, args: [])
|
|
48
|
+
ExecutionPlan.new(
|
|
49
|
+
tasks: tasks,
|
|
50
|
+
hosts: hosts,
|
|
51
|
+
task_filter: task_filter,
|
|
52
|
+
task_runner: new_task_runner(release, args)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
attr_reader :deploy_tasks, :setup_tasks, :hosts, :task_filter, :settings,
|
|
59
|
+
:plugins_registry
|
|
60
|
+
|
|
61
|
+
def new_task_runner(release_type, args)
|
|
62
|
+
run_settings = { release_path: release_path_for(release_type) }
|
|
63
|
+
.merge(settings)
|
|
64
|
+
.merge(run_args: args)
|
|
65
|
+
|
|
66
|
+
TaskRunner.new(plugins_registry: plugins_registry, settings: run_settings)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def release_path_for(type)
|
|
70
|
+
start_time = Time.now
|
|
71
|
+
release = start_time.utc.strftime("%Y%m%d%H%M%S")
|
|
72
|
+
|
|
73
|
+
case type
|
|
74
|
+
when :current then "%<current_path>"
|
|
75
|
+
when :new then "%<releases_path>/#{release}"
|
|
76
|
+
when :tmp then "%<tmp_path>/#{release}"
|
|
77
|
+
else
|
|
78
|
+
raise ArgumentError, "release: must be :current, :new, or :tmp"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/tomo/script.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Script
|
|
3
|
+
attr_reader :script
|
|
4
|
+
|
|
5
|
+
def initialize(script,
|
|
6
|
+
echo: true,
|
|
7
|
+
pty: false,
|
|
8
|
+
raise_on_error: true,
|
|
9
|
+
silent: false)
|
|
10
|
+
@script = script
|
|
11
|
+
@echo = echo
|
|
12
|
+
@pty = pty
|
|
13
|
+
@raise_on_error = raise_on_error
|
|
14
|
+
@silent = silent
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def echo?
|
|
19
|
+
!!@echo
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def echo_string
|
|
23
|
+
return nil unless echo?
|
|
24
|
+
|
|
25
|
+
@echo == true ? script : @echo
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pty?
|
|
29
|
+
!!@pty
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def raise_on_error?
|
|
33
|
+
!!@raise_on_error
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def silent?
|
|
37
|
+
!!@silent
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
script
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require "shellwords"
|
|
2
|
+
|
|
3
|
+
module Tomo
|
|
4
|
+
class ShellBuilder
|
|
5
|
+
def self.raw(string)
|
|
6
|
+
string.define_singleton_method(:shellescape) { string }
|
|
7
|
+
string
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@env = {}
|
|
12
|
+
@chdir = []
|
|
13
|
+
@prefixes = []
|
|
14
|
+
@umask = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def chdir(dir)
|
|
18
|
+
@chdir << dir
|
|
19
|
+
yield
|
|
20
|
+
ensure
|
|
21
|
+
@chdir.pop
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def env(hash)
|
|
25
|
+
orig_env = @env
|
|
26
|
+
@env = orig_env.merge(hash || {})
|
|
27
|
+
yield
|
|
28
|
+
ensure
|
|
29
|
+
@env = orig_env
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def prepend(*command)
|
|
33
|
+
prefixes.unshift(*command)
|
|
34
|
+
yield
|
|
35
|
+
ensure
|
|
36
|
+
prefixes.shift(command.length)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def umask(mask)
|
|
40
|
+
orig_umask = @umask
|
|
41
|
+
@umask = mask
|
|
42
|
+
yield
|
|
43
|
+
ensure
|
|
44
|
+
@umask = orig_umask
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build(*command, default_chdir: nil)
|
|
48
|
+
if @chdir.empty? && default_chdir
|
|
49
|
+
return chdir(default_chdir) { build(*command) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
command_string = command_to_string(*command)
|
|
53
|
+
modifiers = [cd_chdir, unset_env, export_env, set_umask].compact.flatten
|
|
54
|
+
[*modifiers, command_string].join(" && ")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
attr_reader :prefixes
|
|
60
|
+
|
|
61
|
+
def command_to_string(*command)
|
|
62
|
+
command_string = shell_join(*command)
|
|
63
|
+
return command_string if prefixes.empty?
|
|
64
|
+
|
|
65
|
+
"#{shell_join(*prefixes)} #{command_string}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def shell_join(*command)
|
|
69
|
+
return command.first.to_s if command.length == 1
|
|
70
|
+
|
|
71
|
+
command.flatten.compact.map(&:to_s).map(&:shellescape).join(" ")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cd_chdir
|
|
75
|
+
@chdir.map { |dir| "cd #{dir.to_s.shellescape}" }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def unset_env
|
|
79
|
+
unsets = @env.select { |_, value| value.nil? }
|
|
80
|
+
return if unsets.empty?
|
|
81
|
+
|
|
82
|
+
["unset", *unsets.map { |entry| entry.first.to_s.shellescape }].join(" ")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def export_env
|
|
86
|
+
exports = @env.reject { |_, value| value.nil? }
|
|
87
|
+
return if exports.empty?
|
|
88
|
+
|
|
89
|
+
[
|
|
90
|
+
"export",
|
|
91
|
+
*exports.map do |key, value|
|
|
92
|
+
"#{key.to_s.shellescape}=#{value.to_s.shellescape}"
|
|
93
|
+
end
|
|
94
|
+
].join(" ")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def set_umask
|
|
98
|
+
return if @umask.nil?
|
|
99
|
+
|
|
100
|
+
umask_value = if @umask.is_a?(Integer)
|
|
101
|
+
@umask.to_s(8).rjust(4, "0")
|
|
102
|
+
else
|
|
103
|
+
@umask
|
|
104
|
+
end
|
|
105
|
+
"umask #{umask_value.to_s.shellescape}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "shellwords"
|
|
3
|
+
|
|
4
|
+
module Tomo
|
|
5
|
+
module SSH
|
|
6
|
+
class ChildProcess
|
|
7
|
+
def self.execute(*command, on_data: ->(data) {})
|
|
8
|
+
process = new(*command, on_data: on_data)
|
|
9
|
+
process.wait_for_exit
|
|
10
|
+
process.result
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(*command, on_data:)
|
|
14
|
+
@command = *command
|
|
15
|
+
@on_data = on_data
|
|
16
|
+
@stdout_buffer = StringIO.new
|
|
17
|
+
@stderr_buffer = StringIO.new
|
|
18
|
+
Tomo.logger.debug command.map(&:shellescape).join(" ")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def wait_for_exit
|
|
22
|
+
Open3.popen3(*command) do |stdin, stdout, stderr, wait_thread|
|
|
23
|
+
stdin.close
|
|
24
|
+
stdout_thread = start_io_thread(stdout, stdout_buffer)
|
|
25
|
+
stderr_thread = start_io_thread(stderr, stderr_buffer)
|
|
26
|
+
stdout_thread.join
|
|
27
|
+
stderr_thread.join
|
|
28
|
+
@exit_status = wait_thread.value.exitstatus
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def result
|
|
33
|
+
Result.new(
|
|
34
|
+
exit_status: exit_status,
|
|
35
|
+
stdout: stdout_buffer.string,
|
|
36
|
+
stderr: stderr_buffer.string
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :command, :exit_status, :on_data,
|
|
43
|
+
:stdout_buffer, :stderr_buffer
|
|
44
|
+
|
|
45
|
+
def start_io_thread(source, buffer)
|
|
46
|
+
new_thread_inheriting_current_vars do
|
|
47
|
+
begin
|
|
48
|
+
while (line = source.gets)
|
|
49
|
+
on_data&.call(line)
|
|
50
|
+
buffer << line
|
|
51
|
+
end
|
|
52
|
+
rescue IOError # rubocop:disable Lint/HandleExceptions
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def new_thread_inheriting_current_vars(&block)
|
|
58
|
+
Thread.new(Runtime::Current.variables) do |vars|
|
|
59
|
+
Runtime::Current.with(vars, &block)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|