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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
|
|
3
|
+
module Tomo
|
|
4
|
+
module Commands
|
|
5
|
+
class Init < CLI::Command
|
|
6
|
+
include CLI::CommonOptions
|
|
7
|
+
|
|
8
|
+
arg "[APP]"
|
|
9
|
+
|
|
10
|
+
def summary
|
|
11
|
+
"Start a new tomo project with a sample config"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def banner
|
|
15
|
+
<<~BANNER
|
|
16
|
+
Usage: #{green('tomo init')} #{yellow('[APP]')}
|
|
17
|
+
|
|
18
|
+
Set up a new tomo project named #{yellow('APP')}. If #{yellow('APP')} is not specified, the
|
|
19
|
+
name of the current directory will be used.
|
|
20
|
+
|
|
21
|
+
This command creates a #{DEFAULT_CONFIG_PATH} file relative the current
|
|
22
|
+
directory containing some example configuration.
|
|
23
|
+
BANNER
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(*args, _options)
|
|
27
|
+
assert_can_create_tomo_directory!
|
|
28
|
+
assert_no_tomo_project!
|
|
29
|
+
|
|
30
|
+
app = args.first || current_dir_name || "default"
|
|
31
|
+
app = app.gsub(/([^\w\-]|_)+/, "_").downcase
|
|
32
|
+
FileUtils.mkdir_p(".tomo/plugins")
|
|
33
|
+
|
|
34
|
+
# TODO: use a template for this file
|
|
35
|
+
FileUtils.touch(".tomo/plugins/#{app}.rb")
|
|
36
|
+
|
|
37
|
+
IO.write(DEFAULT_CONFIG_PATH, config_rb_template(app))
|
|
38
|
+
|
|
39
|
+
logger.info(green("✔ Created #{DEFAULT_CONFIG_PATH}"))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def assert_can_create_tomo_directory!
|
|
45
|
+
return if Dir.exist?(".tomo")
|
|
46
|
+
return unless File.exist?(".tomo")
|
|
47
|
+
|
|
48
|
+
logger.error("Can't create .tomo directory; a file already exists")
|
|
49
|
+
exit(1)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def assert_no_tomo_project!
|
|
53
|
+
return unless File.exist?(DEFAULT_CONFIG_PATH)
|
|
54
|
+
|
|
55
|
+
logger.error("A #{DEFAULT_CONFIG_PATH} file already exists")
|
|
56
|
+
exit(1)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def current_dir_name
|
|
60
|
+
File.basename(File.expand_path("."))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def git_origin_url
|
|
64
|
+
return unless File.file?(".git/config")
|
|
65
|
+
return unless `git remote -v` =~ /^origin/
|
|
66
|
+
|
|
67
|
+
url = `git remote get-url origin`.chomp
|
|
68
|
+
url.empty? ? nil : url
|
|
69
|
+
rescue SystemCallError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def node_version
|
|
74
|
+
`node --version`.chomp.sub(/^v/i, "")
|
|
75
|
+
rescue SystemCallError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def yarn_version
|
|
80
|
+
`yarn --version`.chomp
|
|
81
|
+
rescue SystemCallError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def config_rb_template(app)
|
|
86
|
+
path = File.expand_path("../templates/config.rb.erb", __dir__)
|
|
87
|
+
template = IO.read(path)
|
|
88
|
+
ERB.new(template).result(binding)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module Commands
|
|
3
|
+
class Run < CLI::Command
|
|
4
|
+
include CLI::DeployOptions
|
|
5
|
+
|
|
6
|
+
option :privileged,
|
|
7
|
+
"--[no-]privileged",
|
|
8
|
+
"Run the task using a privileged user (e.g. root)"
|
|
9
|
+
|
|
10
|
+
include CLI::ProjectOptions
|
|
11
|
+
include CLI::CommonOptions
|
|
12
|
+
|
|
13
|
+
arg "TASK", values: :task_names
|
|
14
|
+
arg "[ARGS...]"
|
|
15
|
+
|
|
16
|
+
def summary
|
|
17
|
+
"Run a specific remote task from the current project"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def banner
|
|
21
|
+
<<~BANNER
|
|
22
|
+
Usage: #{green('tomo run')} #{yellow('[--dry-run] [options] [--] TASK [ARGS...]')}
|
|
23
|
+
|
|
24
|
+
Remotely run one specified #{yellow('TASK')}, optionally passing #{yellow('ARGS')} to that task.
|
|
25
|
+
For example, if this project uses the "rails" plugin, you could run:
|
|
26
|
+
|
|
27
|
+
#{blue('tomo run -- rails:console --sandbox')}
|
|
28
|
+
|
|
29
|
+
This will run the #{blue('rails:console')} task on the host specified in
|
|
30
|
+
#{DEFAULT_CONFIG_PATH}, and will pass the #{blue('--sandbox')} argument to that task.
|
|
31
|
+
The #{blue('--')} is used to separate tomo options from options that are passed
|
|
32
|
+
to the task. If a task does not accept options, the #{blue('--')} can be omitted,
|
|
33
|
+
like this:
|
|
34
|
+
|
|
35
|
+
#{blue('tomo run core:clean_releases')}
|
|
36
|
+
|
|
37
|
+
You can run any task defined by plugins loaded in #{DEFAULT_CONFIG_PATH}.
|
|
38
|
+
To see a list of available tasks, run #{blue('tomo tasks')}.
|
|
39
|
+
|
|
40
|
+
Tomo will auto-complete this command’s options, including the #{yellow('TASK')} name,
|
|
41
|
+
if you are using bash and have tomo’s completion script installed. For
|
|
42
|
+
installation instructions, run #{blue('tomo completion-script')}.
|
|
43
|
+
|
|
44
|
+
For more documentation and examples, visit:
|
|
45
|
+
|
|
46
|
+
#{blue('https://tomo-deploy.com/commands/run')}
|
|
47
|
+
BANNER
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def call(task, *args, options)
|
|
51
|
+
logger.info "tomo run v#{Tomo::VERSION}"
|
|
52
|
+
|
|
53
|
+
runtime = configure_runtime(options)
|
|
54
|
+
plan = runtime.run!(task, *args, privileged: options[:privileged])
|
|
55
|
+
log_completion(task, plan)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def log_completion(task, plan)
|
|
61
|
+
target = "#{task} on #{plan.applicable_hosts_sentence}"
|
|
62
|
+
|
|
63
|
+
if dry_run?
|
|
64
|
+
logger.info(green("* Simulated #{target} (dry run)"))
|
|
65
|
+
else
|
|
66
|
+
logger.info(green("✔ Ran #{target}"))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def task_names(*, options)
|
|
71
|
+
runtime = configure_runtime(options, strict: false)
|
|
72
|
+
runtime.tasks
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module Commands
|
|
3
|
+
class Setup < CLI::Command
|
|
4
|
+
include CLI::DeployOptions
|
|
5
|
+
include CLI::ProjectOptions
|
|
6
|
+
include CLI::CommonOptions
|
|
7
|
+
|
|
8
|
+
def summary
|
|
9
|
+
"Prepare the current project for its first deploy"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def banner
|
|
13
|
+
<<~BANNER
|
|
14
|
+
Usage: #{green('tomo setup')} #{yellow('[--dry-run] [options]')}
|
|
15
|
+
|
|
16
|
+
Prepare the remote host for its first deploy by sequentially running the
|
|
17
|
+
"setup" list of tasks specified in #{DEFAULT_CONFIG_PATH}. These tasks typically
|
|
18
|
+
create directories, initialize data stores, install prerequisite tools,
|
|
19
|
+
and perform other one-time actions that are necessary before a deploy can
|
|
20
|
+
take place.
|
|
21
|
+
|
|
22
|
+
Use the #{blue('--dry-run')} option to quickly simulate the setup without actually
|
|
23
|
+
connecting to the host.
|
|
24
|
+
|
|
25
|
+
More documentation and examples can be found here:
|
|
26
|
+
|
|
27
|
+
#{blue('https://tomo-deploy.com/commands/setup')}
|
|
28
|
+
BANNER
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call(options)
|
|
32
|
+
logger.info "tomo setup v#{Tomo::VERSION}"
|
|
33
|
+
|
|
34
|
+
runtime = configure_runtime(options)
|
|
35
|
+
plan = runtime.setup!
|
|
36
|
+
|
|
37
|
+
log_completion(plan)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def log_completion(plan)
|
|
43
|
+
app = plan.settings[:application]
|
|
44
|
+
target = "#{app} on #{plan.applicable_hosts_sentence}"
|
|
45
|
+
|
|
46
|
+
if dry_run?
|
|
47
|
+
logger.info(green("* Simulated setup of #{target} (dry run)"))
|
|
48
|
+
else
|
|
49
|
+
logger.info(green("✔ Performed setup of #{target}"))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module Commands
|
|
3
|
+
class Tasks < CLI::Command
|
|
4
|
+
include CLI::ProjectOptions
|
|
5
|
+
include CLI::CommonOptions
|
|
6
|
+
|
|
7
|
+
def summary
|
|
8
|
+
"List all tasks that can be used with the #{yellow('run')} command"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def banner
|
|
12
|
+
<<~BANNER
|
|
13
|
+
Usage: #{green('tomo tasks')}
|
|
14
|
+
|
|
15
|
+
List all tomo tasks (i.e. those that can be used with #{blue('tomo run')}).
|
|
16
|
+
|
|
17
|
+
Available tasks are those defined by plugins loaded in #{DEFAULT_CONFIG_PATH}.
|
|
18
|
+
BANNER
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(options)
|
|
22
|
+
runtime = configure_runtime(options, strict: false)
|
|
23
|
+
tasks = runtime.tasks
|
|
24
|
+
|
|
25
|
+
groups = tasks.group_by { |task| task[/^([^:]+):/, 1].to_s }
|
|
26
|
+
groups.keys.sort.each do |group|
|
|
27
|
+
puts groups[group].sort.join("\n")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module Commands
|
|
3
|
+
class Version < CLI::Command
|
|
4
|
+
include CLI::CommonOptions
|
|
5
|
+
|
|
6
|
+
def summary
|
|
7
|
+
"Display tomo’s version"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def banner
|
|
11
|
+
<<~BANNER
|
|
12
|
+
Usage: #{green('tomo version')}
|
|
13
|
+
|
|
14
|
+
Display tomo’s version information.
|
|
15
|
+
BANNER
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(_options)
|
|
19
|
+
puts "tomo/#{Tomo::VERSION} #{RUBY_DESCRIPTION}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
module Commands
|
|
3
|
+
autoload :CompletionScript, "tomo/commands/completion_script"
|
|
4
|
+
autoload :Default, "tomo/commands/default"
|
|
5
|
+
autoload :Deploy, "tomo/commands/deploy"
|
|
6
|
+
autoload :Help, "tomo/commands/help"
|
|
7
|
+
autoload :Init, "tomo/commands/init"
|
|
8
|
+
autoload :Run, "tomo/commands/run"
|
|
9
|
+
autoload :Setup, "tomo/commands/setup"
|
|
10
|
+
autoload :Tasks, "tomo/commands/tasks"
|
|
11
|
+
autoload :Version, "tomo/commands/version"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
module DSL
|
|
4
|
+
class BatchBlock
|
|
5
|
+
def initialize(batch)
|
|
6
|
+
@batch = batch
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def run(task, privileged: false)
|
|
10
|
+
task.extend(Runtime::PrivilegedTask) if privileged
|
|
11
|
+
@batch << task
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
module DSL
|
|
4
|
+
class ConfigFile
|
|
5
|
+
include HostsAndSettings
|
|
6
|
+
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def plugin(name)
|
|
12
|
+
@config.plugins << name.to_s
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def role(name, runs:)
|
|
17
|
+
@config.task_filter.add_role(name, runs)
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def environment(name, &block)
|
|
22
|
+
environment = @config.environments[name.to_s] ||= Environment.new
|
|
23
|
+
EnvironmentBlock.new(environment).instance_eval(&block)
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def deploy(&block)
|
|
28
|
+
TasksBlock.new(@config.deploy_tasks).instance_eval(&block)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def setup(&block)
|
|
33
|
+
TasksBlock.new(@config.setup_tasks).instance_eval(&block)
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
module DSL
|
|
4
|
+
module ErrorFormatter
|
|
5
|
+
def self.decorate(error, path, lines)
|
|
6
|
+
if error.backtrace[0..1].grep(/^#{Regexp.quote(path)}:/).empty?
|
|
7
|
+
return error
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
error.extend(self)
|
|
11
|
+
error.dsl_lines = lines || []
|
|
12
|
+
error.dsl_path = path
|
|
13
|
+
error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
include Colors
|
|
17
|
+
|
|
18
|
+
attr_accessor :dsl_lines, :dsl_path
|
|
19
|
+
|
|
20
|
+
def to_console
|
|
21
|
+
<<~ERROR
|
|
22
|
+
Configuration syntax error in #{yellow(dsl_path)} at line #{yellow(error_line_no)}.
|
|
23
|
+
|
|
24
|
+
#{highlighted_lines}
|
|
25
|
+
#{Colors.red([self.class, message].join(': '))}
|
|
26
|
+
|
|
27
|
+
Visit #{Colors.blue('https://tomo-deploy.com/configuration')} for syntax reference.
|
|
28
|
+
#{trace_hint}
|
|
29
|
+
ERROR
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def trace_hint
|
|
35
|
+
return "" if CLI.show_backtrace
|
|
36
|
+
|
|
37
|
+
<<~HINT
|
|
38
|
+
You can run this command again with #{Colors.blue('--trace')} for a full backtrace.
|
|
39
|
+
HINT
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def error_line_no
|
|
43
|
+
@_error_line_no ||= begin
|
|
44
|
+
pattern = /^#{Regexp.quote(dsl_path)}:(\d+):/
|
|
45
|
+
backtrace.each do |entry|
|
|
46
|
+
match = pattern.match(entry)
|
|
47
|
+
break match[1].to_i if match
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# rubocop:disable Metrics/AbcSize
|
|
53
|
+
# rubocop:disable Metrics/MethodLength
|
|
54
|
+
def highlighted_lines
|
|
55
|
+
first = [1, error_line_no - 1].max
|
|
56
|
+
last = [dsl_lines.length, error_line_no + 1].min
|
|
57
|
+
width = last.to_s.length
|
|
58
|
+
|
|
59
|
+
(first..last).each_with_object("") do |line_no, result|
|
|
60
|
+
line = dsl_lines[line_no - 1]
|
|
61
|
+
line_no_prefix = line_no.to_s.rjust(width)
|
|
62
|
+
|
|
63
|
+
result << if line_no == error_line_no
|
|
64
|
+
yellow("→ #{line_no_prefix}: #{line}")
|
|
65
|
+
else
|
|
66
|
+
" #{line_no_prefix}: #{line}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
# rubocop:enable Metrics/AbcSize
|
|
71
|
+
# rubocop:enable Metrics/MethodLength
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
module DSL
|
|
4
|
+
module HostsAndSettings
|
|
5
|
+
def set(settings)
|
|
6
|
+
@config.settings.merge!(settings)
|
|
7
|
+
self
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def host(address, port: 22, roles: [],
|
|
11
|
+
log_prefix: nil, privileged_user: "root")
|
|
12
|
+
@config.hosts << Host.parse(
|
|
13
|
+
address,
|
|
14
|
+
privileged_user: privileged_user,
|
|
15
|
+
port: port,
|
|
16
|
+
roles: roles,
|
|
17
|
+
log_prefix: log_prefix
|
|
18
|
+
)
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
module DSL
|
|
4
|
+
class TasksBlock
|
|
5
|
+
def initialize(tasks)
|
|
6
|
+
@tasks = tasks
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def batch(&block)
|
|
10
|
+
batch = []
|
|
11
|
+
BatchBlock.new(batch).instance_eval(&block)
|
|
12
|
+
@tasks << batch unless batch.empty?
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run(task, privileged: false)
|
|
17
|
+
task.extend(Runtime::PrivilegedTask) if privileged
|
|
18
|
+
@tasks << task
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
module DSL
|
|
4
|
+
autoload :BatchBlock, "tomo/configuration/dsl/batch_block"
|
|
5
|
+
autoload :ConfigFile, "tomo/configuration/dsl/config_file"
|
|
6
|
+
autoload :EnvironmentBlock, "tomo/configuration/dsl/environment_block"
|
|
7
|
+
autoload :ErrorFormatter, "tomo/configuration/dsl/error_formatter"
|
|
8
|
+
autoload :HostsAndSettings, "tomo/configuration/dsl/hosts_and_settings"
|
|
9
|
+
autoload :TasksBlock, "tomo/configuration/dsl/tasks_block"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
class Glob
|
|
4
|
+
def initialize(spec)
|
|
5
|
+
@spec = spec.to_s.freeze
|
|
6
|
+
regexp_parts = @spec.split(/(\*)/).map do |part|
|
|
7
|
+
part == "*" ? ".*" : Regexp.quote(part)
|
|
8
|
+
end
|
|
9
|
+
@regexp = Regexp.new(regexp_parts.join).freeze
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def match?(str)
|
|
14
|
+
regexp.match?(str)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
spec
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :regexp, :spec
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
class PluginFileNotFoundError < Error
|
|
4
|
+
attr_accessor :path
|
|
5
|
+
|
|
6
|
+
def to_console
|
|
7
|
+
<<~ERROR
|
|
8
|
+
A plugin specified by this project could not be loaded.
|
|
9
|
+
File does not exist: #{yellow(path)}
|
|
10
|
+
ERROR
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
class PluginResolver
|
|
4
|
+
PLUGIN_PREFIX = "tomo/plugin".freeze
|
|
5
|
+
private_constant :PLUGIN_PREFIX
|
|
6
|
+
|
|
7
|
+
def self.resolve(name)
|
|
8
|
+
new(name).plugin_module
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(name)
|
|
12
|
+
@name = name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def plugin_module
|
|
16
|
+
plugin_path = [PLUGIN_PREFIX, name.tr("-", "/")].join("/")
|
|
17
|
+
require plugin_path
|
|
18
|
+
|
|
19
|
+
plugin = constantize(plugin_path)
|
|
20
|
+
assert_compatible_api(plugin)
|
|
21
|
+
|
|
22
|
+
plugin
|
|
23
|
+
rescue LoadError => e
|
|
24
|
+
raise unless e.message.match?(/\s#{Regexp.quote(plugin_path)}$/)
|
|
25
|
+
|
|
26
|
+
raise_unknown_plugin_error(e)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :name
|
|
32
|
+
|
|
33
|
+
def assert_compatible_api(plugin)
|
|
34
|
+
return if plugin.is_a?(::Tomo::PluginDSL)
|
|
35
|
+
|
|
36
|
+
raise "#{plugin} does not extend Tomo::PluginDSL"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def constantize(path)
|
|
40
|
+
parts = path.split("/")
|
|
41
|
+
parts.reduce(Object) do |parent, part|
|
|
42
|
+
child = part.gsub(/^[a-z]|_[a-z]/) { |str| str.chars.last.upcase }
|
|
43
|
+
parent.const_get(child, false)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def raise_unknown_plugin_error(error)
|
|
48
|
+
UnknownPluginError.raise_with(
|
|
49
|
+
error.message,
|
|
50
|
+
name: name,
|
|
51
|
+
gem_name: "#{PLUGIN_PREFIX}/#{name}".tr("/", "-"),
|
|
52
|
+
known_plugins: scan_for_plugins
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def scan_for_plugins
|
|
57
|
+
Gem.find_latest_files("#{PLUGIN_PREFIX}/*.rb").map do |file|
|
|
58
|
+
file[%r{#{PLUGIN_PREFIX}/(.+).rb$}, 1].tr("/", "-")
|
|
59
|
+
end.uniq.sort
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Tomo
|
|
2
|
+
class Configuration
|
|
3
|
+
class PluginsRegistry::FileResolver
|
|
4
|
+
def self.resolve(path)
|
|
5
|
+
new(path).plugin_module
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize(path)
|
|
9
|
+
@path = path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def plugin_module
|
|
13
|
+
raise_file_not_found(path) unless File.file?(path)
|
|
14
|
+
|
|
15
|
+
Tomo.logger.debug("Loading plugin from #{path.inspect}")
|
|
16
|
+
script = IO.read(path)
|
|
17
|
+
plugin = define_anonymous_plugin_class
|
|
18
|
+
plugin.class_eval(script, path.to_s, 1)
|
|
19
|
+
|
|
20
|
+
plugin
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :path
|
|
26
|
+
|
|
27
|
+
def raise_file_not_found(path)
|
|
28
|
+
PluginFileNotFoundError.raise_with(path: path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def define_anonymous_plugin_class
|
|
32
|
+
name = path.to_s
|
|
33
|
+
plugin = Class.new(TaskLibrary)
|
|
34
|
+
plugin.extend(PluginDSL)
|
|
35
|
+
plugin.send(:tasks, plugin)
|
|
36
|
+
plugin.define_singleton_method(:to_s) do
|
|
37
|
+
super().sub(/>$/, "(#{name})>")
|
|
38
|
+
end
|
|
39
|
+
plugin
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|