kdeploy 0.1.0 → 0.3.0
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/README.md +101 -936
- data/exe/kdeploy +6 -0
- data/k.md +149 -0
- data/lib/kdeploy/banner.rb +44 -14
- data/lib/kdeploy/cli.rb +138 -1389
- data/lib/kdeploy/dsl.rb +66 -530
- data/lib/kdeploy/executor.rb +73 -0
- data/lib/kdeploy/initializer.rb +229 -0
- data/lib/kdeploy/runner.rb +40 -180
- data/lib/kdeploy/template.rb +18 -161
- data/lib/kdeploy/version.rb +1 -2
- data/lib/kdeploy.rb +9 -100
- metadata +75 -52
- data/.editorconfig +0 -12
- data/.rspec +0 -3
- data/.rubocop.yml +0 -100
- data/LICENSE +0 -21
- data/Rakefile +0 -45
- data/bin/kdeploy +0 -7
- data/kdeploy.gemspec +0 -49
- data/lib/kdeploy/command.rb +0 -182
- data/lib/kdeploy/configuration.rb +0 -83
- data/lib/kdeploy/host.rb +0 -85
- data/lib/kdeploy/inventory.rb +0 -243
- data/lib/kdeploy/logger.rb +0 -100
- data/lib/kdeploy/pipeline.rb +0 -249
- data/lib/kdeploy/ssh_connection.rb +0 -187
- data/lib/kdeploy/statistics.rb +0 -439
- data/lib/kdeploy/task.rb +0 -240
- data/scripts/common_tasks.rb +0 -218
- data/scripts/deploy.rb +0 -50
data/lib/kdeploy/dsl.rb
CHANGED
@@ -1,566 +1,102 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Kdeploy
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
@inventory = nil
|
10
|
-
@script_dir = script_dir || Dir.pwd
|
11
|
-
@template_manager = nil
|
4
|
+
module DSL
|
5
|
+
def self.extended(base)
|
6
|
+
base.instance_variable_set(:@kdeploy_hosts, {})
|
7
|
+
base.instance_variable_set(:@kdeploy_tasks, {})
|
8
|
+
base.instance_variable_set(:@kdeploy_roles, {})
|
12
9
|
end
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
# @return [Pipeline] Pipeline instance
|
17
|
-
def pipeline(name = nil)
|
18
|
-
@pipeline.instance_variable_set(:@name, name) if name
|
19
|
-
@pipeline
|
11
|
+
def kdeploy_hosts
|
12
|
+
@kdeploy_hosts ||= {}
|
20
13
|
end
|
21
14
|
|
22
|
-
|
23
|
-
|
24
|
-
# @param value [Object] Variable value
|
25
|
-
def set(key, value)
|
26
|
-
@pipeline.set_variable(key, value)
|
15
|
+
def kdeploy_tasks
|
16
|
+
@kdeploy_tasks ||= {}
|
27
17
|
end
|
28
18
|
|
29
|
-
|
30
|
-
|
31
|
-
# @param user [String] SSH user
|
32
|
-
# @param port [Integer] SSH port
|
33
|
-
# @param roles [Array] Host roles
|
34
|
-
# @param vars [Hash] Host variables
|
35
|
-
# @param ssh_options [Hash] SSH connection options
|
36
|
-
# @return [Host] Created host
|
37
|
-
def host(hostname, user: nil, port: nil, roles: [], vars: {}, **ssh_options)
|
38
|
-
@pipeline.add_host(
|
39
|
-
hostname,
|
40
|
-
user: user,
|
41
|
-
port: port,
|
42
|
-
roles: roles,
|
43
|
-
vars: vars,
|
44
|
-
ssh_options: ssh_options
|
45
|
-
)
|
19
|
+
def kdeploy_roles
|
20
|
+
@kdeploy_roles ||= {}
|
46
21
|
end
|
47
22
|
|
48
|
-
|
49
|
-
|
50
|
-
def hosts(hosts_config)
|
51
|
-
@pipeline.add_hosts(hosts_config)
|
23
|
+
def host(name, **options)
|
24
|
+
kdeploy_hosts[name] = options.merge(name: name)
|
52
25
|
end
|
53
26
|
|
54
|
-
|
55
|
-
|
56
|
-
def inventory(inventory_file = nil)
|
57
|
-
inventory_file ||= default_inventory_file
|
58
|
-
resolved_path = resolve_inventory_path(inventory_file)
|
59
|
-
|
60
|
-
return unless File.exist?(resolved_path)
|
61
|
-
|
62
|
-
load_inventory(resolved_path)
|
63
|
-
end
|
64
|
-
|
65
|
-
# Initialize template manager
|
66
|
-
# @param template_dir [String] Template directory path
|
67
|
-
def template_dir(template_dir = nil)
|
68
|
-
template_dir ||= default_template_dir
|
69
|
-
resolved_path = resolve_template_path(template_dir)
|
70
|
-
|
71
|
-
@template_manager = TemplateManager.new(resolved_path, @pipeline.variables)
|
72
|
-
KdeployLogger.info("Template directory set to: #{resolved_path}")
|
73
|
-
end
|
74
|
-
|
75
|
-
# Get or initialize template manager
|
76
|
-
# @return [TemplateManager] Template manager instance
|
77
|
-
def template_manager
|
78
|
-
@template_manager ||= begin
|
79
|
-
dir = default_template_dir
|
80
|
-
resolved_path = resolve_template_path(dir)
|
81
|
-
TemplateManager.new(resolved_path, @pipeline.variables)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
# Define task
|
86
|
-
# @param name [String] Task name
|
87
|
-
# @param on [Array, Symbol] Target hosts or roles
|
88
|
-
# @param parallel [Boolean] Execute in parallel
|
89
|
-
# @param fail_fast [Boolean] Stop on first failure
|
90
|
-
# @param max_concurrent [Integer] Maximum concurrent executions
|
91
|
-
# @return [Task] Created task
|
92
|
-
def task(name, on: nil, parallel: true, fail_fast: false, max_concurrent: nil, &block)
|
93
|
-
target_hosts = resolve_target_hosts(on)
|
94
|
-
|
95
|
-
@current_task = @pipeline.add_task(
|
96
|
-
name,
|
97
|
-
hosts: target_hosts,
|
98
|
-
parallel: parallel,
|
99
|
-
fail_fast: fail_fast,
|
100
|
-
max_concurrent: max_concurrent
|
101
|
-
)
|
102
|
-
|
103
|
-
instance_eval(&block) if block
|
104
|
-
@current_task = nil
|
105
|
-
end
|
106
|
-
|
107
|
-
# Execute command in current task
|
108
|
-
# @param command [String] Command to execute (supports heredoc)
|
109
|
-
# @param name [String] Command name (optional)
|
110
|
-
# @param timeout [Integer] Command timeout
|
111
|
-
# @param retry_count [Integer] Number of retries
|
112
|
-
# @param retry_delay [Integer] Delay between retries
|
113
|
-
# @param ignore_errors [Boolean] Continue on error
|
114
|
-
# @param only [Array, Symbol] Run only on specified roles
|
115
|
-
# @param except [Array, Symbol] Skip specified roles
|
116
|
-
# @return [Array<Command>] Created commands
|
117
|
-
def run(command, name: nil, timeout: nil, retry_count: nil, retry_delay: nil,
|
118
|
-
ignore_errors: false, only: nil, except: nil)
|
119
|
-
raise 'run can only be called within a task block' unless @current_task
|
120
|
-
|
121
|
-
process_commands(command, name).each_with_object([]) do |(cmd_name, cmd), commands|
|
122
|
-
commands << add_command_to_task(
|
123
|
-
cmd_name, cmd, timeout, retry_count, retry_delay,
|
124
|
-
ignore_errors, only, except
|
125
|
-
)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# Execute local command
|
130
|
-
# @param command [String] Local command to execute
|
131
|
-
# @param name [String] Command name (optional)
|
132
|
-
# @return [Array<Hash>] Command results
|
133
|
-
def local(command, name: nil)
|
134
|
-
require 'open3'
|
135
|
-
|
136
|
-
process_commands(command, name).map do |cmd_name, cmd|
|
137
|
-
execute_local_command(cmd, cmd_name)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
# Upload file to remote host
|
142
|
-
# @param local_path [String] Local file path
|
143
|
-
# @param remote_path [String] Remote file path
|
144
|
-
# @param name [String] Command name (optional)
|
145
|
-
# @return [Command] Created upload command
|
146
|
-
def upload(local_path, remote_path, name: nil)
|
147
|
-
raise 'upload can only be called within a task block' unless @current_task
|
148
|
-
|
149
|
-
ensure_remote_directory(remote_path)
|
150
|
-
command_name = name || "upload_#{File.basename(local_path)}"
|
151
|
-
add_upload_command(local_path, remote_path, command_name)
|
152
|
-
end
|
153
|
-
|
154
|
-
# Upload template to remote host
|
155
|
-
# @param template_name [String] Template name
|
156
|
-
# @param remote_path [String] Remote file path
|
157
|
-
# @param variables [Hash] Template variables
|
158
|
-
# @param name [String] Command name (optional)
|
159
|
-
# @return [Command] Created template upload command
|
160
|
-
def upload_template(template_name, remote_path, variables: {}, name: nil)
|
161
|
-
raise 'upload_template can only be called within a task block' unless @current_task
|
162
|
-
|
163
|
-
ensure_remote_directory(remote_path)
|
164
|
-
command_name = name || "upload_template_#{File.basename(template_name)}"
|
165
|
-
add_template_upload_command(template_name, remote_path, variables, command_name)
|
166
|
-
end
|
167
|
-
|
168
|
-
# Download file from remote host
|
169
|
-
# @param remote_path [String] Remote file path
|
170
|
-
# @param local_path [String] Local file path
|
171
|
-
# @param name [String] Command name (optional)
|
172
|
-
# @return [Command] Created download command
|
173
|
-
def download(remote_path, local_path, name: nil)
|
174
|
-
raise 'download can only be called within a task block' unless @current_task
|
175
|
-
|
176
|
-
command_name = name || "download_#{File.basename(remote_path)}"
|
177
|
-
add_download_command(remote_path, local_path, command_name)
|
178
|
-
end
|
179
|
-
|
180
|
-
# Get hosts by role
|
181
|
-
# @param role [String, Symbol] Role to filter by
|
182
|
-
# @return [Array<Host>] Hosts with specified role
|
183
|
-
def role(role)
|
184
|
-
@pipeline.hosts_with_role(role)
|
185
|
-
end
|
186
|
-
|
187
|
-
# Conditional execution
|
188
|
-
# @param condition [Boolean] Condition to check
|
189
|
-
# @yield Block to execute if condition is true
|
190
|
-
def when(condition, &)
|
191
|
-
instance_eval(&) if condition && block_given?
|
192
|
-
end
|
193
|
-
|
194
|
-
# Inverse conditional execution
|
195
|
-
# @param condition [Boolean] Condition to check
|
196
|
-
# @yield Block to execute if condition is false
|
197
|
-
def unless(condition, &)
|
198
|
-
instance_eval(&) if !condition && block_given?
|
27
|
+
def role(name, hosts)
|
28
|
+
kdeploy_roles[name] = hosts
|
199
29
|
end
|
200
30
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
File.join(@script_dir, inventory_file)
|
220
|
-
end
|
221
|
-
|
222
|
-
def load_inventory(inventory_file)
|
223
|
-
@inventory = Inventory.new(inventory_file)
|
224
|
-
add_inventory_hosts
|
225
|
-
set_inventory_variables
|
226
|
-
log_inventory_loaded(inventory_file)
|
227
|
-
end
|
228
|
-
|
229
|
-
def add_inventory_hosts
|
230
|
-
@inventory.all_hosts.each do |host|
|
231
|
-
@pipeline.hosts << host unless @pipeline.hosts.include?(host)
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
def set_inventory_variables
|
236
|
-
@inventory.vars.each do |key, value|
|
237
|
-
@pipeline.set_variable(key, value)
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def log_inventory_loaded(inventory_file)
|
242
|
-
KdeployLogger.info(
|
243
|
-
"Loaded #{@inventory.hosts.size} hosts from inventory: #{inventory_file}"
|
244
|
-
)
|
245
|
-
end
|
246
|
-
|
247
|
-
def default_template_dir
|
248
|
-
Kdeploy.configuration&.template_dir || 'templates'
|
249
|
-
end
|
250
|
-
|
251
|
-
def resolve_template_path(template_dir)
|
252
|
-
return template_dir if File.absolute_path?(template_dir)
|
253
|
-
|
254
|
-
File.join(@script_dir, template_dir)
|
255
|
-
end
|
256
|
-
|
257
|
-
def process_commands(command, base_name)
|
258
|
-
commands = process_heredoc_command(command)
|
259
|
-
commands.each_with_index.map do |cmd, index|
|
260
|
-
cmd = cmd.strip
|
261
|
-
next if cmd.empty? || cmd.start_with?('#')
|
262
|
-
|
263
|
-
[generate_command_name(cmd, base_name, index, commands.size), cmd]
|
264
|
-
end.compact
|
265
|
-
end
|
266
|
-
|
267
|
-
def generate_command_name(cmd, base_name, index, total)
|
268
|
-
return base_name if base_name && total == 1
|
269
|
-
|
270
|
-
prefix = base_name || (cmd.split.first || 'unnamed')
|
271
|
-
total > 1 ? "#{prefix}_#{index + 1}" : prefix
|
272
|
-
end
|
273
|
-
|
274
|
-
def add_command_to_task(name, cmd, timeout, retry_count, retry_delay,
|
275
|
-
ignore_errors, only, except)
|
276
|
-
@current_task.add_command(
|
277
|
-
name,
|
278
|
-
cmd,
|
279
|
-
timeout: timeout,
|
280
|
-
retry_count: retry_count,
|
281
|
-
retry_delay: retry_delay,
|
282
|
-
ignore_errors: ignore_errors,
|
283
|
-
only: only,
|
284
|
-
except: except
|
285
|
-
)
|
286
|
-
end
|
287
|
-
|
288
|
-
def execute_local_command(cmd, name)
|
289
|
-
processed_cmd = process_local_command_variables(cmd)
|
290
|
-
start_time = Time.now
|
291
|
-
|
292
|
-
log_local_command_start(name, processed_cmd)
|
293
|
-
stdout, stderr, status = Open3.capture3(processed_cmd)
|
294
|
-
duration = Time.now - start_time
|
295
|
-
|
296
|
-
handle_local_command_result(name, duration, status.exitstatus, stdout, stderr)
|
297
|
-
end
|
298
|
-
|
299
|
-
def process_local_command_variables(cmd)
|
300
|
-
processed_cmd = cmd.dup
|
301
|
-
@pipeline.variables.each do |key, value|
|
302
|
-
processed_cmd.gsub!(/\$\{#{key}\}/, value.to_s)
|
303
|
-
end
|
304
|
-
processed_cmd
|
305
|
-
end
|
306
|
-
|
307
|
-
def log_local_command_start(name, cmd)
|
308
|
-
KdeployLogger.info("🚀 Executing local command '#{name}'")
|
309
|
-
KdeployLogger.debug("Command: #{cmd}")
|
310
|
-
end
|
311
|
-
|
312
|
-
def handle_local_command_result(name, duration, exit_code, stdout, stderr)
|
313
|
-
if exit_code.zero?
|
314
|
-
log_local_command_success(name, duration, stdout)
|
315
|
-
else
|
316
|
-
log_local_command_failure(name, duration, exit_code, stdout, stderr)
|
317
|
-
end
|
318
|
-
|
319
|
-
{
|
320
|
-
name: name,
|
321
|
-
success: exit_code.zero?,
|
322
|
-
duration: duration,
|
323
|
-
exit_code: exit_code,
|
324
|
-
stdout: stdout,
|
325
|
-
stderr: stderr
|
31
|
+
def task(name, on: nil, roles: nil, &block)
|
32
|
+
kdeploy_tasks[name] = {
|
33
|
+
hosts: if on.is_a?(Array)
|
34
|
+
on
|
35
|
+
else
|
36
|
+
(on ? [on] : nil)
|
37
|
+
end,
|
38
|
+
roles: if roles.is_a?(Array)
|
39
|
+
roles
|
40
|
+
else
|
41
|
+
(roles ? [roles] : nil)
|
42
|
+
end,
|
43
|
+
block: lambda {
|
44
|
+
@kdeploy_commands = []
|
45
|
+
instance_eval(&block)
|
46
|
+
@kdeploy_commands
|
47
|
+
}
|
326
48
|
}
|
327
49
|
end
|
328
50
|
|
329
|
-
def
|
330
|
-
|
331
|
-
|
332
|
-
end
|
333
|
-
|
334
|
-
def log_local_command_failure(name, duration, exit_code, stdout, stderr)
|
335
|
-
KdeployLogger.error("❌ Local command '#{name}' failed in #{duration.round(2)}s (exit code: #{exit_code})")
|
336
|
-
KdeployLogger.error("STDERR: #{stderr}") unless stderr.empty?
|
337
|
-
KdeployLogger.error("STDOUT: #{stdout}") unless stdout.strip.empty?
|
338
|
-
end
|
339
|
-
|
340
|
-
def ensure_remote_directory(remote_path)
|
341
|
-
run("mkdir -p #{File.dirname(remote_path)}", name: 'create directory')
|
342
|
-
end
|
343
|
-
|
344
|
-
def add_upload_command(local_path, remote_path, _command_name)
|
345
|
-
upload_command = UploadCommand.new(local_path, remote_path, @pipeline.variables)
|
346
|
-
@current_task.commands << upload_command
|
347
|
-
upload_command
|
51
|
+
def run(command)
|
52
|
+
@kdeploy_commands ||= []
|
53
|
+
@kdeploy_commands << { type: :run, command: command }
|
348
54
|
end
|
349
55
|
|
350
|
-
def
|
351
|
-
|
352
|
-
|
353
|
-
remote_path,
|
354
|
-
variables,
|
355
|
-
template_manager,
|
356
|
-
@pipeline.variables
|
357
|
-
)
|
358
|
-
@current_task.commands << command
|
359
|
-
command
|
56
|
+
def upload(source, destination)
|
57
|
+
@kdeploy_commands ||= []
|
58
|
+
@kdeploy_commands << { type: :upload, source: source, destination: destination }
|
360
59
|
end
|
361
60
|
|
362
|
-
def
|
363
|
-
|
364
|
-
@
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
Array(target).flat_map { |t| resolve_single_target(t) }.uniq
|
372
|
-
end
|
373
|
-
|
374
|
-
def resolve_single_target(target)
|
375
|
-
case target
|
376
|
-
when Host
|
377
|
-
target
|
378
|
-
when Symbol
|
379
|
-
@pipeline.hosts_with_role(target)
|
380
|
-
when String
|
381
|
-
resolve_host_by_name(target) || @pipeline.hosts_with_role(target.to_sym)
|
382
|
-
when Array
|
383
|
-
target.flat_map { |t| resolve_single_target(t) }
|
384
|
-
else
|
385
|
-
[]
|
386
|
-
end
|
387
|
-
end
|
388
|
-
|
389
|
-
def resolve_host_by_name(name)
|
390
|
-
host = @pipeline.hosts.find { |h| h.hostname == name }
|
391
|
-
[host] if host
|
392
|
-
end
|
393
|
-
|
394
|
-
def process_heredoc_command(command)
|
395
|
-
command.split(/\r?\n/)
|
396
|
-
end
|
397
|
-
end
|
398
|
-
|
399
|
-
# Command class for file upload operations
|
400
|
-
class UploadCommand < Command
|
401
|
-
def initialize(local_path, remote_path, global_variables = {})
|
402
|
-
@local_path = local_path
|
403
|
-
@remote_path = remote_path
|
404
|
-
@global_variables = global_variables
|
405
|
-
super()
|
406
|
-
end
|
407
|
-
|
408
|
-
def execute(host, connection)
|
409
|
-
start_time = Time.now
|
410
|
-
processed_path = process_remote_path(host)
|
411
|
-
|
412
|
-
begin
|
413
|
-
connection.upload(@local_path, processed_path)
|
414
|
-
record_success(start_time, host)
|
415
|
-
rescue StandardError => e
|
416
|
-
record_failure(e, start_time, host)
|
417
|
-
raise
|
418
|
-
end
|
419
|
-
end
|
420
|
-
|
421
|
-
private
|
422
|
-
|
423
|
-
def process_remote_path(host)
|
424
|
-
path = @remote_path.dup
|
425
|
-
variables = @global_variables.merge(host.vars)
|
426
|
-
variables.each do |key, value|
|
427
|
-
path.gsub!(/\$\{#{key}\}/, value.to_s)
|
428
|
-
end
|
429
|
-
path
|
430
|
-
end
|
431
|
-
|
432
|
-
def record_success(start_time, host)
|
433
|
-
duration = Time.now - start_time
|
434
|
-
KdeployLogger.info("✅ Uploaded #{@local_path} to #{host.hostname}:#{@remote_path} in #{duration.round(2)}s")
|
435
|
-
end
|
436
|
-
|
437
|
-
def record_failure(error, start_time, host)
|
438
|
-
duration = Time.now - start_time
|
439
|
-
KdeployLogger.error("❌ Failed to upload #{@local_path} to #{host.hostname}:#{@remote_path} in #{duration.round(2)}s")
|
440
|
-
KdeployLogger.error("Error: #{error.message}")
|
441
|
-
end
|
442
|
-
end
|
443
|
-
|
444
|
-
# Command class for file download operations
|
445
|
-
class DownloadCommand < Command
|
446
|
-
def initialize(remote_path, local_path, global_variables = {})
|
447
|
-
@remote_path = remote_path
|
448
|
-
@local_path = local_path
|
449
|
-
@global_variables = global_variables
|
450
|
-
super()
|
451
|
-
end
|
452
|
-
|
453
|
-
def execute(host, connection)
|
454
|
-
start_time = Time.now
|
455
|
-
processed_path = process_remote_path(host)
|
456
|
-
|
457
|
-
begin
|
458
|
-
connection.download(processed_path, @local_path)
|
459
|
-
record_success(start_time, host)
|
460
|
-
rescue StandardError => e
|
461
|
-
record_failure(e, start_time, host)
|
462
|
-
raise
|
463
|
-
end
|
464
|
-
end
|
465
|
-
|
466
|
-
private
|
467
|
-
|
468
|
-
def process_remote_path(host)
|
469
|
-
path = @remote_path.dup
|
470
|
-
variables = @global_variables.merge(host.vars)
|
471
|
-
variables.each do |key, value|
|
472
|
-
path.gsub!(/\$\{#{key}\}/, value.to_s)
|
473
|
-
end
|
474
|
-
path
|
475
|
-
end
|
476
|
-
|
477
|
-
def record_success(start_time, host)
|
478
|
-
duration = Time.now - start_time
|
479
|
-
KdeployLogger.info("✅ Downloaded #{host.hostname}:#{@remote_path} to #{@local_path} in #{duration.round(2)}s")
|
61
|
+
def upload_template(source, destination, variables = {})
|
62
|
+
@kdeploy_commands ||= []
|
63
|
+
@kdeploy_commands << {
|
64
|
+
type: :upload_template,
|
65
|
+
source: source,
|
66
|
+
destination: destination,
|
67
|
+
variables: variables
|
68
|
+
}
|
480
69
|
end
|
481
70
|
|
482
|
-
def
|
483
|
-
|
484
|
-
KdeployLogger.error("❌ Failed to download #{host.hostname}:#{@remote_path} to #{@local_path} in #{duration.round(2)}s")
|
485
|
-
KdeployLogger.error("Error: #{error.message}")
|
71
|
+
def inventory(&)
|
72
|
+
instance_eval(&) if block_given?
|
486
73
|
end
|
487
|
-
end
|
488
74
|
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
@template_name = template_name
|
493
|
-
@remote_path = remote_path
|
494
|
-
@variables = variables
|
495
|
-
@template_manager = template_manager
|
496
|
-
@global_variables = global_variables
|
497
|
-
super()
|
498
|
-
end
|
75
|
+
def get_task_hosts(task_name)
|
76
|
+
task = kdeploy_tasks[task_name]
|
77
|
+
return kdeploy_hosts.keys if !task || (!task[:hosts] && !task[:roles])
|
499
78
|
|
500
|
-
|
501
|
-
start_time = Time.now
|
502
|
-
host_variables = build_host_variables(host)
|
503
|
-
processed_path = process_remote_path_variables(host_variables)
|
79
|
+
hosts = Set.new
|
504
80
|
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
raise
|
81
|
+
# 添加指定的主机
|
82
|
+
if task[:hosts]
|
83
|
+
task[:hosts].each do |host|
|
84
|
+
hosts.add(host) if kdeploy_hosts.key?(host)
|
85
|
+
end
|
511
86
|
end
|
512
|
-
end
|
513
87
|
|
514
|
-
|
88
|
+
# 添加角色中的主机
|
89
|
+
if task[:roles]
|
90
|
+
task[:roles].each do |role|
|
91
|
+
next unless role_hosts = kdeploy_roles[role]
|
515
92
|
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
port: host.port
|
521
|
-
)
|
522
|
-
end
|
523
|
-
|
524
|
-
def process_remote_path_variables(host_variables)
|
525
|
-
path = @remote_path.dup
|
526
|
-
host_variables.each do |key, value|
|
527
|
-
path.gsub!(/\$\{#{key}\}/, value.to_s)
|
93
|
+
role_hosts.each do |host|
|
94
|
+
hosts.add(host) if kdeploy_hosts.key?(host)
|
95
|
+
end
|
96
|
+
end
|
528
97
|
end
|
529
|
-
path
|
530
|
-
end
|
531
|
-
|
532
|
-
def perform_template_upload(_host, connection, host_variables, processed_path)
|
533
|
-
rendered_content = @template_manager.render(@template_name, host_variables)
|
534
|
-
temp_file = create_temp_file(rendered_content)
|
535
|
-
upload_template_file(connection, temp_file, processed_path)
|
536
|
-
ensure
|
537
|
-
cleanup_temp_file(temp_file) if temp_file
|
538
|
-
end
|
539
|
-
|
540
|
-
def create_temp_file(content)
|
541
|
-
temp_file = "/tmp/kdeploy_template_#{Time.now.to_i}_#{Process.pid}"
|
542
|
-
File.write(temp_file, content)
|
543
|
-
temp_file
|
544
|
-
end
|
545
|
-
|
546
|
-
def upload_template_file(connection, temp_file, processed_path)
|
547
|
-
connection.execute("mkdir -p #{File.dirname(processed_path)}")
|
548
|
-
connection.upload(temp_file, processed_path)
|
549
|
-
end
|
550
|
-
|
551
|
-
def cleanup_temp_file(temp_file)
|
552
|
-
FileUtils.rm_f(temp_file)
|
553
|
-
end
|
554
|
-
|
555
|
-
def record_success(start_time, host)
|
556
|
-
duration = Time.now - start_time
|
557
|
-
KdeployLogger.info("✅ Uploaded template #{@template_name} to #{host.hostname}:#{@remote_path} in #{duration.round(2)}s")
|
558
|
-
end
|
559
98
|
|
560
|
-
|
561
|
-
duration = Time.now - start_time
|
562
|
-
KdeployLogger.error("❌ Failed to upload template #{@template_name} to #{host.hostname}:#{@remote_path} in #{duration.round(2)}s")
|
563
|
-
KdeployLogger.error("Error: #{error.message}")
|
99
|
+
hosts.to_a
|
564
100
|
end
|
565
101
|
end
|
566
102
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/ssh'
|
4
|
+
require 'net/scp'
|
5
|
+
|
6
|
+
module Kdeploy
|
7
|
+
class Executor
|
8
|
+
def initialize(host_config)
|
9
|
+
@host = host_config[:name]
|
10
|
+
@user = host_config[:user]
|
11
|
+
@ip = host_config[:ip]
|
12
|
+
@password = host_config[:password]
|
13
|
+
@key = host_config[:key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(command)
|
17
|
+
Net::SSH.start(@ip, @user, ssh_options) do |ssh|
|
18
|
+
stdout = String.new
|
19
|
+
stderr = String.new
|
20
|
+
|
21
|
+
ssh.exec!(command) do |_channel, stream, data|
|
22
|
+
case stream
|
23
|
+
when :stdout
|
24
|
+
stdout << data
|
25
|
+
when :stderr
|
26
|
+
stderr << data
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
{
|
31
|
+
stdout: stdout.strip,
|
32
|
+
stderr: stderr.strip,
|
33
|
+
command: command
|
34
|
+
}
|
35
|
+
end
|
36
|
+
rescue Net::SSH::AuthenticationFailed => e
|
37
|
+
raise "SSH authentication failed: #{e.message}"
|
38
|
+
rescue StandardError => e
|
39
|
+
raise "SSH execution failed: #{e.message}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def upload(source, destination)
|
43
|
+
Net::SCP.start(@ip, @user, ssh_options) do |scp|
|
44
|
+
scp.upload!(source, destination)
|
45
|
+
end
|
46
|
+
rescue StandardError => e
|
47
|
+
raise "SCP upload failed: #{e.message}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def upload_template(source, destination, variables = {})
|
51
|
+
Template.render_and_upload(self, source, destination, variables)
|
52
|
+
rescue StandardError => e
|
53
|
+
raise "Template upload failed: #{e.message}"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def ssh_options
|
59
|
+
options = {
|
60
|
+
verify_host_key: :never,
|
61
|
+
timeout: 30
|
62
|
+
}
|
63
|
+
|
64
|
+
if @password
|
65
|
+
options[:password] = @password
|
66
|
+
elsif @key
|
67
|
+
options[:keys] = [@key]
|
68
|
+
end
|
69
|
+
|
70
|
+
options
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|