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.
Files changed (135) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +221 -0
  4. data/exe/tomo +4 -0
  5. data/lib/tomo/cli/command.rb +36 -0
  6. data/lib/tomo/cli/common_options.rb +48 -0
  7. data/lib/tomo/cli/completions.rb +70 -0
  8. data/lib/tomo/cli/deploy_options.rb +59 -0
  9. data/lib/tomo/cli/error.rb +16 -0
  10. data/lib/tomo/cli/interrupted_error.rb +9 -0
  11. data/lib/tomo/cli/options.rb +38 -0
  12. data/lib/tomo/cli/parser.rb +92 -0
  13. data/lib/tomo/cli/project_options.rb +47 -0
  14. data/lib/tomo/cli/rules/argument.rb +42 -0
  15. data/lib/tomo/cli/rules/switch.rb +43 -0
  16. data/lib/tomo/cli/rules/value_switch.rb +58 -0
  17. data/lib/tomo/cli/rules.rb +98 -0
  18. data/lib/tomo/cli/rules_evaluator.rb +71 -0
  19. data/lib/tomo/cli/state.rb +29 -0
  20. data/lib/tomo/cli/usage.rb +42 -0
  21. data/lib/tomo/cli.rb +75 -0
  22. data/lib/tomo/colors.rb +46 -0
  23. data/lib/tomo/commands/completion_script.rb +46 -0
  24. data/lib/tomo/commands/default.rb +72 -0
  25. data/lib/tomo/commands/deploy.rb +67 -0
  26. data/lib/tomo/commands/help.rb +9 -0
  27. data/lib/tomo/commands/init.rb +92 -0
  28. data/lib/tomo/commands/run.rb +76 -0
  29. data/lib/tomo/commands/setup.rb +54 -0
  30. data/lib/tomo/commands/tasks.rb +32 -0
  31. data/lib/tomo/commands/version.rb +23 -0
  32. data/lib/tomo/commands.rb +13 -0
  33. data/lib/tomo/configuration/dsl/batch_block.rb +17 -0
  34. data/lib/tomo/configuration/dsl/config_file.rb +39 -0
  35. data/lib/tomo/configuration/dsl/environment_block.rb +13 -0
  36. data/lib/tomo/configuration/dsl/error_formatter.rb +75 -0
  37. data/lib/tomo/configuration/dsl/hosts_and_settings.rb +24 -0
  38. data/lib/tomo/configuration/dsl/tasks_block.rb +24 -0
  39. data/lib/tomo/configuration/dsl.rb +12 -0
  40. data/lib/tomo/configuration/environment.rb +12 -0
  41. data/lib/tomo/configuration/glob.rb +26 -0
  42. data/lib/tomo/configuration/plugin_file_not_found_error.rb +14 -0
  43. data/lib/tomo/configuration/plugin_resolver.rb +63 -0
  44. data/lib/tomo/configuration/plugins_registry/file_resolver.rb +43 -0
  45. data/lib/tomo/configuration/plugins_registry/gem_resolver.rb +63 -0
  46. data/lib/tomo/configuration/plugins_registry.rb +67 -0
  47. data/lib/tomo/configuration/project_not_found_error.rb +28 -0
  48. data/lib/tomo/configuration/role_based_task_filter.rb +42 -0
  49. data/lib/tomo/configuration/unknown_environment_error.rb +46 -0
  50. data/lib/tomo/configuration/unknown_plugin_error.rb +28 -0
  51. data/lib/tomo/configuration/unspecified_environment_error.rb +28 -0
  52. data/lib/tomo/configuration.rb +124 -0
  53. data/lib/tomo/console/key_reader.rb +51 -0
  54. data/lib/tomo/console/menu.rb +109 -0
  55. data/lib/tomo/console.rb +33 -0
  56. data/lib/tomo/error/suggestions.rb +44 -0
  57. data/lib/tomo/error.rb +22 -0
  58. data/lib/tomo/host.rb +57 -0
  59. data/lib/tomo/logger/tagged_io.rb +38 -0
  60. data/lib/tomo/logger.rb +70 -0
  61. data/lib/tomo/path.rb +19 -0
  62. data/lib/tomo/paths.rb +36 -0
  63. data/lib/tomo/plugin/bundler/helpers.rb +14 -0
  64. data/lib/tomo/plugin/bundler/tasks.rb +57 -0
  65. data/lib/tomo/plugin/bundler.rb +17 -0
  66. data/lib/tomo/plugin/core/helpers.rb +65 -0
  67. data/lib/tomo/plugin/core/tasks.rb +138 -0
  68. data/lib/tomo/plugin/core.rb +31 -0
  69. data/lib/tomo/plugin/env/tasks.rb +113 -0
  70. data/lib/tomo/plugin/env.rb +13 -0
  71. data/lib/tomo/plugin/git/helpers.rb +11 -0
  72. data/lib/tomo/plugin/git/tasks.rb +78 -0
  73. data/lib/tomo/plugin/git.rb +19 -0
  74. data/lib/tomo/plugin/nvm/tasks.rb +61 -0
  75. data/lib/tomo/plugin/nvm.rb +14 -0
  76. data/lib/tomo/plugin/puma/tasks.rb +38 -0
  77. data/lib/tomo/plugin/puma.rb +12 -0
  78. data/lib/tomo/plugin/rails/helpers.rb +20 -0
  79. data/lib/tomo/plugin/rails/tasks.rb +79 -0
  80. data/lib/tomo/plugin/rails.rb +11 -0
  81. data/lib/tomo/plugin/rbenv/tasks.rb +55 -0
  82. data/lib/tomo/plugin/rbenv.rb +12 -0
  83. data/lib/tomo/plugin/testing.rb +16 -0
  84. data/lib/tomo/plugin.rb +4 -0
  85. data/lib/tomo/plugin_dsl.rb +23 -0
  86. data/lib/tomo/remote.rb +55 -0
  87. data/lib/tomo/result.rb +28 -0
  88. data/lib/tomo/runtime/concurrent_ruby_load_error.rb +26 -0
  89. data/lib/tomo/runtime/concurrent_ruby_thread_pool.rb +50 -0
  90. data/lib/tomo/runtime/context.rb +21 -0
  91. data/lib/tomo/runtime/current.rb +41 -0
  92. data/lib/tomo/runtime/execution_plan.rb +107 -0
  93. data/lib/tomo/runtime/host_execution_step.rb +49 -0
  94. data/lib/tomo/runtime/inline_thread_pool.rb +27 -0
  95. data/lib/tomo/runtime/privileged_task.rb +6 -0
  96. data/lib/tomo/runtime/settings_interpolation.rb +55 -0
  97. data/lib/tomo/runtime/settings_required_error.rb +33 -0
  98. data/lib/tomo/runtime/task_aborted_error.rb +15 -0
  99. data/lib/tomo/runtime/task_runner.rb +56 -0
  100. data/lib/tomo/runtime/unknown_task_error.rb +23 -0
  101. data/lib/tomo/runtime.rb +82 -0
  102. data/lib/tomo/script.rb +44 -0
  103. data/lib/tomo/shell_builder.rb +108 -0
  104. data/lib/tomo/ssh/child_process.rb +64 -0
  105. data/lib/tomo/ssh/connection.rb +82 -0
  106. data/lib/tomo/ssh/connection_error.rb +16 -0
  107. data/lib/tomo/ssh/connection_validator.rb +87 -0
  108. data/lib/tomo/ssh/error.rb +11 -0
  109. data/lib/tomo/ssh/executable_error.rb +21 -0
  110. data/lib/tomo/ssh/options.rb +67 -0
  111. data/lib/tomo/ssh/permission_error.rb +18 -0
  112. data/lib/tomo/ssh/script_error.rb +23 -0
  113. data/lib/tomo/ssh/unknown_error.rb +13 -0
  114. data/lib/tomo/ssh/unsupported_version_error.rb +15 -0
  115. data/lib/tomo/ssh.rb +36 -0
  116. data/lib/tomo/task_library.rb +51 -0
  117. data/lib/tomo/templates/config.rb.erb +66 -0
  118. data/lib/tomo/testing/Dockerfile +10 -0
  119. data/lib/tomo/testing/connection.rb +34 -0
  120. data/lib/tomo/testing/docker_image.rb +115 -0
  121. data/lib/tomo/testing/docker_plugin_tester.rb +39 -0
  122. data/lib/tomo/testing/host_extensions.rb +27 -0
  123. data/lib/tomo/testing/local.rb +75 -0
  124. data/lib/tomo/testing/mock_plugin_tester.rb +26 -0
  125. data/lib/tomo/testing/mocked_exec_error.rb +6 -0
  126. data/lib/tomo/testing/plugin_tester.rb +49 -0
  127. data/lib/tomo/testing/remote_extensions.rb +10 -0
  128. data/lib/tomo/testing/ssh_extensions.rb +13 -0
  129. data/lib/tomo/testing/tomo_test_ed25519 +7 -0
  130. data/lib/tomo/testing/tomo_test_ed25519.pub +1 -0
  131. data/lib/tomo/testing/ubuntu_setup.sh +33 -0
  132. data/lib/tomo/testing.rb +39 -0
  133. data/lib/tomo/version.rb +3 -0
  134. data/lib/tomo.rb +45 -0
  135. 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,11 @@
1
+ module Tomo
2
+ module SSH
3
+ class Error < Tomo::Error
4
+ attr_accessor :host
5
+
6
+ def to_console
7
+ [debug_suggestion, red(message.strip)].compact.join("\n\n")
8
+ end
9
+ end
10
+ end
11
+ 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,13 @@
1
+ module Tomo
2
+ module SSH
3
+ class UnknownError < Error
4
+ def to_console
5
+ msg = <<~ERROR
6
+ An unknown error occurred trying to SSH to #{yellow(host)}.
7
+ ERROR
8
+
9
+ [msg, super].join("\n")
10
+ end
11
+ end
12
+ end
13
+ 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,10 @@
1
+ FROM ubuntu:18.04
2
+ WORKDIR /provision
3
+ COPY ./tomo_test_ed25519.pub /root/.ssh/authorized_keys
4
+ COPY ./ubuntu_setup.sh ./
5
+ RUN ./ubuntu_setup.sh
6
+ COPY ./custom_setup.sh ./
7
+ RUN ./custom_setup.sh
8
+ EXPOSE 22
9
+ EXPOSE 3000
10
+ CMD ["/usr/sbin/sshd", "-D"]
@@ -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