kdeploy 1.2.13 → 1.2.15
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/lib/kdeploy/banner.rb +26 -10
- data/lib/kdeploy/cli.rb +55 -9
- data/lib/kdeploy/command_executor.rb +6 -6
- data/lib/kdeploy/executor.rb +73 -7
- data/lib/kdeploy/output_formatter.rb +26 -18
- data/lib/kdeploy/runner.rb +69 -12
- data/lib/kdeploy/version.rb +1 -1
- metadata +29 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4402e80ac2b80255ebf2ad70815e7ac1aeacb6722ff5f4969a8e8a34885a6875
|
|
4
|
+
data.tar.gz: 56ffc8befdd3aba9636cd37144bb6a89dfcc7f88b250233af77964d3b100fa71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52c4e621e0cb0f58ab71e6d6ca58483c9b4dd0693d0358553d5c08dde522f4ab555e054f0bb2ad4d39ffa03bb09d4e8091f85d0639c0b4cfa83b97b9b6f4136c
|
|
7
|
+
data.tar.gz: 5023dacd1ef9198ec06688336c35d20153406211371514cc014c71fb644571fb9a3ff94b97630f1c92e4d7c9310a92d4d1a6370fb1a9081365efea22accfbe3d
|
data/lib/kdeploy/banner.rb
CHANGED
|
@@ -28,22 +28,38 @@ module Kdeploy
|
|
|
28
28
|
VERSION
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def show_error(message)
|
|
31
|
+
def show_error(message, include_banner: false)
|
|
32
32
|
pastel = Pastel.new
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
error_msg = pastel.red("Error: #{message}")
|
|
34
|
+
if include_banner
|
|
35
|
+
<<~ERROR
|
|
36
|
+
#{show}
|
|
37
|
+
#{error_msg}
|
|
38
|
+
|
|
39
|
+
ERROR
|
|
40
|
+
else
|
|
41
|
+
<<~ERROR
|
|
42
|
+
#{error_msg}
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
ERROR
|
|
45
|
+
end
|
|
38
46
|
end
|
|
39
47
|
|
|
40
|
-
def show_success(message)
|
|
48
|
+
def show_success(message, include_banner: false)
|
|
41
49
|
pastel = Pastel.new
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
success_msg = pastel.green("Success: #{message}")
|
|
51
|
+
if include_banner
|
|
52
|
+
<<~SUCCESS
|
|
53
|
+
#{show}
|
|
54
|
+
#{success_msg}
|
|
55
|
+
|
|
56
|
+
SUCCESS
|
|
57
|
+
else
|
|
58
|
+
<<~SUCCESS
|
|
59
|
+
#{success_msg}
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
SUCCESS
|
|
62
|
+
end
|
|
47
63
|
end
|
|
48
64
|
|
|
49
65
|
ASCII_LOGO = <<~'LOGO'
|
data/lib/kdeploy/cli.rb
CHANGED
|
@@ -45,9 +45,11 @@ module Kdeploy
|
|
|
45
45
|
method_option :limit, type: :string, desc: 'Limit to specific hosts (comma-separated)'
|
|
46
46
|
method_option :parallel, type: :numeric, default: 10, desc: 'Number of parallel executions'
|
|
47
47
|
method_option :dry_run, type: :boolean, desc: 'Show what would be done'
|
|
48
|
+
method_option :debug, type: :boolean, desc: 'Show detailed command output (stdout/stderr)'
|
|
48
49
|
def execute(task_file, task_name = nil)
|
|
49
50
|
load_config_file
|
|
50
51
|
show_banner_once
|
|
52
|
+
@task_file_dir = File.dirname(File.expand_path(task_file))
|
|
51
53
|
load_task_file(task_file)
|
|
52
54
|
|
|
53
55
|
tasks_to_run = determine_tasks(task_name)
|
|
@@ -96,7 +98,7 @@ module Kdeploy
|
|
|
96
98
|
|
|
97
99
|
def print_dry_run(hosts, task_name)
|
|
98
100
|
formatter = OutputFormatter.new
|
|
99
|
-
|
|
101
|
+
# Banner already shown by show_banner_once, don't show again
|
|
100
102
|
puts formatter.format_dry_run_header
|
|
101
103
|
puts
|
|
102
104
|
|
|
@@ -113,8 +115,8 @@ module Kdeploy
|
|
|
113
115
|
puts
|
|
114
116
|
end
|
|
115
117
|
|
|
116
|
-
def print_results(results, task_name)
|
|
117
|
-
formatter = OutputFormatter.new
|
|
118
|
+
def print_results(results, task_name, show_summary: false, debug: false)
|
|
119
|
+
formatter = OutputFormatter.new(debug: debug)
|
|
118
120
|
puts formatter.format_task_header(task_name)
|
|
119
121
|
|
|
120
122
|
if results.empty?
|
|
@@ -128,7 +130,8 @@ module Kdeploy
|
|
|
128
130
|
print_host_result(host, result, formatter)
|
|
129
131
|
end
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
# Only show summary if explicitly requested (for single task execution)
|
|
134
|
+
print_summary(results, formatter) if show_summary
|
|
132
135
|
end
|
|
133
136
|
|
|
134
137
|
def print_host_result(_host, result, formatter)
|
|
@@ -194,9 +197,16 @@ module Kdeploy
|
|
|
194
197
|
end
|
|
195
198
|
|
|
196
199
|
def execute_tasks(tasks_to_run)
|
|
200
|
+
all_results = {}
|
|
201
|
+
|
|
197
202
|
tasks_to_run.each do |task|
|
|
198
|
-
execute_single_task(task)
|
|
203
|
+
task_results = execute_single_task(task)
|
|
204
|
+
# Collect results for final summary
|
|
205
|
+
all_results[task] = task_results if task_results
|
|
199
206
|
end
|
|
207
|
+
|
|
208
|
+
# Show combined summary at the end for all tasks
|
|
209
|
+
print_all_tasks_summary(all_results) unless all_results.empty?
|
|
200
210
|
end
|
|
201
211
|
|
|
202
212
|
def execute_single_task(task)
|
|
@@ -205,12 +215,12 @@ module Kdeploy
|
|
|
205
215
|
|
|
206
216
|
if hosts.empty?
|
|
207
217
|
puts Kdeploy::Banner.show_error("No hosts found for task: #{task}")
|
|
208
|
-
return
|
|
218
|
+
return nil
|
|
209
219
|
end
|
|
210
220
|
|
|
211
221
|
if options[:dry_run]
|
|
212
222
|
print_dry_run(hosts, task)
|
|
213
|
-
return
|
|
223
|
+
return nil
|
|
214
224
|
end
|
|
215
225
|
|
|
216
226
|
run_task(hosts, task)
|
|
@@ -219,9 +229,45 @@ module Kdeploy
|
|
|
219
229
|
def run_task(hosts, task)
|
|
220
230
|
output = ConsoleOutput.new
|
|
221
231
|
parallel_count = options[:parallel] || Configuration.default_parallel
|
|
222
|
-
|
|
232
|
+
debug_mode = options[:debug] || false
|
|
233
|
+
base_dir = @task_file_dir
|
|
234
|
+
runner = Runner.new(
|
|
235
|
+
hosts, self.class.kdeploy_tasks,
|
|
236
|
+
parallel: parallel_count,
|
|
237
|
+
output: output,
|
|
238
|
+
debug: debug_mode,
|
|
239
|
+
base_dir: base_dir
|
|
240
|
+
)
|
|
223
241
|
results = runner.run(task)
|
|
224
|
-
|
|
242
|
+
# Don't show summary here - it will be shown at the end for all tasks
|
|
243
|
+
print_results(results, task, show_summary: false, debug: debug_mode)
|
|
244
|
+
results
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def print_all_tasks_summary(all_results)
|
|
248
|
+
debug_mode = options[:debug] || false
|
|
249
|
+
formatter = OutputFormatter.new(debug: debug_mode)
|
|
250
|
+
puts formatter.format_summary_header
|
|
251
|
+
|
|
252
|
+
# Collect all hosts from all tasks
|
|
253
|
+
all_hosts = {}
|
|
254
|
+
all_results.each do |task_name, task_results|
|
|
255
|
+
task_results.each do |host, result|
|
|
256
|
+
all_hosts[host] ||= { ok: 0, changed: 0, failed: 0, tasks: [] }
|
|
257
|
+
counts = formatter.calculate_summary_counts(result)
|
|
258
|
+
all_hosts[host][:ok] += counts[:ok]
|
|
259
|
+
all_hosts[host][:changed] += counts[:changed]
|
|
260
|
+
all_hosts[host][:failed] += counts[:failed]
|
|
261
|
+
all_hosts[host][:tasks] << task_name
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
max_host_len = all_hosts.keys.map(&:length).max || 16
|
|
266
|
+
all_hosts.keys.sort.each do |host|
|
|
267
|
+
counts = all_hosts[host]
|
|
268
|
+
line = formatter.build_summary_line(host, counts, max_host_len)
|
|
269
|
+
puts formatter.colorize_summary_line(line, counts)
|
|
270
|
+
end
|
|
225
271
|
end
|
|
226
272
|
|
|
227
273
|
def extract_error_message(result)
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
module Kdeploy
|
|
4
4
|
# Executes a single command and records execution time
|
|
5
5
|
class CommandExecutor
|
|
6
|
-
def initialize(executor, output)
|
|
6
|
+
def initialize(executor, output, debug: false)
|
|
7
7
|
@executor = executor
|
|
8
8
|
@output = output
|
|
9
|
+
@debug = debug
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def execute_run(command, host_name)
|
|
@@ -23,8 +24,8 @@ module Kdeploy
|
|
|
23
24
|
# Show execution time if command took more than 1 second
|
|
24
25
|
@output.write_line(pastel.dim(" [completed in #{format('%.2f', duration)}s]")) if duration > 1.0
|
|
25
26
|
|
|
26
|
-
# Show command output
|
|
27
|
-
show_command_output(result)
|
|
27
|
+
# Show command output only in debug mode
|
|
28
|
+
show_command_output(result) if @debug
|
|
28
29
|
{ command: cmd, output: result, duration: duration, type: :run }
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -86,9 +87,8 @@ module Kdeploy
|
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
def show_command_header(host_name, type, description)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
format_command_by_type(type, description, pastel)
|
|
90
|
+
# Don't show command header during execution - it will be shown in results
|
|
91
|
+
# This reduces noise during execution
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
def pastel_instance
|
data/lib/kdeploy/executor.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'net/ssh'
|
|
4
4
|
require 'net/scp'
|
|
5
|
+
require 'pathname'
|
|
5
6
|
|
|
6
7
|
module Kdeploy
|
|
7
8
|
# SSH/SCP executor for remote command execution and file operations
|
|
@@ -15,6 +16,7 @@ module Kdeploy
|
|
|
15
16
|
@port = host_config[:port] # 新增端口支持
|
|
16
17
|
@use_sudo = host_config[:use_sudo] || false
|
|
17
18
|
@sudo_password = host_config[:sudo_password]
|
|
19
|
+
@base_dir = host_config[:base_dir] # Base directory for resolving relative paths
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def execute(command, use_sudo: nil)
|
|
@@ -63,22 +65,75 @@ module Kdeploy
|
|
|
63
65
|
}
|
|
64
66
|
end
|
|
65
67
|
|
|
66
|
-
def upload(source, destination)
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
def upload(source, destination, use_sudo: nil)
|
|
69
|
+
use_sudo = @use_sudo if use_sudo.nil?
|
|
70
|
+
|
|
71
|
+
# Resolve relative paths relative to base_dir
|
|
72
|
+
resolved_source = resolve_path(source)
|
|
73
|
+
|
|
74
|
+
# If destination requires sudo, upload to temp location first, then move with sudo
|
|
75
|
+
if use_sudo || requires_sudo?(destination)
|
|
76
|
+
upload_with_sudo(resolved_source, destination)
|
|
77
|
+
else
|
|
78
|
+
Net::SCP.start(@ip, @user, ssh_options) do |scp|
|
|
79
|
+
scp.upload!(resolved_source, destination)
|
|
80
|
+
end
|
|
69
81
|
end
|
|
70
82
|
rescue StandardError => e
|
|
71
83
|
raise SCPError.new("SCP upload failed: #{e.message}", e)
|
|
72
84
|
end
|
|
73
85
|
|
|
74
86
|
def upload_template(source, destination, variables = {})
|
|
75
|
-
|
|
87
|
+
# Resolve relative paths relative to base_dir
|
|
88
|
+
resolved_source = resolve_path(source)
|
|
89
|
+
Template.render_and_upload(self, resolved_source, destination, variables)
|
|
76
90
|
rescue StandardError => e
|
|
77
91
|
raise TemplateError.new("Template upload failed: #{e.message}", e)
|
|
78
92
|
end
|
|
79
93
|
|
|
80
94
|
private
|
|
81
95
|
|
|
96
|
+
def upload_with_sudo(source, destination)
|
|
97
|
+
# Generate a unique temp file name
|
|
98
|
+
temp_dest = "/tmp/kdeploy_#{File.basename(destination)}_#{Time.now.to_i}_#{rand(10_000)}"
|
|
99
|
+
|
|
100
|
+
# Upload to temp location first
|
|
101
|
+
Net::SCP.start(@ip, @user, ssh_options) do |scp|
|
|
102
|
+
scp.upload!(source, temp_dest)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Move to final destination with sudo
|
|
106
|
+
move_command = "mv #{temp_dest} #{destination}"
|
|
107
|
+
execute(move_command, use_sudo: true)
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
# Try to clean up temp file if it exists
|
|
110
|
+
begin
|
|
111
|
+
execute("rm -f #{temp_dest}", use_sudo: false) if defined?(temp_dest)
|
|
112
|
+
rescue StandardError
|
|
113
|
+
# Ignore cleanup errors
|
|
114
|
+
end
|
|
115
|
+
raise SCPError.new("SCP upload failed: #{e.message}", e)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def resolve_path(path)
|
|
119
|
+
# If path is absolute, return as is
|
|
120
|
+
return path if Pathname.new(path).absolute?
|
|
121
|
+
|
|
122
|
+
# If base_dir is set, resolve relative to base_dir
|
|
123
|
+
if @base_dir
|
|
124
|
+
File.expand_path(path, @base_dir)
|
|
125
|
+
else
|
|
126
|
+
# Otherwise, resolve relative to current working directory
|
|
127
|
+
File.expand_path(path)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def requires_sudo?(path)
|
|
132
|
+
# Check if path is in system directories that typically require sudo
|
|
133
|
+
system_dirs = %w[/etc /usr /var /opt /sbin /bin /lib /lib64 /root]
|
|
134
|
+
system_dirs.any? { |dir| path.start_with?(dir) }
|
|
135
|
+
end
|
|
136
|
+
|
|
82
137
|
def ssh_options
|
|
83
138
|
options = base_ssh_options
|
|
84
139
|
add_authentication(options)
|
|
@@ -109,9 +164,20 @@ module Kdeploy
|
|
|
109
164
|
# 如果命令已经以 sudo 开头,不重复添加
|
|
110
165
|
return command if command.strip.start_with?('sudo')
|
|
111
166
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
167
|
+
# 对于多行命令或包含 shell 控制结构的命令,使用 bash -c 包装
|
|
168
|
+
is_multiline = command.include?("\n") || command.match?(/\b(if|for|while|case|function)\b/)
|
|
169
|
+
|
|
170
|
+
if is_multiline
|
|
171
|
+
# 转义命令中的单引号,然后用 bash -c 执行
|
|
172
|
+
escaped_command = command.gsub("'", "'\"'\"'")
|
|
173
|
+
if @sudo_password
|
|
174
|
+
escaped_password = @sudo_password.gsub('\'', "'\"'\"'").gsub('$', '\\$').gsub('`', '\\`')
|
|
175
|
+
"echo '#{escaped_password}' | sudo -S bash -c '#{escaped_command}'"
|
|
176
|
+
else
|
|
177
|
+
"sudo bash -c '#{escaped_command}'"
|
|
178
|
+
end
|
|
179
|
+
elsif @sudo_password
|
|
180
|
+
# 单行命令直接包装
|
|
115
181
|
escaped_password = @sudo_password.gsub('\'', "'\"'\"'").gsub('$', '\\$').gsub('`', '\\`')
|
|
116
182
|
"echo '#{escaped_password}' | sudo -S #{command}"
|
|
117
183
|
else
|
|
@@ -6,21 +6,22 @@ require 'tty-box'
|
|
|
6
6
|
module Kdeploy
|
|
7
7
|
# Formats and displays execution results
|
|
8
8
|
class OutputFormatter
|
|
9
|
-
def initialize
|
|
9
|
+
def initialize(debug: false)
|
|
10
10
|
@pastel = Pastel.new
|
|
11
|
+
@debug = debug
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def format_task_header(task_name)
|
|
14
|
-
@pastel.
|
|
15
|
+
"#{@pastel.bright_cyan("\n🚀 Task: #{task_name}")}\n#{@pastel.dim('─' * 60)}"
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def format_host_status(host, status)
|
|
18
19
|
status_str = case status
|
|
19
|
-
when :success then @pastel.green('ok')
|
|
20
|
-
when :changed then @pastel.yellow('changed')
|
|
21
|
-
else @pastel.red('failed')
|
|
20
|
+
when :success then @pastel.green('✓ ok')
|
|
21
|
+
when :changed then @pastel.yellow('~ changed')
|
|
22
|
+
else @pastel.red('✗ failed')
|
|
22
23
|
end
|
|
23
|
-
@pastel.bright_white("
|
|
24
|
+
@pastel.bright_white(" #{host.ljust(20)} #{status_str}")
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def format_upload_steps(steps, shown)
|
|
@@ -31,8 +32,8 @@ module Kdeploy
|
|
|
31
32
|
format_file_steps(steps, shown, :upload_template, @pastel.yellow(' === Template ==='), 'upload_template: ')
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
def format_file_steps(steps, shown, type,
|
|
35
|
-
output = [
|
|
35
|
+
def format_file_steps(steps, shown, type, _header, prefix)
|
|
36
|
+
output = []
|
|
36
37
|
steps.each do |step|
|
|
37
38
|
next if step_already_shown?(step, type, shown)
|
|
38
39
|
|
|
@@ -44,13 +45,16 @@ module Kdeploy
|
|
|
44
45
|
|
|
45
46
|
def format_file_step(step, type, prefix)
|
|
46
47
|
duration_str = format_duration(step[:duration])
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
icon = type == :upload ? '📤' : '📝'
|
|
49
|
+
file_path = step[:command].sub(prefix, '')
|
|
50
|
+
# Truncate long paths for cleaner output
|
|
51
|
+
display_path = file_path.length > 50 ? "...#{file_path[-47..]}" : file_path
|
|
52
|
+
color_method = type == :upload ? :green : :yellow
|
|
53
|
+
@pastel.dim(" #{icon} ") + @pastel.send(color_method, display_path) + duration_str
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
def format_run_steps(steps, shown)
|
|
52
57
|
output = []
|
|
53
|
-
output << @pastel.cyan(' === Run ===')
|
|
54
58
|
steps.each do |step|
|
|
55
59
|
next if step_already_shown?(step, :run, shown)
|
|
56
60
|
|
|
@@ -64,20 +68,24 @@ module Kdeploy
|
|
|
64
68
|
output = []
|
|
65
69
|
duration_str = format_duration(step[:duration])
|
|
66
70
|
command_line = step[:command].to_s.lines.first.strip
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
# Truncate long commands for cleaner output
|
|
72
|
+
display_cmd = command_line.length > 60 ? "#{command_line[0..57]}..." : command_line
|
|
73
|
+
output << (@pastel.dim(' • ') + @pastel.cyan(display_cmd) + duration_str)
|
|
74
|
+
# Only show multiline details in debug mode
|
|
75
|
+
if @debug
|
|
76
|
+
output.concat(format_multiline_command(step[:command]))
|
|
77
|
+
cmd_output = format_command_output(step[:output])
|
|
78
|
+
output.concat(cmd_output) if cmd_output.any?
|
|
79
|
+
end
|
|
72
80
|
output
|
|
73
81
|
end
|
|
74
82
|
|
|
75
83
|
def format_error(error_message)
|
|
76
|
-
@pastel.red("
|
|
84
|
+
@pastel.red(" ✗ ERROR: #{error_message}")
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
def format_summary_header
|
|
80
|
-
@pastel.
|
|
88
|
+
"#{@pastel.bright_cyan("\n📊 Execution Summary")}\n#{@pastel.dim('─' * 60)}"
|
|
81
89
|
end
|
|
82
90
|
|
|
83
91
|
def format_summary_line(host, result, max_host_len)
|
data/lib/kdeploy/runner.rb
CHANGED
|
@@ -5,18 +5,22 @@ require 'concurrent'
|
|
|
5
5
|
module Kdeploy
|
|
6
6
|
# Concurrent task runner for executing tasks across multiple hosts
|
|
7
7
|
class Runner
|
|
8
|
-
def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new
|
|
8
|
+
def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new,
|
|
9
|
+
debug: false, base_dir: nil)
|
|
9
10
|
@hosts = hosts
|
|
10
11
|
@tasks = tasks
|
|
11
12
|
@parallel = parallel
|
|
12
13
|
@output = output
|
|
14
|
+
@debug = debug
|
|
15
|
+
@base_dir = base_dir
|
|
13
16
|
@pool = Concurrent::FixedThreadPool.new(@parallel)
|
|
14
17
|
@results = Concurrent::Hash.new
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
def run(task_name)
|
|
18
21
|
task = find_task(task_name)
|
|
19
|
-
execute_concurrent_tasks(task)
|
|
22
|
+
results = execute_concurrent_tasks(task)
|
|
23
|
+
results
|
|
20
24
|
ensure
|
|
21
25
|
@pool.shutdown
|
|
22
26
|
end
|
|
@@ -32,12 +36,55 @@ module Kdeploy
|
|
|
32
36
|
def execute_concurrent_tasks(task)
|
|
33
37
|
futures = create_task_futures(task)
|
|
34
38
|
|
|
39
|
+
# If no hosts, return empty results immediately
|
|
40
|
+
return @results if futures.empty?
|
|
41
|
+
|
|
35
42
|
# Show progress while waiting for tasks to complete
|
|
36
43
|
total = futures.length
|
|
37
44
|
completed = 0
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
# Collect results from futures
|
|
47
|
+
futures.each_with_index do |future, index|
|
|
48
|
+
host_name = @host_names[index] # Get host name from the stored list
|
|
49
|
+
begin
|
|
50
|
+
# Wait for future to complete and get its value
|
|
51
|
+
# This ensures the future has finished executing
|
|
52
|
+
future_result = future.value
|
|
53
|
+
|
|
54
|
+
# Handle the result
|
|
55
|
+
if future_result.nil?
|
|
56
|
+
# Future returned nil - create a default result
|
|
57
|
+
@results[host_name] = { status: :unknown, error: 'Future returned nil', output: [] }
|
|
58
|
+
elsif future_result.is_a?(Array) && future_result.length == 2
|
|
59
|
+
name, result = future_result
|
|
60
|
+
# Store the result using the name from the future
|
|
61
|
+
@results[name] = result
|
|
62
|
+
else
|
|
63
|
+
# Handle unexpected result format - create a default result
|
|
64
|
+
@results[host_name] = {
|
|
65
|
+
status: :unknown,
|
|
66
|
+
error: "Unexpected result format: #{future_result.class}",
|
|
67
|
+
output: []
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check if future raised an exception
|
|
72
|
+
if future.rejected?
|
|
73
|
+
error = begin
|
|
74
|
+
future.reason
|
|
75
|
+
rescue StandardError
|
|
76
|
+
'Unknown error'
|
|
77
|
+
end
|
|
78
|
+
@results[host_name] = { status: :failed, error: error, output: [] } unless @results.key?(host_name)
|
|
79
|
+
end
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
# If future.value raises an exception, create an error result
|
|
82
|
+
@results[host_name] = { status: :failed, error: "#{e.class}: #{e.message}", output: [] }
|
|
83
|
+
ensure
|
|
84
|
+
# Ensure we always have a result for this host
|
|
85
|
+
@results[host_name] ||= { status: :unknown, error: 'No result collected', output: [] }
|
|
86
|
+
end
|
|
87
|
+
|
|
41
88
|
completed += 1
|
|
42
89
|
# Show progress for multiple hosts
|
|
43
90
|
next unless total > 1
|
|
@@ -51,6 +98,8 @@ module Kdeploy
|
|
|
51
98
|
end
|
|
52
99
|
|
|
53
100
|
def create_task_futures(task)
|
|
101
|
+
# Store host names in order to match with futures
|
|
102
|
+
@host_names = @hosts.keys
|
|
54
103
|
@hosts.map do |name, config|
|
|
55
104
|
Concurrent::Future.execute(executor: @pool) do
|
|
56
105
|
execute_task_for_host(name, config, task)
|
|
@@ -61,14 +110,22 @@ module Kdeploy
|
|
|
61
110
|
private
|
|
62
111
|
|
|
63
112
|
def execute_task_for_host(name, config, task)
|
|
64
|
-
|
|
65
|
-
|
|
113
|
+
# Add base_dir to config for path resolution
|
|
114
|
+
config_with_base_dir = config.merge(base_dir: @base_dir)
|
|
115
|
+
executor = Executor.new(config_with_base_dir)
|
|
116
|
+
command_executor = CommandExecutor.new(executor, @output, debug: @debug)
|
|
66
117
|
result = { status: :success, output: [] }
|
|
67
118
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
119
|
+
begin
|
|
120
|
+
execute_grouped_commands(task, command_executor, name, result)
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
# Ensure result is always set, even on error
|
|
123
|
+
# Don't re-raise, as it would cause future.value to fail
|
|
124
|
+
result = { status: :failed, error: e.message, output: [] }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Return the result so it can be collected from the future
|
|
128
|
+
[name, result]
|
|
72
129
|
end
|
|
73
130
|
|
|
74
131
|
def execute_grouped_commands(task, command_executor, name, result)
|
|
@@ -111,8 +168,8 @@ module Kdeploy
|
|
|
111
168
|
end
|
|
112
169
|
|
|
113
170
|
def show_task_header(task_desc)
|
|
114
|
-
|
|
115
|
-
|
|
171
|
+
# Don't show command header during execution - it will be shown in results
|
|
172
|
+
# This reduces noise during execution
|
|
116
173
|
end
|
|
117
174
|
end
|
|
118
175
|
end
|
data/lib/kdeploy/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kdeploy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kk
|
|
@@ -10,6 +10,20 @@ bindir: exe
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2025-11-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bcrypt_pbkdf
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.1'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: concurrent-ruby
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -24,6 +38,20 @@ dependencies:
|
|
|
24
38
|
- - "~>"
|
|
25
39
|
- !ruby/object:Gem::Version
|
|
26
40
|
version: '1.3'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: ed25519
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.2'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.2'
|
|
27
55
|
- !ruby/object:Gem::Dependency
|
|
28
56
|
name: net-scp
|
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|