tomo 1.1.1 → 1.4.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +47 -29
  3. data/lib/tomo/cli.rb +1 -3
  4. data/lib/tomo/cli/common_options.rb +4 -12
  5. data/lib/tomo/cli/deploy_options.rb +2 -7
  6. data/lib/tomo/cli/parser.rb +1 -6
  7. data/lib/tomo/cli/project_options.rb +1 -3
  8. data/lib/tomo/cli/rules.rb +4 -22
  9. data/lib/tomo/cli/rules/argument.rb +2 -2
  10. data/lib/tomo/cli/rules/switch.rb +1 -5
  11. data/lib/tomo/cli/rules/value_switch.rb +2 -3
  12. data/lib/tomo/cli/rules_evaluator.rb +3 -13
  13. data/lib/tomo/cli/usage.rb +1 -3
  14. data/lib/tomo/commands/default.rb +1 -3
  15. data/lib/tomo/commands/init.rb +28 -1
  16. data/lib/tomo/commands/run.rb +1 -3
  17. data/lib/tomo/configuration.rb +6 -12
  18. data/lib/tomo/configuration/dsl/error_formatter.rb +1 -1
  19. data/lib/tomo/configuration/dsl/hosts_and_settings.rb +1 -2
  20. data/lib/tomo/configuration/plugins_registry.rb +1 -2
  21. data/lib/tomo/configuration/plugins_registry/gem_resolver.rb +1 -1
  22. data/lib/tomo/configuration/unknown_environment_error.rb +1 -4
  23. data/lib/tomo/console.rb +6 -11
  24. data/lib/tomo/console/menu.rb +1 -2
  25. data/lib/tomo/host.rb +1 -2
  26. data/lib/tomo/paths.rb +1 -1
  27. data/lib/tomo/plugin/bundler/tasks.rb +2 -7
  28. data/lib/tomo/plugin/core/helpers.rb +1 -1
  29. data/lib/tomo/plugin/core/tasks.rb +3 -12
  30. data/lib/tomo/plugin/env/tasks.rb +31 -9
  31. data/lib/tomo/plugin/git.rb +1 -4
  32. data/lib/tomo/plugin/git/tasks.rb +4 -14
  33. data/lib/tomo/plugin/nodenv/tasks.rb +1 -3
  34. data/lib/tomo/plugin/puma.rb +0 -3
  35. data/lib/tomo/plugin/puma/tasks.rb +6 -15
  36. data/lib/tomo/plugin/rails/tasks.rb +6 -4
  37. data/lib/tomo/plugin/rbenv/tasks.rb +16 -2
  38. data/lib/tomo/plugin/testing.rb +1 -3
  39. data/lib/tomo/remote.rb +1 -3
  40. data/lib/tomo/runtime.rb +3 -6
  41. data/lib/tomo/runtime/concurrent_ruby_thread_pool.rb +1 -4
  42. data/lib/tomo/runtime/execution_plan.rb +1 -4
  43. data/lib/tomo/runtime/explanation.rb +1 -7
  44. data/lib/tomo/runtime/settings_required_error.rb +1 -3
  45. data/lib/tomo/runtime/task_runner.rb +1 -5
  46. data/lib/tomo/runtime/unknown_task_error.rb +1 -4
  47. data/lib/tomo/script.rb +1 -5
  48. data/lib/tomo/shell_builder.rb +5 -10
  49. data/lib/tomo/ssh/child_process.rb +2 -7
  50. data/lib/tomo/ssh/connection.rb +5 -18
  51. data/lib/tomo/ssh/connection_validator.rb +5 -8
  52. data/lib/tomo/ssh/executable_error.rb +1 -2
  53. data/lib/tomo/ssh/options.rb +2 -5
  54. data/lib/tomo/task_api.rb +4 -15
  55. data/lib/tomo/templates/config.rb.erb +9 -1
  56. data/lib/tomo/testing/cli_extensions.rb +1 -1
  57. data/lib/tomo/testing/connection.rb +1 -6
  58. data/lib/tomo/testing/docker_image.rb +5 -11
  59. data/lib/tomo/testing/local.rb +1 -3
  60. data/lib/tomo/testing/ubuntu_setup.sh +1 -2
  61. data/lib/tomo/version.rb +1 -1
  62. metadata +8 -135
  63. data/lib/tomo/configuration/plugin_resolver.rb +0 -63
