kdeploy 1.2.1 → 1.2.3
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/cli.rb +134 -190
- data/lib/kdeploy/command_executor.rb +28 -11
- data/lib/kdeploy/dsl.rb +42 -21
- data/lib/kdeploy/executor.rb +48 -25
- data/lib/kdeploy/help_formatter.rb +67 -0
- data/lib/kdeploy/output.rb +2 -0
- data/lib/kdeploy/output_formatter.rb +193 -0
- data/lib/kdeploy/post_install.rb +69 -49
- data/lib/kdeploy/runner.rb +37 -17
- data/lib/kdeploy/template.rb +10 -2
- data/lib/kdeploy/version.rb +1 -1
- data/lib/kdeploy.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a675dda751570dd0f5fce2042bfc5a68411e03e869d78bf8569739644bf8021f
|
|
4
|
+
data.tar.gz: 3b4861d5ba0c55f57ab2ace6a9e4bf3ae963076767e0db23f5a444b380efb861
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e2952dfbc5956c61ad1f58cdc8fb45a9d050a331f1f224ea3233d8f9e6618d06f5c86382e6446fecbdcc86f624a24600c672dda5fcd2ea26c535d3f16068fc9
|
|
7
|
+
data.tar.gz: 7edd9ae73dfe74015798197cd9cf03309aeb4a6d3ca065a1224710f4dc02b42fdbc4a6a17a541cba40a83f4637e324746627b023e7ce5475a3481a04d163e81e
|
data/lib/kdeploy/cli.rb
CHANGED
|
@@ -28,41 +28,7 @@ module Kdeploy
|
|
|
28
28
|
if command
|
|
29
29
|
super
|
|
30
30
|
else
|
|
31
|
-
|
|
32
|
-
puts Kdeploy::Banner.show
|
|
33
|
-
puts <<~HELP
|
|
34
|
-
#{pastel.bright_white('📖 Available Commands:')}
|
|
35
|
-
|
|
36
|
-
#{pastel.bright_yellow('🚀')} #{pastel.bright_white('execute TASK_FILE [TASK]')} Execute deployment tasks from file
|
|
37
|
-
#{pastel.dim(' --limit HOSTS')} Limit to specific hosts (comma-separated)
|
|
38
|
-
#{pastel.dim(' --parallel NUM')} Number of parallel executions (default: 5)
|
|
39
|
-
#{pastel.dim(' --dry-run')} Show what would be done without executing
|
|
40
|
-
|
|
41
|
-
#{pastel.bright_yellow('🆕')} #{pastel.bright_white('init [DIR]')} Initialize new deployment project
|
|
42
|
-
#{pastel.bright_yellow('ℹ️')} #{pastel.bright_white('version')} Show version information
|
|
43
|
-
#{pastel.bright_yellow('❓')} #{pastel.bright_white('help [COMMAND]')} Show help information
|
|
44
|
-
|
|
45
|
-
#{pastel.bright_white('💡 Examples:')}
|
|
46
|
-
|
|
47
|
-
#{pastel.dim('# Initialize a new project')}
|
|
48
|
-
#{pastel.bright_cyan('kdeploy init my-deployment')}
|
|
49
|
-
|
|
50
|
-
#{pastel.dim('# Deploy to web servers')}
|
|
51
|
-
#{pastel.bright_cyan('kdeploy execute deploy.rb deploy_web')}
|
|
52
|
-
|
|
53
|
-
#{pastel.dim('# Backup database')}
|
|
54
|
-
#{pastel.bright_cyan('kdeploy execute deploy.rb backup_db')}
|
|
55
|
-
|
|
56
|
-
#{pastel.dim('# Run maintenance on specific hosts')}
|
|
57
|
-
#{pastel.bright_cyan('kdeploy execute deploy.rb maintenance --limit web01')}
|
|
58
|
-
|
|
59
|
-
#{pastel.dim('# Preview deployment')}
|
|
60
|
-
#{pastel.bright_cyan('kdeploy execute deploy.rb deploy_web --dry-run')}
|
|
61
|
-
|
|
62
|
-
#{pastel.bright_white('📚 Documentation:')}
|
|
63
|
-
#{pastel.bright_cyan('https://github.com/kevin197011/kdeploy')}
|
|
64
|
-
|
|
65
|
-
HELP
|
|
31
|
+
show_general_help
|
|
66
32
|
end
|
|
67
33
|
end
|
|
68
34
|
|
|
@@ -80,40 +46,11 @@ module Kdeploy
|
|
|
80
46
|
method_option :parallel, type: :numeric, default: 10, desc: 'Number of parallel executions'
|
|
81
47
|
method_option :dry_run, type: :boolean, desc: 'Show what would be done'
|
|
82
48
|
def execute(task_file, task_name = nil)
|
|
83
|
-
|
|
84
|
-
@banner_printed ||= false
|
|
85
|
-
unless @banner_printed
|
|
86
|
-
puts Kdeploy::Banner.show
|
|
87
|
-
@banner_printed = true
|
|
88
|
-
end
|
|
89
|
-
|
|
49
|
+
show_banner_once
|
|
90
50
|
load_task_file(task_file)
|
|
91
51
|
|
|
92
|
-
tasks_to_run =
|
|
93
|
-
|
|
94
|
-
else
|
|
95
|
-
self.class.kdeploy_tasks.keys
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
tasks_to_run.each do |task|
|
|
99
|
-
task_hosts = self.class.get_task_hosts(task)
|
|
100
|
-
hosts = filter_hosts(options[:limit], task_hosts)
|
|
101
|
-
|
|
102
|
-
if hosts.empty?
|
|
103
|
-
puts Kdeploy::Banner.show_error("No hosts found for task: #{task}")
|
|
104
|
-
next
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
if options[:dry_run]
|
|
108
|
-
print_dry_run(hosts, task)
|
|
109
|
-
next
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
output = ConsoleOutput.new
|
|
113
|
-
runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: options[:parallel], output: output)
|
|
114
|
-
results = runner.run(task)
|
|
115
|
-
print_results(results, task)
|
|
116
|
-
end
|
|
52
|
+
tasks_to_run = determine_tasks(task_name)
|
|
53
|
+
execute_tasks(tasks_to_run)
|
|
117
54
|
rescue StandardError => e
|
|
118
55
|
puts Kdeploy::Banner.show_error(e.message)
|
|
119
56
|
exit 1
|
|
@@ -122,11 +59,7 @@ module Kdeploy
|
|
|
122
59
|
private
|
|
123
60
|
|
|
124
61
|
def load_task_file(file)
|
|
125
|
-
|
|
126
|
-
puts Kdeploy::Banner.show_error("Task file not found: #{file}")
|
|
127
|
-
exit 1
|
|
128
|
-
end
|
|
129
|
-
|
|
62
|
+
validate_task_file(file)
|
|
130
63
|
# 用 instance_eval 并传递顶层 binding,兼容 heredoc
|
|
131
64
|
self.class.module_eval(File.read(file), file)
|
|
132
65
|
rescue StandardError => e
|
|
@@ -135,6 +68,19 @@ module Kdeploy
|
|
|
135
68
|
raise
|
|
136
69
|
end
|
|
137
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
|
+
|
|
138
84
|
def filter_hosts(limit, task_hosts)
|
|
139
85
|
hosts = self.class.kdeploy_hosts.slice(*task_hosts)
|
|
140
86
|
return hosts unless limit
|
|
@@ -144,139 +90,137 @@ module Kdeploy
|
|
|
144
90
|
end
|
|
145
91
|
|
|
146
92
|
def print_dry_run(hosts, task_name)
|
|
93
|
+
formatter = OutputFormatter.new
|
|
147
94
|
puts Kdeploy::Banner.show
|
|
148
|
-
|
|
149
|
-
puts TTY::Box.frame(
|
|
150
|
-
'Showing what would be done without executing any commands',
|
|
151
|
-
title: { top_left: ' Dry Run Mode ', bottom_right: ' Kdeploy ' },
|
|
152
|
-
style: {
|
|
153
|
-
border: {
|
|
154
|
-
fg: :blue
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
)
|
|
95
|
+
puts formatter.format_dry_run_header
|
|
158
96
|
puts
|
|
159
97
|
|
|
160
98
|
hosts.each do |name, config|
|
|
161
|
-
|
|
162
|
-
output = commands.map do |command|
|
|
163
|
-
case command[:type]
|
|
164
|
-
when :run
|
|
165
|
-
"#{pastel.green('>')} #{command[:command]}"
|
|
166
|
-
when :upload
|
|
167
|
-
"#{pastel.blue('>')} Upload: #{command[:source]} -> #{command[:destination]}"
|
|
168
|
-
end
|
|
169
|
-
end.join("\n")
|
|
170
|
-
|
|
171
|
-
puts TTY::Box.frame(
|
|
172
|
-
output,
|
|
173
|
-
title: { top_left: " #{name} (#{config[:ip]}) " },
|
|
174
|
-
style: {
|
|
175
|
-
border: {
|
|
176
|
-
fg: :yellow
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
)
|
|
180
|
-
puts
|
|
99
|
+
print_dry_run_for_host(name, config, task_name, formatter)
|
|
181
100
|
end
|
|
182
101
|
end
|
|
183
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
|
+
|
|
184
111
|
def print_results(results, task_name)
|
|
185
|
-
|
|
186
|
-
puts
|
|
112
|
+
formatter = OutputFormatter.new
|
|
113
|
+
puts formatter.format_task_header(task_name)
|
|
187
114
|
|
|
188
115
|
results.each do |host, result|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
when :upload_template
|
|
212
|
-
puts pastel.yellow(' === Template ===')
|
|
213
|
-
steps.each do |step|
|
|
214
|
-
key = [step[:command], type].hash
|
|
215
|
-
next if shown[key]
|
|
216
|
-
|
|
217
|
-
shown[key] = true
|
|
218
|
-
duration_str = step[:duration] ? pastel.dim(" [#{format('%.2f', step[:duration])}s]") : ''
|
|
219
|
-
puts pastel.yellow(" [template] #{step[:command].sub('upload_template: ', '')}#{duration_str}")
|
|
220
|
-
end
|
|
221
|
-
when :run
|
|
222
|
-
puts pastel.cyan(' === Run ===')
|
|
223
|
-
steps.each do |step|
|
|
224
|
-
key = [step[:command], type].hash
|
|
225
|
-
next if shown[key]
|
|
226
|
-
|
|
227
|
-
shown[key] = true
|
|
228
|
-
duration_str = step[:duration] ? pastel.dim(" [#{format('%.2f', step[:duration])}s]") : ''
|
|
229
|
-
puts pastel.cyan(" [run] #{step[:command].to_s.lines.first.strip}#{duration_str}")
|
|
230
|
-
# 多行命令内容高亮
|
|
231
|
-
cmd_lines = step[:command].to_s.lines[1..].map(&:strip).reject(&:empty?)
|
|
232
|
-
cmd_lines.each { |line| puts pastel.cyan(" > #{line}") } if cmd_lines.any?
|
|
233
|
-
if step[:output].is_a?(Hash) && step[:output][:stdout]
|
|
234
|
-
step[:output][:stdout].each_line do |line|
|
|
235
|
-
puts pastel.green(" #{line.rstrip}") unless line.strip.empty?
|
|
236
|
-
end
|
|
237
|
-
elsif step[:output].is_a?(String)
|
|
238
|
-
step[:output].each_line do |line|
|
|
239
|
-
puts pastel.green(" #{line.rstrip}") unless line.strip.empty?
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
else
|
|
246
|
-
# 失败主机高亮错误
|
|
247
|
-
err = result[:error] || (if result[:output].is_a?(Array)
|
|
248
|
-
result[:output].map do |o|
|
|
249
|
-
o[:output][:stderr] if o[:output].is_a?(Hash)
|
|
250
|
-
end.compact.join("\n")
|
|
251
|
-
else
|
|
252
|
-
result[:output].to_s
|
|
253
|
-
end)
|
|
254
|
-
puts pastel.red(" ERROR: #{err}")
|
|
255
|
-
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 }
|
|
256
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
|
|
257
162
|
|
|
258
|
-
|
|
259
|
-
puts
|
|
163
|
+
def print_summary(results, formatter)
|
|
164
|
+
puts formatter.format_summary_header
|
|
260
165
|
max_host_len = results.keys.map(&:length).max || 16
|
|
261
|
-
|
|
262
|
-
changed_w = 11
|
|
263
|
-
failed_w = 10
|
|
166
|
+
|
|
264
167
|
results.keys.sort.each do |host|
|
|
265
168
|
result = results[host]
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
280
224
|
end
|
|
281
225
|
end
|
|
282
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 = pastel_instance
|
|
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 pastel_instance
|
|
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
|
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,33 @@ 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
|
+
|
|
45
|
+
return [on] if on
|
|
46
|
+
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def normalize_roles_option(roles)
|
|
51
|
+
return roles if roles.is_a?(Array)
|
|
52
|
+
|
|
53
|
+
return [roles] if roles
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def create_task_block(block)
|
|
59
|
+
lambda {
|
|
60
|
+
@kdeploy_commands = []
|
|
61
|
+
instance_eval(&block)
|
|
62
|
+
@kdeploy_commands
|
|
50
63
|
}
|
|
51
64
|
end
|
|
52
65
|
|
|
@@ -76,25 +89,33 @@ module Kdeploy
|
|
|
76
89
|
|
|
77
90
|
def get_task_hosts(task_name)
|
|
78
91
|
task = kdeploy_tasks[task_name]
|
|
79
|
-
return kdeploy_hosts.keys if
|
|
92
|
+
return kdeploy_hosts.keys if task_empty?(task)
|
|
80
93
|
|
|
81
94
|
hosts = Set.new
|
|
95
|
+
add_explicit_hosts(task, hosts)
|
|
96
|
+
add_role_hosts(task, hosts)
|
|
97
|
+
hosts.to_a
|
|
98
|
+
end
|
|
82
99
|
|
|
83
|
-
|
|
100
|
+
def task_empty?(task)
|
|
101
|
+
!task || (!task[:hosts] && !task[:roles])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def add_explicit_hosts(task, hosts)
|
|
84
105
|
task[:hosts]&.each do |host|
|
|
85
106
|
hosts.add(host) if kdeploy_hosts.key?(host)
|
|
86
107
|
end
|
|
108
|
+
end
|
|
87
109
|
|
|
88
|
-
|
|
110
|
+
def add_role_hosts(task, hosts)
|
|
89
111
|
task[:roles]&.each do |role|
|
|
90
|
-
|
|
112
|
+
role_hosts = kdeploy_roles[role]
|
|
113
|
+
next unless role_hosts
|
|
91
114
|
|
|
92
115
|
role_hosts.each do |host|
|
|
93
116
|
hosts.add(host) if kdeploy_hosts.key?(host)
|
|
94
117
|
end
|
|
95
118
|
end
|
|
96
|
-
|
|
97
|
-
hosts.to_a
|
|
98
119
|
end
|
|
99
120
|
end
|
|
100
121
|
end
|
data/lib/kdeploy/executor.rb
CHANGED
|
@@ -17,28 +17,7 @@ module Kdeploy
|
|
|
17
17
|
|
|
18
18
|
def execute(command)
|
|
19
19
|
Net::SSH.start(@ip, @user, ssh_options) do |ssh|
|
|
20
|
-
|
|
21
|
-
stderr = String.new
|
|
22
|
-
|
|
23
|
-
ssh.open_channel do |channel|
|
|
24
|
-
channel.exec(command) do |_ch, success|
|
|
25
|
-
raise SSHError, "Could not execute command: #{command}" unless success
|
|
26
|
-
|
|
27
|
-
channel.on_data do |_ch, data|
|
|
28
|
-
stdout << data
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
channel.on_extended_data do |_ch, _type, data|
|
|
32
|
-
stderr << data
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
ssh.loop
|
|
37
|
-
{
|
|
38
|
-
stdout: stdout.strip,
|
|
39
|
-
stderr: stderr.strip,
|
|
40
|
-
command: command
|
|
41
|
-
}
|
|
20
|
+
execute_command_on_ssh(ssh, command)
|
|
42
21
|
end
|
|
43
22
|
rescue Net::SSH::AuthenticationFailed => e
|
|
44
23
|
raise SSHError.new("SSH authentication failed: #{e.message}", e)
|
|
@@ -46,6 +25,39 @@ module Kdeploy
|
|
|
46
25
|
raise SSHError.new("SSH execution failed: #{e.message}", e)
|
|
47
26
|
end
|
|
48
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
|
+
|
|
49
61
|
def upload(source, destination)
|
|
50
62
|
Net::SCP.start(@ip, @user, ssh_options) do |scp|
|
|
51
63
|
scp.upload!(source, destination)
|
|
@@ -63,18 +75,29 @@ module Kdeploy
|
|
|
63
75
|
private
|
|
64
76
|
|
|
65
77
|
def ssh_options
|
|
66
|
-
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
|
+
{
|
|
67
86
|
verify_host_key: :never,
|
|
68
87
|
timeout: 30
|
|
69
88
|
}
|
|
89
|
+
end
|
|
70
90
|
|
|
91
|
+
def add_authentication(options)
|
|
71
92
|
if @password
|
|
72
93
|
options[:password] = @password
|
|
73
94
|
elsif @key
|
|
74
95
|
options[:keys] = [@key]
|
|
75
96
|
end
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def add_port_option(options)
|
|
100
|
+
options[:port] = @port if @port
|
|
78
101
|
end
|
|
79
102
|
end
|
|
80
103
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pastel'
|
|
4
|
+
|
|
5
|
+
module Kdeploy
|
|
6
|
+
# Formats help text for CLI
|
|
7
|
+
class HelpFormatter
|
|
8
|
+
def initialize
|
|
9
|
+
@pastel = Pastel.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def format_help
|
|
13
|
+
<<~HELP
|
|
14
|
+
#{@pastel.bright_white('📖 Available Commands:')}
|
|
15
|
+
|
|
16
|
+
#{format_commands}
|
|
17
|
+
|
|
18
|
+
#{format_examples}
|
|
19
|
+
|
|
20
|
+
#{format_documentation}
|
|
21
|
+
HELP
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def format_commands
|
|
27
|
+
<<~COMMANDS
|
|
28
|
+
#{@pastel.bright_yellow('🚀')} #{@pastel.bright_white('execute TASK_FILE [TASK]')} Execute deployment tasks from file
|
|
29
|
+
#{@pastel.dim(' --limit HOSTS')} Limit to specific hosts (comma-separated)
|
|
30
|
+
#{@pastel.dim(' --parallel NUM')} Number of parallel executions (default: 5)
|
|
31
|
+
#{@pastel.dim(' --dry-run')} Show what would be done without executing
|
|
32
|
+
|
|
33
|
+
#{@pastel.bright_yellow('🆕')} #{@pastel.bright_white('init [DIR]')} Initialize new deployment project
|
|
34
|
+
#{@pastel.bright_yellow('ℹ️')} #{@pastel.bright_white('version')} Show version information
|
|
35
|
+
#{@pastel.bright_yellow('❓')} #{@pastel.bright_white('help [COMMAND]')} Show help information
|
|
36
|
+
COMMANDS
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def format_examples
|
|
40
|
+
<<~EXAMPLES
|
|
41
|
+
#{@pastel.bright_white('💡 Examples:')}
|
|
42
|
+
|
|
43
|
+
#{@pastel.dim('# Initialize a new project')}
|
|
44
|
+
#{@pastel.bright_cyan('kdeploy init my-deployment')}
|
|
45
|
+
|
|
46
|
+
#{@pastel.dim('# Deploy to web servers')}
|
|
47
|
+
#{@pastel.bright_cyan('kdeploy execute deploy.rb deploy_web')}
|
|
48
|
+
|
|
49
|
+
#{@pastel.dim('# Backup database')}
|
|
50
|
+
#{@pastel.bright_cyan('kdeploy execute deploy.rb backup_db')}
|
|
51
|
+
|
|
52
|
+
#{@pastel.dim('# Run maintenance on specific hosts')}
|
|
53
|
+
#{@pastel.bright_cyan('kdeploy execute deploy.rb maintenance --limit web01')}
|
|
54
|
+
|
|
55
|
+
#{@pastel.dim('# Preview deployment')}
|
|
56
|
+
#{@pastel.bright_cyan('kdeploy execute deploy.rb deploy_web --dry-run')}
|
|
57
|
+
EXAMPLES
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def format_documentation
|
|
61
|
+
<<~DOCS
|
|
62
|
+
#{@pastel.bright_white('📚 Documentation:')}
|
|
63
|
+
#{@pastel.bright_cyan('https://github.com/kevin197011/kdeploy')}
|
|
64
|
+
DOCS
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/kdeploy/output.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Kdeploy
|
|
|
21
21
|
# Console output implementation
|
|
22
22
|
class ConsoleOutput < Output
|
|
23
23
|
def initialize
|
|
24
|
+
super
|
|
24
25
|
@pastel = Pastel.new
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -44,6 +45,7 @@ module Kdeploy
|
|
|
44
45
|
attr_reader :messages, :errors
|
|
45
46
|
|
|
46
47
|
def initialize
|
|
48
|
+
super
|
|
47
49
|
@messages = []
|
|
48
50
|
@errors = []
|
|
49
51
|
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pastel'
|
|
4
|
+
require 'tty-box'
|
|
5
|
+
|
|
6
|
+
module Kdeploy
|
|
7
|
+
# Formats and displays execution results
|
|
8
|
+
class OutputFormatter
|
|
9
|
+
def initialize
|
|
10
|
+
@pastel = Pastel.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def format_task_header(task_name)
|
|
14
|
+
@pastel.cyan("\nPLAY [#{task_name}] " + ('*' * 64))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def format_host_status(host, status)
|
|
18
|
+
status_str = case status
|
|
19
|
+
when :success then @pastel.green('ok')
|
|
20
|
+
when :changed then @pastel.yellow('changed')
|
|
21
|
+
else @pastel.red('failed')
|
|
22
|
+
end
|
|
23
|
+
@pastel.bright_white("\n#{host.ljust(24)} : #{status_str}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def format_upload_steps(steps, shown)
|
|
27
|
+
format_file_steps(steps, shown, :upload, @pastel.green(' === Upload ==='), 'upload: ')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def format_template_steps(steps, shown)
|
|
31
|
+
format_file_steps(steps, shown, :upload_template, @pastel.yellow(' === Template ==='), 'upload_template: ')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def format_file_steps(steps, shown, type, header, prefix)
|
|
35
|
+
output = [header]
|
|
36
|
+
steps.each do |step|
|
|
37
|
+
next if step_already_shown?(step, type, shown)
|
|
38
|
+
|
|
39
|
+
mark_step_as_shown(step, type, shown)
|
|
40
|
+
output << format_file_step(step, type, prefix)
|
|
41
|
+
end
|
|
42
|
+
output
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_file_step(step, type, prefix)
|
|
46
|
+
duration_str = format_duration(step[:duration])
|
|
47
|
+
label = type == :upload ? '[upload]' : '[template]'
|
|
48
|
+
color(" #{label} #{step[:command].sub(prefix, '')}#{duration_str}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def format_run_steps(steps, shown)
|
|
52
|
+
output = []
|
|
53
|
+
output << @pastel.cyan(' === Run ===')
|
|
54
|
+
steps.each do |step|
|
|
55
|
+
next if step_already_shown?(step, :run, shown)
|
|
56
|
+
|
|
57
|
+
mark_step_as_shown(step, :run, shown)
|
|
58
|
+
output.concat(format_single_run_step(step))
|
|
59
|
+
end
|
|
60
|
+
output
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def format_single_run_step(step)
|
|
64
|
+
output = []
|
|
65
|
+
duration_str = format_duration(step[:duration])
|
|
66
|
+
command_line = step[:command].to_s.lines.first.strip
|
|
67
|
+
output << @pastel.cyan(" [run] #{command_line}#{duration_str}")
|
|
68
|
+
output.concat(format_multiline_command(step[:command]))
|
|
69
|
+
output.concat(format_command_output(step[:output]))
|
|
70
|
+
output
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def format_error(error_message)
|
|
74
|
+
@pastel.red(" ERROR: #{error_message}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def format_summary_header
|
|
78
|
+
@pastel.cyan("\nPLAY RECAP #{'*' * 64}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def format_summary_line(host, result, max_host_len)
|
|
82
|
+
counts = calculate_summary_counts(result)
|
|
83
|
+
line = build_summary_line(host, counts, max_host_len)
|
|
84
|
+
colorize_summary_line(line, counts)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def calculate_summary_counts(result)
|
|
88
|
+
ok = %i[success changed].include?(result[:status]) ? result[:output].size : 0
|
|
89
|
+
failed = result[:status] == :failed ? 1 : 0
|
|
90
|
+
changed = result[:status] == :changed ? result[:output].size : 0
|
|
91
|
+
{ ok: ok, failed: failed, changed: changed }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_summary_line(host, counts, max_host_len)
|
|
95
|
+
ok_w = 7
|
|
96
|
+
changed_w = 11
|
|
97
|
+
failed_w = 10
|
|
98
|
+
|
|
99
|
+
ok_str = @pastel.green("ok=#{counts[:ok].to_s.ljust(ok_w - 3)}")
|
|
100
|
+
changed_str = @pastel.yellow("changed=#{counts[:changed].to_s.ljust(changed_w - 8)}")
|
|
101
|
+
failed_str = @pastel.red("failed=#{counts[:failed].to_s.ljust(failed_w - 7)}")
|
|
102
|
+
"#{host.ljust(max_host_len)} : #{ok_str} #{changed_str} #{failed_str}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def colorize_summary_line(line, counts)
|
|
106
|
+
if counts[:failed].positive?
|
|
107
|
+
@pastel.red(line)
|
|
108
|
+
elsif counts[:ok].positive? && counts[:failed].zero?
|
|
109
|
+
@pastel.green(line)
|
|
110
|
+
else
|
|
111
|
+
line
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def format_dry_run_box(title, content)
|
|
116
|
+
TTY::Box.frame(
|
|
117
|
+
content,
|
|
118
|
+
title: { top_left: " #{title} " },
|
|
119
|
+
style: {
|
|
120
|
+
border: {
|
|
121
|
+
fg: :yellow
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def format_dry_run_header
|
|
128
|
+
TTY::Box.frame(
|
|
129
|
+
'Showing what would be done without executing any commands',
|
|
130
|
+
title: { top_left: ' Dry Run Mode ', bottom_right: ' Kdeploy ' },
|
|
131
|
+
style: {
|
|
132
|
+
border: {
|
|
133
|
+
fg: :blue
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def format_command_for_dry_run(command)
|
|
140
|
+
case command[:type]
|
|
141
|
+
when :run
|
|
142
|
+
"#{@pastel.green('>')} #{command[:command]}"
|
|
143
|
+
when :upload
|
|
144
|
+
"#{@pastel.blue('>')} Upload: #{command[:source]} -> #{command[:destination]}"
|
|
145
|
+
when :upload_template
|
|
146
|
+
"#{@pastel.blue('>')} Template: #{command[:source]} -> #{command[:destination]}"
|
|
147
|
+
else
|
|
148
|
+
"#{@pastel.blue('>')} #{command[:type]}: #{command}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def format_duration(duration)
|
|
155
|
+
duration ? @pastel.dim(" [#{format('%.2f', duration)}s]") : ''
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def format_multiline_command(command)
|
|
159
|
+
output = []
|
|
160
|
+
cmd_lines = command.to_s.lines[1..].map(&:strip).reject(&:empty?)
|
|
161
|
+
cmd_lines.each { |line| output << @pastel.cyan(" > #{line}") } if cmd_lines.any?
|
|
162
|
+
output
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def format_command_output(output)
|
|
166
|
+
result = []
|
|
167
|
+
return result unless output
|
|
168
|
+
|
|
169
|
+
if output.is_a?(Hash) && output[:stdout]
|
|
170
|
+
format_stdout_lines(output[:stdout], result)
|
|
171
|
+
elsif output.is_a?(String)
|
|
172
|
+
format_stdout_lines(output, result)
|
|
173
|
+
end
|
|
174
|
+
result
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def format_stdout_lines(stdout, result)
|
|
178
|
+
stdout.each_line do |line|
|
|
179
|
+
result << @pastel.green(" #{line.rstrip}") unless line.strip.empty?
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def step_already_shown?(step, type, shown)
|
|
184
|
+
key = [step[:command], type].hash
|
|
185
|
+
shown[key]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def mark_step_as_shown(step, type, shown)
|
|
189
|
+
key = [step[:command], type].hash
|
|
190
|
+
shown[key] = true
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/kdeploy/post_install.rb
CHANGED
|
@@ -22,20 +22,7 @@ module Kdeploy
|
|
|
22
22
|
return if completion_path.nil?
|
|
23
23
|
|
|
24
24
|
source_line = "source \"#{completion_path}\""
|
|
25
|
-
|
|
26
|
-
# 检查是否已经配置
|
|
27
|
-
content = File.read(bashrc_path)
|
|
28
|
-
if content.match?(/source.*kdeploy\.bash/)
|
|
29
|
-
# 更新现有的配置
|
|
30
|
-
new_content = content.gsub(/source.*kdeploy\.bash.*$/, source_line)
|
|
31
|
-
File.write(bashrc_path, new_content)
|
|
32
|
-
else
|
|
33
|
-
# 添加新配置
|
|
34
|
-
File.open(bashrc_path, 'a') do |f|
|
|
35
|
-
f.puts "\n# Kdeploy completion"
|
|
36
|
-
f.puts source_line
|
|
37
|
-
end
|
|
38
|
-
end
|
|
25
|
+
update_shell_config(bashrc_path, source_line, /source.*kdeploy\.bash/)
|
|
39
26
|
puts "✅ Bash completion configured in #{bashrc_path}"
|
|
40
27
|
rescue StandardError => e
|
|
41
28
|
puts "⚠️ Failed to configure Bash completion: #{e.message}"
|
|
@@ -47,64 +34,97 @@ module Kdeploy
|
|
|
47
34
|
completion_path = find_completion_file('kdeploy.zsh')
|
|
48
35
|
return if completion_path.nil?
|
|
49
36
|
|
|
50
|
-
source_lines =
|
|
37
|
+
source_lines = build_zsh_source_lines(completion_path)
|
|
38
|
+
update_zsh_config(zshrc_path, source_lines)
|
|
39
|
+
puts "✅ Zsh completion configured in #{zshrc_path}"
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
puts "⚠️ Failed to configure Zsh completion: #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def find_completion_file(filename)
|
|
45
|
+
paths = build_completion_paths(filename)
|
|
46
|
+
|
|
47
|
+
paths.each do |path|
|
|
48
|
+
found_path = search_path(path)
|
|
49
|
+
return found_path if found_path
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
report_completion_file_not_found(filename, paths)
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def bashrc_path
|
|
57
|
+
File.join(Dir.home, '.bashrc')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def zshrc_path
|
|
61
|
+
File.join(Dir.home, '.zshrc')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def update_shell_config(config_path, source_line, pattern)
|
|
65
|
+
content = File.read(config_path)
|
|
66
|
+
if content.match?(pattern)
|
|
67
|
+
new_content = content.gsub(/#{pattern.source}.*$/, source_line)
|
|
68
|
+
File.write(config_path, new_content)
|
|
69
|
+
else
|
|
70
|
+
append_shell_config(config_path, source_line)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def append_shell_config(config_path, source_line)
|
|
75
|
+
File.open(config_path, 'a') do |f|
|
|
76
|
+
f.puts "\n# Kdeploy completion"
|
|
77
|
+
f.puts source_line
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_zsh_source_lines(completion_path)
|
|
82
|
+
[
|
|
51
83
|
"source \"#{completion_path}\"",
|
|
52
84
|
'autoload -Uz compinit && compinit'
|
|
53
85
|
]
|
|
86
|
+
end
|
|
54
87
|
|
|
88
|
+
def update_zsh_config(zshrc_path, source_lines)
|
|
55
89
|
content = File.read(zshrc_path)
|
|
56
|
-
|
|
57
|
-
# 检查是否已经配置
|
|
58
90
|
if content.match?(/source.*kdeploy\.zsh/)
|
|
59
|
-
# 更新现有的配置
|
|
60
91
|
new_content = content.gsub(/source.*kdeploy\.zsh.*$/, source_lines[0])
|
|
61
92
|
File.write(zshrc_path, new_content)
|
|
62
93
|
else
|
|
63
|
-
|
|
64
|
-
File.open(zshrc_path, 'a') do |f|
|
|
65
|
-
f.puts "\n# Kdeploy completion"
|
|
66
|
-
source_lines.each { |line| f.puts line unless content.include?(line) }
|
|
67
|
-
end
|
|
94
|
+
append_zsh_config(zshrc_path, source_lines, content)
|
|
68
95
|
end
|
|
69
|
-
puts "✅ Zsh completion configured in #{zshrc_path}"
|
|
70
|
-
rescue StandardError => e
|
|
71
|
-
puts "⚠️ Failed to configure Zsh completion: #{e.message}"
|
|
72
96
|
end
|
|
73
97
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
98
|
+
def append_zsh_config(zshrc_path, source_lines, content)
|
|
99
|
+
File.open(zshrc_path, 'a') do |f|
|
|
100
|
+
f.puts "\n# Kdeploy completion"
|
|
101
|
+
source_lines.each { |line| f.puts line unless content.include?(line) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_completion_paths(filename)
|
|
106
|
+
[
|
|
78
107
|
File.expand_path("../completions/#{filename}", __FILE__),
|
|
79
|
-
# RubyGems 安装路径
|
|
80
108
|
*Gem.path.map { |path| File.join(path, "gems/kdeploy-*/lib/kdeploy/completions/#{filename}") },
|
|
81
|
-
# 系统路径
|
|
82
109
|
"/usr/local/share/kdeploy/completions/#{filename}",
|
|
83
110
|
"/usr/share/kdeploy/completions/#{filename}"
|
|
84
111
|
]
|
|
112
|
+
end
|
|
85
113
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return path
|
|
93
|
-
end
|
|
114
|
+
def search_path(path)
|
|
115
|
+
if path.include?('*')
|
|
116
|
+
matches = Dir.glob(path)
|
|
117
|
+
return matches.first if matches.any?
|
|
118
|
+
elsif File.exist?(path)
|
|
119
|
+
return path
|
|
94
120
|
end
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
95
123
|
|
|
124
|
+
def report_completion_file_not_found(filename, paths)
|
|
96
125
|
puts "⚠️ Could not find completion file: #{filename}"
|
|
97
126
|
puts 'Searched paths:'
|
|
98
127
|
paths.each { |path| puts " - #{path}" }
|
|
99
|
-
nil
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def bashrc_path
|
|
103
|
-
File.join(Dir.home, '.bashrc')
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def zshrc_path
|
|
107
|
-
File.join(Dir.home, '.zshrc')
|
|
108
128
|
end
|
|
109
129
|
end
|
|
110
130
|
end
|
data/lib/kdeploy/runner.rb
CHANGED
|
@@ -15,19 +15,32 @@ module Kdeploy
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def run(task_name)
|
|
18
|
+
task = find_task(task_name)
|
|
19
|
+
execute_concurrent_tasks(task)
|
|
20
|
+
ensure
|
|
21
|
+
@pool.shutdown
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find_task(task_name)
|
|
18
25
|
task = @tasks[task_name]
|
|
26
|
+
|
|
19
27
|
raise TaskNotFoundError, task_name unless task
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
task
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def execute_concurrent_tasks(task)
|
|
33
|
+
futures = create_task_futures(task)
|
|
34
|
+
futures.each(&:wait)
|
|
35
|
+
@results
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_task_futures(task)
|
|
39
|
+
@hosts.map do |name, config|
|
|
22
40
|
Concurrent::Future.execute(executor: @pool) do
|
|
23
41
|
execute_task_for_host(name, config, task)
|
|
24
42
|
end
|
|
25
43
|
end
|
|
26
|
-
|
|
27
|
-
futures.each(&:wait)
|
|
28
|
-
@results
|
|
29
|
-
ensure
|
|
30
|
-
@pool.shutdown
|
|
31
44
|
end
|
|
32
45
|
|
|
33
46
|
private
|
|
@@ -37,23 +50,30 @@ module Kdeploy
|
|
|
37
50
|
command_executor = CommandExecutor.new(executor, @output)
|
|
38
51
|
result = { status: :success, output: [] }
|
|
39
52
|
|
|
53
|
+
execute_grouped_commands(task, command_executor, name, result)
|
|
54
|
+
@results[name] = result
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
@results[name] = { status: :failed, error: e.message }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def execute_grouped_commands(task, command_executor, name, result)
|
|
40
60
|
commands = task[:block].call
|
|
41
61
|
grouped_commands = CommandGrouper.group(commands)
|
|
42
62
|
|
|
43
63
|
grouped_commands.each_value do |command_group|
|
|
44
|
-
|
|
45
|
-
task_desc = CommandGrouper.task_description(first_cmd)
|
|
46
|
-
show_task_header(task_desc)
|
|
47
|
-
|
|
48
|
-
command_group.each do |command|
|
|
49
|
-
step_result = execute_command(command_executor, command, name)
|
|
50
|
-
result[:output] << step_result
|
|
51
|
-
end
|
|
64
|
+
execute_command_group(command_group, command_executor, name, result)
|
|
52
65
|
end
|
|
66
|
+
end
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
def execute_command_group(command_group, command_executor, name, result)
|
|
69
|
+
first_cmd = command_group.first
|
|
70
|
+
task_desc = CommandGrouper.task_description(first_cmd)
|
|
71
|
+
show_task_header(task_desc)
|
|
72
|
+
|
|
73
|
+
command_group.each do |command|
|
|
74
|
+
step_result = execute_command(command_executor, command, name)
|
|
75
|
+
result[:output] << step_result
|
|
76
|
+
end
|
|
57
77
|
end
|
|
58
78
|
|
|
59
79
|
def execute_command(command_executor, command, host_name)
|
data/lib/kdeploy/template.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'erb'
|
|
4
|
-
require 'ostruct'
|
|
5
4
|
require 'tempfile'
|
|
6
5
|
|
|
7
6
|
module Kdeploy
|
|
@@ -9,10 +8,19 @@ module Kdeploy
|
|
|
9
8
|
class Template
|
|
10
9
|
def self.render(template_path, variables = {})
|
|
11
10
|
template_content = File.read(template_path)
|
|
12
|
-
context =
|
|
11
|
+
context = create_template_context(variables)
|
|
13
12
|
ERB.new(template_content).result(context.instance_eval { binding })
|
|
14
13
|
end
|
|
15
14
|
|
|
15
|
+
def self.create_template_context(variables)
|
|
16
|
+
# Use a simple class instead of OpenStruct for better performance
|
|
17
|
+
context_class = Class.new
|
|
18
|
+
variables.each do |key, value|
|
|
19
|
+
context_class.define_method(key) { value }
|
|
20
|
+
end
|
|
21
|
+
context_class.new
|
|
22
|
+
end
|
|
23
|
+
|
|
16
24
|
def self.render_and_upload(executor, template_path, destination, variables = {})
|
|
17
25
|
rendered_content = render(template_path, variables)
|
|
18
26
|
|
data/lib/kdeploy/version.rb
CHANGED
data/lib/kdeploy.rb
CHANGED
|
@@ -9,6 +9,8 @@ require_relative 'kdeploy/dsl'
|
|
|
9
9
|
require_relative 'kdeploy/executor'
|
|
10
10
|
require_relative 'kdeploy/command_grouper'
|
|
11
11
|
require_relative 'kdeploy/command_executor'
|
|
12
|
+
require_relative 'kdeploy/output_formatter'
|
|
13
|
+
require_relative 'kdeploy/help_formatter'
|
|
12
14
|
require_relative 'kdeploy/runner'
|
|
13
15
|
require_relative 'kdeploy/initializer'
|
|
14
16
|
require_relative 'kdeploy/template'
|
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kk
|
|
@@ -146,8 +146,10 @@ files:
|
|
|
146
146
|
- lib/kdeploy/dsl.rb
|
|
147
147
|
- lib/kdeploy/errors.rb
|
|
148
148
|
- lib/kdeploy/executor.rb
|
|
149
|
+
- lib/kdeploy/help_formatter.rb
|
|
149
150
|
- lib/kdeploy/initializer.rb
|
|
150
151
|
- lib/kdeploy/output.rb
|
|
152
|
+
- lib/kdeploy/output_formatter.rb
|
|
151
153
|
- lib/kdeploy/post_install.rb
|
|
152
154
|
- lib/kdeploy/runner.rb
|
|
153
155
|
- lib/kdeploy/template.rb
|