tomo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +221 -0
- data/exe/tomo +4 -0
- data/lib/tomo/cli/command.rb +36 -0
- data/lib/tomo/cli/common_options.rb +48 -0
- data/lib/tomo/cli/completions.rb +70 -0
- data/lib/tomo/cli/deploy_options.rb +59 -0
- data/lib/tomo/cli/error.rb +16 -0
- data/lib/tomo/cli/interrupted_error.rb +9 -0
- data/lib/tomo/cli/options.rb +38 -0
- data/lib/tomo/cli/parser.rb +92 -0
- data/lib/tomo/cli/project_options.rb +47 -0
- data/lib/tomo/cli/rules/argument.rb +42 -0
- data/lib/tomo/cli/rules/switch.rb +43 -0
- data/lib/tomo/cli/rules/value_switch.rb +58 -0
- data/lib/tomo/cli/rules.rb +98 -0
- data/lib/tomo/cli/rules_evaluator.rb +71 -0
- data/lib/tomo/cli/state.rb +29 -0
- data/lib/tomo/cli/usage.rb +42 -0
- data/lib/tomo/cli.rb +75 -0
- data/lib/tomo/colors.rb +46 -0
- data/lib/tomo/commands/completion_script.rb +46 -0
- data/lib/tomo/commands/default.rb +72 -0
- data/lib/tomo/commands/deploy.rb +67 -0
- data/lib/tomo/commands/help.rb +9 -0
- data/lib/tomo/commands/init.rb +92 -0
- data/lib/tomo/commands/run.rb +76 -0
- data/lib/tomo/commands/setup.rb +54 -0
- data/lib/tomo/commands/tasks.rb +32 -0
- data/lib/tomo/commands/version.rb +23 -0
- data/lib/tomo/commands.rb +13 -0
- data/lib/tomo/configuration/dsl/batch_block.rb +17 -0
- data/lib/tomo/configuration/dsl/config_file.rb +39 -0
- data/lib/tomo/configuration/dsl/environment_block.rb +13 -0
- data/lib/tomo/configuration/dsl/error_formatter.rb +75 -0
- data/lib/tomo/configuration/dsl/hosts_and_settings.rb +24 -0
- data/lib/tomo/configuration/dsl/tasks_block.rb +24 -0
- data/lib/tomo/configuration/dsl.rb +12 -0
- data/lib/tomo/configuration/environment.rb +12 -0
- data/lib/tomo/configuration/glob.rb +26 -0
- data/lib/tomo/configuration/plugin_file_not_found_error.rb +14 -0
- data/lib/tomo/configuration/plugin_resolver.rb +63 -0
- data/lib/tomo/configuration/plugins_registry/file_resolver.rb +43 -0
- data/lib/tomo/configuration/plugins_registry/gem_resolver.rb +63 -0
- data/lib/tomo/configuration/plugins_registry.rb +67 -0
- data/lib/tomo/configuration/project_not_found_error.rb +28 -0
- data/lib/tomo/configuration/role_based_task_filter.rb +42 -0
- data/lib/tomo/configuration/unknown_environment_error.rb +46 -0
- data/lib/tomo/configuration/unknown_plugin_error.rb +28 -0
- data/lib/tomo/configuration/unspecified_environment_error.rb +28 -0
- data/lib/tomo/configuration.rb +124 -0
- data/lib/tomo/console/key_reader.rb +51 -0
- data/lib/tomo/console/menu.rb +109 -0
- data/lib/tomo/console.rb +33 -0
- data/lib/tomo/error/suggestions.rb +44 -0
- data/lib/tomo/error.rb +22 -0
- data/lib/tomo/host.rb +57 -0
- data/lib/tomo/logger/tagged_io.rb +38 -0
- data/lib/tomo/logger.rb +70 -0
- data/lib/tomo/path.rb +19 -0
- data/lib/tomo/paths.rb +36 -0
- data/lib/tomo/plugin/bundler/helpers.rb +14 -0
- data/lib/tomo/plugin/bundler/tasks.rb +57 -0
- data/lib/tomo/plugin/bundler.rb +17 -0
- data/lib/tomo/plugin/core/helpers.rb +65 -0
- data/lib/tomo/plugin/core/tasks.rb +138 -0
- data/lib/tomo/plugin/core.rb +31 -0
- data/lib/tomo/plugin/env/tasks.rb +113 -0
- data/lib/tomo/plugin/env.rb +13 -0
- data/lib/tomo/plugin/git/helpers.rb +11 -0
- data/lib/tomo/plugin/git/tasks.rb +78 -0
- data/lib/tomo/plugin/git.rb +19 -0
- data/lib/tomo/plugin/nvm/tasks.rb +61 -0
- data/lib/tomo/plugin/nvm.rb +14 -0
- data/lib/tomo/plugin/puma/tasks.rb +38 -0
- data/lib/tomo/plugin/puma.rb +12 -0
- data/lib/tomo/plugin/rails/helpers.rb +20 -0
- data/lib/tomo/plugin/rails/tasks.rb +79 -0
- data/lib/tomo/plugin/rails.rb +11 -0
- data/lib/tomo/plugin/rbenv/tasks.rb +55 -0
- data/lib/tomo/plugin/rbenv.rb +12 -0
- data/lib/tomo/plugin/testing.rb +16 -0
- data/lib/tomo/plugin.rb +4 -0
- data/lib/tomo/plugin_dsl.rb +23 -0
- data/lib/tomo/remote.rb +55 -0
- data/lib/tomo/result.rb +28 -0
- data/lib/tomo/runtime/concurrent_ruby_load_error.rb +26 -0
- data/lib/tomo/runtime/concurrent_ruby_thread_pool.rb +50 -0
- data/lib/tomo/runtime/context.rb +21 -0
- data/lib/tomo/runtime/current.rb +41 -0
- data/lib/tomo/runtime/execution_plan.rb +107 -0
- data/lib/tomo/runtime/host_execution_step.rb +49 -0
- data/lib/tomo/runtime/inline_thread_pool.rb +27 -0
- data/lib/tomo/runtime/privileged_task.rb +6 -0
- data/lib/tomo/runtime/settings_interpolation.rb +55 -0
- data/lib/tomo/runtime/settings_required_error.rb +33 -0
- data/lib/tomo/runtime/task_aborted_error.rb +15 -0
- data/lib/tomo/runtime/task_runner.rb +56 -0
- data/lib/tomo/runtime/unknown_task_error.rb +23 -0
- data/lib/tomo/runtime.rb +82 -0
- data/lib/tomo/script.rb +44 -0
- data/lib/tomo/shell_builder.rb +108 -0
- data/lib/tomo/ssh/child_process.rb +64 -0
- data/lib/tomo/ssh/connection.rb +82 -0
- data/lib/tomo/ssh/connection_error.rb +16 -0
- data/lib/tomo/ssh/connection_validator.rb +87 -0
- data/lib/tomo/ssh/error.rb +11 -0
- data/lib/tomo/ssh/executable_error.rb +21 -0
- data/lib/tomo/ssh/options.rb +67 -0
- data/lib/tomo/ssh/permission_error.rb +18 -0
- data/lib/tomo/ssh/script_error.rb +23 -0
- data/lib/tomo/ssh/unknown_error.rb +13 -0
- data/lib/tomo/ssh/unsupported_version_error.rb +15 -0
- data/lib/tomo/ssh.rb +36 -0
- data/lib/tomo/task_library.rb +51 -0
- data/lib/tomo/templates/config.rb.erb +66 -0
- data/lib/tomo/testing/Dockerfile +10 -0
- data/lib/tomo/testing/connection.rb +34 -0
- data/lib/tomo/testing/docker_image.rb +115 -0
- data/lib/tomo/testing/docker_plugin_tester.rb +39 -0
- data/lib/tomo/testing/host_extensions.rb +27 -0
- data/lib/tomo/testing/local.rb +75 -0
- data/lib/tomo/testing/mock_plugin_tester.rb +26 -0
- data/lib/tomo/testing/mocked_exec_error.rb +6 -0
- data/lib/tomo/testing/plugin_tester.rb +49 -0
- data/lib/tomo/testing/remote_extensions.rb +10 -0
- data/lib/tomo/testing/ssh_extensions.rb +13 -0
- data/lib/tomo/testing/tomo_test_ed25519 +7 -0
- data/lib/tomo/testing/tomo_test_ed25519.pub +1 -0
- data/lib/tomo/testing/ubuntu_setup.sh +33 -0
- data/lib/tomo/testing.rb +39 -0
- data/lib/tomo/version.rb +3 -0
- data/lib/tomo.rb +45 -0
- 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
|
data/lib/tomo/logger.rb
ADDED
@@ -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
|