careacademy-runbook 1.2.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 (104) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +17 -0
  3. data/.github/workflows/ruby.yml +43 -0
  4. data/.gitignore +16 -0
  5. data/.rspec +2 -0
  6. data/.ruby-gemset +1 -0
  7. data/.ruby-version +1 -0
  8. data/Appraisals +8 -0
  9. data/CHANGELOG.md +143 -0
  10. data/CODE_OF_CONDUCT.md +74 -0
  11. data/Gemfile +6 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +1272 -0
  14. data/Rakefile +12 -0
  15. data/TODO.md +316 -0
  16. data/bin/console +16 -0
  17. data/bin/setup +8 -0
  18. data/dockerfiles/Dockerfile-runbook +18 -0
  19. data/dockerfiles/Dockerfile-sshd +4 -0
  20. data/examples/hooks_runbook.rb +72 -0
  21. data/examples/layout_runbook.rb +26 -0
  22. data/examples/restart_nginx.rb +26 -0
  23. data/examples/simple_runbook.rb +41 -0
  24. data/examples/suppress_capture_output.rb +47 -0
  25. data/exe/runbook +5 -0
  26. data/gemfiles/.bundle/config +2 -0
  27. data/gemfiles/activesupport_5.gemfile +7 -0
  28. data/gemfiles/activesupport_6.gemfile +7 -0
  29. data/images/runbook_anatomy_diagram.png +0 -0
  30. data/images/runbook_example.gif +0 -0
  31. data/images/runbook_execution_modes.png +0 -0
  32. data/lib/hacks/ssh_kit.rb +58 -0
  33. data/lib/runbook/airbrussh_context.rb +25 -0
  34. data/lib/runbook/cli.rb +115 -0
  35. data/lib/runbook/cli_base.rb +38 -0
  36. data/lib/runbook/configuration.rb +120 -0
  37. data/lib/runbook/dsl.rb +21 -0
  38. data/lib/runbook/entities/book.rb +17 -0
  39. data/lib/runbook/entities/section.rb +7 -0
  40. data/lib/runbook/entities/setup.rb +7 -0
  41. data/lib/runbook/entities/step.rb +7 -0
  42. data/lib/runbook/entity.rb +129 -0
  43. data/lib/runbook/errors.rb +7 -0
  44. data/lib/runbook/extensions/add.rb +14 -0
  45. data/lib/runbook/extensions/description.rb +14 -0
  46. data/lib/runbook/extensions/sections.rb +19 -0
  47. data/lib/runbook/extensions/setup.rb +17 -0
  48. data/lib/runbook/extensions/shared_variables.rb +51 -0
  49. data/lib/runbook/extensions/ssh_config.rb +78 -0
  50. data/lib/runbook/extensions/statements.rb +31 -0
  51. data/lib/runbook/extensions/steps.rb +24 -0
  52. data/lib/runbook/extensions/tmux.rb +13 -0
  53. data/lib/runbook/generator.rb +38 -0
  54. data/lib/runbook/generators/base.rb +45 -0
  55. data/lib/runbook/generators/dsl_extension/dsl_extension.rb +29 -0
  56. data/lib/runbook/generators/dsl_extension/templates/dsl_extension.tt +25 -0
  57. data/lib/runbook/generators/generator/generator.rb +43 -0
  58. data/lib/runbook/generators/generator/templates/generator.tt +53 -0
  59. data/lib/runbook/generators/project/project.rb +301 -0
  60. data/lib/runbook/generators/project/templates/Gemfile.tt +6 -0
  61. data/lib/runbook/generators/project/templates/README.md.tt +29 -0
  62. data/lib/runbook/generators/project/templates/Runbookfile.tt +11 -0
  63. data/lib/runbook/generators/project/templates/base_file.rb.tt +8 -0
  64. data/lib/runbook/generators/runbook/runbook.rb +22 -0
  65. data/lib/runbook/generators/runbook/templates/runbook.tt +20 -0
  66. data/lib/runbook/generators/statement/statement.rb +18 -0
  67. data/lib/runbook/generators/statement/templates/statement.tt +34 -0
  68. data/lib/runbook/helpers/format_helper.rb +11 -0
  69. data/lib/runbook/helpers/ssh_kit_helper.rb +143 -0
  70. data/lib/runbook/helpers/tmux_helper.rb +176 -0
  71. data/lib/runbook/hooks.rb +88 -0
  72. data/lib/runbook/initializer.rb +85 -0
  73. data/lib/runbook/node.rb +33 -0
  74. data/lib/runbook/run.rb +290 -0
  75. data/lib/runbook/runner.rb +66 -0
  76. data/lib/runbook/runs/ssh_kit.rb +193 -0
  77. data/lib/runbook/statement.rb +20 -0
  78. data/lib/runbook/statements/ask.rb +12 -0
  79. data/lib/runbook/statements/assert.rb +34 -0
  80. data/lib/runbook/statements/capture.rb +14 -0
  81. data/lib/runbook/statements/capture_all.rb +14 -0
  82. data/lib/runbook/statements/command.rb +11 -0
  83. data/lib/runbook/statements/confirm.rb +10 -0
  84. data/lib/runbook/statements/description.rb +9 -0
  85. data/lib/runbook/statements/download.rb +12 -0
  86. data/lib/runbook/statements/layout.rb +10 -0
  87. data/lib/runbook/statements/note.rb +10 -0
  88. data/lib/runbook/statements/notice.rb +10 -0
  89. data/lib/runbook/statements/ruby_command.rb +9 -0
  90. data/lib/runbook/statements/tmux_command.rb +11 -0
  91. data/lib/runbook/statements/upload.rb +12 -0
  92. data/lib/runbook/statements/wait.rb +10 -0
  93. data/lib/runbook/toolbox.rb +37 -0
  94. data/lib/runbook/util/repo.rb +57 -0
  95. data/lib/runbook/util/runbook.rb +43 -0
  96. data/lib/runbook/util/sticky_hash.rb +26 -0
  97. data/lib/runbook/util/stored_pose.rb +55 -0
  98. data/lib/runbook/version.rb +3 -0
  99. data/lib/runbook/view.rb +24 -0
  100. data/lib/runbook/viewer.rb +24 -0
  101. data/lib/runbook/views/markdown.rb +117 -0
  102. data/lib/runbook.rb +140 -0
  103. data/runbook.gemspec +53 -0
  104. metadata +419 -0