@@ -34,9 +34,7 @@ module Tomo::Plugin::Nodenv
34
34
  require_setting :nodenv_node_version
35
35
  node_version = settings[:nodenv_node_version]
36
36
 
37
- unless node_installed?(node_version)
38
- remote.run "nodenv install #{node_version.shellescape}"
39
- end
37
+ remote.run "nodenv install #{node_version.shellescape}" unless node_installed?(node_version)
40
38
  remote.run "nodenv global #{node_version.shellescape}"
41
39
  end
42
40
 
@@ -5,8 +5,6 @@ module Tomo::Plugin
5
5
  extend Tomo::PluginDSL
6
6
 
7
7
  tasks Tomo::Plugin::Puma::Tasks
8
-
9
- # rubocop:disable Layout/LineLength
10
8
  defaults puma_check_timeout: 15,
11
9
  puma_host: "0.0.0.0",
12
10
  puma_port: "3000",
@@ -16,6 +14,5 @@ module Tomo::Plugin
16
14
  puma_systemd_socket_path: ".config/systemd/user/%{puma_systemd_socket}",
17
15
  puma_systemd_service_template_path: File.expand_path("puma/systemd/service.erb", __dir__),
18
16
  puma_systemd_socket_template_path: File.expand_path("puma/systemd/socket.erb", __dir__)
19
- # rubocop:enable Layout/LineLength
20
17
  end
21
18
  end
@@ -2,8 +2,7 @@ module Tomo::Plugin::Puma
2
2
  class Tasks < Tomo::TaskLibrary
3
3
  SystemdUnit = Struct.new(:name, :template, :path)
4
4
 
5
- # rubocop:disable Metrics/AbcSize
6
- def setup_systemd
5
+ def setup_systemd # rubocop:disable Metrics/AbcSize
7
6
  linger_must_be_enabled!
8
7
 
9
8
  setup_directories
@@ -13,7 +12,6 @@ module Tomo::Plugin::Puma
13
12
  remote.run "systemctl --user daemon-reload"
14
13
  remote.run "systemctl", "--user", "enable", service.name, socket.name
15
14
  end
16
- # rubocop:enable Metrics/AbcSize
17
15
 
18
16
  %i[start stop status].each do |action|
19
17
  define_method(action) do
@@ -37,9 +35,7 @@ module Tomo::Plugin::Puma
37
35
  end
38
36
 
39
37
  def log
40
- remote.attach "journalctl", "-q",
41
- raw("--user-unit=#{service.name.shellescape}"),
42
- *settings[:run_args]
38
+ remote.attach "journalctl", "-q", raw("--user-unit=#{service.name.shellescape}"), *settings[:run_args]
43
39
  end
44
40
 
45
41
  private
@@ -102,22 +98,17 @@ module Tomo::Plugin::Puma
102
98
  end
103
99
 
104
100
  def assert_active!
105
- return true if remote.run? "systemctl", "--user", "is-active",
106
- service.name,
107
- silent: true, raise_on_error: false
101
+ return true if remote.run? "systemctl", "--user", "is-active", service.name, silent: true, raise_on_error: false
108
102
 
109
- remote.run "systemctl", "--user", "status", service.name,
110
- raise_on_error: false
111
- remote.run "journalctl -q -n 50 --user-unit=#{service.name.shellescape}",
112
- raise_on_error: false
103
+ remote.run "systemctl", "--user", "status", service.name, raise_on_error: false
104
+ remote.run "journalctl -q -n 50 --user-unit=#{service.name.shellescape}", raise_on_error: false
113
105
 
114
106
  die "puma failed to start (see previous systemctl and journalctl output)"
