tomo 0.1.0

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