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,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
|