115
107
  end
116
108
 
117
109
  def listening?
118
110
  test_url = "http://localhost:#{port}"
119
- remote.run? "curl -sS --connect-timeout 1 --max-time 10 #{test_url}"\
120
- " > /dev/null"
111
+ remote.run? "curl -sS --connect-timeout 1 --max-time 10 #{test_url} > /dev/null"
121
112
  end
122
113
  end
123
114
  end
@@ -8,6 +8,10 @@ module Tomo::Plugin::Rails
8
8
  remote.rails("console", settings[:run_args], attach: true)
9
9
  end
10
10
 
11
+ def db_console
12
+ remote.rails("dbconsole", "--include-password", settings[:run_args], attach: true)
13
+ end
14
+
11
15
  def db_migrate
12
16
  remote.rake("db:migrate")
13
17
  end
@@ -40,11 +44,9 @@ module Tomo::Plugin::Rails
40
44
 
41
45
  def db_structure_load
42
46
  if !structure_sql_present?
43
- logger.warn "db/structure.sql is not present; "\
44
- "skipping db:structure:load."
47
+ logger.warn "db/structure.sql is not present; skipping db:structure:load."
45
48
  elsif database_schema_loaded?
46
- logger.info "Database structure already loaded; "\
47
- "skipping db:structure:load."
49
+ logger.info "Database structure already loaded; skipping db:structure:load."
48
50
  else
49
51
  remote.rake("db:structure:load")
50
52
  end
@@ -31,8 +31,7 @@ module Tomo::Plugin::Rbenv
31
31
  end
32
32
 
33
33
  def compile_ruby
34
- require_setting :rbenv_ruby_version
35
- ruby_version = settings[:rbenv_ruby_version]
34
+ ruby_version = version_setting || extract_ruby_ver_from_version_file
36
35
 
37
36
  unless ruby_installed?(ruby_version)
