dk 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +643 -1
  3. data/bin/dk +7 -0
  4. data/dk.gemspec +7 -3
  5. data/lib/dk/ansi.rb +98 -0
  6. data/lib/dk/cli.rb +173 -0
  7. data/lib/dk/config.rb +217 -0
  8. data/lib/dk/config_runner.rb +24 -0
  9. data/lib/dk/dk_runner.rb +13 -0
  10. data/lib/dk/dry_runner.rb +43 -0
  11. data/lib/dk/has_set_param.rb +42 -0
  12. data/lib/dk/has_ssh_opts.rb +36 -0
  13. data/lib/dk/has_the_runs.rb +23 -0
  14. data/lib/dk/has_the_stubs.rb +116 -0
  15. data/lib/dk/local.rb +84 -0
  16. data/lib/dk/null_logger.rb +13 -0
  17. data/lib/dk/remote.rb +132 -0
  18. data/lib/dk/runner.rb +202 -0
  19. data/lib/dk/task.rb +266 -0
  20. data/lib/dk/task_run.rb +17 -0
  21. data/lib/dk/test_runner.rb +54 -0
  22. data/lib/dk/tree_runner.rb +64 -0
  23. data/lib/dk/version.rb +1 -1
  24. data/lib/dk.rb +23 -1
  25. data/test/helper.rb +6 -1
  26. data/test/support/config/dk.rb +7 -0
  27. data/test/support/config/task_defs.rb +10 -0
  28. data/test/support/factory.rb +38 -0
  29. data/test/support/log/.gitkeep +0 -0
  30. data/test/system/has_the_stubs_tests.rb +355 -0
  31. data/test/system/runner_tests.rb +222 -0
  32. data/test/unit/ansi_tests.rb +40 -0
  33. data/test/unit/cli_tests.rb +317 -0
  34. data/test/unit/config_runner_tests.rb +60 -0
  35. data/test/unit/config_tests.rb +427 -0
  36. data/test/unit/dk_runner_tests.rb +34 -0
  37. data/test/unit/dk_tests.rb +49 -0
  38. data/test/unit/dry_runner_tests.rb +71 -0
  39. data/test/unit/has_set_param_tests.rb +46 -0
  40. data/test/unit/has_ssh_opts_tests.rb +81 -0
  41. data/test/unit/has_the_runs_tests.rb +37 -0
  42. data/test/unit/has_the_stubs_tests.rb +279 -0
  43. data/test/unit/local_tests.rb +174 -0
  44. data/test/unit/null_logger_tests.rb +17 -0
  45. data/test/unit/remote_tests.rb +330 -0
  46. data/test/unit/runner_tests.rb +398 -0
  47. data/test/unit/task_run_tests.rb +40 -0
  48. data/test/unit/task_tests.rb +943 -0
  49. data/test/unit/test_runner_tests.rb +189 -0
  50. data/test/unit/tree_runner_tests.rb +152 -0
  51. metadata +106 -9
