tomo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +221 -0
  4. data/exe/tomo +4 -0
  5. data/lib/tomo/cli/command.rb +36 -0
  6. data/lib/tomo/cli/common_options.rb +48 -0
  7. data/lib/tomo/cli/completions.rb +70 -0
  8. data/lib/tomo/cli/deploy_options.rb +59 -0
  9. data/lib/tomo/cli/error.rb +16 -0
  10. data/lib/tomo/cli/interrupted_error.rb +9 -0
  11. data/lib/tomo/cli/options.rb +38 -0
  12. data/lib/tomo/cli/parser.rb +92 -0
  13. data/lib/tomo/cli/project_options.rb +47 -0
  14. data/lib/tomo/cli/rules/argument.rb +42 -0
  15. data/lib/tomo/cli/rules/switch.rb +43 -0
  16. data/lib/tomo/cli/rules/value_switch.rb +58 -0
  17. data/lib/tomo/cli/rules.rb +98 -0
  18. data/lib/tomo/cli/rules_evaluator.rb +71 -0
  19. data/lib/tomo/cli/state.rb +29 -0
  20. data/lib/tomo/cli/usage.rb +42 -0
  21. data/lib/tomo/cli.rb +75 -0
  22. data/lib/tomo/colors.rb +46 -0
  23. data/lib/tomo/commands/completion_script.rb +46 -0
  24. data/lib/tomo/commands/default.rb +72 -0
  25. data/lib/tomo/commands/deploy.rb +67 -0
  26. data/lib/tomo/commands/help.rb +9 -0
  27. data/lib/tomo/commands/init.rb +92 -0
  28. data/lib/tomo/commands/run.rb +76 -0
  29. data/lib/tomo/commands/setup.rb +54 -0
  30. data/lib/tomo/commands/tasks.rb +32 -0
  31. data/lib/tomo/commands/version.rb +23 -0
  32. data/lib/tomo/commands.rb +13 -0
  33. data/lib/tomo/configuration/dsl/batch_block.rb +17 -0
  34. data/lib/tomo/configuration/dsl/config_file.rb +39 -0
  35. data/lib/tomo/configuration/dsl/environment_block.rb +13 -0
  36. data/lib/tomo/configuration/dsl/error_formatter.rb +75 -0
  37. data/lib/tomo/configuration/dsl/hosts_and_settings.rb +24 -0
  38. data/lib/tomo/configuration/dsl/tasks_block.rb +24 -0
  39. data/lib/tomo/configuration/dsl.rb +12 -0
  40. data/lib/tomo/configuration/environment.rb +12 -0
  41. data/lib/tomo/configuration/glob.rb +26 -0
  42. data/lib/tomo/configuration/plugin_file_not_found_error.rb +14 -0
  43. data/lib/tomo/configuration/plugin_resolver.rb +63 -0
  44. data/lib/tomo/configuration/plugins_registry/file_resolver.rb +43 -0
  45. data/lib/tomo/configuration/plugins_registry/gem_resolver.rb +63 -0
  46. data/lib/tomo/configuration/plugins_registry.rb +67 -0
  47. data/lib/tomo/configuration/project_not_found_error.rb +28 -0
  48. data/lib/tomo/configuration/role_based_task_filter.rb +42 -0
  49. data/lib/tomo/configuration/unknown_environment_error.rb +46 -0
  50. data/lib/tomo/configuration/unknown_plugin_error.rb +28 -0
  51. data/lib/tomo/configuration/unspecified_environment_error.rb +28 -0
  52. data/lib/tomo/configuration.rb +124 -0
  53. data/lib/tomo/console/key_reader.rb +51 -0
  54. data/lib/tomo/console/menu.rb +109 -0
  55. data/lib/tomo/console.rb +33 -0
  56. data/lib/tomo/error/suggestions.rb +44 -0
  57. data/lib/tomo/error.rb +22 -0
  58. data/lib/tomo/host.rb +57 -0
  59. data/lib/tomo/logger/tagged_io.rb +38 -0
  60. data/lib/tomo/logger.rb +70 -0
  61. data/lib/tomo/path.rb +19 -0
  62. data/lib/tomo/paths.rb +36 -0
  63. data/lib/tomo/plugin/bundler/helpers.rb +14 -0
  64. data/lib/tomo/plugin/bundler/tasks.rb +57 -0
  65. data/lib/tomo/plugin/bundler.rb +17 -0
  66. data/lib/tomo/plugin/core/helpers.rb +65 -0
  67. data/lib/tomo/plugin/core/tasks.rb +138 -0
  68. data/lib/tomo/plugin/core.rb +31 -0
  69. data/lib/tomo/plugin/env/tasks.rb +113 -0
  70. data/lib/tomo/plugin/env.rb +13 -0
  71. data/lib/tomo/plugin/git/helpers.rb +11 -0
  72. data/lib/tomo/plugin/git/tasks.rb +78 -0
  73. data/lib/tomo/plugin/git.rb +19 -0
  74. data/lib/tomo/plugin/nvm/tasks.rb +61 -0
  75. data/lib/tomo/plugin/nvm.rb +14 -0
  76. data/lib/tomo/plugin/puma/tasks.rb +38 -0
  77. data/lib/tomo/plugin/puma.rb +12 -0
  78. data/lib/tomo/plugin/rails/helpers.rb +20 -0
  79. data/lib/tomo/plugin/rails/tasks.rb +79 -0
  80. data/lib/tomo/plugin/rails.rb +11 -0
  81. data/lib/tomo/plugin/rbenv/tasks.rb +55 -0
  82. data/lib/tomo/plugin/rbenv.rb +12 -0
  83. data/lib/tomo/plugin/testing.rb +16 -0
  84. data/lib/tomo/plugin.rb +4 -0
  85. data/lib/tomo/plugin_dsl.rb +23 -0
  86. data/lib/tomo/remote.rb +55 -0
  87. data/lib/tomo/result.rb +28 -0
  88. data/lib/tomo/runtime/concurrent_ruby_load_error.rb +26 -0
  89. data/lib/tomo/runtime/concurrent_ruby_thread_pool.rb +50 -0
  90. data/lib/tomo/runtime/context.rb +21 -0
  91. data/lib/tomo/runtime/current.rb +41 -0
  92. data/lib/tomo/runtime/execution_plan.rb +107 -0
  93. data/lib/tomo/runtime/host_execution_step.rb +49 -0
  94. data/lib/tomo/runtime/inline_thread_pool.rb +27 -0
  95. data/lib/tomo/runtime/privileged_task.rb +6 -0
  96. data/lib/tomo/runtime/settings_interpolation.rb +55 -0
  97. data/lib/tomo/runtime/settings_required_error.rb +33 -0
  98. data/lib/tomo/runtime/task_aborted_error.rb +15 -0
  99. data/lib/tomo/runtime/task_runner.rb +56 -0
  100. data/lib/tomo/runtime/unknown_task_error.rb +23 -0
  101. data/lib/tomo/runtime.rb +82 -0
  102. data/lib/tomo/script.rb +44 -0
  103. data/lib/tomo/shell_builder.rb +108 -0
  104. data/lib/tomo/ssh/child_process.rb +64 -0
  105. data/lib/tomo/ssh/connection.rb +82 -0
  106. data/lib/tomo/ssh/connection_error.rb +16 -0
  107. data/lib/tomo/ssh/connection_validator.rb +87 -0
  108. data/lib/tomo/ssh/error.rb +11 -0
  109. data/lib/tomo/ssh/executable_error.rb +21 -0
  110. data/lib/tomo/ssh/options.rb +67 -0
  111. data/lib/tomo/ssh/permission_error.rb +18 -0
  112. data/lib/tomo/ssh/script_error.rb +23 -0
  113. data/lib/tomo/ssh/unknown_error.rb +13 -0
  114. data/lib/tomo/ssh/unsupported_version_error.rb +15 -0
  115. data/lib/tomo/ssh.rb +36 -0
  116. data/lib/tomo/task_library.rb +51 -0
  117. data/lib/tomo/templates/config.rb.erb +66 -0
  118. data/lib/tomo/testing/Dockerfile +10 -0
  119. data/lib/tomo/testing/connection.rb +34 -0
  120. data/lib/tomo/testing/docker_image.rb +115 -0
  121. data/lib/tomo/testing/docker_plugin_tester.rb +39 -0
  122. data/lib/tomo/testing/host_extensions.rb +27 -0
  123. data/lib/tomo/testing/local.rb +75 -0
  124. data/lib/tomo/testing/mock_plugin_tester.rb +26 -0
  125. data/lib/tomo/testing/mocked_exec_error.rb +6 -0
  126. data/lib/tomo/testing/plugin_tester.rb +49 -0
  127. data/lib/tomo/testing/remote_extensions.rb +10 -0
  128. data/lib/tomo/testing/ssh_extensions.rb +13 -0
  129. data/lib/tomo/testing/tomo_test_ed25519 +7 -0
  130. data/lib/tomo/testing/tomo_test_ed25519.pub +1 -0
  131. data/lib/tomo/testing/ubuntu_setup.sh +33 -0
  132. data/lib/tomo/testing.rb +39 -0
  133. data/lib/tomo/version.rb +3 -0
  134. data/lib/tomo.rb +45 -0
  135. metadata +308 -0
@@ -0,0 +1,9 @@
1
+ module Tomo
2
+ module Commands
3
+ class Help
4
+ def self.parse(argv)
5
+ Default.parse([*argv, "--help"])
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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,13 @@
1
+ module Tomo
2
+ class Configuration
3
+ module DSL
4
+ class EnvironmentBlock
5
+ include HostsAndSettings
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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,12 @@
1
+ module Tomo
2
+ class Configuration
3
+ class Environment
4
+ attr_accessor :hosts, :settings
5
+
6
+ def initialize
7
+ @hosts = []
8
+ @settings = {}
9
+ end
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