38
37
  logger.info(
@@ -51,5 +50,20 @@ module Tomo::Plugin::Rbenv
51
50
  end
52
51
  false
53
52
  end
53
+
54
+ def version_setting
55
+ settings[:rbenv_ruby_version]
56
+ end
57
+
58
+ def extract_ruby_ver_from_version_file
59
+ path = paths.release.join(".ruby-version")
60
+ version = remote.capture("cat", path, raise_on_error: false).strip
61
+ return version unless version.empty?
62
+
63
+ die <<~REASON
64
+ Could not guess ruby version from .ruby-version file.
65
+ Use the :rbenv_ruby_version setting to specify the version of ruby to install.
66
+ REASON
67
+ end
54
68
  end
55
69
  end
@@ -1,6 +1,4 @@
1
- unless defined?(Tomo::Testing)
2
- raise "The testing plugin cannot be used outside of unit tests"
3
- end
1
+ raise "The testing plugin cannot be used outside of unit tests" unless defined?(Tomo::Testing)
4
2
 
5
3
  module Tomo::Plugin
6
4
  class Testing < Tomo::TaskLibrary
@@ -21,9 +21,7 @@ module Tomo
21
21
 
22
22
  def attach(*command, default_chdir: nil, **command_opts)
23
23
  full_command = shell_builder.build(*command, default_chdir: default_chdir)
24
- ssh.ssh_exec(
25
- Script.new(full_command, **{ pty: true }.merge(command_opts))
26
- )
24
+ ssh.ssh_exec(Script.new(full_command, **{ pty: true }.merge(command_opts)))
27
25
  end
28
26
 
29
27
  def run(*command, attach: false, default_chdir: nil, **command_opts)
@@ -3,8 +3,7 @@ require "time"
3
3
  module Tomo
4
4
  class Runtime
5
5
  autoload :ConcurrentRubyLoadError, "tomo/runtime/concurrent_ruby_load_error"
6
- autoload :ConcurrentRubyThreadPool,
7
- "tomo/runtime/concurrent_ruby_thread_pool"
6
+ autoload :ConcurrentRubyThreadPool, "tomo/runtime/concurrent_ruby_thread_pool"
8
7
  autoload :Context, "tomo/runtime/context"
9
8
  autoload :Current, "tomo/runtime/current"
10
9
  autoload :ExecutionPlan, "tomo/runtime/execution_plan"
@@ -28,8 +27,7 @@ module Tomo
28
27
 
29
28
  attr_reader :tasks
30
29
 
31
- def initialize(deploy_tasks:, setup_tasks:, hosts:, task_filter:,
32
- settings:, plugins_registry:)
30
+ def initialize(deploy_tasks:, setup_tasks:, hosts:, task_filter:, settings:, plugins_registry:)
33
31
  @deploy_tasks = deploy_tasks.freeze
34
32
  @setup_tasks = setup_tasks.freeze
35
33
  @hosts = hosts.freeze
@@ -68,8 +66,7 @@ module Tomo
68
66
 
69
67
  private
70
68
 
71
- attr_reader :deploy_tasks, :setup_tasks, :hosts, :task_filter, :settings,
72
- :plugins_registry
69
+ attr_reader :deploy_tasks, :setup_tasks, :hosts, :task_filter, :settings, :plugins_registry
73
70
 
74
71
  def new_task_runner(release_type, args)
75
72
  run_settings = { release_path: release_path_for(release_type) }
@@ -4,10 +4,7 @@ begin
4
4
  gem "concurrent-ruby", concurrent_ver
5
5
  require "concurrent"
6
6
  rescue LoadError => e
7
- Tomo::Runtime::ConcurrentRubyLoadError.raise_with(
8
- e.message,
9
- version: concurrent_ver
10
- )
7
+ Tomo::Runtime::ConcurrentRubyLoadError.raise_with(e.message, version: concurrent_ver)
11
8
  end
12
9
 
13
10
  module Tomo
@@ -77,10 +77,7 @@ module Tomo
77
77
  def build_plan(tasks, task_filter)
78
78
  tasks.each_with_object([]) do |task, result|
79
79
  steps = hosts.map do |host|
80
- HostExecutionStep.new(
81
- tasks: task, host: host,
82
- task_filter: task_filter, task_runner: task_runner
83
- )
80
+ HostExecutionStep.new(tasks: task, host: host, task_filter: task_filter, task_runner: task_runner)
84
81
  end
85
82
  steps.reject!(&:empty?)
86
83
  result << steps unless steps.empty?
@@ -7,10 +7,7 @@ module Tomo
7
7
  @concurrency = concurrency
8
8
  end
9
9
 
10
- # rubocop:disable Metrics/MethodLength
11
- # rubocop:disable Metrics/AbcSize
12
- # rubocop:disable Metrics/CyclomaticComplexity
13
- def to_s
10
+ def to_s # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
14
11
  desc = []
15
12
  threads = [applicable_hosts.length, concurrency].min
16
13
  desc << "CONCURRENTLY (#{threads} THREADS):" if threads > 1
@@ -32,9 +29,6 @@ module Tomo
32
29
  end
33
30
  desc.join("\n")
34
31
  end
35
- # rubocop:enable Metrics/MethodLength
36
- # rubocop:enable Metrics/AbcSize
37
- # rubocop:enable Metrics/CyclomaticComplexity
38
32
 
39
33
  private
40
34
 
@@ -21,9 +21,7 @@ module Tomo
21
21
  private
22
22
 
23
23
  def settings_sentence
24
- if settings.length == 1
25
- return "a value for the #{yellow(settings.first.to_s)} setting."
26
- end
24
+ return "a value for the #{yellow(settings.first.to_s)} setting." if settings.length == 1
27
25
 
28
26
  sentence = "values for these settings:\n\n "
29
27
  sentence << settings.map { |s| yellow(s.to_s) }.join("\n ")
@@ -19,11 +19,7 @@ module Tomo
19
19
  def validate_task!(name)
20
20
  return if tasks_by_name.key?(name)
21
21
 
22
- UnknownTaskError.raise_with(
23
- name,
24
- unknown_task: name,
25
- known_tasks: tasks_by_name.keys
26
- )
22
+ UnknownTaskError.raise_with(name, unknown_task: name, known_tasks: tasks_by_name.keys)
27
23
  end
28
24
 
29
25
  def run(task:, remote:)
@@ -17,10 +17,7 @@ module Tomo
17
17
  private
18
18
 
19
19
  def spelling_suggestion
20
- sugg = Error::Suggestions.new(
21
- dictionary: known_tasks,
22
- word: unknown_task
23
- )
20
+ sugg = Error::Suggestions.new(dictionary: known_tasks, word: unknown_task)
24
21
  sugg.to_console if sugg.any?
25
22
  end
26
23
 
@@ -2,11 +2,7 @@ module Tomo
2
2
  class Script
3
3
  attr_reader :script
4
4
 
5
- def initialize(script,
6
- echo: true,
7
- pty: false,
8
- raise_on_error: true,
9
- silent: false)
5
+ def initialize(script, echo: true, pty: false, raise_on_error: true, silent: false)
10
6
  @script = script
11
7
  @echo = echo
12
8
  @pty = pty
@@ -3,8 +3,9 @@ require "shellwords"
3
3
  module Tomo
4
4
  class ShellBuilder
5
5
  def self.raw(string)
6
- string.define_singleton_method(:shellescape) { string }
7
- string
6
+ string.dup.tap do |raw_string|
7
+ raw_string.define_singleton_method(:shellescape) { string }
8
+ end
8
9
  end
9
10
 
10
11
  def initialize
@@ -45,9 +46,7 @@ module Tomo
45
46
  end
46
47
 
47
48
  def build(*command, default_chdir: nil)
48
- if @chdir.empty? && default_chdir
49
- return chdir(default_chdir) { build(*command) }
50
- end
49
+ return chdir(default_chdir) { build(*command) } if @chdir.empty? && default_chdir
51
50
 
52
51
  command_string = command_to_string(*command)
53
52
  modifiers = [cd_chdir, unset_env, export_env, set_umask].compact.flatten
@@ -97,11 +96,7 @@ module Tomo
97
96
  def set_umask
98
97
  return if @umask.nil?
99
98
 
100
- umask_value = if @umask.is_a?(Integer)
101
- @umask.to_s(8).rjust(4, "0")
102
- else
103
- @umask
104
- end
99
+ umask_value = @umask.is_a?(Integer) ? @umask.to_s(8).rjust(4, "0") : @umask
105
100
  "umask #{umask_value.to_s.shellescape}"
106
101
  end
107
102
  end
@@ -30,17 +30,12 @@ module Tomo
30
30
  end
31
31
 
32
32
  def result
33
- Result.new(
34
- exit_status: exit_status,
35
- stdout: stdout_buffer.string,
36
- stderr: stderr_buffer.string
37
- )
33
+ Result.new(exit_status: exit_status, stdout: stdout_buffer.string, stderr: stderr_buffer.string)
38
34
  end
39
35
 
40
36
  private
41
37
 
42
- attr_reader :command, :exit_status, :on_data,
43
- :stdout_buffer, :stderr_buffer
38
+ attr_reader :command, :exit_status, :on_data, :stdout_buffer, :stderr_buffer
44
39
 
45
40
  def start_io_thread(source, buffer)
46
41
  new_thread_inheriting_current_vars do
@@ -6,12 +6,7 @@ module Tomo
6
6
  module SSH
7
7
  class Connection
8
8
  def self.dry_run(host, options)
9
- new(
10
- host,
11
- options,
12
- exec_proc: proc { CLI.exit },
13
- child_proc: proc { Result.empty_success }
14
- )
9
+ new(host, options, exec_proc: proc { CLI.exit }, child_proc: proc { Result.empty_success })
15
10
  end
16
11
 
17
12
  attr_reader :host
@@ -31,16 +26,14 @@ module Tomo
31
26
  end
32
27
 
33
28
  def ssh_subprocess(script, verbose: false)
34
- ssh_args = build_args(script, verbose)
29
+ ssh_args = build_args(script, verbose: verbose)
35
30
  handle_data = ->(data) { logger.script_output(script, data) }
36
31
 
37
32
  logger.script_start(script)
38
33
  result = child_proc.call(*ssh_args, on_data: handle_data)
39
34
  logger.script_end(script, result)
40
35
 
41
- if result.failure? && script.raise_on_error?
42
- raise_run_error(script, ssh_args, result)
43
- end
36
+ raise_run_error(script, ssh_args, result) if result.failure? && script.raise_on_error?
44
37
 
45
38
  result
46
39
  end
@@ -57,7 +50,7 @@ module Tomo
57
50
  Tomo.logger
58
51
  end
59
52
 
60
- def build_args(script, verbose=false)
53
+ def build_args(script, verbose: false)
61
54
  options.build_args(host, script, control_path, verbose)
62
55
  end
63
56
 
@@ -69,13 +62,7 @@ module Tomo
69
62
  end
70
63
 
71
64
  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
- )
65
+ ScriptError.raise_with(result.output, host: host, result: result, script: script, ssh_args: ssh_args)
79
66
  end
