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,78 @@
1
+ require "shellwords"
2
+ require "time"
3
+
4
+ module Tomo::Plugin::Git
5
+ class Tasks < Tomo::TaskLibrary
6
+ # rubocop:disable Metrics/AbcSize
7
+ def clone
8
+ require_setting :git_url
9
+
10
+ if remote.directory?(paths.git_repo) && !dry_run?
11
+ set_origin_url
12
+ else
13
+ remote.mkdir_p(paths.git_repo.dirname)
14
+ remote.git(
15
+ "clone", "--mirror", settings[:git_url], paths.git_repo
16
+ )
17
+ end
18
+ end
19
+
20
+ def create_release
21
+ configure_git_attributes
22
+ remote.chdir(paths.git_repo) do
23
+ remote.git("remote update --prune")
24
+ remote.mkdir_p(paths.release)
25
+ remote.git(
26
+ "archive #{branch.shellescape} | "\
27
+ "tar -x -f - -C #{paths.release.shellescape}"
28
+ )
29
+ end
30
+ store_release_info
31
+ end
32
+ # rubocop:enable Metrics/AbcSize
33
+
34
+ private
35
+
36
+ def branch
37
+ settings[:git_branch]
38
+ end
39
+
40
+ def set_origin_url
41
+ remote.chdir(paths.git_repo) do
42
+ remote.git("remote", "set-url", "origin", settings[:git_url])
43
+ end
44
+ end
45
+
46
+ def configure_git_attributes
47
+ exclusions = settings[:git_exclusions] || []
48
+ attributes = exclusions.map { |excl| "#{excl} export-ignore" }.join("\n")
49
+
50
+ remote.write(
51
+ text: attributes,
52
+ to: paths.git_repo.join("info/attributes")
53
+ )
54
+ end
55
+
56
+ # rubocop:disable Metrics/AbcSize
57
+ # rubocop:disable Metrics/MethodLength
58
+ def store_release_info
59
+ log = remote.chdir(paths.git_repo) do
60
+ remote.git(
61
+ 'log -n1 --date=iso --pretty=format:"%H/%cd/%ae" '\
62
+ "#{branch.shellescape}",
63
+ silent: true
64
+ ).stdout.strip
65
+ end
66
+
67
+ sha, date, email = log.split("/", 3)
68
+ remote.release[:branch] = branch
69
+ remote.release[:author] = email
70
+ remote.release[:revision] = sha
71
+ remote.release[:revision_date] = date
72
+ remote.release[:deploy_date] = Time.now.to_s
73
+ remote.release[:deploy_user] = ENV["USER"] || ENV["USERNAME"]
74
+ end
75
+ # rubocop:enable Metrics/MethodLength
76
+ # rubocop:enable Metrics/AbcSize
77
+ end
78
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "git/helpers"
2
+ require_relative "git/tasks"
3
+
4
+ module Tomo::Plugin
5
+ module Git
6
+ extend Tomo::PluginDSL
7
+
8
+ helpers Tomo::Plugin::Git::Helpers
9
+ tasks Tomo::Plugin::Git::Tasks
10
+
11
+ # rubocop:disable Metrics/LineLength
12
+ defaults git_branch: "master",
13
+ git_repo_path: "%<deploy_to>/git_repo",
14
+ git_exclusions: [],
15
+ git_env: { GIT_SSH_COMMAND: "ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no" },
16
+ git_url: nil
17
+ # rubocop:enable Metrics/LineLength
18
+ end
19
+ end
@@ -0,0 +1,61 @@
1
+ require "shellwords"
2
+
3
+ module Tomo::Plugin::Nvm
4
+ class Tasks < Tomo::TaskLibrary
5
+ def install
6
+ remote.mkdir_p raw("$HOME/.nvm")
7
+ modify_bashrc
8
+ run_installer
9
+ install_node
10
+ install_yarn
11
+ end
12
+
13
+ private
14
+
15
+ def run_installer
16
+ require_setting :nvm_version
17
+
18
+ nvm_version = settings[:nvm_version]
19
+ install_url = "https://raw.githubusercontent.com/creationix/nvm/"\
20
+ "v#{nvm_version}/install.sh"
21
+ remote.run("curl -o- #{install_url.shellescape} | bash")
22
+ end
23
+
24
+ def modify_bashrc
25
+ existing_rc = remote.capture("cat", paths.bashrc, raise_on_error: false)
26
+ return if existing_rc.include?("nvm.sh")
27
+
28
+ remote.write(text: <<~BASHRC + existing_rc, to: paths.bashrc)
29
+ export NVM_DIR="$HOME/.nvm"
30
+ [ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh"
31
+
32
+ BASHRC
33
+ end
34
+
35
+ def install_node
36
+ require_setting :nvm_node_version
37
+ node_version = settings[:nvm_node_version]
38
+
39
+ unless node_installed?(node_version)
40
+ remote.run "nvm", "install", node_version
41
+ end
42
+ remote.run "nvm", "alias", "default", node_version
43
+ end
44
+
45
+ def install_yarn
46
+ version = settings[:nvm_yarn_version]
47
+ return remote.run "npm i -g yarn@#{version.shellescape}" if version
48
+
49
+ logger.info "No :nvm_yarn_version specified; skipping yarn installation."
50
+ end
51
+
52
+ def node_installed?(version)
53
+ versions = remote.capture("nvm ls", raise_on_error: false)
54
+ if versions.include?("v#{version}")
55
+ logger.info("Node #{version} is already installed.")
56
+ return true
57
+ end
58
+ false
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "nvm/tasks"
2
+
3
+ module Tomo::Plugin
4
+ module Nvm
5
+ extend Tomo::PluginDSL
6
+
7
+ defaults bashrc_path: ".bashrc",
8
+ nvm_version: "0.34.0",
9
+ nvm_node_version: nil,
10
+ nvm_yarn_version: nil
11
+
12
+ tasks Tomo::Plugin::Nvm::Tasks
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ module Tomo::Plugin::Puma
2
+ class Tasks < Tomo::TaskLibrary
3
+ def restart
4
+ return if try_restart
5
+
6
+ remote.chdir(paths.current) do
7
+ logger.info "Puma is not running. Starting it now."
8
+ remote.bundle("exec", "puma", "--daemon", *control_options)
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def try_restart
15
+ ctl_result = remote.chdir(paths.current) do
16
+ ctl_result = remote.bundle(
17
+ "exec", "pumactl", *control_options, "restart",
18
+ raise_on_error: false,
19
+ silent: true
20
+ )
21
+ end
22
+
23
+ return false if dry_run? || ctl_result.failure?
24
+
25
+ logger.info(ctl_result.output)
26
+ true
27
+ end
28
+
29
+ def control_options
30
+ require_settings :puma_control_token, :puma_control_url
31
+
32
+ [
33
+ "--control-url", settings[:puma_control_url],
34
+ "--control-token", settings[:puma_control_token]
35
+ ]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "puma/tasks"
2
+
3
+ module Tomo::Plugin
4
+ module Puma
5
+ extend Tomo::PluginDSL
6
+
7
+ tasks Tomo::Plugin::Puma::Tasks
8
+
9
+ defaults puma_control_token: "tomo",
10
+ puma_control_url: "tcp://127.0.0.1:9293"
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ module Tomo::Plugin::Rails
2
+ module Helpers
3
+ def rails(*args, **opts)
4
+ prepend("exec", "rails") do
5
+ bundle(*args, **opts)
6
+ end
7
+ end
8
+
9
+ def rake(*args, **opts)
10
+ prepend("exec", "rails") do
11
+ bundle(*args, **opts)
12
+ end
13
+ end
14
+
15
+ def rake?(*args, **opts)
16
+ result = rake(*args, **opts.merge(raise_on_error: false))
17
+ result.success?
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,79 @@
1
+ module Tomo::Plugin::Rails
2
+ class Tasks < Tomo::TaskLibrary
3
+ def assets_precompile
4
+ remote.rake("assets:precompile")
5
+ end
6
+
7
+ def console
8
+ remote.rails("console", settings[:run_args], attach: true)
9
+ end
10
+
11
+ def db_migrate
12
+ remote.rake("db:migrate")
13
+ end
14
+
15
+ def db_seed
16
+ remote.rake("db:seed")
17
+ end
18
+
19
+ def db_create
20
+ return remote.rake("db:create") unless database_exists?
21
+
22
+ logger.info "Database already exists; skipping db:create."
23
+ end
24
+
25
+ def db_setup
26
+ return remote.rake("db:setup") unless database_exists?
27
+
28
+ logger.info "Database already exists; skipping db:setup."
29
+ end
30
+
31
+ def db_schema_load
32
+ if !schema_rb_present?
33
+ logger.warn "db/schema.rb is not present; skipping schema:load."
34
+ elsif database_schema_loaded?
35
+ logger.info "Database schema already loaded; skipping db:schema:load."
36
+ else
37
+ remote.rake("db:schema:load")
38
+ end
39
+ end
40
+
41
+ def db_structure_load
42
+ if !structure_sql_present?
43
+ logger.warn "db/structure.sql is not present; "\
44
+ "skipping db:structure:load."
45
+ elsif database_schema_loaded?
46
+ logger.info "Database structure already loaded; "\
47
+ "skipping db:structure:load."
48
+ else
49
+ remote.rake("db:structure:load")
50
+ end
51
+ end
52
+
53
+ def log_tail
54
+ log_path = raw("#{paths.release.to_s.shellescape}/log/${RAILS_ENV}.log")
55
+ remote.run("tail", settings[:run_args], log_path)
56
+ end
57
+
58
+ private
59
+
60
+ def database_exists?
61
+ remote.rake?("db:version", silent: true) && !dry_run?
62
+ end
63
+
64
+ def database_schema_loaded?
65
+ result = remote.rake("db:version", silent: true, raise_on_error: false)
66
+ schema_version = result.output[/version:\s*(\d+)$/i, 1].to_i
67
+
68
+ result.success? && schema_version.positive? && !dry_run?
69
+ end
70
+
71
+ def schema_rb_present?
72
+ remote.file?(paths.release.join("db/schema.rb"))
73
+ end
74
+
75
+ def structure_sql_present?
76
+ remote.file?(paths.release.join("db/structure.sql"))
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,11 @@
1
+ require_relative "rails/helpers"
2
+ require_relative "rails/tasks"
3
+
4
+ module Tomo::Plugin
5
+ module Rails
6
+ extend Tomo::PluginDSL
7
+
8
+ helpers Tomo::Plugin::Rails::Helpers
9
+ tasks Tomo::Plugin::Rails::Tasks
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ require "shellwords"
2
+
3
+ module Tomo::Plugin::Rbenv
4
+ class Tasks < Tomo::TaskLibrary
5
+ def install
6
+ run_installer
7
+ modify_bashrc
8
+ compile_ruby
9
+ end
10
+
11
+ private
12
+
13
+ def run_installer
14
+ install_url = "https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-installer"
15
+ remote.env PATH: raw("$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH") do
16
+ remote.run("curl -fsSL #{install_url.shellescape} | bash")
17
+ end
18
+ end
19
+
20
+ def modify_bashrc
21
+ existing_rc = remote.capture("cat", paths.bashrc, raise_on_error: false)
22
+ return if existing_rc.include?("rbenv init")
23
+
24
+ remote.write(text: <<~BASHRC + existing_rc, to: paths.bashrc)
25
+ if [ -d $HOME/.rbenv ]; then
26
+ export PATH="$HOME/.rbenv/bin:$PATH"
27
+ eval "$(rbenv init -)"
28
+ fi
29
+
30
+ BASHRC
31
+ end
32
+
33
+ def compile_ruby
34
+ require_setting :rbenv_ruby_version
35
+ ruby_version = settings[:rbenv_ruby_version]
36
+
37
+ unless ruby_installed?(ruby_version)
38
+ logger.info(
39
+ "Installing ruby #{ruby_version} -- this may take several minutes"
40
+ )
41
+ remote.run "CFLAGS=-O3 rbenv install #{ruby_version.shellescape}"
42
+ end
43
+ remote.run "rbenv global #{ruby_version.shellescape}"
44
+ end
45
+
46
+ def ruby_installed?(version)
47
+ versions = remote.capture("rbenv versions", raise_on_error: false)
48
+ if versions.include?(version)
49
+ logger.info("Ruby #{version} is already installed.")
50
+ return true
51
+ end
52
+ false
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "rbenv/tasks"
2
+
3
+ module Tomo::Plugin
4
+ module Rbenv
5
+ extend Tomo::PluginDSL
6
+
7
+ defaults bashrc_path: ".bashrc",
8
+ rbenv_ruby_version: nil
9
+
10
+ tasks Tomo::Plugin::Rbenv::Tasks
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ unless defined?(Tomo::Testing)
2
+ raise "The testing plugin cannot be used outside of unit tests"
3
+ end
4
+
5
+ module Tomo::Plugin
6
+ class Testing < Tomo::TaskLibrary
7
+ extend Tomo::PluginDSL
8
+ tasks self
9
+
10
+ def call_helper
11
+ helper, args, kwargs = settings[:run_args]
12
+ value = remote.public_send(helper, *args, **(kwargs || {}))
13
+ remote.host.helper_values << value
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ module Tomo
2
+ module Plugin
3
+ end
4
+ end
@@ -0,0 +1,23 @@
1
+ module Tomo
2
+ module PluginDSL
3
+ def self.extended(mod)
4
+ mod.instance_variable_set(:@helper_modules, [])
5
+ mod.instance_variable_set(:@default_settings, {})
6
+ mod.instance_variable_set(:@tasks_classes, [])
7
+ end
8
+
9
+ attr_reader :helper_modules, :default_settings, :tasks_classes
10
+
11
+ def helpers(mod, *more_mods)
12
+ @helper_modules.push(mod, *more_mods)
13
+ end
14
+
15
+ def defaults(settings)
16
+ @default_settings.merge!(settings)
17
+ end
18
+
19
+ def tasks(tasks_class, *more_tasks_classes)
20
+ @tasks_classes.push(tasks_class, *more_tasks_classes)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ require "forwardable"
2
+
3
+ module Tomo
4
+ class Remote
5
+ extend Forwardable
6
+ def_delegators :ssh, :close, :host
7
+ def_delegators :shell_builder, :chdir, :env, :prepend, :umask
8
+
9
+ attr_reader :release
10
+
11
+ def initialize(ssh, context, helper_modules)
12
+ @ssh = ssh
13
+ @context = context
14
+ @release = {}
15
+ @shell_builder = ShellBuilder.new
16
+ helper_modules.each { |mod| extend(mod) }
17
+ freeze
18
+ end
19
+
20
+ def attach(*command, default_chdir: nil, **command_opts)
21
+ full_command = shell_builder.build(*command, default_chdir: default_chdir)
22
+ ssh.ssh_exec(
23
+ Script.new(full_command, { pty: true }.merge(**command_opts))
24
+ )
25
+ end
26
+
27
+ def run(*command, attach: false, default_chdir: nil, **command_opts)
28
+ attach(*command, default_chdir: default_chdir, **command_opts) if attach
29
+
30
+ full_command = shell_builder.build(*command, default_chdir: default_chdir)
31
+ ssh.ssh_subprocess(Script.new(full_command, **command_opts))
32
+ end
33
+
34
+ private
35
+
36
+ def_delegators :context, :paths, :settings
37
+ attr_reader :context, :ssh, :shell_builder
38
+
39
+ def dry_run?
40
+ Tomo.dry_run?
41
+ end
42
+
43
+ def logger
44
+ Tomo.logger
45
+ end
46
+
47
+ def raw(str)
48
+ ShellBuilder.raw(str)
49
+ end
50
+
51
+ def remote
52
+ self
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ module Tomo
2
+ class Result
3
+ def self.empty_success
4
+ new(stdout: "", stderr: "", exit_status: 0)
5
+ end
6
+
7
+ attr_reader :stdout, :stderr, :exit_status
8
+
9
+ def initialize(stdout:, stderr:, exit_status:)
10
+ @stdout = stdout
11
+ @stderr = stderr
12
+ @exit_status = exit_status
13
+ freeze
14
+ end
15
+
16
+ def success?
17
+ exit_status.zero?
18
+ end
19
+
20
+ def failure?
21
+ !success?
22
+ end
23
+
24
+ def output
25
+ [stdout, stderr].compact.join
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ module Tomo
2
+ class Runtime
3
+ class ConcurrentRubyLoadError < Tomo::Error
4
+ attr_accessor :version
5
+
6
+ def to_console
7
+ <<~ERROR
8
+ Running tasks on multiple hosts requires the #{yellow('concurrent-ruby')} gem.
9
+ To install it, #{install_instructions}
10
+ ERROR
11
+ end
12
+
13
+ private
14
+
15
+ def install_instructions
16
+ if Tomo.bundled?
17
+ gem_entry = %Q(gem "concurrent-ruby", "#{version}")
18
+ "add this entry to your Gemfile:\n\n #{blue(gem_entry)}"
19
+ else
20
+ gem_install = "gem install concurrent-ruby -v '#{version}'"
21
+ "run:\n\n #{blue(gem_install)}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,50 @@
1
+ concurrent_ver = "~> 1.1"
2
+
3
+ begin
4
+ gem "concurrent-ruby", concurrent_ver
5
+ require "concurrent"
6
+ rescue LoadError => e
7
+ Tomo::Runtime::ConcurrentRubyLoadError.raise_with(
8
+ e.message,
9
+ version: concurrent_ver
10
+ )
11
+ end
12
+
13
+ module Tomo
14
+ class Runtime
15
+ class ConcurrentRubyThreadPool
16
+ include ::Concurrent::Promises::FactoryMethods
17
+
18
+ def initialize(size)
19
+ @executor = ::Concurrent::FixedThreadPool.new(size)
20
+ @promises = []
21
+ end
22
+
23
+ def post(*args, &block)
24
+ return if failure?
25
+
26
+ promises << future_on(executor, *args, &block)
27
+ .on_rejection_using(executor) do |reason|
28
+ self.failure = reason
29
+ end
30
+ nil
31
+ end
32
+
33
+ def run_to_completion
34
+ promises_to_wait = promises.dup
35
+ promises.clear
36
+ zip_futures_on(executor, *promises_to_wait).value
37
+ raise failure if failure?
38
+ end
39
+
40
+ def failure?
41
+ !!failure
42
+ end
43
+
44
+ private
45
+
46
+ attr_accessor :failure
47
+ attr_reader :executor, :promises
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ module Tomo
2
+ class Runtime
3
+ class Context
4
+ attr_reader :paths, :settings
5
+
6
+ def initialize(settings)
7
+ @paths = Paths.new(settings)
8
+ @settings = settings.freeze
9
+ freeze
10
+ end
11
+
12
+ def current_remote
13
+ Current.remote
14
+ end
15
+
16
+ def current_task
17
+ Current.task
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ module Tomo
2
+ class Runtime
3
+ module Current
4
+ class << self
5
+ def host
6
+ fiber_locals[:host] || remote&.host
7
+ end
8
+
9
+ def remote
10
+ fiber_locals[:remote]
11
+ end
12
+
13
+ def task
14
+ fiber_locals[:task]
15
+ end
16
+
17
+ def with(new_locals)
18
+ old_locals = slice(*new_locals.keys)
19
+ fiber_locals.merge!(new_locals)
20
+ yield
21
+ ensure
22
+ fiber_locals.merge!(old_locals)
23
+ end
24
+
25
+ def variables
26
+ fiber_locals.dup.freeze
27
+ end
28
+
29
+ private
30
+
31
+ def slice(*keys)
32
+ Hash[keys.map { |key| [key, fiber_locals[key]] }]
33
+ end
34
+
35
+ def fiber_locals
36
+ Thread.current["Tomo::Runtime::Current"] ||= {}
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end