runbook 0.12.1
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/.gitignore +12 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +46 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +999 -0
- data/Rakefile +6 -0
- data/TODO.md +38 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/runbook +5 -0
- data/images/runbook_anatomy_diagram.png +0 -0
- data/images/runbook_example.gif +0 -0
- data/images/runbook_execution_modes.png +0 -0
- data/lib/hacks/ssh_kit.rb +58 -0
- data/lib/runbook/cli.rb +90 -0
- data/lib/runbook/configuration.rb +110 -0
- data/lib/runbook/dsl.rb +21 -0
- data/lib/runbook/entities/book.rb +17 -0
- data/lib/runbook/entities/section.rb +7 -0
- data/lib/runbook/entities/step.rb +7 -0
- data/lib/runbook/entity.rb +127 -0
- data/lib/runbook/errors.rb +7 -0
- data/lib/runbook/extensions/add.rb +13 -0
- data/lib/runbook/extensions/description.rb +14 -0
- data/lib/runbook/extensions/sections.rb +15 -0
- data/lib/runbook/extensions/shared_variables.rb +51 -0
- data/lib/runbook/extensions/ssh_config.rb +76 -0
- data/lib/runbook/extensions/statements.rb +26 -0
- data/lib/runbook/extensions/steps.rb +14 -0
- data/lib/runbook/extensions/tmux.rb +13 -0
- data/lib/runbook/helpers/format_helper.rb +11 -0
- data/lib/runbook/helpers/ssh_kit_helper.rb +143 -0
- data/lib/runbook/helpers/tmux_helper.rb +174 -0
- data/lib/runbook/hooks.rb +88 -0
- data/lib/runbook/node.rb +23 -0
- data/lib/runbook/run.rb +283 -0
- data/lib/runbook/runner.rb +64 -0
- data/lib/runbook/runs/ssh_kit.rb +186 -0
- data/lib/runbook/statement.rb +22 -0
- data/lib/runbook/statements/ask.rb +11 -0
- data/lib/runbook/statements/assert.rb +25 -0
- data/lib/runbook/statements/capture.rb +14 -0
- data/lib/runbook/statements/capture_all.rb +14 -0
- data/lib/runbook/statements/command.rb +11 -0
- data/lib/runbook/statements/confirm.rb +10 -0
- data/lib/runbook/statements/description.rb +9 -0
- data/lib/runbook/statements/download.rb +12 -0
- data/lib/runbook/statements/layout.rb +10 -0
- data/lib/runbook/statements/note.rb +10 -0
- data/lib/runbook/statements/notice.rb +10 -0
- data/lib/runbook/statements/ruby_command.rb +9 -0
- data/lib/runbook/statements/tmux_command.rb +11 -0
- data/lib/runbook/statements/upload.rb +12 -0
- data/lib/runbook/statements/wait.rb +10 -0
- data/lib/runbook/toolbox.rb +43 -0
- data/lib/runbook/util/repo.rb +56 -0
- data/lib/runbook/util/runbook.rb +25 -0
- data/lib/runbook/util/sticky_hash.rb +26 -0
- data/lib/runbook/util/stored_pose.rb +54 -0
- data/lib/runbook/version.rb +3 -0
- data/lib/runbook/view.rb +24 -0
- data/lib/runbook/viewer.rb +24 -0
- data/lib/runbook/views/markdown.rb +109 -0
- data/lib/runbook.rb +110 -0
- data/runbook.gemspec +48 -0
- data/samples/hooks_runbook.rb +72 -0
- data/samples/layout_runbook.rb +26 -0
- data/samples/restart_nginx.rb +26 -0
- data/samples/simple_runbook.rb +41 -0
- metadata +324 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
module Runbook::Extensions
|
2
|
+
module SSHConfig
|
3
|
+
def self.blank_ssh_config
|
4
|
+
{
|
5
|
+
servers: [],
|
6
|
+
parallelization: {},
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
def ssh_config
|
11
|
+
@ssh_config ||= Runbook::Extensions::SSHConfig.blank_ssh_config
|
12
|
+
end
|
13
|
+
|
14
|
+
module DSL
|
15
|
+
def ssh_config(&block)
|
16
|
+
config = Class.new do
|
17
|
+
attr_reader :dsl
|
18
|
+
prepend Runbook::Extensions::SSHConfig
|
19
|
+
end.new
|
20
|
+
dsl_class = Runbook::DSL.class(
|
21
|
+
Runbook::Extensions::SSHConfig::DSL,
|
22
|
+
)
|
23
|
+
config.instance_variable_set(:@dsl, dsl_class.new(config))
|
24
|
+
config.dsl.instance_eval(&block)
|
25
|
+
config.ssh_config
|
26
|
+
end
|
27
|
+
|
28
|
+
def parallelization(strategy: , limit: 2, wait: 2)
|
29
|
+
parent.ssh_config[:parallelization] = {
|
30
|
+
strategy: strategy,
|
31
|
+
limit: limit,
|
32
|
+
wait: wait,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def server(server)
|
37
|
+
parent.ssh_config[:servers].clear
|
38
|
+
parent.ssh_config[:servers] << server
|
39
|
+
end
|
40
|
+
|
41
|
+
def servers(*servers)
|
42
|
+
parent.ssh_config[:servers].clear
|
43
|
+
servers.flatten.each do |server|
|
44
|
+
parent.ssh_config[:servers] << server
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def path(path)
|
49
|
+
parent.ssh_config[:path] = path
|
50
|
+
end
|
51
|
+
|
52
|
+
def user(user)
|
53
|
+
parent.ssh_config[:user] = user
|
54
|
+
end
|
55
|
+
|
56
|
+
def group(group)
|
57
|
+
parent.ssh_config[:group] = group
|
58
|
+
end
|
59
|
+
|
60
|
+
def env(env)
|
61
|
+
parent.ssh_config[:env] = env
|
62
|
+
end
|
63
|
+
|
64
|
+
def umask(umask)
|
65
|
+
parent.ssh_config[:umask] = umask
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Runbook::Entities::Step.prepend(SSHConfig)
|
71
|
+
Runbook::Entities::Step::DSL.prepend(SSHConfig::DSL)
|
72
|
+
Runbook::Entities::Section.prepend(SSHConfig)
|
73
|
+
Runbook::Entities::Section::DSL.prepend(SSHConfig::DSL)
|
74
|
+
Runbook::Entities::Book.prepend(SSHConfig)
|
75
|
+
Runbook::Entities::Book::DSL.prepend(SSHConfig::DSL)
|
76
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Runbook::Extensions
|
2
|
+
module Statements
|
3
|
+
module DSL
|
4
|
+
def method_missing(name, *args, &block)
|
5
|
+
if (klass = Statements::DSL._statement_class(name))
|
6
|
+
klass.new(*args, &block).tap do |statement|
|
7
|
+
parent.add(statement)
|
8
|
+
end
|
9
|
+
else
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def respond_to?(name, include_private = false)
|
15
|
+
!!(Statements::DSL._statement_class(name) || super)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self._statement_class(name)
|
19
|
+
"Runbook::Statements::#{name.to_s.camelize}".constantize
|
20
|
+
rescue NameError
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Runbook::Entities::Step::DSL.prepend(Statements::DSL)
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Runbook::Extensions
|
2
|
+
module Steps
|
3
|
+
module DSL
|
4
|
+
def step(title=nil, &block)
|
5
|
+
Runbook::Entities::Step.new(title).tap do |step|
|
6
|
+
parent.add(step)
|
7
|
+
step.dsl.instance_eval(&block) if block
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Runbook::Entities::Section::DSL.prepend(Steps::DSL)
|
14
|
+
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,174 @@
|
|
1
|
+
module Runbook::Helpers
|
2
|
+
module TmuxHelper
|
3
|
+
def setup_layout(structure, runbook_title:)
|
4
|
+
_remove_stale_layouts
|
5
|
+
layout_file = _layout_file(_slug(runbook_title))
|
6
|
+
if File.exists?(layout_file)
|
7
|
+
stored_layout = ::YAML::load_file(layout_file)
|
8
|
+
if _all_panes_exist?(stored_layout)
|
9
|
+
return stored_layout
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
_setup_layout(structure).tap do |layout_panes|
|
14
|
+
File.open(layout_file, 'w') do |f|
|
15
|
+
f.write(layout_panes.to_yaml)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def send_keys(command, target)
|
21
|
+
`tmux send-keys -t #{target} #{_pager_escape_sequence} '#{command}' C-m`
|
22
|
+
end
|
23
|
+
|
24
|
+
def kill_all_panes(layout_panes)
|
25
|
+
runbook_pane = _runbook_pane
|
26
|
+
layout_panes.values.each do |pane_id|
|
27
|
+
_kill_pane(pane_id) unless pane_id == runbook_pane
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def _pager_escape_sequence
|
32
|
+
"q C-u"
|
33
|
+
end
|
34
|
+
|
35
|
+
def _kill_pane(pane_id)
|
36
|
+
`tmux kill-pane -t #{pane_id}`
|
37
|
+
end
|
38
|
+
|
39
|
+
def _layout_file(runbook_title)
|
40
|
+
`tmux display-message -p -t $TMUX_PANE "#{Dir.tmpdir}/runbook_layout_\#{pid}_\#{session_name}_\#{pane_pid}_\#{pane_id}_#{runbook_title}.yml"`.strip
|
41
|
+
end
|
42
|
+
|
43
|
+
def _slug(title)
|
44
|
+
title.titleize.gsub(/\s+/, "").underscore.dasherize
|
45
|
+
end
|
46
|
+
|
47
|
+
def _all_panes_exist?(stored_layout)
|
48
|
+
(stored_layout.values - _session_panes).empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
def _remove_stale_layouts
|
52
|
+
session_panes = _session_panes
|
53
|
+
session_layout_files = _session_layout_files
|
54
|
+
session_layout_files.each do |file|
|
55
|
+
File.delete(file) unless session_panes.any? { |pane| /_#{pane}_/ =~ file }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def _session_panes
|
60
|
+
`tmux list-panes -s -F '#D'`.split("\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
def _session_layout_files
|
64
|
+
session_layout_glob = `tmux display-message -p "#{Dir.tmpdir}/runbook_layout_\#{pid}_\#{session_name}_*.yml"`.strip
|
65
|
+
Dir[session_layout_glob]
|
66
|
+
end
|
67
|
+
|
68
|
+
def _setup_layout(structure)
|
69
|
+
current_pane = _runbook_pane
|
70
|
+
panes_to_init = []
|
71
|
+
{}.tap do |layout_panes|
|
72
|
+
if structure.is_a?(Hash)
|
73
|
+
first_window = true
|
74
|
+
structure.each do |name, window|
|
75
|
+
if first_window
|
76
|
+
_rename_window(name)
|
77
|
+
first_window = false
|
78
|
+
else
|
79
|
+
current_pane = _new_window(name)
|
80
|
+
end
|
81
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, window)
|
82
|
+
end
|
83
|
+
else
|
84
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure)
|
85
|
+
end
|
86
|
+
_swap_runbook_pane(panes_to_init, layout_panes)
|
87
|
+
_initialize_panes(panes_to_init, layout_panes)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def _setup_panes(layout_panes, panes_to_init, current_pane, structure, depth=0)
|
92
|
+
return if structure.empty?
|
93
|
+
case structure
|
94
|
+
when Array
|
95
|
+
case structure.size
|
96
|
+
when 1
|
97
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure.shift, depth+1)
|
98
|
+
else
|
99
|
+
size = 100 - 100 / structure.size
|
100
|
+
new_pane = _split(current_pane, depth, size)
|
101
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure.shift, depth+1)
|
102
|
+
_setup_panes(layout_panes, panes_to_init, new_pane, structure, depth)
|
103
|
+
end
|
104
|
+
when Hash
|
105
|
+
if structure.values.all? { |v| v.is_a?(Numeric) }
|
106
|
+
total_size = structure.values.reduce(:+)
|
107
|
+
case structure.size
|
108
|
+
when 1
|
109
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure.keys[0], depth+1)
|
110
|
+
else
|
111
|
+
size = (total_size - structure.values[0]) * 100 / total_size
|
112
|
+
new_pane = _split(current_pane, depth, size)
|
113
|
+
first_struct = structure.keys[0]
|
114
|
+
structure.delete(first_struct)
|
115
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, first_struct, depth+1)
|
116
|
+
_setup_panes(layout_panes, panes_to_init, new_pane, structure, depth)
|
117
|
+
end
|
118
|
+
else
|
119
|
+
layout_panes[structure[:name]] = current_pane
|
120
|
+
panes_to_init << structure
|
121
|
+
end
|
122
|
+
when Symbol
|
123
|
+
layout_panes[structure] = current_pane
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def _swap_runbook_pane(panes_to_init, layout_panes)
|
128
|
+
if (runbook_pane = panes_to_init.find { |pane| pane[:runbook_pane] })
|
129
|
+
current_runbook_pane_name = layout_panes.keys.find do |k|
|
130
|
+
layout_panes[k] == _runbook_pane
|
131
|
+
end
|
132
|
+
target_pane_id = layout_panes[runbook_pane[:name]]
|
133
|
+
layout_panes[runbook_pane[:name]] = _runbook_pane
|
134
|
+
layout_panes[current_runbook_pane_name] = target_pane_id
|
135
|
+
_swap_panes(target_pane_id, _runbook_pane)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def _initialize_panes(panes_to_init, layout_panes)
|
140
|
+
panes_to_init.each do |pane|
|
141
|
+
target = layout_panes[pane[:name]]
|
142
|
+
_set_directory(pane[:directory], target) if pane[:directory]
|
143
|
+
send_keys(pane[:command], target) if pane[:command]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def _runbook_pane
|
148
|
+
@runbook_pane ||= `tmux display-message -p '#D'`.strip
|
149
|
+
end
|
150
|
+
|
151
|
+
def _rename_window(name)
|
152
|
+
`tmux rename-window "#{name}"`
|
153
|
+
end
|
154
|
+
|
155
|
+
def _new_window(name)
|
156
|
+
`tmux new-window -n "#{name}" -P -F '#D' -d`.strip
|
157
|
+
end
|
158
|
+
|
159
|
+
def _split(current_pane, depth, size)
|
160
|
+
direction = depth.even? ? "h" : "v"
|
161
|
+
command = "tmux split-window"
|
162
|
+
args = "-#{direction} -t #{current_pane} -p #{size} -P -F '#D' -d"
|
163
|
+
`#{command} #{args}`.strip
|
164
|
+
end
|
165
|
+
|
166
|
+
def _swap_panes(target_pane, source_pane)
|
167
|
+
`tmux swap-pane -d -t #{target_pane} -s #{source_pane}`
|
168
|
+
end
|
169
|
+
|
170
|
+
def _set_directory(directory, target)
|
171
|
+
send_keys("cd #{directory}; clear", target)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
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
|
data/lib/runbook/node.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Runbook
|
2
|
+
class Node
|
3
|
+
def initialize
|
4
|
+
raise "Should not be initialized"
|
5
|
+
end
|
6
|
+
|
7
|
+
def dynamic!
|
8
|
+
@dynamic = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def visited!
|
12
|
+
@visited = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def dynamic?
|
16
|
+
@dynamic
|
17
|
+
end
|
18
|
+
|
19
|
+
def visited?
|
20
|
+
@visited
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|