80
67
  end
81
68
  end
@@ -15,10 +15,10 @@ module Tomo
15
15
 
16
16
  def assert_valid_executable!
17
17
  result = begin
18
- ChildProcess.execute(executable, "-V")
19
- rescue StandardError => e
20
- handle_bad_executable(e)
21
- end
18
+ ChildProcess.execute(executable, "-V")
19
+ rescue StandardError => e
20
+ handle_bad_executable(e)
21
+ end
22
22
 
23
23
  Tomo.logger.debug(result.output)
24
24
  return if result.success? && supported?(result.output)
@@ -27,10 +27,7 @@ module Tomo
27
27
  end
28
28
 
29
29
  def assert_valid_connection!
30
- script = Script.new(
31
- "echo hi",
32
- silent: !Tomo.debug?, echo: false, raise_on_error: false
33
- )
30
+ script = Script.new("echo hi", silent: !Tomo.debug?, echo: false, raise_on_error: false)
34
31
  res = connection.ssh_subprocess(script, verbose: Tomo.debug?)
35
32
  raise_connection_failure(res) if res.exit_status == 255
36
33
  raise_unknown_error(res) if res.failure? || res.stdout.chomp != "hi"
@@ -7,8 +7,7 @@ module Tomo
7
7
  hint = if executable.to_s.include?("/")
