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,82 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Tomo
|
|
6
|
+
module SSH
|
|
7
|
+
class Connection
|
|
8
|
+
def self.dry_run(host, options)
|
|
9
|
+
new(
|
|
10
|
+
host,
|
|
11
|
+
options,
|
|
12
|
+
exec_proc: proc { exit(0) },
|
|
13
|
+
child_proc: proc { Result.empty_success }
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :host
|
|
18
|
+
|
|
19
|
+
def initialize(host, options, exec_proc: nil, child_proc: nil)
|
|
20
|
+
@host = host
|
|
21
|
+
@options = options
|
|
22
|
+
@exec_proc = exec_proc || Process.method(:exec)
|
|
23
|
+
@child_proc = child_proc || ChildProcess.method(:execute)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ssh_exec(script)
|
|
27
|
+
ssh_args = build_args(script)
|
|
28
|
+
logger.script_start(script)
|
|
29
|
+
Tomo.logger.debug ssh_args.map(&:shellescape).join(" ")
|
|
30
|
+
exec_proc.call(*ssh_args)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ssh_subprocess(script, verbose: false)
|
|
34
|
+
ssh_args = build_args(script, verbose)
|
|
35
|
+
handle_data = ->(data) { logger.script_output(script, data) }
|
|
36
|
+
|
|
37
|
+
logger.script_start(script)
|
|
38
|
+
result = child_proc.call(*ssh_args, on_data: handle_data)
|
|
39
|
+
logger.script_end(script, result)
|
|
40
|
+
|
|
41
|
+
if result.failure? && script.raise_on_error?
|
|
42
|
+
raise_run_error(script, ssh_args, result)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def close
|
|
49
|
+
FileUtils.rm_f(control_path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
attr_reader :options, :exec_proc, :child_proc
|
|
55
|
+
|
|
56
|
+
def logger
|
|
57
|
+
Tomo.logger
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_args(script, verbose=false)
|
|
61
|
+
options.build_args(host, script, control_path, verbose)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def control_path
|
|
65
|
+
@control_path ||= begin
|
|
66
|
+
token = SecureRandom.hex(8)
|
|
67
|
+
File.join(Dir.tmpdir, "tomo_ssh_#{token}")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def raise_run_error(script, ssh_args, result)
|
|
72
|
+
ScriptError.raise_with(
|
|
73
|
+
result.output,
|
|
74
|
+
host: host,
|
|
75
|
+
result: result,
|
|
76
|
+
script: script,
|
|
77
|
+
ssh_args: ssh_args
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module SSH
|
|
3
|
+
class ConnectionError < Error
|
|
4
|
+
def to_console
|
|
5
|
+
msg = <<~ERROR
|
|
6
|
+
Unable to connect via SSH to #{yellow(host.address)} on port #{yellow(host.port)}.
|
|
7
|
+
|
|
8
|
+
Make sure the hostname and port are correct and that you have the
|
|
9
|
+
necessary network (or VPN) access.
|
|
10
|
+
ERROR
|
|
11
|
+
|
|
12
|
+
[msg, super].join("\n")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "forwardable"
|
|
2
|
+
|
|
3
|
+
module Tomo
|
|
4
|
+
module SSH
|
|
5
|
+
class ConnectionValidator
|
|
6
|
+
MINIMUM_OPENSSH_VERSION = 7.4
|
|
7
|
+
private_constant :MINIMUM_OPENSSH_VERSION
|
|
8
|
+
|
|
9
|
+
extend Forwardable
|
|
10
|
+
|
|
11
|
+
def initialize(executable, connection)
|
|
12
|
+
@executable = executable
|
|
13
|
+
@connection = connection
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def assert_valid_executable!
|
|
17
|
+
result = begin
|
|
18
|
+
ChildProcess.execute(executable, "-V")
|
|
19
|
+
rescue StandardError => e
|
|
20
|
+
handle_bad_executable(e)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Tomo.logger.debug(result.output)
|
|
24
|
+
return if result.success? && supported?(result.output)
|
|
25
|
+
|
|
26
|
+
raise_unsupported_version(result.output)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assert_valid_connection!
|
|
30
|
+
script = Script.new(
|
|
31
|
+
"echo hi",
|
|
32
|
+
silent: !Tomo.debug?, echo: false, raise_on_error: false
|
|
33
|
+
)
|
|
34
|
+
res = connection.ssh_subprocess(script, verbose: Tomo.debug?)
|
|
35
|
+
raise_connection_failure(res) if res.exit_status == 255
|
|
36
|
+
raise_unknown_error(res) if res.failure? || res.stdout.chomp != "hi"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dump_env
|
|
40
|
+
script = Script.new("env", silent: true, echo: false)
|
|
41
|
+
res = connection.ssh_subprocess(script)
|
|
42
|
+
Tomo.logger.debug("#{host} environment:\n#{res.stdout.strip}")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def_delegators :connection, :host
|
|
48
|
+
attr_reader :executable, :connection
|
|
49
|
+
|
|
50
|
+
def supported?(version)
|
|
51
|
+
version[/OpenSSH_(\d+\.\d+)/i, 1].to_f >= MINIMUM_OPENSSH_VERSION
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def handle_bad_executable(error)
|
|
55
|
+
ExecutableError.raise_with(error, executable: executable)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def raise_unsupported_version(ver)
|
|
59
|
+
UnsupportedVersionError.raise_with(
|
|
60
|
+
ver,
|
|
61
|
+
host: host,
|
|
62
|
+
command: "#{executable} -V",
|
|
63
|
+
expected_version: "OpenSSH_#{MINIMUM_OPENSSH_VERSION}"
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def raise_connection_failure(result)
|
|
68
|
+
case result.output
|
|
69
|
+
when /Permission denied/i
|
|
70
|
+
PermissionError.raise_with(result.output, host: host)
|
|
71
|
+
when /(Could not resolve|Operation timed out|Connection refused)/i
|
|
72
|
+
ConnectionError.raise_with(result.output, host: host)
|
|
73
|
+
else
|
|
74
|
+
UnknownError.raise_with(result.output, host: host)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def raise_unknown_error(result)
|
|
79
|
+
UnknownError.raise_with(<<~ERROR.strip, host: host)
|
|
80
|
+
Unexpected output from `ssh`. Expected `echo hi` to return "hi" but got:
|
|
81
|
+
#{result.output}
|
|
82
|
+
(exited with code #{result.exit_status})
|
|
83
|
+
ERROR
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module SSH
|
|
3
|
+
class ExecutableError < Error
|
|
4
|
+
attr_accessor :executable
|
|
5
|
+
|
|
6
|
+
def to_console
|
|
7
|
+
hint = if executable.to_s.include?("/")
|
|
8
|
+
"Is the ssh binary properly installed in this location?"
|
|
9
|
+
else
|
|
10
|
+
"Is #{yellow(executable)} installed and in your "\
|
|
11
|
+
"#{blue('$PATH')}?"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
<<~ERROR
|
|
15
|
+
Failed to execute #{yellow(executable)}.
|
|
16
|
+
#{hint}
|
|
17
|
+
ERROR
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module SSH
|
|
3
|
+
class Options
|
|
4
|
+
attr_reader :executable
|
|
5
|
+
|
|
6
|
+
def initialize(settings)
|
|
7
|
+
@executable = settings.fetch(:ssh_executable)
|
|
8
|
+
@extra_opts = settings.fetch(:ssh_extra_opts)
|
|
9
|
+
@forward_agent = settings.fetch(:ssh_forward_agent)
|
|
10
|
+
@reuse_connections = settings.fetch(:ssh_reuse_connections)
|
|
11
|
+
@connect_timeout = settings.fetch(:ssh_connect_timeout)
|
|
12
|
+
@strict_host_key_checking = settings.fetch(
|
|
13
|
+
:ssh_strict_host_key_checking
|
|
14
|
+
)
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# rubocop:disable Metrics/AbcSize
|
|
19
|
+
def build_args(host, script, control_path, verbose)
|
|
20
|
+
args = [verbose ? "-v" : ["-o", "LogLevel=ERROR"]]
|
|
21
|
+
args << "-A" if forward_agent
|
|
22
|
+
args << connect_timeout_option
|
|
23
|
+
args << strict_host_key_checking_option
|
|
24
|
+
args.push(*control_opts(control_path, verbose)) if reuse_connections
|
|
25
|
+
args.push(*extra_opts) if extra_opts
|
|
26
|
+
args << "-tt" if script.pty?
|
|
27
|
+
args << host.to_ssh_args
|
|
28
|
+
args << "--"
|
|
29
|
+
|
|
30
|
+
[executable, args, script.to_s].flatten
|
|
31
|
+
end
|
|
32
|
+
# rubocop:enable Metrics/AbcSize
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :connect_timeout, :extra_opts, :forward_agent,
|
|
37
|
+
:reuse_connections, :strict_host_key_checking
|
|
38
|
+
|
|
39
|
+
def control_opts(path, verbose)
|
|
40
|
+
opts = [
|
|
41
|
+
"-o", "ControlMaster=auto",
|
|
42
|
+
"-o", "ControlPath=#{path}",
|
|
43
|
+
"-o"
|
|
44
|
+
]
|
|
45
|
+
opts << (verbose ? "ControlPersist=1s" : "ControlPersist=30s")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def connect_timeout_option
|
|
49
|
+
return [] if connect_timeout.nil?
|
|
50
|
+
|
|
51
|
+
["-o", "ConnectTimeout=#{connect_timeout}"]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def strict_host_key_checking_option
|
|
55
|
+
return [] if strict_host_key_checking.nil?
|
|
56
|
+
|
|
57
|
+
value = case strict_host_key_checking
|
|
58
|
+
when true then "yes"
|
|
59
|
+
when false then "no"
|
|
60
|
+
else strict_host_key_checking
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
["-o", "StrictHostKeyChecking=#{value}"]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module SSH
|
|
3
|
+
class PermissionError < Error
|
|
4
|
+
def to_console
|
|
5
|
+
as_user = host.user && " as user #{yellow(host.user)}"
|
|
6
|
+
|
|
7
|
+
msg = <<~ERROR
|
|
8
|
+
Unable to connect via SSH to #{yellow(host.address)}#{as_user}.
|
|
9
|
+
|
|
10
|
+
Check that you’ve specified the correct username and that your public key
|
|
11
|
+
is properly installed on the server.
|
|
12
|
+
ERROR
|
|
13
|
+
|
|
14
|
+
[msg, super].join("\n")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "shellwords"
|
|
2
|
+
|
|
3
|
+
module Tomo
|
|
4
|
+
module SSH
|
|
5
|
+
class ScriptError < Error
|
|
6
|
+
attr_accessor :result, :script, :ssh_args
|
|
7
|
+
|
|
8
|
+
def to_console
|
|
9
|
+
msg = <<~ERROR
|
|
10
|
+
The following script failed on #{yellow(host)} (exit status #{red(result.exit_status)}).
|
|
11
|
+
|
|
12
|
+
#{yellow(script)}
|
|
13
|
+
|
|
14
|
+
You can manually re-execute the script via SSH as follows:
|
|
15
|
+
|
|
16
|
+
#{gray(ssh_args.map(&:shellescape).join(' '))}
|
|
17
|
+
ERROR
|
|
18
|
+
|
|
19
|
+
[msg, super].join("\n")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module SSH
|
|
3
|
+
class UnsupportedVersionError < Error
|
|
4
|
+
attr_accessor :command, :expected_version
|
|
5
|
+
|
|
6
|
+
def to_console
|
|
7
|
+
msg = <<~ERROR
|
|
8
|
+
Expected #{yellow(command)} to return #{blue(expected_version)} or higher.
|
|
9
|
+
ERROR
|
|
10
|
+
|
|
11
|
+
[msg, super].join("\n")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/tomo/ssh.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module SSH
|
|
3
|
+
autoload :ChildProcess, "tomo/ssh/child_process"
|
|
4
|
+
autoload :Connection, "tomo/ssh/connection"
|
|
5
|
+
autoload :ConnectionValidator, "tomo/ssh/connection_validator"
|
|
6
|
+
autoload :ConnectionError, "tomo/ssh/connection_error"
|
|
7
|
+
autoload :Error, "tomo/ssh/error"
|
|
8
|
+
autoload :ExecutableError, "tomo/ssh/executable_error"
|
|
9
|
+
autoload :Options, "tomo/ssh/options"
|
|
10
|
+
autoload :PermissionError, "tomo/ssh/permission_error"
|
|
11
|
+
autoload :ScriptError, "tomo/ssh/script_error"
|
|
12
|
+
autoload :UnknownError, "tomo/ssh/unknown_error"
|
|
13
|
+
autoload :UnsupportedVersionError, "tomo/ssh/unsupported_version_error"
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def connect(host:, options:)
|
|
17
|
+
Tomo.logger.connect(host)
|
|
18
|
+
return Connection.dry_run(host, options) if Tomo.dry_run?
|
|
19
|
+
|
|
20
|
+
build_connection(host, options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def build_connection(host, options)
|
|
26
|
+
conn = Connection.new(host, options)
|
|
27
|
+
validator = ConnectionValidator.new(options.executable, conn)
|
|
28
|
+
validator.assert_valid_executable!
|
|
29
|
+
validator.assert_valid_connection!
|
|
30
|
+
validator.dump_env if Tomo.debug?
|
|
31
|
+
|
|
32
|
+
conn
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "forwardable"
|
|
2
|
+
|
|
3
|
+
module Tomo
|
|
4
|
+
class TaskLibrary
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
def initialize(context)
|
|
8
|
+
@context = context
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def_delegators :context, :paths, :settings
|
|
14
|
+
attr_reader :context
|
|
15
|
+
|
|
16
|
+
def die(reason)
|
|
17
|
+
Runtime::TaskAbortedError.raise_with(
|
|
18
|
+
reason,
|
|
19
|
+
task: context.current_task,
|
|
20
|
+
host: remote.host
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dry_run?
|
|
25
|
+
Tomo.dry_run?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def logger
|
|
29
|
+
Tomo.logger
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def raw(string)
|
|
33
|
+
ShellBuilder.raw(string)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remote
|
|
37
|
+
context.current_remote
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def require_setting(*names)
|
|
41
|
+
missing = names.flatten.select { |sett| settings[sett].nil? }
|
|
42
|
+
return if missing.empty?
|
|
43
|
+
|
|
44
|
+
Runtime::SettingsRequiredError.raise_with(
|
|
45
|
+
settings: missing,
|
|
46
|
+
task: context.current_task
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
alias require_settings require_setting
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
plugin "git"
|
|
2
|
+
plugin "env"
|
|
3
|
+
plugin "bundler"
|
|
4
|
+
plugin "rails"
|
|
5
|
+
plugin "nvm"
|
|
6
|
+
plugin "puma"
|
|
7
|
+
plugin "rbenv"
|
|
8
|
+
plugin "./plugins/<%= app %>.rb"
|
|
9
|
+
|
|
10
|
+
host "user@hostname.or.ip.address"
|
|
11
|
+
|
|
12
|
+
set application: <%= app.inspect %>
|
|
13
|
+
set deploy_to: "/var/www/%<application>"
|
|
14
|
+
set nvm_node_version: <%= node_version&.inspect || "nil # FIXME" %>
|
|
15
|
+
set nvm_yarn_version: <%= yarn_version.inspect %>
|
|
16
|
+
set rbenv_ruby_version: <%= RUBY_VERSION.inspect %>
|
|
17
|
+
set git_url: <%= git_origin_url&.inspect || "nil # FIXME" %>
|
|
18
|
+
set git_branch: "master"
|
|
19
|
+
set git_exclusions: %w[
|
|
20
|
+
.tomo/
|
|
21
|
+
spec/
|
|
22
|
+
test/
|
|
23
|
+
]
|
|
24
|
+
set env_vars: {
|
|
25
|
+
RAILS_ENV: "production",
|
|
26
|
+
RACK_ENV: "production",
|
|
27
|
+
DATABASE_URL: :prompt,
|
|
28
|
+
SECRET_KEY_BASE: :prompt
|
|
29
|
+
}
|
|
30
|
+
set linked_dirs: %w[
|
|
31
|
+
.bundle
|
|
32
|
+
log
|
|
33
|
+
node_modules
|
|
34
|
+
public/assets
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
setup do
|
|
38
|
+
run "env:setup"
|
|
39
|
+
run "core:setup_directories"
|
|
40
|
+
run "git:clone"
|
|
41
|
+
run "git:create_release"
|
|
42
|
+
run "core:symlink_shared"
|
|
43
|
+
run "nvm:install"
|
|
44
|
+
run "rbenv:install"
|
|
45
|
+
run "bundler:upgrade_bundler"
|
|
46
|
+
run "bundler:install"
|
|
47
|
+
run "rails:db_create"
|
|
48
|
+
run "rails:db_schema_load"
|
|
49
|
+
run "rails:db_seed"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
deploy do
|
|
53
|
+
run "env:update"
|
|
54
|
+
run "git:create_release"
|
|
55
|
+
run "core:symlink_shared"
|
|
56
|
+
run "core:write_release_json"
|
|
57
|
+
run "bundler:install"
|
|
58
|
+
run "rails:db_migrate"
|
|
59
|
+
run "rails:db_seed"
|
|
60
|
+
run "rails:assets_precompile"
|
|
61
|
+
run "core:symlink_current"
|
|
62
|
+
run "puma:restart"
|
|
63
|
+
run "core:clean_releases"
|
|
64
|
+
run "bundler:clean"
|
|
65
|
+
run "core:log_revision"
|
|
66
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module Testing
|
|
3
|
+
class Connection < Tomo::SSH::Connection
|
|
4
|
+
def initialize(host, options)
|
|
5
|
+
super(
|
|
6
|
+
host,
|
|
7
|
+
options,
|
|
8
|
+
exec_proc: proc { raise MockedExecError },
|
|
9
|
+
child_proc: method(:mock_child_process)
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def ssh_exec(script)
|
|
14
|
+
host.scripts << script
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ssh_subprocess(script, verbose: false)
|
|
19
|
+
host.scripts << script
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def mock_child_process(*_ssh_args, on_data:)
|
|
26
|
+
result = host.result_for(host.scripts.last)
|
|
27
|
+
|
|
28
|
+
on_data.call(result.stdout) unless result.stdout.empty?
|
|
29
|
+
on_data.call(result.stderr) unless result.stderr.empty?
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
at_exit { Tomo::Testing::DockerImage.running_images.each(&:stop) }
|
|
8
|
+
|
|
9
|
+
module Tomo
|
|
10
|
+
module Testing
|
|
11
|
+
class DockerImage
|
|
12
|
+
class << self
|
|
13
|
+
attr_reader :running_images
|
|
14
|
+
end
|
|
15
|
+
@running_images = []
|
|
16
|
+
|
|
17
|
+
attr_accessor :setup_script
|
|
18
|
+
attr_reader :host
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@setup_script = "#!/bin/bash\n"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build_and_run
|
|
25
|
+
raise "Already running!" if frozen?
|
|
26
|
+
|
|
27
|
+
set_up_build_dir
|
|
28
|
+
pull_base_image_if_needed
|
|
29
|
+
@image_id = build_image
|
|
30
|
+
@container_id = start_container
|
|
31
|
+
@host = Host.parse("deployer@localhost", port: find_ssh_port)
|
|
32
|
+
DockerImage.running_images << self
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stop
|
|
37
|
+
DockerImage.running_images.delete(self)
|
|
38
|
+
Local.capture("docker stop #{container_id}", raise_on_error: false)
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def puma_port
|
|
43
|
+
return 3000 if ENV["_TOMO_CONTAINER"]
|
|
44
|
+
|
|
45
|
+
Local.capture("docker port #{container_id} 3000")[/:(\d+)/, 1].to_i
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Connecting to SSH servers on local docker containers often triggers
|
|
49
|
+
# known_hosts errors due to each container potentially having a
|
|
50
|
+
# different host key. Work around this by using an empty blank temp file
|
|
51
|
+
# for storing known_hosts.
|
|
52
|
+
def ssh_settings
|
|
53
|
+
hosts_file = File.join(Dir.tmpdir, "tomo_#{SecureRandom.hex(8)}_hosts")
|
|
54
|
+
key_file = File.expand_path("tomo_test_ed25519", __dir__)
|
|
55
|
+
FileUtils.chmod(0o600, key_file)
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
ssh_extra_opts: [
|
|
59
|
+
"-o", "UserKnownHostsFile=#{hosts_file}",
|
|
60
|
+
"-o", "IdentityFile=#{key_file}"
|
|
61
|
+
],
|
|
62
|
+
ssh_strict_host_key_checking: false
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
attr_reader :container_id, :image_id
|
|
69
|
+
|
|
70
|
+
def pull_base_image_if_needed
|
|
71
|
+
images = Local.capture('docker images --format "{{.ID}}" ubuntu:18.04')
|
|
72
|
+
Local.capture("docker pull ubuntu:18.04") if images.strip.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_image
|
|
76
|
+
Local.capture(
|
|
77
|
+
"docker build #{build_dir}"
|
|
78
|
+
)[/Successfully built (\S+)$/i, 1]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def start_container
|
|
82
|
+
host_container = ENV["_TOMO_CONTAINER"]
|
|
83
|
+
args = "--detach --init #{image_id}"
|
|
84
|
+
if host_container
|
|
85
|
+
args.prepend("--network=container:#{host_container} ")
|
|
86
|
+
else
|
|
87
|
+
args.prepend("--publish-all ")
|
|
88
|
+
end
|
|
89
|
+
Local.capture("docker run #{args}")[/\S+/]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def find_ssh_port
|
|
93
|
+
return 22 if ENV["_TOMO_CONTAINER"]
|
|
94
|
+
|
|
95
|
+
Local.capture("docker port #{container_id} 22")[/:(\d+)/, 1].to_i
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def set_up_build_dir
|
|
99
|
+
FileUtils.mkdir_p(build_dir)
|
|
100
|
+
files = %w[Dockerfile tomo_test_ed25519.pub ubuntu_setup.sh]
|
|
101
|
+
files.each do |file|
|
|
102
|
+
FileUtils.cp(File.expand_path(file, __dir__), build_dir)
|
|
103
|
+
end
|
|
104
|
+
IO.write(File.join(build_dir, "custom_setup.sh"), setup_script)
|
|
105
|
+
FileUtils.chmod(0o755, File.join(build_dir, "custom_setup.sh"))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_dir
|
|
109
|
+
@_build_dir ||= begin
|
|
110
|
+
File.join(Dir.tmpdir, "tomo_docker_#{SecureRandom.hex(8)}")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|