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
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