8
8
  "Is the ssh binary properly installed in this location?"
9
9
  else
10
- "Is #{yellow(executable)} installed and in your "\
11
- "#{blue('$PATH')}?"
10
+ "Is #{yellow(executable)} installed and in your #{blue('$PATH')}?"
12
11
  end
13
12
 
14
13
  <<~ERROR
@@ -20,8 +20,7 @@ module Tomo
20
20
  freeze
21
21
  end
22
22
 
23
- # rubocop:disable Metrics/AbcSize
24
- def build_args(host, script, control_path, verbose)
23
+ def build_args(host, script, control_path, verbose) # rubocop:disable Metrics/AbcSize
25
24
  args = [verbose ? "-v" : ["-o", "LogLevel=ERROR"]]
26
25
  args << "-A" if forward_agent
27
26
  args << connect_timeout_option
@@ -34,13 +33,11 @@ module Tomo
34
33
 
35
34
  [executable, args, script.to_s].flatten
36
35
  end
37
- # rubocop:enable Metrics/AbcSize
38
36
 
39
37
  private
40
38
 
41
39
  attr_writer :executable
42
- attr_accessor :connect_timeout, :extra_opts, :forward_agent,
43
- :reuse_connections, :strict_host_key_checking
40
+ attr_accessor :connect_timeout, :extra_opts, :forward_agent, :reuse_connections, :strict_host_key_checking
44
41
 
45
42
  def control_opts(path, verbose)
46
43
  opts = [