kdeploy 1.2.0 → 1.2.2
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 +4 -4
- data/OPTIMIZATION_COMPLETE.md +211 -0
- data/REFACTORING_SUMMARY.md +174 -0
- data/lib/kdeploy/banner.rb +1 -0
- data/lib/kdeploy/cli.rb +135 -190
- data/lib/kdeploy/command_executor.rb +28 -11
- data/lib/kdeploy/command_grouper.rb +1 -3
- data/lib/kdeploy/dsl.rb +42 -23
- data/lib/kdeploy/executor.rb +49 -25
- data/lib/kdeploy/help_formatter.rb +67 -0
- data/lib/kdeploy/initializer.rb +1 -0
- data/lib/kdeploy/output.rb +2 -0
- data/lib/kdeploy/output_formatter.rb +194 -0
- data/lib/kdeploy/post_install.rb +1 -0
- data/lib/kdeploy/runner.rb +37 -17
- data/lib/kdeploy/template.rb +11 -2
- data/lib/kdeploy/version.rb +1 -1
- data/lib/kdeploy.rb +3 -0
- metadata +5 -43
data/lib/kdeploy/cli.rb
CHANGED
|
@@ -7,6 +7,7 @@ require 'tty-box'
|
|
|
7
7
|
require 'fileutils'
|
|
8
8
|
|
|
9
9
|
module Kdeploy
|
|
10
|
+
# Command-line interface for Kdeploy
|
|
10
11
|
class CLI < Thor
|
|
11
12
|
extend DSL
|
|
12
13
|
|
|
@@ -27,41 +28,7 @@ module Kdeploy
|
|
|
27
28
|
if command
|
|
28
29
|
super
|
|
29
30
|
else
|
|
30
|
-
|
|
31
|
-
puts Kdeploy::Banner.show
|
|
32
|
-
puts <<~HELP
|
|
33
|
-
#{pastel.bright_white('📖 Available Commands:')}
|
|
34
|
-
|
|
35
|
-
#{pastel.bright_yellow('🚀')} #{pastel.bright_white('execute TASK_FILE [TASK]')} Execute deployment tasks from file
|
|
36
|
-
#{pastel.dim(' --limit HOSTS')} Limit to specific hosts (comma-separated)
|
|
37
|
-
#{pastel.dim(' --parallel NUM')} Number of parallel executions (default: 5)
|
|
38
|
-
#{pastel.dim(' --dry-run')} Show what would be done without executing
|
|
39
|
-
|
|
40
|
-
#{pastel.bright_yellow('🆕')} #{pastel.bright_white('init [DIR]')} Initialize new deployment project
|
|
41
|
-
#{pastel.bright_yellow('ℹ️')} #{pastel.bright_white('version')} Show version information
|
|
42
|
-
#{pastel.bright_yellow('❓')} #{pastel.bright_white('help [COMMAND]')} Show help information
|
|
43
|
-
|
|
44
|
-
#{pastel.bright_white('💡 Examples:')}
|
|
45
|
-
|
|
46
|
-
#{pastel.dim('# Initialize a new project')}
|
|
47
|
-
#{pastel.bright_cyan('kdeploy init my-deployment')}
|
|
48
|
-
|
|
49
|
-
#{pastel.dim('# Deploy to web servers')}
|
|
50
|
-
#{pastel.bright_cyan('kdeploy execute deploy.rb deploy_web')}
|
|
51
|
-
|
|
52
|
-
#{pastel.dim('# Backup database')}
|
|
53
|
-
#{pastel.bright_cyan('kdeploy execute deploy.rb backup_db')}
|
|
54
|
-
|
|
55
|
-
#{pastel.dim('# Run maintenance on specific hosts')}
|
|
56
|
-
#{pastel.bright_cyan('kdeploy execute deploy.rb maintenance --limit web01')}
|
|
57
|
-
|
|
58
|
-
#{pastel.dim('# Preview deployment')}
|
|
59
|
-
#{pastel.bright_cyan('kdeploy execute deploy.rb deploy_web --dry-run')}
|
|
60
|
-
|
|
61
|
-
#{pastel.bright_white('📚 Documentation:')}
|
|
62
|
-
#{pastel.bright_cyan('https://github.com/kevin197011/kdeploy')}
|
|
63
|
-
|
|
64
|
-
HELP
|
|
31
|
+
show_general_help
|
|
65
32
|
end
|
|
66
33
|
end
|
|
67
34
|
|
|
@@ -79,40 +46,11 @@ module Kdeploy
|
|
|
79
46
|
method_option :parallel, type: :numeric, default: 10, desc: 'Number of parallel executions'
|
|
80
47
|
method_option :dry_run, type: :boolean, desc: 'Show what would be done'
|
|
81
48
|
def execute(task_file, task_name = nil)
|
|
82
|
-
|
|
83
|
-
@banner_printed ||= false
|
|
84
|
-
unless @banner_printed
|
|
85
|
-
puts Kdeploy::Banner.show
|
|
86
|
-
@banner_printed = true
|
|
87
|
-
end
|
|
88
|
-
|
|
49
|
+
show_banner_once
|
|
89
50
|
load_task_file(task_file)
|
|
90
51
|
|
|
91
|
-
tasks_to_run =
|
|
92
|
-
|
|
93
|
-
else
|
|
94
|
-
self.class.kdeploy_tasks.keys
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
tasks_to_run.each do |task|
|
|
98
|
-
task_hosts = self.class.get_task_hosts(task)
|
|
99
|
-
hosts = filter_hosts(options[:limit], task_hosts)
|
|
100
|
-
|
|
101
|
-
if hosts.empty?
|
|
102
|
-
puts Kdeploy::Banner.show_error("No hosts found for task: #{task}")
|
|
103
|
-
next
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
if options[:dry_run]
|
|
107
|
-
print_dry_run(hosts, task)
|
|
108
|
-
next
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
output = ConsoleOutput.new
|
|
112
|
-
runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: options[:parallel], output: output)
|
|
113
|
-
results = runner.run(task)
|
|
114
|
-
print_results(results, task)
|
|
115
|
-
end
|
|
52
|
+
tasks_to_run = determine_tasks(task_name)
|
|
53
|
+
execute_tasks(tasks_to_run)
|
|
116
54
|
rescue StandardError => e
|
|
117
55
|
puts Kdeploy::Banner.show_error(e.message)
|
|
118
56
|
exit 1
|
|
@@ -121,11 +59,7 @@ module Kdeploy
|
|
|
121
59
|
private
|
|
122
60
|
|
|
123
61
|
def load_task_file(file)
|
|
124
|
-
|
|
125
|
-
puts Kdeploy::Banner.show_error("Task file not found: #{file}")
|
|
126
|
-
exit 1
|
|
127
|
-
end
|
|
128
|
-
|
|
62
|
+
validate_task_file(file)
|
|
129
63
|
# 用 instance_eval 并传递顶层 binding,兼容 heredoc
|
|
130
64
|
self.class.module_eval(File.read(file), file)
|
|
131
65
|
rescue StandardError => e
|
|
@@ -134,6 +68,19 @@ module Kdeploy
|
|
|
134
68
|
raise
|
|
135
69
|
end
|
|
136
70
|
|
|
71
|
+
def validate_task_file(file)
|
|
72
|
+
return if File.exist?(file)
|
|
73
|
+
|
|
74
|
+
puts Kdeploy::Banner.show_error("Task file not found: #{file}")
|
|
75
|
+
exit 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def show_general_help
|
|
79
|
+
formatter = HelpFormatter.new
|
|
80
|
+
puts Kdeploy::Banner.show
|
|
81
|
+
puts formatter.format_help
|
|
82
|
+
end
|
|
83
|
+
|
|
137
84
|
def filter_hosts(limit, task_hosts)
|
|
138
85
|
hosts = self.class.kdeploy_hosts.slice(*task_hosts)
|
|
139
86
|
return hosts unless limit
|
|
@@ -143,139 +90,137 @@ module Kdeploy
|
|
|
143
90
|
end
|
|
144
91
|
|
|
145
92
|
def print_dry_run(hosts, task_name)
|
|
93
|
+
formatter = OutputFormatter.new
|
|
146
94
|
puts Kdeploy::Banner.show
|
|
147
|
-
|
|
148
|
-
puts TTY::Box.frame(
|
|
149
|
-
'Showing what would be done without executing any commands',
|
|
150
|
-
title: { top_left: ' Dry Run Mode ', bottom_right: ' Kdeploy ' },
|
|
151
|
-
style: {
|
|
152
|
-
border: {
|
|
153
|
-
fg: :blue
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
)
|
|
95
|
+
puts formatter.format_dry_run_header
|
|
157
96
|
puts
|
|
158
97
|
|
|
159
98
|
hosts.each do |name, config|
|
|
160
|
-
|
|
161
|
-
output = commands.map do |command|
|
|
162
|
-
case command[:type]
|
|
163
|
-
when :run
|
|
164
|
-
"#{pastel.green('>')} #{command[:command]}"
|
|
165
|
-
when :upload
|
|
166
|
-
"#{pastel.blue('>')} Upload: #{command[:source]} -> #{command[:destination]}"
|
|
167
|
-
end
|
|
168
|
-
end.join("\n")
|
|
169
|
-
|
|
170
|
-
puts TTY::Box.frame(
|
|
171
|
-
output,
|
|
172
|
-
title: { top_left: " #{name} (#{config[:ip]}) " },
|
|
173
|
-
style: {
|
|
174
|
-
border: {
|
|
175
|
-
fg: :yellow
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
)
|
|
179
|
-
puts
|
|
99
|
+
print_dry_run_for_host(name, config, task_name, formatter)
|
|
180
100
|
end
|
|
181
101
|
end
|
|
182
102
|
|
|
103
|
+
def print_dry_run_for_host(name, config, task_name, formatter)
|
|
104
|
+
commands = self.class.kdeploy_tasks[task_name][:block].call
|
|
105
|
+
output = commands.map { |cmd| formatter.format_command_for_dry_run(cmd) }.join("\n")
|
|
106
|
+
title = "#{name} (#{config[:ip]})"
|
|
107
|
+
puts formatter.format_dry_run_box(title, output)
|
|
108
|
+
puts
|
|
109
|
+
end
|
|
110
|
+
|
|
183
111
|
def print_results(results, task_name)
|
|
184
|
-
|
|
185
|
-
puts
|
|
112
|
+
formatter = OutputFormatter.new
|
|
113
|
+
puts formatter.format_task_header(task_name)
|
|
186
114
|
|
|
187
115
|
results.each do |host, result|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
when :upload_template
|
|
211
|
-
puts pastel.yellow(' === Template ===')
|
|
212
|
-
steps.each do |step|
|
|
213
|
-
key = [step[:command], type].hash
|
|
214
|
-
next if shown[key]
|
|
215
|
-
|
|
216
|
-
shown[key] = true
|
|
217
|
-
duration_str = step[:duration] ? pastel.dim(" [#{'%.2f' % step[:duration]}s]") : ''
|
|
218
|
-
puts pastel.yellow(" [template] #{step[:command].sub('upload_template: ', '')}#{duration_str}")
|
|
219
|
-
end
|
|
220
|
-
when :run
|
|
221
|
-
puts pastel.cyan(' === Run ===')
|
|
222
|
-
steps.each do |step|
|
|
223
|
-
key = [step[:command], type].hash
|
|
224
|
-
next if shown[key]
|
|
225
|
-
|
|
226
|
-
shown[key] = true
|
|
227
|
-
duration_str = step[:duration] ? pastel.dim(" [#{'%.2f' % step[:duration]}s]") : ''
|
|
228
|
-
puts pastel.cyan(" [run] #{step[:command].to_s.lines.first.strip}#{duration_str}")
|
|
229
|
-
# 多行命令内容高亮
|
|
230
|
-
cmd_lines = step[:command].to_s.lines[1..].map(&:strip).reject(&:empty?)
|
|
231
|
-
cmd_lines.each { |line| puts pastel.cyan(" > #{line}") } if cmd_lines.any?
|
|
232
|
-
if step[:output].is_a?(Hash) && step[:output][:stdout]
|
|
233
|
-
step[:output][:stdout].each_line do |line|
|
|
234
|
-
puts pastel.green(" #{line.rstrip}") unless line.strip.empty?
|
|
235
|
-
end
|
|
236
|
-
elsif step[:output].is_a?(String)
|
|
237
|
-
step[:output].each_line do |line|
|
|
238
|
-
puts pastel.green(" #{line.rstrip}") unless line.strip.empty?
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
else
|
|
245
|
-
# 失败主机高亮错误
|
|
246
|
-
err = result[:error] || (if result[:output].is_a?(Array)
|
|
247
|
-
result[:output].map do |o|
|
|
248
|
-
o[:output][:stderr] if o[:output].is_a?(Hash)
|
|
249
|
-
end.compact.join("\n")
|
|
250
|
-
else
|
|
251
|
-
result[:output].to_s
|
|
252
|
-
end)
|
|
253
|
-
puts pastel.red(" ERROR: #{err}")
|
|
254
|
-
end
|
|
116
|
+
puts formatter.format_host_status(host, result[:status])
|
|
117
|
+
print_host_result(host, result, formatter)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
print_summary(results, formatter)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def print_host_result(_host, result, formatter)
|
|
124
|
+
if %i[success changed].include?(result[:status])
|
|
125
|
+
print_success_result(result, formatter)
|
|
126
|
+
else
|
|
127
|
+
print_failure_result(result, formatter)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def print_success_result(result, formatter)
|
|
132
|
+
shown = {}
|
|
133
|
+
grouped = group_output_by_type(result[:output])
|
|
134
|
+
|
|
135
|
+
grouped.each do |type, steps|
|
|
136
|
+
output_lines = format_steps_by_type(type, steps, shown, formatter)
|
|
137
|
+
output_lines.each { |line| puts line }
|
|
255
138
|
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def group_output_by_type(output)
|
|
142
|
+
output.group_by { |step| step[:type] || :run }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def format_steps_by_type(type, steps, shown, formatter)
|
|
146
|
+
case type
|
|
147
|
+
when :upload
|
|
148
|
+
formatter.format_upload_steps(steps, shown)
|
|
149
|
+
when :upload_template
|
|
150
|
+
formatter.format_template_steps(steps, shown)
|
|
151
|
+
when :run
|
|
152
|
+
formatter.format_run_steps(steps, shown)
|
|
153
|
+
else
|
|
154
|
+
[]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def print_failure_result(result, formatter)
|
|
159
|
+
error_message = extract_error_message(result)
|
|
160
|
+
puts formatter.format_error(error_message)
|
|
161
|
+
end
|
|
256
162
|
|
|
257
|
-
|
|
258
|
-
puts
|
|
163
|
+
def print_summary(results, formatter)
|
|
164
|
+
puts formatter.format_summary_header
|
|
259
165
|
max_host_len = results.keys.map(&:length).max || 16
|
|
260
|
-
|
|
261
|
-
changed_w = 11
|
|
262
|
-
failed_w = 10
|
|
166
|
+
|
|
263
167
|
results.keys.sort.each do |host|
|
|
264
168
|
result = results[host]
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
169
|
+
puts formatter.format_summary_line(host, result, max_host_len)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def show_banner_once
|
|
174
|
+
@banner_printed ||= false
|
|
175
|
+
return if @banner_printed
|
|
176
|
+
|
|
177
|
+
puts Kdeploy::Banner.show
|
|
178
|
+
@banner_printed = true
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def determine_tasks(task_name)
|
|
182
|
+
task_name ? [task_name.to_sym] : self.class.kdeploy_tasks.keys
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def execute_tasks(tasks_to_run)
|
|
186
|
+
tasks_to_run.each do |task|
|
|
187
|
+
execute_single_task(task)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def execute_single_task(task)
|
|
192
|
+
task_hosts = self.class.get_task_hosts(task)
|
|
193
|
+
hosts = filter_hosts(options[:limit], task_hosts)
|
|
194
|
+
|
|
195
|
+
if hosts.empty?
|
|
196
|
+
puts Kdeploy::Banner.show_error("No hosts found for task: #{task}")
|
|
197
|
+
return
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if options[:dry_run]
|
|
201
|
+
print_dry_run(hosts, task)
|
|
202
|
+
return
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
run_task(hosts, task)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def run_task(hosts, task)
|
|
209
|
+
output = ConsoleOutput.new
|
|
210
|
+
runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: options[:parallel], output: output)
|
|
211
|
+
results = runner.run(task)
|
|
212
|
+
print_results(results, task)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def extract_error_message(result)
|
|
216
|
+
return result[:error] if result[:error]
|
|
217
|
+
|
|
218
|
+
if result[:output].is_a?(Array)
|
|
219
|
+
result[:output].map do |o|
|
|
220
|
+
o[:output][:stderr] if o[:output].is_a?(Hash)
|
|
221
|
+
end.compact.join("\n")
|
|
222
|
+
else
|
|
223
|
+
result[:output].to_s
|
|
279
224
|
end
|
|
280
225
|
end
|
|
281
226
|
end
|
|
@@ -54,35 +54,52 @@ module Kdeploy
|
|
|
54
54
|
return unless output.is_a?(Hash)
|
|
55
55
|
|
|
56
56
|
pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
|
|
57
|
+
show_stdout(output[:stdout])
|
|
58
|
+
show_stderr(output[:stderr], pastel)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def show_stdout(stdout)
|
|
62
|
+
return unless stdout && !stdout.empty?
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
output
|
|
60
|
-
@output.write_line(" #{line.rstrip}") unless line.strip.empty?
|
|
61
|
-
end
|
|
64
|
+
stdout.each_line do |line|
|
|
65
|
+
@output.write_line(" #{line.rstrip}") unless line.strip.empty?
|
|
62
66
|
end
|
|
67
|
+
end
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
def show_stderr(stderr, pastel)
|
|
70
|
+
return unless stderr && !stderr.empty?
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
stderr.each_line do |line|
|
|
67
73
|
@output.write_line(pastel.green(" #{line.rstrip}")) unless line.strip.empty?
|
|
68
74
|
end
|
|
69
75
|
end
|
|
70
76
|
|
|
71
77
|
def show_command_header(host_name, type, description)
|
|
72
|
-
pastel =
|
|
78
|
+
pastel = get_pastel
|
|
73
79
|
@output.write_line(pastel.bright_white("\n#{host_name.ljust(24)} : "))
|
|
80
|
+
format_command_by_type(type, description, pastel)
|
|
81
|
+
end
|
|
74
82
|
|
|
83
|
+
def get_pastel
|
|
84
|
+
@output.respond_to?(:pastel) ? @output.pastel : Pastel.new
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def format_command_by_type(type, description, pastel)
|
|
75
88
|
case type
|
|
76
89
|
when :run
|
|
77
|
-
|
|
78
|
-
description.lines[1..].each do |line|
|
|
79
|
-
@output.write_line(" > #{line.strip}") unless line.strip.empty?
|
|
80
|
-
end
|
|
90
|
+
format_run_command(description, pastel)
|
|
81
91
|
when :upload
|
|
82
92
|
@output.write_line(pastel.green(" [upload] #{description}"))
|
|
83
93
|
when :upload_template
|
|
84
94
|
@output.write_line(pastel.yellow(" [template] #{description}"))
|
|
85
95
|
end
|
|
86
96
|
end
|
|
97
|
+
|
|
98
|
+
def format_run_command(description, pastel)
|
|
99
|
+
@output.write_line(pastel.cyan(" [run] #{description.lines.first.strip}"))
|
|
100
|
+
description.lines[1..].each do |line|
|
|
101
|
+
@output.write_line(" > #{line.strip}") unless line.strip.empty?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
87
104
|
end
|
|
88
105
|
end
|
|
@@ -11,12 +11,10 @@ module Kdeploy
|
|
|
11
11
|
|
|
12
12
|
def self.group_key_for(cmd)
|
|
13
13
|
case cmd[:type]
|
|
14
|
-
when :upload
|
|
14
|
+
when :upload, :upload_template
|
|
15
15
|
"#{cmd[:type]}_#{cmd[:source]}"
|
|
16
16
|
when :run
|
|
17
17
|
"#{cmd[:type]}_#{cmd[:command].to_s.lines.first.strip}"
|
|
18
|
-
when :upload_template
|
|
19
|
-
"#{cmd[:type]}_#{cmd[:source]}"
|
|
20
18
|
else
|
|
21
19
|
cmd[:type].to_s
|
|
22
20
|
end
|
data/lib/kdeploy/dsl.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'set'
|
|
4
4
|
|
|
5
5
|
module Kdeploy
|
|
6
|
+
# Domain-specific language for defining hosts, roles, and tasks
|
|
6
7
|
module DSL
|
|
7
8
|
def self.extended(base)
|
|
8
9
|
base.instance_variable_set(:@kdeploy_hosts, {})
|
|
@@ -32,21 +33,31 @@ module Kdeploy
|
|
|
32
33
|
|
|
33
34
|
def task(name, on: nil, roles: nil, &block)
|
|
34
35
|
kdeploy_tasks[name] = {
|
|
35
|
-
hosts:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
hosts: normalize_hosts_option(on),
|
|
37
|
+
roles: normalize_roles_option(roles),
|
|
38
|
+
block: create_task_block(block)
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize_hosts_option(on)
|
|
43
|
+
return on if on.is_a?(Array)
|
|
44
|
+
return [on] if on
|
|
45
|
+
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def normalize_roles_option(roles)
|
|
50
|
+
return roles if roles.is_a?(Array)
|
|
51
|
+
return [roles] if roles
|
|
52
|
+
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def create_task_block(block)
|
|
57
|
+
lambda {
|
|
58
|
+
@kdeploy_commands = []
|
|
59
|
+
instance_eval(&block)
|
|
60
|
+
@kdeploy_commands
|
|
50
61
|
}
|
|
51
62
|
end
|
|
52
63
|
|
|
@@ -70,31 +81,39 @@ module Kdeploy
|
|
|
70
81
|
}
|
|
71
82
|
end
|
|
72
83
|
|
|
73
|
-
def inventory(&)
|
|
74
|
-
instance_eval(&) if block_given?
|
|
84
|
+
def inventory(&block)
|
|
85
|
+
instance_eval(&block) if block_given?
|
|
75
86
|
end
|
|
76
87
|
|
|
77
88
|
def get_task_hosts(task_name)
|
|
78
89
|
task = kdeploy_tasks[task_name]
|
|
79
|
-
return kdeploy_hosts.keys if
|
|
90
|
+
return kdeploy_hosts.keys if task_empty?(task)
|
|
80
91
|
|
|
81
92
|
hosts = Set.new
|
|
93
|
+
add_explicit_hosts(task, hosts)
|
|
94
|
+
add_role_hosts(task, hosts)
|
|
95
|
+
hosts.to_a
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def task_empty?(task)
|
|
99
|
+
!task || (!task[:hosts] && !task[:roles])
|
|
100
|
+
end
|
|
82
101
|
|
|
83
|
-
|
|
102
|
+
def add_explicit_hosts(task, hosts)
|
|
84
103
|
task[:hosts]&.each do |host|
|
|
85
104
|
hosts.add(host) if kdeploy_hosts.key?(host)
|
|
86
105
|
end
|
|
106
|
+
end
|
|
87
107
|
|
|
88
|
-
|
|
108
|
+
def add_role_hosts(task, hosts)
|
|
89
109
|
task[:roles]&.each do |role|
|
|
90
|
-
|
|
110
|
+
role_hosts = kdeploy_roles[role]
|
|
111
|
+
next unless role_hosts
|
|
91
112
|
|
|
92
113
|
role_hosts.each do |host|
|
|
93
114
|
hosts.add(host) if kdeploy_hosts.key?(host)
|
|
94
115
|
end
|
|
95
116
|
end
|
|
96
|
-
|
|
97
|
-
hosts.to_a
|
|
98
117
|
end
|
|
99
118
|
end
|
|
100
119
|
end
|
data/lib/kdeploy/executor.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'net/ssh'
|
|
|
4
4
|
require 'net/scp'
|
|
5
5
|
|
|
6
6
|
module Kdeploy
|
|
7
|
+
# SSH/SCP executor for remote command execution and file operations
|
|
7
8
|
class Executor
|
|
8
9
|
def initialize(host_config)
|
|
9
10
|
@host = host_config[:name]
|
|
@@ -16,28 +17,7 @@ module Kdeploy
|
|
|
16
17
|
|
|
17
18
|
def execute(command)
|
|
18
19
|
Net::SSH.start(@ip, @user, ssh_options) do |ssh|
|
|
19
|
-
|
|
20
|
-
stderr = String.new
|
|
21
|
-
|
|
22
|
-
ssh.open_channel do |channel|
|
|
23
|
-
channel.exec(command) do |_ch, success|
|
|
24
|
-
raise SSHError, "Could not execute command: #{command}" unless success
|
|
25
|
-
|
|
26
|
-
channel.on_data do |_ch, data|
|
|
27
|
-
stdout << data
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
channel.on_extended_data do |_ch, _type, data|
|
|
31
|
-
stderr << data
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
ssh.loop
|
|
36
|
-
{
|
|
37
|
-
stdout: stdout.strip,
|
|
38
|
-
stderr: stderr.strip,
|
|
39
|
-
command: command
|
|
40
|
-
}
|
|
20
|
+
execute_command_on_ssh(ssh, command)
|
|
41
21
|
end
|
|
42
22
|
rescue Net::SSH::AuthenticationFailed => e
|
|
43
23
|
raise SSHError.new("SSH authentication failed: #{e.message}", e)
|
|
@@ -45,6 +25,39 @@ module Kdeploy
|
|
|
45
25
|
raise SSHError.new("SSH execution failed: #{e.message}", e)
|
|
46
26
|
end
|
|
47
27
|
|
|
28
|
+
def execute_command_on_ssh(ssh, command)
|
|
29
|
+
stdout = String.new
|
|
30
|
+
stderr = String.new
|
|
31
|
+
|
|
32
|
+
ssh.open_channel do |channel|
|
|
33
|
+
channel.exec(command) do |_ch, success|
|
|
34
|
+
raise SSHError, "Could not execute command: #{command}" unless success
|
|
35
|
+
|
|
36
|
+
setup_channel_handlers(channel, stdout, stderr)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
ssh.loop
|
|
40
|
+
build_command_result(stdout, stderr, command)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def setup_channel_handlers(channel, stdout, stderr)
|
|
44
|
+
channel.on_data do |_ch, data|
|
|
45
|
+
stdout << data
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
channel.on_extended_data do |_ch, _type, data|
|
|
49
|
+
stderr << data
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_command_result(stdout, stderr, command)
|
|
54
|
+
{
|
|
55
|
+
stdout: stdout.strip,
|
|
56
|
+
stderr: stderr.strip,
|
|
57
|
+
command: command
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
48
61
|
def upload(source, destination)
|
|
49
62
|
Net::SCP.start(@ip, @user, ssh_options) do |scp|
|
|
50
63
|
scp.upload!(source, destination)
|
|
@@ -62,18 +75,29 @@ module Kdeploy
|
|
|
62
75
|
private
|
|
63
76
|
|
|
64
77
|
def ssh_options
|
|
65
|
-
options =
|
|
78
|
+
options = base_ssh_options
|
|
79
|
+
add_authentication(options)
|
|
80
|
+
add_port_option(options)
|
|
81
|
+
options
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def base_ssh_options
|
|
85
|
+
{
|
|
66
86
|
verify_host_key: :never,
|
|
67
87
|
timeout: 30
|
|
68
88
|
}
|
|
89
|
+
end
|
|
69
90
|
|
|
91
|
+
def add_authentication(options)
|
|
70
92
|
if @password
|
|
71
93
|
options[:password] = @password
|
|
72
94
|
elsif @key
|
|
73
95
|
options[:keys] = [@key]
|
|
74
96
|
end
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def add_port_option(options)
|
|
100
|
+
options[:port] = @port if @port
|
|
77
101
|
end
|
|
78
102
|
end
|
|
79
103
|
end
|