kdeploy 0.1.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 +7 -0
- data/.editorconfig +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +100 -0
- data/LICENSE +21 -0
- data/README.md +1030 -0
- data/Rakefile +45 -0
- data/bin/kdeploy +7 -0
- data/kdeploy.gemspec +49 -0
- data/lib/kdeploy/banner.rb +28 -0
- data/lib/kdeploy/cli.rb +1452 -0
- data/lib/kdeploy/command.rb +182 -0
- data/lib/kdeploy/configuration.rb +83 -0
- data/lib/kdeploy/dsl.rb +566 -0
- data/lib/kdeploy/host.rb +85 -0
- data/lib/kdeploy/inventory.rb +243 -0
- data/lib/kdeploy/logger.rb +100 -0
- data/lib/kdeploy/pipeline.rb +249 -0
- data/lib/kdeploy/runner.rb +190 -0
- data/lib/kdeploy/ssh_connection.rb +187 -0
- data/lib/kdeploy/statistics.rb +439 -0
- data/lib/kdeploy/task.rb +240 -0
- data/lib/kdeploy/template.rb +173 -0
- data/lib/kdeploy/version.rb +6 -0
- data/lib/kdeploy.rb +106 -0
- data/scripts/common_tasks.rb +218 -0
- data/scripts/deploy.rb +50 -0
- metadata +178 -0
data/lib/kdeploy/dsl.rb
ADDED
@@ -0,0 +1,566 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kdeploy
|
4
|
+
# Domain Specific Language for deployment scripts
|
5
|
+
class DSL
|
6
|
+
def initialize(script_dir = nil)
|
7
|
+
@pipeline = Pipeline.new
|
8
|
+
@current_task = nil
|
9
|
+
@inventory = nil
|
10
|
+
@script_dir = script_dir || Dir.pwd
|
11
|
+
@template_manager = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get pipeline or set pipeline name
|
15
|
+
# @param name [String] Pipeline name (optional)
|
16
|
+
# @return [Pipeline] Pipeline instance
|
17
|
+
def pipeline(name = nil)
|
18
|
+
@pipeline.instance_variable_set(:@name, name) if name
|
19
|
+
@pipeline
|
20
|
+
end
|
21
|
+
|
22
|
+
# Set global variable
|
23
|
+
# @param key [String, Symbol] Variable key
|
24
|
+
# @param value [Object] Variable value
|
25
|
+
def set(key, value)
|
26
|
+
@pipeline.set_variable(key, value)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Define host
|
30
|
+
# @param hostname [String] Hostname or IP address
|
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
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Define multiple hosts
|
49
|
+
# @param hosts_config [Hash] Hosts configuration
|
50
|
+
def hosts(hosts_config)
|
51
|
+
@pipeline.add_hosts(hosts_config)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Load hosts from inventory file
|
55
|
+
# @param inventory_file [String] Path to inventory file
|
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?
|
199
|
+
end
|
200
|
+
|
201
|
+
# Include external script
|
202
|
+
# @param script_path [String] Path to script file
|
203
|
+
def include(script_path)
|
204
|
+
return unless File.exist?(script_path)
|
205
|
+
|
206
|
+
script_content = File.read(script_path)
|
207
|
+
instance_eval(script_content, script_path)
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def default_inventory_file
|
213
|
+
Kdeploy.configuration&.inventory_file || 'inventory.yml'
|
214
|
+
end
|
215
|
+
|
216
|
+
def resolve_inventory_path(inventory_file)
|
217
|
+
return inventory_file if File.absolute_path?(inventory_file)
|
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
|
326
|
+
}
|
327
|
+
end
|
328
|
+
|
329
|
+
def log_local_command_success(name, duration, stdout)
|
330
|
+
KdeployLogger.info("✅ Local command '#{name}' completed in #{duration.round(2)}s")
|
331
|
+
KdeployLogger.debug("Output: #{stdout}") unless stdout.strip.empty?
|
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
|
348
|
+
end
|
349
|
+
|
350
|
+
def add_template_upload_command(template_name, remote_path, variables, _command_name)
|
351
|
+
command = TemplateUploadCommand.new(
|
352
|
+
template_name,
|
353
|
+
remote_path,
|
354
|
+
variables,
|
355
|
+
template_manager,
|
356
|
+
@pipeline.variables
|
357
|
+
)
|
358
|
+
@current_task.commands << command
|
359
|
+
command
|
360
|
+
end
|
361
|
+
|
362
|
+
def add_download_command(remote_path, local_path, _command_name)
|
363
|
+
command = DownloadCommand.new(remote_path, local_path, @pipeline.variables)
|
364
|
+
@current_task.commands << command
|
365
|
+
command
|
366
|
+
end
|
367
|
+
|
368
|
+
def resolve_target_hosts(target)
|
369
|
+
return @pipeline.hosts if target.nil?
|
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")
|
480
|
+
end
|
481
|
+
|
482
|
+
def record_failure(error, start_time, host)
|
483
|
+
duration = Time.now - start_time
|
484
|
+
KdeployLogger.error("❌ Failed to download #{host.hostname}:#{@remote_path} to #{@local_path} in #{duration.round(2)}s")
|
485
|
+
KdeployLogger.error("Error: #{error.message}")
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
# Command class for template upload operations
|
490
|
+
class TemplateUploadCommand < Command
|
491
|
+
def initialize(template_name, remote_path, variables, template_manager, global_variables = {})
|
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
|
499
|
+
|
500
|
+
def execute(host, connection)
|
501
|
+
start_time = Time.now
|
502
|
+
host_variables = build_host_variables(host)
|
503
|
+
processed_path = process_remote_path_variables(host_variables)
|
504
|
+
|
505
|
+
begin
|
506
|
+
perform_template_upload(host, connection, host_variables, processed_path)
|
507
|
+
record_success(start_time, host)
|
508
|
+
rescue StandardError => e
|
509
|
+
record_failure(e, start_time, host)
|
510
|
+
raise
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
private
|
515
|
+
|
516
|
+
def build_host_variables(host)
|
517
|
+
@global_variables.merge(@variables).merge(host.vars).merge(
|
518
|
+
hostname: host.hostname,
|
519
|
+
user: host.user,
|
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)
|
528
|
+
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
|
+
|
560
|
+
def record_failure(error, start_time, host)
|
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}")
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
data/lib/kdeploy/host.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kdeploy
|
4
|
+
# Host class for managing remote host configuration and connection details
|
5
|
+
class Host
|
6
|
+
attr_reader :hostname, :user, :port, :ssh_options, :roles, :vars
|
7
|
+
|
8
|
+
def initialize(hostname, user: nil, port: nil, ssh_options: {}, roles: [], vars: {})
|
9
|
+
@hostname = hostname
|
10
|
+
@user = user || Kdeploy.configuration&.default_user || ENV.fetch('USER', nil)
|
11
|
+
@port = port || Kdeploy.configuration&.default_port || 22
|
12
|
+
@ssh_options = ssh_options
|
13
|
+
@roles = Array(roles)
|
14
|
+
@vars = vars || {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Check if host has specific role
|
18
|
+
# @param role [String, Symbol] Role to check
|
19
|
+
# @return [Boolean] True if host has role
|
20
|
+
def has_role?(role)
|
21
|
+
@roles.include?(role.to_s) || @roles.include?(role.to_sym)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get variable value
|
25
|
+
# @param key [String, Symbol] Variable key
|
26
|
+
# @return [Object] Variable value
|
27
|
+
def var(key)
|
28
|
+
@vars[key.to_s] || @vars[key.to_sym]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set variable value
|
32
|
+
# @param key [String, Symbol] Variable key
|
33
|
+
# @param value [Object] Variable value
|
34
|
+
# @return [Object] Set value
|
35
|
+
def set_var(key, value)
|
36
|
+
@vars[key.to_s] = value
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get connection string for display
|
40
|
+
# @return [String] Connection string
|
41
|
+
def connection_string
|
42
|
+
"#{@user}@#{@hostname}:#{@port}"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get SSH connection options
|
46
|
+
# @return [Hash] SSH options
|
47
|
+
def connection_options
|
48
|
+
base_options = Kdeploy.configuration&.merged_ssh_options(@ssh_options) || @ssh_options
|
49
|
+
base_options.merge(
|
50
|
+
timeout: Kdeploy.configuration&.ssh_timeout || 30
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# String representation of the host
|
55
|
+
# @return [String] Connection string
|
56
|
+
def to_s
|
57
|
+
connection_string
|
58
|
+
end
|
59
|
+
|
60
|
+
# Detailed string representation of the host
|
61
|
+
# @return [String] Host details
|
62
|
+
def inspect
|
63
|
+
"#<Kdeploy::Host #{connection_string} roles=#{@roles} vars=#{@vars.keys}>"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Compare hosts for equality
|
67
|
+
# @param other [Host] Host to compare with
|
68
|
+
# @return [Boolean] True if hosts are equal
|
69
|
+
def ==(other)
|
70
|
+
return false unless other.is_a?(Host)
|
71
|
+
|
72
|
+
hostname == other.hostname &&
|
73
|
+
user == other.user &&
|
74
|
+
port == other.port
|
75
|
+
end
|
76
|
+
|
77
|
+
alias eql? ==
|
78
|
+
|
79
|
+
# Generate hash code for host
|
80
|
+
# @return [Integer] Hash code
|
81
|
+
def hash
|
82
|
+
[hostname, user, port].hash
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|