@@ -0,0 +1,116 @@
1
+ require 'much-plugin'
2
+ require 'dk/local'
3
+ require 'dk/remote'
4
+
5
+ module Dk
6
+
7
+ module HasTheStubs
8
+ include MuchPlugin
9
+
10
+ plugin_included do
11
+ include InstanceMethods
12
+
13
+ end
14
+
15
+ module InstanceMethods
16
+
17
+ # cmd stub api
18
+
19
+ def local_cmd_stubs
20
+ @local_cmd_stubs ||= []
21
+ end
22
+
23
+ def stub_cmd(cmd_str, args = nil, &block)
24
+ args ||= {}
25
+
26
+ cmd_str_proc = get_cmd_ssh_proc(cmd_str)
27
+ input_proc = get_cmd_ssh_proc(args[:input])
28
+ given_opts_proc = get_cmd_ssh_proc(args[:opts])
29
+
30
+ local_cmd_stubs.unshift(
31
+ Stub.new(cmd_str_proc, input_proc, given_opts_proc, block)
32
+ )
33
+ end
34
+
35
+ def unstub_all_cmds
36
+ local_cmd_stubs.clear
37
+ end
38
+
39
+ # ssh stub API
40
+
41
+ def remote_cmd_stubs
42
+ @remote_cmd_stubs ||= []
43
+ end
44
+
45
+ def stub_ssh(cmd_str, args = nil, &block)
46
+ args ||= {}
47
+
48
+ cmd_str_proc = get_cmd_ssh_proc(cmd_str)
49
+ input_proc = get_cmd_ssh_proc(args[:input])
50
+ given_opts_proc = get_cmd_ssh_proc(args[:opts])
51
+
52
+ remote_cmd_stubs.unshift(
53
+ Stub.new(cmd_str_proc, input_proc, given_opts_proc, block)
54
+ )
55
+ end
56
+
57
+ def unstub_all_ssh
58
+ remote_cmd_stubs.clear
59
+ end
60
+
61
+ private
62
+
63
+ def get_cmd_ssh_proc(obj)
64
+ obj.kind_of?(::Proc) ? obj : proc{ obj }
65
+ end
66
+
67
+ def find_cmd_ssh_stub_block(stubs, task, cmd_str, input, given_opts)
68
+ stub = stubs.find do |stub|
69
+ task.instance_eval(&stub.cmd_str_proc) == cmd_str &&
70
+ task.instance_eval(&stub.input_proc) == input &&
71
+ task.instance_eval(&stub.given_opts_proc) == given_opts
72
+ end
73
+ stub ? stub.block : nil
74
+ end
75
+
76
+ # if the cmd is stubbed, build a spy and apply the stub (or return the
77
+ # cached spy), otherwise let the runner decide how to handle the local
78
+ # cmd
79
+ def build_local_cmd(task, cmd_str, input, given_opts)
80
+ b = find_cmd_ssh_stub_block(local_cmd_stubs, task, cmd_str, input, given_opts)
81
+ if b
82
+ Local::CmdSpy.new(cmd_str, given_opts).tap(&b)
83
+ else
84
+ has_the_stubs_build_local_cmd(cmd_str, given_opts)
85
+ end
86
+ end
87
+
88
+ def has_the_stubs_build_local_cmd(cmd_str, given_opts)
89
+ raise NotImplementedError
90
+ end
91
+
92
+ # if the cmd is stubbed, build a spy and apply the stub (or return the
93
+ # cached spy), otherwise let the runner decide how to handle the remote
94
+ # cmd; when building the spy use the ssh opts, this allows stubbing and
95
+ # calling ssh cmds with the same opts but also allows building a valid
96
+ # remote cmd that has an ssh host
97
+ def build_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
98
+ b = find_cmd_ssh_stub_block(remote_cmd_stubs, task, cmd_str, input, given_opts)
99
+ if b
100
+ Remote::CmdSpy.new(cmd_str, ssh_opts).tap(&b)
101
+ else
102
+ has_the_stubs_build_remote_cmd(cmd_str, ssh_opts)
103
+ end
104
+ end
105
+
106
+ def has_the_stubs_build_remote_cmd(cmd_str, ssh_opts)
107
+ raise NotImplementedError
108
+ end
109
+
110
+ end
111
+
112
+ Stub = Struct.new(:cmd_str_proc, :input_proc, :given_opts_proc, :block)
113
+
114
+ end
115
+
116
+ end
data/lib/dk/local.rb ADDED
@@ -0,0 +1,84 @@
1
+ require 'scmd'
2
+
3
+ module Dk; end
4
+ module Dk::Local
5
+
6
+ class BaseCmd
7
+
8
+ attr_reader :scmd, :cmd_str
9
+
10
+ def initialize(scmd_or_spy_klass, cmd_str, opts)
11
+ opts ||= {}
12
+
13
+ @cmd_str = cmd_str
14
+ @scmd = scmd_or_spy_klass.new(@cmd_str, :env => opts[:env])
15
+ end
16
+
17
+ def to_s; self.cmd_str; end
18
+
19
+ def run(input = nil)
20
+ @scmd.run(input)
21
+ self
22
+ end
23
+
24
+ def stdout; @scmd.stdout; end
25
+ def stderr; @scmd.stderr; end
26
+ def success?; @scmd.success?; end
27
+
28
+ def output_lines
29
+ build_stdout_lines(self.stdout) + build_stderr_lines(self.stderr)
30
+ end
31
+
32
+ private
33
+
34
+ def build_stdout_lines(stdout)
35
+ build_output_lines('stdout', stdout)
36
+ end
37
+
38
+ def build_stderr_lines(stderr)
39
+ build_output_lines('stderr', stderr)
40
+ end
41
+
42
+ def build_output_lines(name, output)
43
+ output.to_s.strip.split("\n").map{ |line| OutputLine.new(name, line) }
44
+ end
45
+
46
+ OutputLine = Struct.new(:name, :line)
47
+
48
+ end
49
+
50
+ class Cmd < BaseCmd
51
+
52
+ def initialize(cmd_str, opts = nil)
53
+ super(Scmd, cmd_str, opts)
54
+ end
55
+
56
+ end
57
+
58
+ class CmdSpy < BaseCmd
59
+
60
+ attr_reader :cmd_opts
61
+
62
+ def initialize(cmd_str, opts = nil)
63
+ require 'scmd/command_spy'
64
+ super(Scmd::CommandSpy, cmd_str, opts)
65
+ @cmd_opts = opts
66
+ end
67
+
68
+ def run_input
69
+ return nil unless self.run_called?
70
+ self.run_calls.first.input
71
+ end
72
+
73
+ def stdout=(value); @scmd.stdout = value; end
74
+ def stderr=(value); @scmd.stderr = value; end
75
+ def exitstatus=(value); @scmd.exitstatus = value; end
76
+
77
+ def run_calls; @scmd.run_calls; end
78
+ def run_called?; @scmd.run_called?; end
79
+
80
+ def ssh?; false; end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,13 @@
1
+ require 'logger'
2
+
3
+ module Dk
4
+
5
+ class NullLogger
6
+
7
+ ::Logger::Severity.constants.each do |name|
8
+ define_method(name.downcase){ |*args| } # no-op
9
+ end
10
+
11
+ end
12
+
13
+ end
data/lib/dk/remote.rb ADDED
@@ -0,0 +1,132 @@
1
+ require 'dk/config'
2
+ require 'dk/local'
3
+
4
+ module Dk; end
5
+ module Dk::Remote
6
+
7
+ def self.ssh_cmd_str(cmd_str, host, args, host_args)
8
+ host_args = host_args[host.to_s] if !host.nil?
9
+ val = "\"#{cmd_str.gsub(/\s+/, ' ')}\"".gsub("\\", "\\\\\\").gsub('"', '\"')
10
+ "ssh #{args} #{host_args} #{host} -- \"sh -c #{val}\""
11
+ end
12
+
13
+ class BaseCmd
14
+
15
+ attr_reader :hosts, :ssh_args, :host_ssh_args, :cmd_str, :local_cmds
16
+
17
+ def initialize(local_cmd_or_spy_klass, cmd_str, opts)
18
+ opts ||= {}
19
+ if nil_or_empty_or_missing_hosts(opts[:hosts])
20
+ raise NoHostsError, "no hosts to run cmd on (#{opts[:hosts].inspect})"
21
+ end
22
+
23
+ @hosts = opts[:hosts].sort
24
+ @ssh_args = opts[:ssh_args] || Dk::Config::DEFAULT_SSH_ARGS.dup
25
+ @host_ssh_args = opts[:host_ssh_args] || Dk::Config::DEFAULT_HOST_SSH_ARGS.dup
26
+ @cmd_str = cmd_str
27
+
28
+ @local_cmds = @hosts.inject({}) do |cmds, host|
29
+ cmds[host] = local_cmd_or_spy_klass.new(self.ssh_cmd_str(host), {
30
+ :env => opts[:env],
31
+ :dry_tree_run => opts[:dry_tree_run]
32
+ })
33
+ cmds
34
+ end
35
+ end
36
+
37
+ def to_s; self.cmd_str; end
38
+
39
+ def ssh_cmd_str(host)
40
+ build_ssh_cmd_str(@cmd_str, host, @ssh_args, @host_ssh_args)
41
+ end
42
+
43
+ def run(input = nil)
44
+ self.hosts.each{ |host| @local_cmds[host].scmd.start(input) }
45
+ self.hosts.each{ |host| @local_cmds[host].scmd.wait }
46
+ self
47
+ end
48
+
49
+ def stdout
50
+ self.hosts.inject('') do |out, host|
51
+ out.empty? ? @local_cmds[host].stdout.to_s : out
52
+ end
53
+ end
54
+
55
+ def stderr
56
+ self.hosts.inject('') do |err, host|
57
+ err.empty? ? @local_cmds[host].stderr.to_s : err
58
+ end
59
+ end
60
+
61
+ def success?
62
+ self.hosts.inject(true) do |success, host|
63
+ success && @local_cmds[host].success?
64
+ end
65
+ end
66
+
67
+ def output_lines
68
+ self.hosts.inject([]) do |lines, host|
69
+ lines + build_output_lines(host, @local_cmds[host].output_lines)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def nil_or_empty_or_missing_hosts(h)
76
+ h.nil? ||
77
+ !h.respond_to?(:empty?) || h.empty? ||
78
+ !h.respond_to?(:select) || h.select(&:nil?).size > 0
79
+ end
80
+
81
+ # escape everything properly; run in sh to ensure full profile is loaded
82
+ def build_ssh_cmd_str(cmd_str, host, args, host_args)
83
+ Dk::Remote.ssh_cmd_str(cmd_str, host, args, host_args)
84
+ end
85
+
86
+ def build_output_lines(host, local_cmd_output_lines)
87
+ local_cmd_output_lines.map{ |ol| OutputLine.new(host, ol.name, ol.line) }
88
+ end
89
+
90
+ OutputLine = Struct.new(:host, :name, :line)
91
+
92
+ end
93
+
94
+ NoHostsError = Class.new(ArgumentError)
95
+
96
+ class Cmd < BaseCmd
97
+
98
+ def initialize(cmd_str, opts = nil)
99
+ super(Dk::Local::Cmd, cmd_str, opts)
100
+ end
101
+
102
+ end
103
+
104
+ class CmdSpy < BaseCmd
105
+
106
+ attr_reader :cmd_opts
107
+
108
+ def initialize(cmd_str, opts = nil)
109
+ super(Dk::Local::CmdSpy, cmd_str, opts)
110
+ @cmd_opts = opts
111
+ @first_local_cmd_spy = @local_cmds[@hosts.first]
112
+ end
113
+
114
+ def run_input
115
+ return nil unless self.run_called?
116
+ self.run_calls.first.input
117
+ end
118
+
119
+ # just set the first local cmd, this will have an overall effect
120
+ def stdout=(value); @first_local_cmd_spy.stdout = value; end
121
+ def stderr=(value); @first_local_cmd_spy.stderr = value; end
122
+ def exitstatus=(value); @first_local_cmd_spy.exitstatus = value; end
123
+
124
+ # just query the firs tlocal cmd - if run for one it was run for all
125
+ def run_calls; @first_local_cmd_spy.scmd.start_calls; end
126
+ def run_called?; @first_local_cmd_spy.scmd.start_called?; end
127
+
128
+ def ssh?; true; end
129
+
130
+ end
131
+
132
+ end
data/lib/dk/runner.rb ADDED
@@ -0,0 +1,202 @@
1
+ require 'benchmark'
2
+ require 'set'
3
+ require 'dk'
4
+ require 'dk/ansi'
5
+ require 'dk/config'
6
+ require 'dk/has_set_param'
7
+ require 'dk/has_ssh_opts'
8
+ require 'dk/local'
9
+ require 'dk/null_logger'
10
+ require 'dk/remote'
11
+
12
+ module Dk
13
+
14
+ class Runner
15
+ include Dk::HasSetParam
16
+ include Dk::HasSSHOpts
17
+
18
+ TASK_START_LOG_PREFIX = ' >>> '.freeze
19
+ TASK_END_LOG_PREFIX = ' <<< '.freeze
20
+ INDENT_LOG_PREFIX = ' '.freeze
21
+ CMD_LOG_PREFIX = '[CMD] '.freeze
22
+ SSH_LOG_PREFIX = '[SSH] '.freeze
23
+ CMD_SSH_OUT_LOG_PREFIX = "> ".freeze
24
+
25
+ attr_reader :params, :logger
26
+
27
+ def initialize(opts = nil)
28
+ opts ||= {}
29
+ @params = Hash.new{ |h, k| raise Dk::NoParamError, "no param named `#{k}`" }
30
+ @params.merge!(dk_normalize_params(opts[:params]))
31
+
32
+ d = Config::DEFAULT_CALLBACKS
33
+ @task_callbacks = {
34
+ 'before' => opts[:before_callbacks] || d.dup,
35
+ 'prepend_before' => opts[:prepend_before_callbacks] || d.dup,
36
+ 'after' => opts[:after_callbacks] || d.dup,
37
+ 'prepend_after' => opts[:prepend_after_callbacks] || d.dup
38
+ }
39
+
40
+ @ssh_hosts = opts[:ssh_hosts] || Config::DEFAULT_SSH_HOSTS.dup
41
+ @ssh_args = opts[:ssh_args] || Config::DEFAULT_SSH_ARGS.dup
42
+ @host_ssh_args = opts[:host_ssh_args] || Config::DEFAULT_HOST_SSH_ARGS.dup
43
+
44
+ @logger = opts[:logger] || NullLogger.new
45
+
46
+ @has_run_task_classes = Set.new
47
+ end
48
+
49
+ def task_callbacks(named, task_class)
50
+ @task_callbacks[named][task_class] || []
51
+ end
52
+
53
+ def task_callback_task_classes(named, task_class)
54
+ task_callbacks(named, task_class).map(&:task_class)
55
+ end
56
+
57
+ def add_task_callback(named, subject_task_class, callback_task_class, params)
58
+ @task_callbacks[named][subject_task_class] << Task::Callback.new(
59
+ callback_task_class,
60
+ params
61
+ )
62
+ end
63
+
64
+ # called by CLI on top-level tasks
65
+ def run(task_class, params = nil)
66
+ check_run_once_and_build_and_run_task(task_class, params)
67
+ end
68
+
69
+ # called by other tasks on sub-tasks
70
+ def run_task(task_class, params = nil)
71
+ check_run_once_and_build_and_run_task(task_class, params)
72
+ end
73
+
74
+ def log_info(msg, *ansi_styles)
75
+ self.logger.info("#{INDENT_LOG_PREFIX}#{Ansi.styled_msg(msg, *ansi_styles)}")
76
+ end
77
+
78
+ def log_debug(msg, *ansi_styles)
79
+ self.logger.debug("#{INDENT_LOG_PREFIX}#{Ansi.styled_msg(msg, *ansi_styles)}")
80
+ end
81
+
82
+ def log_error(msg, *ansi_styles)
83
+ self.logger.error("#{INDENT_LOG_PREFIX}#{Ansi.styled_msg(msg, *ansi_styles)}")
84
+ end
85
+
86
+ def log_task_run(task_class, &run_block)
87
+ self.logger.debug "#{TASK_START_LOG_PREFIX}#{task_class}"
88
+ time = Benchmark.realtime(&run_block)
89
+ self.logger.debug "#{TASK_END_LOG_PREFIX}#{task_class} (#{self.pretty_run_time(time)})"
90
+ end
91
+
92
+ def log_cli_task_run(task_name, &run_block)
93
+ self.logger.info "Starting `#{task_name}`."
94
+ time = Benchmark.realtime(&run_block)
95
+ self.logger.info "`#{task_name}` finished in #{self.pretty_run_time(time)}."
96
+ self.logger.info ""
97
+ self.logger.info ""
98
+ end
99
+
100
+ def log_cli_run(cli_argv, &run_block)
101
+ 15.times{ self.logger.debug "" }
102
+ self.logger.debug "===================================="
103
+ self.logger.debug ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> `#{cli_argv}`"
104
+ self.logger.debug "===================================="
105
+ time = Benchmark.realtime(&run_block)
106
+ self.logger.info "(#{self.pretty_run_time(time)})"
107
+ self.logger.debug "===================================="
108
+ self.logger.debug "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< `#{cli_argv}`"
109
+ self.logger.debug "===================================="
110
+ end
111
+
112
+ def cmd(task, cmd_str, input, given_opts)
113
+ build_and_run_local_cmd(task, cmd_str, input, given_opts)
114
+ end
115
+
116
+ def ssh(task, cmd_str, input, given_opts, ssh_opts)
117
+ build_and_run_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
118
+ end
119
+
120
+ def has_run_task?(task_class)
121
+ @has_run_task_classes.include?(task_class)
122
+ end
123
+
124
+ def pretty_run_time(raw_run_time)
125
+ if raw_run_time > 1.5 # seconds
126
+ "#{raw_run_time.to_i / 60}:#{(raw_run_time.round % 60).to_i.to_s.rjust(2, '0')}s"
127
+ else
128
+ "#{(raw_run_time * 1000 * 10.0).round / 10.0}ms"
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ def check_run_once_and_build_and_run_task(task_class, params = nil)
135
+ if task_class.run_only_once && self.has_run_task?(task_class)
136
+ build_task(task_class, params)
137
+ else
138
+ build_and_run_task(task_class, params)
139
+ end
140
+ end
141
+
142
+ def build_and_run_task(task_class, params = nil)
143
+ build_task(task_class, params).tap do |task|
144
+ task.dk_run
145
+ @has_run_task_classes << task_class
146
+ end
147
+ end
148
+
149
+ def build_task(task_class, params = nil)
150
+ task_class.new(self, params)
151
+ end
152
+
153
+ def build_and_run_local_cmd(task, cmd_str, input, given_opts)
154
+ local_cmd = build_local_cmd(task, cmd_str, input, given_opts)
155
+ log_local_cmd(local_cmd){ |cmd| cmd.run(input) }
156
+ end
157
+
158
+ # input is needed for the `TestRunner` so it can use it with stubbing
159
+ # otherwise it is ignored when building a local cmd
160
+ def build_local_cmd(task, cmd_str, input, given_opts)
161
+ Local::Cmd.new(cmd_str, given_opts)
162
+ end
163
+
164
+ def log_local_cmd(cmd, &block)
165
+ self.logger.info("#{CMD_LOG_PREFIX}#{cmd.cmd_str}")
166
+ time = Benchmark.realtime{ block.call(cmd) }
167
+ self.logger.info("#{INDENT_LOG_PREFIX}(#{self.pretty_run_time(time)})")
168
+ cmd.output_lines.each do |output_line|
169
+ self.logger.debug("#{INDENT_LOG_PREFIX}#{CMD_SSH_OUT_LOG_PREFIX}#{output_line.line}")
170
+ end
171
+ cmd
172
+ end
173
+
174
+ def build_and_run_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
175
+ remote_cmd = build_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
176
+ log_remote_cmd(remote_cmd){ |cmd| cmd.run(input) }
177
+ end
178
+
179
+ # input and given opts are needed for the `TestRunner` so it can use it with
180
+ # stubbing otherwise they are ignored when building a remote cmd
181
+ def build_remote_cmd(task, cmd_str, input, given_opts, ssh_opts)
182
+ Remote::Cmd.new(cmd_str, ssh_opts)
183
+ end
184
+
185
+ def log_remote_cmd(cmd, &block)
186
+ self.logger.info("#{SSH_LOG_PREFIX}#{cmd.cmd_str}")
187
+ self.logger.debug("#{INDENT_LOG_PREFIX}#{cmd.ssh_cmd_str('<host>')}")
188
+ cmd.hosts.each do |host|
189
+ self.logger.info("#{INDENT_LOG_PREFIX}[#{host}]")
190
+ end
191
+ time = Benchmark.realtime{ block.call(cmd) }
192
+ self.logger.info("#{INDENT_LOG_PREFIX}(#{self.pretty_run_time(time)})")
193
+ cmd.output_lines.each do |ol|
194
+ self.logger.debug "#{INDENT_LOG_PREFIX}[#{ol.host}] " \
195
+ "#{CMD_SSH_OUT_LOG_PREFIX}#{ol.line}"
196
+ end
197
+ cmd
198
+ end
199
+
200
+ end
201
+
202
+ end