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