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