careacademy-runbook 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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