@@ -0,0 +1,29 @@
1
+ # <%= name.titleize %>
2
+
3
+ ## Deployment
4
+
5
+ This code is best deployed by cloning this project using git.
6
+
7
+ ## Usage
8
+
9
+ Set up this repository by running `bin/setup`. This will install all necessary dependencies for your runbooks.
10
+
11
+ Execute your runbooks using `bundle exec runbook exec <PATH_TO_YOUR_RUNBOOK>`
12
+
13
+ Get runbook help executing `bundle exec runbook help` or visiting https://github.com/braintree/runbook
14
+
15
+ ## This Repository
16
+
17
+ Runbook code should live in the following locations:
18
+
19
+ `runbooks` - Place your runbooks in this directory
20
+
21
+ `<%= @shared_lib_dir %>` - Place code shared across your runbooks in this directory
22
+
23
+ `lib/runbook/extensions` - Place code that extends Runbook's functionality here. For example, adding a new statement to Runbook's DSL would live in this directory. Require these files in your `Runbookfile` so Runbook can load them when it is launched. See https://github.com/braintree/runbook#extending-runbook for more details on extending Runbook.
24
+
25
+ `lib/runbook/generators` - Place Runbook generators in this directory. Require these files in your `Runbookfile` to expose the generators via Runbook's command line interface.
26
+
27
+ ## Development
28
+
29
+ Run `bin/setup` to install any necessary dependencies for this project. Run `bin/console` to use your code via an interactive prompt.
@@ -0,0 +1,11 @@
1
+ # Your Runbookfile serves two purposes. Use your Runbookfile to
2
+ # (1) require any runbook plugins, extensions, or generators that
3
+ # you want available to your runbooks and (2) configure Runbook
4
+ # using a `Runbook.configure` block.
5
+
6
+ # Use this block to configure Runbook. See
7
+ # https://github.com/braintree/runbook#configuration for more
8
+ # details.
9
+ #
10
+ # Runbook.configure do |config|
11
+ # end
@@ -0,0 +1,8 @@
1
+ # Require your runbook shared code here so it can easily be
2
+ # included in your runbooks and accessed from the interactive
3
+ # console using `bin/console`.
4
+
5
+ require "runbook"
6
+
7
+ module <%= name.camelize %>
8
+ end
@@ -0,0 +1,22 @@
1
+ module Runbook::Generators
2
+ class Runbook < Thor::Group
3
+ include ::Runbook::Generators::Base
4
+
5
+ source_root File.dirname(__FILE__)
6
+
7
+ def self.usage
8
+ "runbook NAME [options]"
9
+ end
10
+
11
+ def self.description
12
+ "Generate a runbook named NAME, e.x. deploy_nginx"
13
+ end
14
+
15
+ argument :name, desc: "The name of your runbook, e.x. deploy_nginx"
16
+
17
+ def create_runbook
18
+ target = File.join(options[:root], "#{name.underscore}.rb")
19
+ template('templates/runbook.tt', target)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ require "runbook"
3
+
4
+ runbook = Runbook.book "<%= name.titleize %>" do
5
+ description <<-DESC
6
+ This is a runbook that...
7
+ DESC
8
+
9
+ section "SECTION" do
10
+ step "STEP" do
11
+ # Add statements here
12
+ end
13
+ end
14
+ end
15
+
16
+ if __FILE__ == $0
17
+ Runbook::Runner.new(runbook).run
18
+ else
19
+ runbook
20
+ end
@@ -0,0 +1,18 @@
1
+ module Runbook::Generators
2
+ class Statement < Thor::Group
3
+ include ::Runbook::Generators::Base
4
+
5
+ source_root File.dirname(__FILE__)
6
+
7
+ def self.description
8
+ "Generate a statement named NAME (e.x. ruby_command) that can be used in your runbooks"
9
+ end
10
+
11
+ argument :name, desc: "The name of your statement, e.x. ruby_command"
12
+
13
+ def create_statement
14
+ target = File.join(parent_options[:root], "#{name.underscore}.rb")
15
+ template('templates/statement.tt', target)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ # Remember to require this file in a runbook config file
2
+ # or in your project so it is available in your runbooks
3
+ module Runbook::Statements
4
+ class <%= name.classify %> < Runbook::Statement
5
+ # Add all attributes for your statement
6
+ attr_reader :attr1, :attr2
7
+
8
+ # Define the initialize method signature to
9
+ # match the method signature of your dsl statement
10
+ def initialize(attr1, attr2)
11
+ @attr1 = attr1
12
+ @attr2 = attr2
13
+ end
14
+ end
15
+ end
16
+
17
+ # scope this module using your project's namespace
18
+ module MyProject::RunbookExtensions
19
+ module <%= "#{name.underscore}_markdown".classify %>
20
+ def runbook__statements__<%= name.underscore %>(object, output, metadata)
21
+ # Format how your statement will be displayed when rendered with markdown
22
+ output << "#{object.attr1}#{object.attr2}"
23
+ end
24
+ end
25
+ Runbook::Views::Markdown.singleton_class.prepend(<%= "#{name.underscore}_markdown".classify %>)
26
+
27
+ module <%= "#{name.underscore}_sshkit".classify %>
28
+ def runbook__statements__<%= name.underscore %>(object, metadata)
29
+ # Execute your behavior using object which is your instantiated statement
30
+ # and the current metadata for this step of the execution
31
+ end
32
+ end
33
+ Runbook::Runs::SSHKit.singleton_class.prepend(<%= "#{name.underscore}_sshkit".classify %>)
34
+ end
@@ -0,0 +1,11 @@
1
+ module Runbook::Helpers
2
+ module FormatHelper
3
+ def deindent(str, padding: 0)
4
+ lines = str.split("\n")
5
+ indentations = lines.map { |l| l.size - l.gsub(/^\s+/, "").size }
6
+ min_indentation = indentations.min
7
+ lines.map! { |line| " " * padding + line[min_indentation..-1] }
8
+ lines.join("\n")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,143 @@
1
+ module Runbook::Helpers
2
+ module SSHKitHelper
3
+ def ssh_kit_command(cmd, raw: false)
4
+ return [cmd] if raw
5
+ cmd, args = cmd.split(" ", 2)
6
+ [cmd.to_sym, args]
7
+ end
8
+
9
+ def ssh_kit_command_options(ssh_config)
10
+ {}.tap do |options|
11
+ if ssh_config[:user] && Runbook.configuration.enable_sudo_prompt
12
+ options[:interaction_handler] ||= ::SSHKit::Sudo::InteractionHandler.new
13
+ end
14
+ end
15
+ end
16
+
17
+ def find_ssh_config(object, ssh_config_method=:ssh_config)
18
+ blank_config = Runbook::Extensions::SSHConfig.blank_ssh_config
19
+ nil_or_blank = ->(config) { config.nil? || config == blank_config }
20
+ ssh_config = object.send(ssh_config_method)
21
+ return ssh_config unless nil_or_blank.call(ssh_config)
22
+ object = object.parent
23
+
24
+ while object
25
+ ssh_config = object.ssh_config
26
+ return ssh_config unless nil_or_blank.call(ssh_config)
27
+ object = object.parent
28
+ end
29
+
30
+ return blank_config
31
+ end
32
+
33
+ def render_ssh_config_output(ssh_config)
34
+ "".tap do |output|
35
+ if (servers = ssh_config[:servers]).any?
36
+ server_str = servers.join(", ")
37
+ if server_str.size > 80
38
+ server_str = "#{server_str[0..38]}...#{server_str[-38..-1]}"
39
+ end
40
+ output << " on: #{server_str}\n"
41
+ end
42
+
43
+ if (strategy = ssh_config[:parallelization][:strategy])
44
+ limit = ssh_config[:parallelization][:limit]
45
+ wait = ssh_config[:parallelization][:wait]
46
+ in_str = " in: #{strategy}"
47
+ in_str << ", limit: #{limit}" if strategy == :groups
48
+ in_str << ", wait: #{wait}" if [:sequence, :groups].include?(strategy)
49
+ output << "#{in_str}\n"
50
+ end
51
+
52
+ if ssh_config[:user] || ssh_config[:group]
53
+ user = ssh_config[:user]
54
+ group = ssh_config[:group]
55
+ as_str = " as:"
56
+ as_str << " user: #{user}" if user
57
+ as_str << " group: #{group}" if group
58
+ output << "#{as_str}\n"
59
+ end
60
+
61
+ if (path = ssh_config[:path])
62
+ output << " within: #{path}\n"
63
+ end
64
+
65
+ if (env = ssh_config[:env])
66
+ env_str = env.map do |k, v|
67
+ "#{k.to_s.upcase}=#{v}"
68
+ end.join(" ")
69
+ output << " with: #{env_str}\n"
70
+ end
71
+
72
+ if (umask = ssh_config[:umask])
73
+ output << " umask: #{umask}\n"
74
+ end
75
+ end
76
+ end
77
+
78
+ def with_ssh_config(ssh_config, &exec_block)
79
+ user = ssh_config[:user]
80
+ group = ssh_config[:group]
81
+ as_block = _as_block(user, group, &exec_block)
82
+ within_block = _within_block(ssh_config[:path], &as_block)
83
+ with_block = _with_block(ssh_config[:env], &within_block)
84
+
85
+ _with_umask(ssh_config[:umask]) do
86
+ servers = _servers(ssh_config[:servers])
87
+ parallelization = ssh_config[:parallelization]
88
+ coordinator_options = _coordinator_options(parallelization)
89
+ SSHKit::Coordinator.new(servers).each(coordinator_options) do
90
+ instance_exec(&with_block)
91
+ end
92
+ end
93
+ end
94
+
95
+ def _servers(ssh_config_servers)
96
+ return :local if ssh_config_servers.empty?
97
+ return :local if ssh_config_servers == [:local]
98
+ ssh_config_servers
99
+ end
100
+
101
+ def _coordinator_options(ssh_config_parallelization)
102
+ ssh_config_parallelization.clone.tap do |options|
103
+ if options[:strategy]
104
+ options[:in] = options.delete(:strategy)
105
+ end
106
+ end
107
+ end
108
+
109
+ def _as_block(user, group, &block)
110
+ if user || group
111
+ -> { as({user: user, group: group}) { instance_exec(&block) } }
112
+ else
113
+ -> { instance_exec(&block) }
114
+ end
115
+ end
116
+
117
+ def _with_block(env, &block)
118
+ if env
119
+ -> { with(env) { instance_exec(&block) } }
120
+ else
121
+ -> { instance_exec(&block) }
122
+ end
123
+ end
124
+
125
+ def _within_block(path, &block)
126
+ if path
127
+ -> { within(path) { instance_exec(&block) } }
128
+ else
129
+ -> { instance_exec(&block) }
130
+ end
131
+ end
132
+
133
+ def _with_umask(umask, &block)
134
+ old_umask = SSHKit.config.umask
135
+ begin
136
+ SSHKit.config.umask = umask if umask
137
+ instance_exec(&block)
138
+ ensure
139
+ SSHKit.config.umask = old_umask
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,176 @@
1
+ module Runbook::Helpers
2
+ module TmuxHelper
3
+ FILE_PERMISSIONS = 0600
4
+
5
+ def setup_layout(structure, runbook_title:)
6
+ _remove_stale_layouts
7
+ layout_file = _layout_file(_slug(runbook_title))
8
+ if File.exist?(layout_file)
9
+ stored_layout = ::YAML::load_file(layout_file)
10
+ if _all_panes_exist?(stored_layout)
11
+ return stored_layout
12
+ end
13
+ end
14
+
15
+ _setup_layout(structure).tap do |layout_panes|
16
+ File.open(layout_file, 'w', FILE_PERMISSIONS) do |f|
17
+ f.write(layout_panes.to_yaml)
18
+ end
19
+ end
20
+ end
21
+
22
+ def send_keys(command, target)
23
+ `tmux send-keys -t #{target} #{_pager_escape_sequence} #{Shellwords.escape(command)} C-m`
24
+ end
25
+
26
+ def kill_all_panes(layout_panes)
27
+ runbook_pane = _runbook_pane
28
+ layout_panes.values.each do |pane_id|
29
+ _kill_pane(pane_id) unless pane_id == runbook_pane
30
+ end
31
+ end
32
+
33
+ def _pager_escape_sequence
34
+ "q C-u"
35
+ end
36
+
37
+ def _kill_pane(pane_id)
38
+ `tmux kill-pane -t #{pane_id}`
39
+ end
40
+
41
+ def _layout_file(runbook_title)
42
+ `tmux display-message -p -t $TMUX_PANE "#{Dir.tmpdir}/runbook_layout_\#{pid}_\#{session_name}_\#{pane_pid}_\#{pane_id}_#{runbook_title}.yml"`.strip
43
+ end
44
+
45
+ def _slug(title)
46
+ title.titleize.gsub(/[\/\s]+/, "").underscore.dasherize
47
+ end
48
+
49
+ def _all_panes_exist?(stored_layout)
50
+ (stored_layout.values - _session_panes).empty?
51
+ end
52
+
53
+ def _remove_stale_layouts
54
+ session_panes = _session_panes
55
+ session_layout_files = _session_layout_files
56
+ session_layout_files.each do |file|
57
+ File.delete(file) unless session_panes.any? { |pane| /_#{pane}_/ =~ file }
58
+ end
59
+ end
60
+
61
+ def _session_panes
62
+ `tmux list-panes -s -F '#D'`.split("\n")
63
+ end
64
+
65
+ def _session_layout_files
66
+ session_layout_glob = `tmux display-message -p "#{Dir.tmpdir}/runbook_layout_\#{pid}_\#{session_name}_*.yml"`.strip
67
+ Dir[session_layout_glob]
68
+ end
69
+
70
+ def _setup_layout(structure)
71
+ current_pane = _runbook_pane
72
+ panes_to_init = []
73
+ {}.tap do |layout_panes|
74
+ if structure.is_a?(Hash)
75
+ first_window = true
76
+ structure.each do |name, window|
77
+ if first_window
78
+ _rename_window(name)
79
+ first_window = false
80
+ else
81
+ current_pane = _new_window(name)
82
+ end
83
+ _setup_panes(layout_panes, panes_to_init, current_pane, window)
84
+ end
85
+ else
86
+ _setup_panes(layout_panes, panes_to_init, current_pane, structure)
87
+ end
88
+ _swap_runbook_pane(panes_to_init, layout_panes)
89
+ _initialize_panes(panes_to_init, layout_panes)
90
+ end
91
+ end
92
+
93
+ def _setup_panes(layout_panes, panes_to_init, current_pane, structure, depth=0)
94
+ return if structure.empty?
95
+ case structure
96
+ when Array
97
+ case structure.size
98
+ when 1
99
+ _setup_panes(layout_panes, panes_to_init, current_pane, structure.shift, depth+1)
100
+ else
101
+ size = 100 - 100 / structure.size
102
+ new_pane = _split(current_pane, depth, size)
103
+ _setup_panes(layout_panes, panes_to_init, current_pane, structure.shift, depth+1)
104
+ _setup_panes(layout_panes, panes_to_init, new_pane, structure, depth)
105
+ end
106
+ when Hash
107
+ if structure.values.all? { |v| v.is_a?(Numeric) }
108
+ total_size = structure.values.reduce(:+)
109
+ case structure.size
110
+ when 1
111
+ _setup_panes(layout_panes, panes_to_init, current_pane, structure.keys[0], depth+1)
112
+ else
113
+ size = (total_size - structure.values[0]) * 100 / total_size
114
+ new_pane = _split(current_pane, depth, size)
115
+ first_struct = structure.keys[0]
116
+ structure.delete(first_struct)
117
+ _setup_panes(layout_panes, panes_to_init, current_pane, first_struct, depth+1)
118
+ _setup_panes(layout_panes, panes_to_init, new_pane, structure, depth)
119
+ end
120
+ else
121
+ layout_panes[structure[:name]] = current_pane
122
+ panes_to_init << structure
123
+ end
124
+ when Symbol
125
+ layout_panes[structure] = current_pane
126
+ end
127
+ end
128
+
129
+ def _swap_runbook_pane(panes_to_init, layout_panes)
130
+ if (runbook_pane = panes_to_init.find { |pane| pane[:runbook_pane] })
131
+ current_runbook_pane_name = layout_panes.keys.find do |k|
132
+ layout_panes[k] == _runbook_pane
133
+ end
134
+ target_pane_id = layout_panes[runbook_pane[:name]]
135
+ layout_panes[runbook_pane[:name]] = _runbook_pane
136
+ layout_panes[current_runbook_pane_name] = target_pane_id
137
+ _swap_panes(target_pane_id, _runbook_pane)
138
+ end
139
+ end
140
+
141
+ def _initialize_panes(panes_to_init, layout_panes)
142
+ panes_to_init.each do |pane|
143
+ target = layout_panes[pane[:name]]
144
+ _set_directory(pane[:directory], target) if pane[:directory]
145
+ send_keys(pane[:command], target) if pane[:command]
146
+ end
147
+ end
148
+
149
+ def _runbook_pane
150
+ @runbook_pane ||= `tmux display-message -p '#D'`.strip
151
+ end
152
+
153
+ def _rename_window(name)
154
+ `tmux rename-window "#{name}"`
155
+ end
156
+
157
+ def _new_window(name)
158
+ `tmux new-window -n "#{name}" -P -F '#D' -d`.strip
159
+ end
160
+
161
+ def _split(current_pane, depth, size)
162
+ direction = depth.even? ? "h" : "v"
163
+ command = "tmux split-window"
164
+ args = "-#{direction} -t #{current_pane} -p #{size} -P -F '#D' -d"
165
+ `#{command} #{args}`.strip
166
+ end
167
+
168
+ def _swap_panes(target_pane, source_pane)
169
+ `tmux swap-pane -d -t #{target_pane} -s #{source_pane}`
170
+ end
171
+
172
+ def _set_directory(directory, target)
173
+ send_keys("cd #{directory}; clear", target)
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,88 @@
1
+ module Runbook
2
+ module Hooks
3
+ def hooks
4
+ @hooks ||= []
5
+ end
6
+
7
+ def register_hook(name, type, klass, before: nil, &block)
8
+ hook = {
9
+ name: name,
10
+ type: type,
11
+ klass: klass,
12
+ block: block,
13
+ }
14
+
15
+ if before
16
+ hooks.insert(_hook_index(before), hook)
17
+ else
18
+ hooks << hook
19
+ end
20
+ end
21
+
22
+ def hooks_for(type, klass)
23
+ hooks.select do |hook|
24
+ hook[:type] == type && klass <= hook[:klass]
25
+ end
26
+ end
27
+
28
+ def _hook_index(hook_name)
29
+ hooks.index { |hook| hook[:name] == hook_name } || -1
30
+ end
31
+
32
+ module Invoker
33
+ def invoke_with_hooks(executor, object, *args, &block)
34
+ skip_before = skip_around = skip_after = false
35
+ if executor <= Runbook::Run
36
+ if executor.should_skip?(args[0])
37
+ if executor.start_at_is_substep?(object, args[0])
38
+ skip_before = skip_around = true
39
+ else
40
+ skip_before = skip_around = skip_after = true
41
+ end
42
+ end
43
+ end
44
+
45
+ unless skip_before
46
+ _execute_before_hooks(executor, object, *args)
47
+ end
48
+
49
+ if skip_around
50
+ block.call
51
+ else
52
+ _execute_around_hooks(executor, object, *args, &block)
53
+ end
54
+
55
+ unless skip_after
56
+ _execute_after_hooks(executor, object, *args)
57
+ end
58
+ end
59
+
60
+ def _execute_before_hooks(executor, object, *args)
61
+ executor.hooks_for(:before, object.class).each do |hook|
62
+ executor.instance_exec(object, *args, &hook[:block])
63
+ end
64
+ end
65
+
66
+ def _execute_around_hooks(executor, object, *args)
67
+ executor.hooks_for(:around, object.class).reverse.reduce(
68
+ -> (object, *args) {
69
+ yield
70
+ }
71
+ ) do |inner_method, hook|
72
+ -> (object, *args) {
73
+ inner_block = Proc.new do |object, *args|
74
+ inner_method.call(object, *args)
75
+ end
76
+ executor.instance_exec(object, *args, inner_block, &hook[:block])
77
+ }
78
+ end.call(object, *args)
79
+ end
80
+
81
+ def _execute_after_hooks(executor, object, *args)
82
+ executor.hooks_for(:after, object.class).each do |hook|
83
+ executor.instance_exec(object, *args, &hook[:block])
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,85 @@
1
+ module Runbook
2
+ class Initializer < Thor::Group
3
+ include Thor::Actions
4
+
5
+ source_root File.join(
6
+ File.dirname(__FILE__),
7
+ "generators",
8
+ "project",
9
+ )
10
+
11
+ add_runtime_options!
12
+ check_unknown_options!
13
+
14
+ def create_runbookfile
15
+ template(
16
+ "templates/Runbookfile.tt",
17
+ "Runbookfile",
18
+ )
19
+ end
20
+
21
+ def create_runbooks_directory
22
+ target = "runbooks"
23
+ empty_directory(target)
24
+ _keep_dir(target)
25
+ end
26
+
27
+ def create_lib_directory
28
+ dirs = [
29
+ "lib",
30
+ "runbook",
31
+ ]
32
+ target = File.join(*dirs)
33
+
34
+ empty_directory(target)
35
+ _keep_dir(target)
36
+ end
37
+
38
+ def create_extensions_directory
39
+ dirs = [
40
+ "lib",
41
+ "runbook",
42
+ "extensions",
43
+ ]
44
+ target = File.join(*dirs)
45
+
46
+ empty_directory(target)
47
+ _keep_dir(target)
48
+ end
49
+
50
+ def create_generators_directory
51
+ dirs = [
52
+ "lib",
53
+ "runbook",
54
+ "generators",
55
+ ]
56
+ target = File.join(*dirs)
57
+
58
+ empty_directory(target)
59
+ _keep_dir(target)
60
+ end
61
+
62
+ def runbook_initialization_overview
63
+ msg = [
64
+ "",
65
+ "Runbook was successfully initialized.",
66
+ "Add runbooks to the `runbooks` directory.",
67
+ "Add shared code to `lib/runbook`.",
68
+ "Execute runbooks using `bundle exec runbook exec <RUNBOOK_PATH>`",
69
+ "from your project root.",
70
+ "\n",
71
+ ]
72
+
73
+ say(msg.join("\n"))
74
+ end
75
+
76
+ private
77
+
78
+ def _keep_dir(dir)
79
+ create_file(
80
+ File.join(dir, ".keep"),
81
+ verbose: false,
82
+ )
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,33 @@
1
+ module Runbook
2
+ class Node
3
+ attr_accessor :parent
4
+
5
+ def initialize
6
+ raise "Should not be initialized"
7
+ end
8
+
9
+ def dynamic!
10
+ @dynamic = true
11
+ end
12
+
13
+ def visited!
14
+ @visited = true
15
+ end
16
+
17
+ def dynamic?
18
+ @dynamic
19
+ end
20
+
21
+ def visited?
22
+ @visited
23
+ end
24
+
25
+ def parent_entity
26
+ node = self
27
+ while(node && !node.is_a?(Runbook::Entity))
28
+ node = node.parent
29
+ end
30
+ node
31
+ end
32
+ end
33
+ end