kdeploy 1.3.1 → 1.3.4
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 +42 -1072
- data/README_EN.md +42 -1080
- data/lib/kdeploy/{cli.rb → cli/cli.rb} +95 -21
- data/lib/kdeploy/{help_formatter.rb → cli/help_formatter.rb} +3 -0
- data/lib/kdeploy/{configuration.rb → config/configuration.rb} +41 -1
- data/lib/kdeploy/{dsl.rb → dsl/dsl.rb} +9 -7
- data/lib/kdeploy/{initializer.rb → dsl/initializer.rb} +3 -3
- data/lib/kdeploy/errors.rb +7 -0
- data/lib/kdeploy/executor/command_executor.rb +155 -0
- data/lib/kdeploy/{executor.rb → executor/executor.rb} +151 -20
- data/lib/kdeploy/{output_formatter.rb → output/output_formatter.rb} +4 -7
- data/lib/kdeploy/{runner.rb → runner/runner.rb} +9 -2
- data/lib/kdeploy/{template.rb → template/template.rb} +25 -2
- data/lib/kdeploy/version.rb +1 -1
- data/lib/kdeploy.rb +12 -12
- metadata +14 -15
- data/AGENTS.md +0 -18
- data/lib/kdeploy/command_executor.rb +0 -104
- /data/lib/kdeploy/{file_filter.rb → executor/file_filter.rb} +0 -0
- /data/lib/kdeploy/{output.rb → output/output.rb} +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
4
|
require 'json'
|
|
5
|
+
require 'yaml'
|
|
5
6
|
require 'pastel'
|
|
6
7
|
require 'tty-table'
|
|
7
8
|
require 'tty-box'
|
|
@@ -53,17 +54,20 @@ module Kdeploy
|
|
|
53
54
|
method_option :retry_delay, type: :numeric, desc: 'Retry delay seconds (default: 1)'
|
|
54
55
|
method_option :retry_on_nonzero, type: :boolean, desc: 'Retry commands on nonzero exit status (default: false)'
|
|
55
56
|
method_option :timeout, type: :numeric, desc: 'Per-host execution timeout seconds (default: none)'
|
|
57
|
+
method_option :step_timeout, type: :numeric, desc: 'Per-step execution timeout seconds (default: none)'
|
|
58
|
+
method_option :retry_policy, type: :string, desc: 'Retry policy JSON to override config file'
|
|
59
|
+
method_option :retry_policy_file, type: :string, desc: 'Retry policy JSON file to override config file'
|
|
56
60
|
def execute(task_file, task_name = nil)
|
|
57
|
-
load_config_file
|
|
58
|
-
show_banner_once
|
|
59
61
|
@task_file_dir = File.dirname(File.expand_path(task_file))
|
|
62
|
+
Configuration.load_for_execute(task_dir: @task_file_dir)
|
|
63
|
+
show_banner_once
|
|
60
64
|
load_task_file(task_file)
|
|
61
65
|
|
|
62
66
|
tasks_to_run = determine_tasks(task_name)
|
|
63
67
|
all_results = execute_tasks(tasks_to_run)
|
|
64
68
|
|
|
65
|
-
# Exit non-zero if any executed host failed.
|
|
66
|
-
exit 1 if any_failed?(all_results)
|
|
69
|
+
# Exit non-zero if any executed host failed or a task could not run.
|
|
70
|
+
exit 1 if any_failed?(all_results) || @task_errors
|
|
67
71
|
rescue StandardError => e
|
|
68
72
|
puts Kdeploy::Banner.show_error(e.message)
|
|
69
73
|
exit 1
|
|
@@ -72,12 +76,13 @@ module Kdeploy
|
|
|
72
76
|
private
|
|
73
77
|
|
|
74
78
|
def load_config_file
|
|
75
|
-
Configuration.
|
|
79
|
+
# ponytail: kept for compatibility; prefer Configuration.load_for_execute
|
|
80
|
+
Configuration.load_for_execute(task_dir: @task_file_dir)
|
|
76
81
|
end
|
|
77
82
|
|
|
78
83
|
def load_task_file(file)
|
|
79
84
|
validate_task_file(file)
|
|
80
|
-
#
|
|
85
|
+
# Use module_eval with top-level binding to keep heredoc compatible
|
|
81
86
|
self.class.module_eval(File.read(file), file)
|
|
82
87
|
rescue StandardError => e
|
|
83
88
|
raise FileNotFoundError, file if e.message.include?('not found')
|
|
@@ -103,6 +108,8 @@ module Kdeploy
|
|
|
103
108
|
return hosts unless limit
|
|
104
109
|
|
|
105
110
|
host_names = limit.split(',').map(&:strip)
|
|
111
|
+
unknown = host_names.reject { |name| hosts.key?(name) }
|
|
112
|
+
warn "Warning: --limit ignored unknown host(s): #{unknown.join(', ')}" if unknown.any?
|
|
106
113
|
hosts.slice(*host_names)
|
|
107
114
|
end
|
|
108
115
|
|
|
@@ -145,7 +152,7 @@ module Kdeploy
|
|
|
145
152
|
end
|
|
146
153
|
|
|
147
154
|
def print_host_result(host, result, formatter)
|
|
148
|
-
if %i[success
|
|
155
|
+
if %i[success].include?(result[:status])
|
|
149
156
|
print_success_result(host, result, formatter)
|
|
150
157
|
else
|
|
151
158
|
print_failure_result(host, result, formatter)
|
|
@@ -221,19 +228,31 @@ module Kdeploy
|
|
|
221
228
|
end
|
|
222
229
|
|
|
223
230
|
def determine_tasks(task_name)
|
|
224
|
-
|
|
231
|
+
if task_name
|
|
232
|
+
sym = task_name.to_sym
|
|
233
|
+
raise TaskNotFoundError, task_name unless self.class.kdeploy_tasks.key?(sym)
|
|
234
|
+
|
|
235
|
+
[sym]
|
|
236
|
+
else
|
|
237
|
+
self.class.kdeploy_tasks.keys
|
|
238
|
+
end
|
|
225
239
|
end
|
|
226
240
|
|
|
227
241
|
def execute_tasks(tasks_to_run)
|
|
228
242
|
all_results = {}
|
|
243
|
+
@task_errors = false
|
|
244
|
+
|
|
245
|
+
if tasks_to_run.empty?
|
|
246
|
+
puts Kdeploy::Banner.show_error('No tasks defined in task file')
|
|
247
|
+
@task_errors = true
|
|
248
|
+
return all_results
|
|
249
|
+
end
|
|
229
250
|
|
|
230
251
|
tasks_to_run.each do |task|
|
|
231
252
|
task_results = execute_single_task(task)
|
|
232
|
-
# Collect results for final summary
|
|
233
253
|
all_results[task] = task_results if task_results
|
|
234
254
|
|
|
235
|
-
|
|
236
|
-
break if task_failed?(task_results)
|
|
255
|
+
break if task_failed?(task_results) || @task_errors
|
|
237
256
|
end
|
|
238
257
|
|
|
239
258
|
# Show combined summary at the end for all tasks
|
|
@@ -248,6 +267,8 @@ module Kdeploy
|
|
|
248
267
|
|
|
249
268
|
if hosts.empty?
|
|
250
269
|
puts Kdeploy::Banner.show_error("No hosts found for task: #{task}")
|
|
270
|
+
puts 'This usually means no hosts matched the task configuration or --limit filter.'
|
|
271
|
+
@task_errors = true
|
|
251
272
|
return nil
|
|
252
273
|
end
|
|
253
274
|
|
|
@@ -272,6 +293,8 @@ module Kdeploy
|
|
|
272
293
|
retry_on_nonzero =
|
|
273
294
|
options[:retry_on_nonzero].nil? ? Configuration.default_retry_on_nonzero : options[:retry_on_nonzero]
|
|
274
295
|
host_timeout = options[:timeout].nil? ? Configuration.default_host_timeout : options[:timeout]
|
|
296
|
+
step_timeout = options[:step_timeout].nil? ? Configuration.default_step_timeout : options[:step_timeout]
|
|
297
|
+
retry_policy = resolve_retry_policy
|
|
275
298
|
base_dir = @task_file_dir
|
|
276
299
|
runner = Runner.new(
|
|
277
300
|
hosts, self.class.kdeploy_tasks,
|
|
@@ -282,7 +305,9 @@ module Kdeploy
|
|
|
282
305
|
retries: retries,
|
|
283
306
|
retry_delay: retry_delay,
|
|
284
307
|
retry_on_nonzero: retry_on_nonzero,
|
|
285
|
-
host_timeout: host_timeout
|
|
308
|
+
host_timeout: host_timeout,
|
|
309
|
+
step_timeout: step_timeout,
|
|
310
|
+
retry_policy: retry_policy
|
|
286
311
|
)
|
|
287
312
|
results = runner.run(task)
|
|
288
313
|
if options[:format] == 'json'
|
|
@@ -315,10 +340,9 @@ module Kdeploy
|
|
|
315
340
|
all_hosts = {}
|
|
316
341
|
all_results.each do |task_name, task_results|
|
|
317
342
|
task_results.each do |host, result|
|
|
318
|
-
all_hosts[host] ||= { ok: 0,
|
|
343
|
+
all_hosts[host] ||= { ok: 0, failed: 0, tasks: [] }
|
|
319
344
|
counts = formatter.calculate_summary_counts(result)
|
|
320
345
|
all_hosts[host][:ok] += counts[:ok]
|
|
321
|
-
all_hosts[host][:changed] += counts[:changed]
|
|
322
346
|
all_hosts[host][:failed] += counts[:failed]
|
|
323
347
|
all_hosts[host][:tasks] << task_name
|
|
324
348
|
end
|
|
@@ -370,15 +394,13 @@ module Kdeploy
|
|
|
370
394
|
duration: step[:duration]
|
|
371
395
|
}
|
|
372
396
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
out[:result] = step[:result] if step[:type] == :sync
|
|
376
|
-
|
|
377
|
-
if options[:debug] && step[:type] == :run && step[:output].is_a?(Hash)
|
|
397
|
+
if step[:output].is_a?(Hash)
|
|
378
398
|
out[:stdout] = step[:output][:stdout]
|
|
379
399
|
out[:stderr] = step[:output][:stderr]
|
|
400
|
+
out[:exit_status] = step[:output][:exit_status]
|
|
380
401
|
end
|
|
381
402
|
|
|
403
|
+
out[:result] = step[:result] if step[:type] == :sync
|
|
382
404
|
out
|
|
383
405
|
end
|
|
384
406
|
|
|
@@ -397,7 +419,9 @@ module Kdeploy
|
|
|
397
419
|
destination: cmd[:destination],
|
|
398
420
|
ignore: cmd[:ignore] || [],
|
|
399
421
|
exclude: cmd[:exclude] || [],
|
|
400
|
-
delete: cmd[:delete] || false
|
|
422
|
+
delete: cmd[:delete] || false,
|
|
423
|
+
fast: cmd[:fast],
|
|
424
|
+
parallel: cmd[:parallel]
|
|
401
425
|
}
|
|
402
426
|
else
|
|
403
427
|
{ type: cmd[:type].to_s }
|
|
@@ -409,11 +433,61 @@ module Kdeploy
|
|
|
409
433
|
|
|
410
434
|
if result[:output].is_a?(Array)
|
|
411
435
|
result[:output].map do |o|
|
|
412
|
-
|
|
436
|
+
next unless o.is_a?(Hash)
|
|
437
|
+
|
|
438
|
+
err = o[:output]
|
|
439
|
+
next unless err.is_a?(Hash)
|
|
440
|
+
|
|
441
|
+
pieces = []
|
|
442
|
+
pieces << "command=#{err[:command]}" if err[:command]
|
|
443
|
+
pieces << "exit_status=#{err[:exit_status]}" if err[:exit_status]
|
|
444
|
+
pieces << "stderr=#{err[:stderr]}" if err[:stderr]
|
|
445
|
+
pieces.join(' ')
|
|
413
446
|
end.compact.join("\n")
|
|
414
447
|
else
|
|
415
448
|
result[:output].to_s
|
|
416
449
|
end
|
|
417
450
|
end
|
|
451
|
+
|
|
452
|
+
def parse_retry_policy(raw)
|
|
453
|
+
policy = JSON.parse(raw)
|
|
454
|
+
raise ArgumentError, 'retry_policy must be a JSON object' unless policy.is_a?(Hash)
|
|
455
|
+
|
|
456
|
+
policy
|
|
457
|
+
rescue JSON::ParserError => e
|
|
458
|
+
raise ArgumentError, "retry_policy JSON parse error: #{e.message}"
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def resolve_retry_policy
|
|
462
|
+
if options[:retry_policy_file]
|
|
463
|
+
path = options[:retry_policy_file]
|
|
464
|
+
unless File.exist?(path)
|
|
465
|
+
raise ArgumentError,
|
|
466
|
+
"retry_policy file not found: #{path} (examples: retry_policy.example.json / retry_policy.example.yml)"
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
return parse_retry_policy_file(path)
|
|
470
|
+
end
|
|
471
|
+
return parse_retry_policy(options[:retry_policy]) if options[:retry_policy]
|
|
472
|
+
|
|
473
|
+
Configuration.default_retry_policy
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def parse_retry_policy_file(path)
|
|
477
|
+
ext = File.extname(path).downcase
|
|
478
|
+
raw = File.read(path)
|
|
479
|
+
policy =
|
|
480
|
+
case ext
|
|
481
|
+
when '.yml', '.yaml'
|
|
482
|
+
YAML.safe_load(raw) || {}
|
|
483
|
+
else
|
|
484
|
+
JSON.parse(raw)
|
|
485
|
+
end
|
|
486
|
+
raise ArgumentError, 'retry_policy must be a JSON/YAML object' unless policy.is_a?(Hash)
|
|
487
|
+
|
|
488
|
+
policy
|
|
489
|
+
rescue JSON::ParserError, Psych::SyntaxError => e
|
|
490
|
+
raise ArgumentError, "retry_policy parse error: #{e.message}"
|
|
491
|
+
end
|
|
418
492
|
end
|
|
419
493
|
end
|
|
@@ -36,6 +36,9 @@ module Kdeploy
|
|
|
36
36
|
#{@pastel.dim(' --retry-delay SECONDS')} Retry delay seconds (default: 1; overridden by .kdeploy.yml)
|
|
37
37
|
#{@pastel.dim(' --retry-on-nonzero')} Retry commands on nonzero exit status (default: false; overridden by .kdeploy.yml)
|
|
38
38
|
#{@pastel.dim(' --timeout SECONDS')} Per-host execution timeout seconds (default: none; overridden by .kdeploy.yml)
|
|
39
|
+
#{@pastel.dim(' --step-timeout SECONDS')} Per-step execution timeout seconds (default: none; overridden by .kdeploy.yml)
|
|
40
|
+
#{@pastel.dim(' --retry-policy JSON')} Retry policy JSON (overrides .kdeploy.yml)
|
|
41
|
+
#{@pastel.dim(' --retry-policy-file PATH')} Retry policy JSON file (overrides .kdeploy.yml)
|
|
39
42
|
|
|
40
43
|
#{@pastel.bright_yellow('🆕')} #{@pastel.bright_white('init [DIR]')} Initialize new deployment project
|
|
41
44
|
#{@pastel.bright_yellow('ℹ️')} #{@pastel.bright_white('version')} Show version information
|
|
@@ -12,6 +12,10 @@ module Kdeploy
|
|
|
12
12
|
DEFAULT_RETRY_DELAY = 1
|
|
13
13
|
DEFAULT_HOST_TIMEOUT = nil
|
|
14
14
|
DEFAULT_RETRY_ON_NONZERO = false
|
|
15
|
+
DEFAULT_SYNC_FAST = false
|
|
16
|
+
DEFAULT_STEP_TIMEOUT = nil
|
|
17
|
+
DEFAULT_RETRY_POLICY = nil
|
|
18
|
+
DEFAULT_SYNC_PARALLEL = 1
|
|
15
19
|
CONFIG_FILE_NAME = '.kdeploy.yml'
|
|
16
20
|
|
|
17
21
|
class << self
|
|
@@ -21,7 +25,11 @@ module Kdeploy
|
|
|
21
25
|
:default_retries,
|
|
22
26
|
:default_retry_delay,
|
|
23
27
|
:default_host_timeout,
|
|
24
|
-
:default_retry_on_nonzero
|
|
28
|
+
:default_retry_on_nonzero,
|
|
29
|
+
:default_sync_fast,
|
|
30
|
+
:default_step_timeout,
|
|
31
|
+
:default_retry_policy,
|
|
32
|
+
:default_sync_parallel
|
|
25
33
|
|
|
26
34
|
def reset
|
|
27
35
|
@default_parallel = DEFAULT_PARALLEL
|
|
@@ -31,6 +39,10 @@ module Kdeploy
|
|
|
31
39
|
@default_retry_delay = DEFAULT_RETRY_DELAY
|
|
32
40
|
@default_host_timeout = DEFAULT_HOST_TIMEOUT
|
|
33
41
|
@default_retry_on_nonzero = DEFAULT_RETRY_ON_NONZERO
|
|
42
|
+
@default_sync_fast = DEFAULT_SYNC_FAST
|
|
43
|
+
@default_step_timeout = DEFAULT_STEP_TIMEOUT
|
|
44
|
+
@default_retry_policy = DEFAULT_RETRY_POLICY
|
|
45
|
+
@default_sync_parallel = DEFAULT_SYNC_PARALLEL
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
def load_from_file(config_path = nil)
|
|
@@ -44,6 +56,22 @@ module Kdeploy
|
|
|
44
56
|
nil
|
|
45
57
|
end
|
|
46
58
|
|
|
59
|
+
# Load config for execute: CWD walk-up, then task file dir (overrides), then env vars.
|
|
60
|
+
def load_for_execute(task_dir: nil, cwd: Dir.pwd)
|
|
61
|
+
reset
|
|
62
|
+
load_from_file(find_config_file(cwd))
|
|
63
|
+
load_from_file(find_config_file(task_dir)) if task_dir
|
|
64
|
+
apply_env_overrides
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def apply_env_overrides
|
|
68
|
+
parallel = positive_int_env('KDEPLOY_PARALLEL')
|
|
69
|
+
@default_parallel = parallel if parallel
|
|
70
|
+
|
|
71
|
+
ssh_timeout = positive_int_env('KDEPLOY_SSH_TIMEOUT')
|
|
72
|
+
@default_ssh_timeout = ssh_timeout if ssh_timeout
|
|
73
|
+
end
|
|
74
|
+
|
|
47
75
|
def find_config_file(start_dir = Dir.pwd)
|
|
48
76
|
current_dir = File.expand_path(start_dir)
|
|
49
77
|
|
|
@@ -72,6 +100,18 @@ module Kdeploy
|
|
|
72
100
|
@default_retry_delay = config['retry_delay'] if config.key?('retry_delay')
|
|
73
101
|
@default_host_timeout = config['host_timeout'] if config.key?('host_timeout')
|
|
74
102
|
@default_retry_on_nonzero = config['retry_on_nonzero'] if config.key?('retry_on_nonzero')
|
|
103
|
+
@default_sync_fast = config['sync_fast'] if config.key?('sync_fast')
|
|
104
|
+
@default_step_timeout = config['step_timeout'] if config.key?('step_timeout')
|
|
105
|
+
@default_retry_policy = config['retry_policy'] if config.key?('retry_policy')
|
|
106
|
+
@default_sync_parallel = config['sync_parallel'] if config.key?('sync_parallel')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def positive_int_env(key)
|
|
110
|
+
raw = ENV.fetch(key, nil)
|
|
111
|
+
return nil if raw.nil? || raw.to_s.strip.empty?
|
|
112
|
+
|
|
113
|
+
value = raw.to_i
|
|
114
|
+
value.positive? ? value : nil
|
|
75
115
|
end
|
|
76
116
|
|
|
77
117
|
def parse_verify_host_key(value)
|
|
@@ -161,7 +161,7 @@ module Kdeploy
|
|
|
161
161
|
}
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
def sync(source, destination, ignore: [], delete: false, exclude: [])
|
|
164
|
+
def sync(source, destination, ignore: [], delete: false, exclude: [], fast: nil, parallel: nil)
|
|
165
165
|
@kdeploy_commands ||= []
|
|
166
166
|
@kdeploy_commands << {
|
|
167
167
|
type: :sync,
|
|
@@ -169,7 +169,9 @@ module Kdeploy
|
|
|
169
169
|
destination: destination,
|
|
170
170
|
ignore: Array(ignore),
|
|
171
171
|
exclude: Array(exclude),
|
|
172
|
-
delete: delete
|
|
172
|
+
delete: delete,
|
|
173
|
+
fast: fast,
|
|
174
|
+
parallel: parallel
|
|
173
175
|
}
|
|
174
176
|
end
|
|
175
177
|
|
|
@@ -177,14 +179,14 @@ module Kdeploy
|
|
|
177
179
|
# Chef-style resource DSL (compiles to run/upload/upload_template)
|
|
178
180
|
# -------------------------------------------------------------------------
|
|
179
181
|
|
|
180
|
-
#
|
|
182
|
+
# Install a system package. Defaults to apt; supports platform: :yum.
|
|
181
183
|
def package(name, version: nil, platform: :apt)
|
|
182
184
|
@kdeploy_commands ||= []
|
|
183
185
|
cmd = build_package_command(name, version, platform)
|
|
184
186
|
@kdeploy_commands << { type: :run, command: cmd, sudo: true }
|
|
185
187
|
end
|
|
186
188
|
|
|
187
|
-
#
|
|
189
|
+
# Manage a systemd service. action supports :start, :stop, :restart, :reload, :enable, :disable.
|
|
188
190
|
def service(name, action: :start)
|
|
189
191
|
@kdeploy_commands ||= []
|
|
190
192
|
actions = Array(action)
|
|
@@ -194,7 +196,7 @@ module Kdeploy
|
|
|
194
196
|
end
|
|
195
197
|
end
|
|
196
198
|
|
|
197
|
-
#
|
|
199
|
+
# Deploy an ERB template to a remote path. Supports block and keyword args.
|
|
198
200
|
def template(destination, source: nil, variables: nil, &block)
|
|
199
201
|
@kdeploy_commands ||= []
|
|
200
202
|
if block
|
|
@@ -211,13 +213,13 @@ module Kdeploy
|
|
|
211
213
|
upload_template(src, destination, vars)
|
|
212
214
|
end
|
|
213
215
|
|
|
214
|
-
#
|
|
216
|
+
# Upload a local file to a remote path.
|
|
215
217
|
def file(destination, source:)
|
|
216
218
|
@kdeploy_commands ||= []
|
|
217
219
|
upload(source, destination)
|
|
218
220
|
end
|
|
219
221
|
|
|
220
|
-
#
|
|
222
|
+
# Ensure a remote directory exists. Supports mode option.
|
|
221
223
|
def directory(path, mode: nil)
|
|
222
224
|
@kdeploy_commands ||= []
|
|
223
225
|
cmd = "mkdir -p #{Shellwords.escape(path.to_s)}"
|
|
@@ -96,11 +96,11 @@ module Kdeploy
|
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def create_config_files
|
|
99
|
-
#
|
|
99
|
+
# Create config directory
|
|
100
100
|
config_dir = File.join(@target_dir, 'config')
|
|
101
101
|
FileUtils.mkdir_p(config_dir)
|
|
102
102
|
|
|
103
|
-
#
|
|
103
|
+
# Create Nginx ERB template
|
|
104
104
|
File.write(File.join(config_dir, 'nginx.conf.erb'), <<~CONF)
|
|
105
105
|
user nginx;
|
|
106
106
|
worker_processes <%= worker_processes %>;
|
|
@@ -152,7 +152,7 @@ module Kdeploy
|
|
|
152
152
|
}
|
|
153
153
|
CONF
|
|
154
154
|
|
|
155
|
-
#
|
|
155
|
+
# Create static config file example
|
|
156
156
|
File.write(File.join(config_dir, 'app.conf'), <<~CONF)
|
|
157
157
|
location /api {
|
|
158
158
|
proxy_pass http://localhost:3000;
|
data/lib/kdeploy/errors.rb
CHANGED
|
@@ -52,6 +52,13 @@ module Kdeploy
|
|
|
52
52
|
attr_reader :original_error
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Raised when a step exceeds configured timeout
|
|
56
|
+
class StepTimeoutError < Error
|
|
57
|
+
def initialize(message)
|
|
58
|
+
super("Step timeout: #{message}")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
55
62
|
# Raised when configuration is invalid
|
|
56
63
|
class ConfigurationError < Error
|
|
57
64
|
def initialize(message)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'timeout'
|
|
4
|
+
|
|
5
|
+
module Kdeploy
|
|
6
|
+
# Executes a single command and records execution time
|
|
7
|
+
class CommandExecutor
|
|
8
|
+
def initialize(executor, output, debug: false, retries: 0, retry_delay: 1, retry_on_nonzero: false,
|
|
9
|
+
step_timeout: nil, retry_policy: nil)
|
|
10
|
+
@executor = executor
|
|
11
|
+
@output = output
|
|
12
|
+
@debug = debug
|
|
13
|
+
@retries = retries.to_i
|
|
14
|
+
@retry_delay = retry_delay.to_f
|
|
15
|
+
@retry_on_nonzero = retry_on_nonzero
|
|
16
|
+
@step_timeout = step_timeout&.to_f
|
|
17
|
+
@retry_policy = retry_policy
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def execute_run(command, _host_name)
|
|
21
|
+
cmd = command[:command]
|
|
22
|
+
use_sudo = command[:sudo]
|
|
23
|
+
|
|
24
|
+
result, duration = measure_time do
|
|
25
|
+
with_retries(step_type: :run) do
|
|
26
|
+
with_timeout { @executor.execute(cmd, use_sudo: use_sudo) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
{ command: cmd, output: result, duration: duration, type: :run }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def execute_upload(command, _host_name)
|
|
34
|
+
_result, duration = measure_time do
|
|
35
|
+
with_retries(step_type: :upload) do
|
|
36
|
+
with_timeout { @executor.upload(command[:source], command[:destination]) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
{
|
|
40
|
+
command: "upload: #{command[:source]} -> #{command[:destination]}",
|
|
41
|
+
duration: duration,
|
|
42
|
+
type: :upload
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def execute_upload_template(command, _host_name)
|
|
47
|
+
_result, duration = measure_time do
|
|
48
|
+
with_retries(step_type: :upload_template) do
|
|
49
|
+
with_timeout do
|
|
50
|
+
@executor.upload_template(command[:source], command[:destination], command[:variables])
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
{
|
|
55
|
+
command: "upload_template: #{command[:source]} -> #{command[:destination]}",
|
|
56
|
+
duration: duration,
|
|
57
|
+
type: :upload_template
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def execute_sync(command, _host_name)
|
|
62
|
+
source = command[:source]
|
|
63
|
+
destination = command[:destination]
|
|
64
|
+
fast = command.key?(:fast) ? command[:fast] : Configuration.default_sync_fast
|
|
65
|
+
parallel = command.key?(:parallel) ? command[:parallel] : Configuration.default_sync_parallel
|
|
66
|
+
|
|
67
|
+
result, duration = measure_time do
|
|
68
|
+
with_retries(step_type: :sync) do
|
|
69
|
+
with_timeout do
|
|
70
|
+
@executor.sync_directory(
|
|
71
|
+
source,
|
|
72
|
+
destination,
|
|
73
|
+
ignore: command[:ignore] || [],
|
|
74
|
+
exclude: command[:exclude] || [],
|
|
75
|
+
delete: command[:delete] || false,
|
|
76
|
+
fast: fast,
|
|
77
|
+
parallel: parallel
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
build_sync_result(source, destination, result, duration)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def build_sync_result(source, destination, result, duration)
|
|
89
|
+
{
|
|
90
|
+
command: "sync: #{source} -> #{destination}",
|
|
91
|
+
duration: duration,
|
|
92
|
+
type: :sync,
|
|
93
|
+
result: result,
|
|
94
|
+
uploaded: result[:uploaded],
|
|
95
|
+
deleted: result[:deleted],
|
|
96
|
+
total: result[:total]
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def measure_time
|
|
101
|
+
start_time = Time.now
|
|
102
|
+
result = yield
|
|
103
|
+
duration = Time.now - start_time
|
|
104
|
+
[result, duration]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def with_retries(step_type: nil)
|
|
108
|
+
attempts = 0
|
|
109
|
+
max_retries = retries_for(step_type)
|
|
110
|
+
exit_codes = retry_exit_codes_for(step_type)
|
|
111
|
+
begin
|
|
112
|
+
attempts += 1
|
|
113
|
+
yield
|
|
114
|
+
rescue SSHError, SCPError, TemplateError => e
|
|
115
|
+
raise if e.is_a?(SSHError) && e.exit_status && !retry_on_exit_status?(e.exit_status, exit_codes)
|
|
116
|
+
raise if attempts > (max_retries + 1)
|
|
117
|
+
|
|
118
|
+
sleep(@retry_delay) if @retry_delay.positive?
|
|
119
|
+
retry
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def with_timeout(&block)
|
|
124
|
+
return yield unless @step_timeout&.positive?
|
|
125
|
+
|
|
126
|
+
Timeout.timeout(@step_timeout, &block)
|
|
127
|
+
rescue Timeout::Error
|
|
128
|
+
raise StepTimeoutError, "exceeded #{@step_timeout}s"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def retries_for(step_type)
|
|
132
|
+
return @retries unless @retry_policy.is_a?(Hash) && step_type
|
|
133
|
+
|
|
134
|
+
policy = @retry_policy[step_type.to_s] || @retry_policy[step_type.to_sym]
|
|
135
|
+
return @retries unless policy.is_a?(Hash)
|
|
136
|
+
|
|
137
|
+
policy.fetch('retries', policy.fetch(:retries, @retries)).to_i
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def retry_exit_codes_for(step_type)
|
|
141
|
+
return nil unless @retry_policy.is_a?(Hash) && step_type
|
|
142
|
+
|
|
143
|
+
policy = @retry_policy[step_type.to_s] || @retry_policy[step_type.to_sym]
|
|
144
|
+
return nil unless policy.is_a?(Hash)
|
|
145
|
+
|
|
146
|
+
policy['retry_on_exit_codes'] || policy[:retry_on_exit_codes]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def retry_on_exit_status?(exit_status, exit_codes)
|
|
150
|
+
return @retry_on_nonzero if exit_codes.nil?
|
|
151
|
+
|
|
152
|
+
Array(exit_codes).map(&:to_i).include?(exit_status.to_i)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|