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
data/lib/tomo/host.rb ADDED
@@ -0,0 +1,57 @@
1
+ module Tomo
2
+ class Host
3
+ PATTERN = /^(?:(\S+)@)?(\S*?)$/.freeze
4
+ private_constant :PATTERN
5
+
6
+ attr_reader :address, :log_prefix, :user, :port, :roles, :as_privileged
7
+
8
+ def self.parse(host, **kwargs)
9
+ host = host.to_s.strip
10
+ user, address = host.match(PATTERN).captures
11
+ raise ArgumentError, "host cannot be blank" if address.empty?
12
+
13
+ new(**{ user: user, address: address }.merge(kwargs))
14
+ end
15
+
16
+ def initialize(address:, port: nil, log_prefix: nil, roles: nil,
17
+ user: nil, privileged_user: "root")
18
+ @user = user.freeze
19
+ @port = (port || 22).to_i.freeze
20
+ @address = address.freeze
21
+ @log_prefix = log_prefix.freeze
22
+ @roles = Array(roles).map(&:freeze).freeze
23
+ @as_privileged = privileged_copy(privileged_user)
24
+ freeze
25
+ end
26
+
27
+ def with_log_prefix(prefix)
28
+ copy = dup
29
+ copy.instance_variable_set(:@log_prefix, prefix)
30
+ copy.freeze
31
+ end
32
+
33
+ def to_s
34
+ str = user ? "#{user}@#{address}" : address
35
+ str << ":#{port}" unless port == 22
36
+ str
37
+ end
38
+
39
+ def to_ssh_args
40
+ args = [user ? "#{user}@#{address}" : address]
41
+ args.push("-p", port.to_s) unless port == 22
42
+ args
43
+ end
44
+
45
+ private
46
+
47
+ def privileged_copy(priv_user)
48
+ return self if user == priv_user
49
+
50
+ new_prefix = Colors.red([log_prefix, priv_user].compact.join(":"))
51
+ copy = dup
52
+ copy.instance_variable_set(:@user, priv_user)
53
+ copy.instance_variable_set(:@log_prefix, new_prefix)
54
+ copy.freeze
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,38 @@
1
+ module Tomo
2
+ class Logger
3
+ class TaggedIO
4
+ include Colors
5
+
6
+ def initialize(io)
7
+ @io = io
8
+ end
9
+
10
+ def puts(str)
11
+ io.puts(str.to_s.gsub(/^/, prefix))
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :io
17
+
18
+ def prefix
19
+ host = Runtime::Current.host
20
+ return "" if host.nil?
21
+
22
+ tags = []
23
+ tags << red("*") if Tomo.dry_run?
24
+ tags << grayish("[#{host.log_prefix}]") unless host.log_prefix.nil?
25
+ return "" if tags.empty?
26
+
27
+ "#{tags.join(' ')} "
28
+ end
29
+
30
+ def grayish(str)
31
+ parts = str.split(/(\e.*?\e\[0m)/)
32
+ parts.map! do |part|
33
+ part.start_with?("\e") ? part : gray(part)
34
+ end.join
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,70 @@
1
+ require "forwardable"
2
+
3
+ module Tomo
4
+ class Logger
5
+ autoload :TaggedIO, "tomo/logger/tagged_io"
6
+
7
+ extend Forwardable
8
+ include Tomo::Colors
9
+
10
+ def initialize(stdout: $stdout, stderr: $stderr)
11
+ @stdout = TaggedIO.new(stdout)
12
+ @stderr = TaggedIO.new(stderr)
13
+ end
14
+
15
+ def script_start(script)
16
+ return unless script.echo?
17
+
18
+ puts yellow(script.echo_string)
19
+ end
20
+
21
+ def script_output(script, output)
22
+ return if script.silent?
23
+
24
+ puts output
25
+ end
26
+
27
+ def script_end(script, result)
28
+ return unless result.failure?
29
+ return unless script.silent?
30
+ return unless script.raise_on_error?
31
+
32
+ puts result.output
33
+ end
34
+
35
+ def connect(host)
36
+ puts gray("→ Connecting to #{host}")
37
+ end
38
+
39
+ def task_start(task)
40
+ puts blue("• #{task}")
41
+ end
42
+
43
+ def info(message)
44
+ puts message
45
+ end
46
+
47
+ def error(message)
48
+ stderr.puts indent("\n" + red("ERROR: ") + message.strip + "\n\n")
49
+ end
50
+
51
+ def warn(message)
52
+ stderr.puts red("WARNING: ") + message
53
+ end
54
+
55
+ def debug(message)
56
+ return unless Tomo.debug?
57
+
58
+ stderr.puts gray("DEBUG: #{message}")
59
+ end
60
+
61
+ private
62
+
63
+ def_delegators :@stdout, :puts
64
+ attr_reader :stderr
65
+
66
+ def indent(message, prefix=" ")
67
+ message.gsub(/^/, prefix)
68
+ end
69
+ end
70
+ end
data/lib/tomo/path.rb ADDED
@@ -0,0 +1,19 @@
1
+ require "forwardable"
2
+ require "pathname"
3
+
4
+ module Tomo
5
+ class Path < SimpleDelegator
6
+ def initialize(path)
7
+ super(path.to_s)
8
+ freeze
9
+ end
10
+
11
+ def join(*other)
12
+ self.class.new(Pathname.new(self).join(*other))
13
+ end
14
+
15
+ def dirname
16
+ self.class.new(Pathname.new(self).dirname)
17
+ end
18
+ end
19
+ end
data/lib/tomo/paths.rb ADDED
@@ -0,0 +1,36 @@
1
+ module Tomo
2
+ class Paths
3
+ def initialize(settings)
4
+ @settings = settings
5
+ freeze
6
+ end
7
+
8
+ def deploy_to
9
+ path(:deploy_to)
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :settings
15
+
16
+ def method_missing(method, *args)
17
+ return super unless setting?(method)
18
+ raise ArgumentError, "#{method} takes no arguments" unless args.empty?
19
+
20
+ path(:"#{method}_path")
21
+ end
22
+
23
+ def respond_to_missing?(method, include_private=false)
24
+ setting?(method) || super
25
+ end
26
+
27
+ def setting?(name)
28
+ settings.key?(:"#{name}_path")
29
+ end
30
+
31
+ def path(setting)
32
+ path = settings.fetch(setting).to_s.gsub(%r{//+}, "/")
33
+ Path.new(path)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ module Tomo::Plugin::Bundler
2
+ module Helpers
3
+ def bundle(*args, **opts)
4
+ prepend("bundle") do
5
+ run(*args, **opts.merge(default_chdir: paths.release))
6
+ end
7
+ end
8
+
9
+ def bundle?(*args, **opts)
10
+ result = bundle(*args, **opts.merge(raise_on_error: false))
11
+ result.success?
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,57 @@
1
+ module Tomo::Plugin::Bundler
2
+ class Tasks < Tomo::TaskLibrary
3
+ def install
4
+ return if remote.bundle?("check", *check_options) && !dry_run?
5
+
6
+ remote.bundle("install", *install_options)
7
+ end
8
+
9
+ def clean
10
+ remote.bundle("clean")
11
+ end
12
+
13
+ def upgrade_bundler
14
+ needed_bundler_ver = extract_bundler_ver_from_lockfile
15
+ return if needed_bundler_ver.nil?
16
+
17
+ remote.run(
18
+ "gem", "install", "bundler",
19
+ "--conservative", "--no-document",
20
+ "-v", needed_bundler_ver
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ def check_options
27
+ gemfile = settings[:bundler_gemfile]
28
+ path = paths.bundler
29
+
30
+ options = []
31
+ options.push("--gemfile", gemfile) if gemfile
32
+ options.push("--path", path) if path
33
+ options
34
+ end
35
+
36
+ def install_options
37
+ jobs = settings[:bundler_jobs]
38
+ without = settings[:bundler_without]
39
+ flags = settings[:bundler_install_flags]
40
+
41
+ options = check_options.dup
42
+ options.push("--jobs", jobs) if jobs
43
+ options.push("--without", without) if without
44
+ options.push(flags) if flags
45
+
46
+ options.flatten
47
+ end
48
+
49
+ def extract_bundler_ver_from_lockfile
50
+ lockfile_tail = remote.capture(
51
+ "tail", "-n", "10", paths.release.join("Gemfile.lock"),
52
+ raise_on_error: false
53
+ )
54
+ lockfile_tail[/BUNDLED WITH\n (\S+)$/, 1]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ require_relative "bundler/helpers"
2
+ require_relative "bundler/tasks"
3
+
4
+ module Tomo::Plugin
5
+ module Bundler
6
+ extend Tomo::PluginDSL
7
+
8
+ tasks Tomo::Plugin::Bundler::Tasks
9
+ helpers Tomo::Plugin::Bundler::Helpers
10
+
11
+ defaults bundler_install_flags: ["--deployment"],
12
+ bundler_gemfile: nil,
13
+ bundler_jobs: "4",
14
+ bundler_path: "%<shared_path>/bundle",
15
+ bundler_without: %w[development test]
16
+ end
17
+ end
@@ -0,0 +1,65 @@
1
+ require "shellwords"
2
+
3
+ module Tomo::Plugin::Core
4
+ module Helpers
5
+ def capture(*command, **run_opts)
6
+ result = run(*command, **{ silent: true }.merge(run_opts))
7
+ result.stdout
8
+ end
9
+
10
+ def run?(*command, **run_opts)
11
+ result = run(*command, **run_opts.merge(raise_on_error: false))
12
+ result.success?
13
+ end
14
+
15
+ def write(text:, to:, append: false, **run_opts)
16
+ message = "Writing #{text.bytesize} bytes to #{to}"
17
+ run(
18
+ "echo -n #{text.shellescape} #{append ? '>>' : '>'} #{to.shellescape}",
19
+ **{ echo: message }.merge(run_opts)
20
+ )
21
+ end
22
+
23
+ def ln_sf(target, link, **run_opts)
24
+ run("ln", "-sf", target, link, **run_opts)
25
+ end
26
+
27
+ def ln_sfn(target, link, **run_opts)
28
+ run("ln", "-sfn", target, link, **run_opts)
29
+ end
30
+
31
+ def mkdir_p(*directories, **run_opts)
32
+ run("mkdir", "-p", *directories, **run_opts)
33
+ end
34
+
35
+ def rm_rf(*paths, **run_opts)
36
+ run("rm", "-rf", *paths, **run_opts)
37
+ end
38
+
39
+ def list_files(directory=nil, **run_opts)
40
+ capture("ls", "-A1", directory, **run_opts).strip.split("\n")
41
+ end
42
+
43
+ def command_available?(command_name, **run_opts)
44
+ run?("which", command_name, **{ silent: true }.merge(run_opts))
45
+ end
46
+
47
+ def file?(file, **run_opts)
48
+ flag?("-f", file, **run_opts)
49
+ end
50
+
51
+ def executable?(file, **run_opts)
52
+ flag?("-x", file, **run_opts)
53
+ end
54
+
55
+ def directory?(directory, **run_opts)
56
+ flag?("-d", directory, **run_opts)
57
+ end
58
+
59
+ private
60
+
61
+ def flag?(flag, path, **run_opts)
62
+ run?("[ #{flag} #{path.to_s.shellescape} ]", **run_opts)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,138 @@
1
+ require "json"
2
+ require "securerandom"
3
+
4
+ module Tomo::Plugin::Core
5
+ class Tasks < Tomo::TaskLibrary
6
+ RELEASE_REGEXP = /\d{14}/.freeze
7
+ private_constant :RELEASE_REGEXP
8
+
9
+ # rubocop:disable Metrics/AbcSize
10
+ def setup_directories
11
+ dirs = [
12
+ paths.deploy_to,
13
+ paths.current.dirname,
14
+ paths.releases,
15
+ paths.revision_log.dirname,
16
+ paths.shared
17
+ ].map(&:to_s).uniq
18
+
19
+ remote.mkdir_p(*dirs)
20
+ end
21
+ # rubocop:enable Metrics/AbcSize
22
+
23
+ def symlink_shared
24
+ return if linked_dirs.empty? && linked_files.empty?
25
+
26
+ remote.mkdir_p(*shared_directories, *link_dirnames)
27
+ symlink_shared_directories
28
+ symlink_shared_files
29
+ end
30
+
31
+ def symlink_current
32
+ return if paths.release == paths.current
33
+
34
+ tmp_link = "#{paths.current}-#{SecureRandom.hex(8)}"
35
+ remote.ln_sf paths.release, tmp_link
36
+ remote.run "mv", "-fT", tmp_link, paths.current
37
+ end
38
+
39
+ # rubocop:disable Metrics/AbcSize
40
+ def clean_releases
41
+ desired_count = settings[:keep_releases].to_i
42
+ return if desired_count < 1
43
+
44
+ current = read_current_release
45
+
46
+ remote.chdir(paths.releases) do
47
+ releases = remote.list_files.grep(/^#{RELEASE_REGEXP}$/).sort
48
+ desired_count -= 1 if releases.delete(current)
49
+ return if releases.length <= desired_count
50
+
51
+ remote.rm_rf(*releases.take(releases.length - desired_count))
52
+ end
53
+ end
54
+ # rubocop:enable Metrics/AbcSize
55
+
56
+ def write_release_json
57
+ json = JSON.pretty_generate(remote.release)
58
+ remote.write(text: json, to: paths.release_json)
59
+ end
60
+
61
+ # rubocop:disable Metrics/AbcSize
62
+ def log_revision
63
+ message = remote.release[:deploy_date].to_s
64
+ message << " - #{remote.release[:revision] || '<unknown>'}"
65
+ message << " (#{remote.release[:branch] || '<unknown>'})"
66
+ message << " deployed by #{remote.release[:deploy_user] || '<unknown>'}"
67
+ message << "\n"
68
+
69
+ remote.write(text: message, to: paths.revision_log, append: true)
70
+ end
71
+ # rubocop:enable Metrics/AbcSize
72
+
73
+ private
74
+
75
+ def linked_dirs
76
+ settings[:linked_dirs] || []
77
+ end
78
+
79
+ def linked_files
80
+ settings[:linked_files] || []
81
+ end
82
+
83
+ # rubocop:disable Metrics/AbcSize
84
+ def shared_directories
85
+ result = linked_dirs.map { |name| paths.shared.join(name) }
86
+ linked_files.each do |name|
87
+ result << paths.shared.join(name).dirname
88
+ end
89
+ result.map(&:to_s).uniq - [paths.shared.to_s]
90
+ end
91
+ # rubocop:enable Metrics/AbcSize
92
+
93
+ def symlink_shared_files
94
+ return if linked_files.empty?
95
+
96
+ linked_files.each do |file|
97
+ remote.ln_sfn paths.shared.join(file), paths.release.join(file)
98
+ end
99
+ end
100
+
101
+ def symlink_shared_directories
102
+ return if linked_dirs.empty?
103
+
104
+ remove_existing_link_targets
105
+ linked_dirs.each do |dir|
106
+ remote.ln_sf paths.shared.join(dir), paths.release.join(dir)
107
+ end
108
+ end
109
+
110
+ def link_dirnames
111
+ parents = (linked_dirs + linked_files).map do |target|
112
+ paths.release.join(target).dirname
113
+ end
114
+
115
+ parents.map(&:to_s).uniq - [paths.release.to_s]
116
+ end
117
+
118
+ def remove_existing_link_targets
119
+ return if linked_dirs.empty?
120
+
121
+ remote.chdir(paths.release) do
122
+ remote.rm_rf(*linked_dirs)
123
+ end
124
+ end
125
+
126
+ def read_current_release
127
+ result = remote.run(
128
+ "readlink",
129
+ paths.current,
130
+ raise_on_error: false,
131
+ silent: true
132
+ )
133
+ return nil if result.failure?
134
+
135
+ result.stdout.strip[%r{/(#{RELEASE_REGEXP})$}, 1]
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "core/helpers"
2
+ require_relative "core/tasks"
3
+
4
+ module Tomo::Plugin
5
+ module Core
6
+ extend Tomo::PluginDSL
7
+
8
+ helpers Tomo::Plugin::Core::Helpers
9
+ tasks Tomo::Plugin::Core::Tasks
10
+
11
+ defaults application: "default",
12
+ concurrency: 10,
13
+ current_path: "%<deploy_to>/current",
14
+ deploy_to: "/var/www/%<application>",
15
+ keep_releases: 10,
16
+ linked_dirs: [],
17
+ linked_files: [],
18
+ release_json_path: "%<release_path>/.tomo_release.json",
19
+ releases_path: "%<deploy_to>/releases",
20
+ revision_log_path: "%<deploy_to>/revisions.log",
21
+ shared_path: "%<deploy_to>/shared",
22
+ tmp_path: "/tmp/tomo",
23
+ run_args: [],
24
+ ssh_connect_timeout: 5,
25
+ ssh_executable: "ssh",
26
+ ssh_extra_opts: %w[-o PasswordAuthentication=no],
27
+ ssh_forward_agent: true,
28
+ ssh_reuse_connections: true,
29
+ ssh_strict_host_key_checking: "accept-new"
30
+ end
31
+ end
@@ -0,0 +1,113 @@
1
+ require "monitor"
2
+
3
+ module Tomo::Plugin::Env
4
+ class Tasks < Tomo::TaskLibrary
5
+ include MonitorMixin
6
+
7
+ def show
8
+ env = read_existing
9
+ logger.info env.gsub(/^export /, "").strip
10
+ end
11
+
12
+ def setup
13
+ update
14
+ modify_bashrc
15
+ end
16
+
17
+ def update
18
+ return if settings[:env_vars].empty?
19
+
20
+ modify_env_file do |env|
21
+ settings[:env_vars].each do |name, value|
22
+ next if value == :prompt && contains_entry?(env, name)
23
+
24
+ value = prompt_for(name) if value == :prompt
25
+ replace_entry(env, name, value)
26
+ end
27
+ end
28
+ end
29
+
30
+ def set
31
+ return if settings[:run_args].empty?
32
+
33
+ modify_env_file do |env|
34
+ settings[:run_args].each do |arg|
35
+ name, value = arg.split("=", 2)
36
+ value ||= prompt_for(name)
37
+ replace_entry(env, name, value)
38
+ end
39
+ end
40
+ end
41
+
42
+ def unset
43
+ return if settings[:run_args].empty?
44
+
45
+ modify_env_file do |env|
46
+ settings[:run_args].each do |name|
47
+ remove_entry(env, name)
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def modify_env_file
55
+ env = read_existing
56
+ original = env.dup
57
+ yield(env)
58
+ return if env == original
59
+
60
+ remote.mkdir_p(paths.env.dirname) if original.empty?
61
+ remote.write(text: env, to: paths.env)
62
+ end
63
+
64
+ def read_existing
65
+ remote.capture(
66
+ "cat", paths.env,
67
+ raise_on_error: false, echo: false, silent: true
68
+ )
69
+ end
70
+
71
+ def replace_entry(text, name, value)
72
+ remove_entry(text, name)
73
+ prepend_entry(text, name, value)
74
+ end
75
+
76
+ def remove_entry(text, name)
77
+ text.gsub!(/^export #{Regexp.quote(name.to_s.shellescape)}=.*\n/, "")
78
+ end
79
+
80
+ def prepend_entry(text, name, value)
81
+ text.prepend("\n") unless text.start_with?("\n")
82
+ text.prepend("export #{name.to_s.shellescape}=#{value.shellescape}")
83
+ end
84
+
85
+ def contains_entry?(text, name)
86
+ return true if dry_run?
87
+
88
+ text.match?(/^export #{Regexp.quote(name.to_s.shellescape)}=/)
89
+ end
90
+
91
+ def prompt_for(name)
92
+ synchronize do
93
+ @answers ||= {}
94
+ next @answers[name] if @answers.key?(name)
95
+
96
+ @answers[name] = Tomo::Console.prompt("#{name}? ")
97
+ end
98
+ end
99
+
100
+ def modify_bashrc
101
+ env_path = paths.env.shellescape
102
+ existing_rc = remote.capture("cat", paths.bashrc, raise_on_error: false)
103
+ return if existing_rc.include?(". #{env_path}")
104
+
105
+ remote.write(text: <<~BASHRC + existing_rc, to: paths.bashrc)
106
+ if [ -f #{env_path} ]; then
107
+ . #{env_path}
108
+ fi
109
+
110
+ BASHRC
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "env/tasks"
2
+
3
+ module Tomo::Plugin
4
+ module Env
5
+ extend Tomo::PluginDSL
6
+
7
+ tasks Tomo::Plugin::Env::Tasks
8
+
9
+ defaults bashrc_path: ".bashrc",
10
+ env_path: "%<deploy_to>/envrc",
11
+ env_vars: {}
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Tomo::Plugin::Git
2
+ module Helpers
3
+ def git(*args, **opts)
4
+ env(settings[:git_env]) do
5
+ prepend("git") do
6
+ run(*args, **opts)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end