kdeploy 1.3.1 → 1.3.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/README.md +45 -0
- data/README_EN.md +78 -2
- data/lib/kdeploy/{cli.rb → cli/cli.rb} +67 -9
- data/lib/kdeploy/{help_formatter.rb → cli/help_formatter.rb} +3 -0
- data/lib/kdeploy/{configuration.rb → config/configuration.rb} +17 -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} +1 -0
- data/lib/kdeploy/{runner.rb → runner/runner.rb} +21 -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 -14
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd3c4b1e7ad1dc2062bfa0e732144b64f0c3c81655320dc96defeaed10816392
|
|
4
|
+
data.tar.gz: 2ac13e3f6b7093042d4a313c139ad997e59e06b1fa2d6dc7372dc33919f3eebc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f224afb29cf1d419a80aa7797841b1fd935030841c0f80ad403e25573635e45956b8779c6518f6c37b4150c110c19f200baa1247ea03353e442ba6e0a5061a65
|
|
7
|
+
data.tar.gz: d6cae91436717b81f3a527707f7bed7e93107d868bf457b3ec9e62d859b42b241103f8600315155fd5e9409a09671319f487f7e0e4ea0f276d9dd8a48cd3ec2a
|
data/README.md
CHANGED
|
@@ -209,6 +209,8 @@ kdeploy execute deploy.rb deploy_web
|
|
|
209
209
|
- `--retry-delay SECONDS`: 每次重试间隔秒数(默认 `1`)
|
|
210
210
|
- `--retry-on-nonzero`: 非零退出码重试开关(默认 `false`)
|
|
211
211
|
- `--timeout SECONDS`: 单 host 执行超时(秒,默认不启用)
|
|
212
|
+
- `--step-timeout SECONDS`: 单 step 执行超时(秒,默认不启用)
|
|
213
|
+
- `--retry-policy JSON`: 重试策略 JSON(覆盖 `.kdeploy.yml`)
|
|
212
214
|
|
|
213
215
|
**示例:**
|
|
214
216
|
```bash
|
|
@@ -236,6 +238,15 @@ kdeploy execute deploy.rb deploy_web --retries 2 --retry-on-nonzero
|
|
|
236
238
|
# 设置单 host 超时(秒)
|
|
237
239
|
kdeploy execute deploy.rb deploy_web --timeout 120
|
|
238
240
|
|
|
241
|
+
# 设置单 step 超时(秒)
|
|
242
|
+
kdeploy execute deploy.rb deploy_web --step-timeout 30
|
|
243
|
+
|
|
244
|
+
# 使用 CLI 覆盖重试策略(JSON)
|
|
245
|
+
kdeploy execute deploy.rb deploy_web --retry-policy '{"run":{"retries":2,"retry_on_exit_codes":[2]}}'
|
|
246
|
+
|
|
247
|
+
# 使用文件覆盖重试策略(JSON)
|
|
248
|
+
kdeploy execute deploy.rb deploy_web --retry-policy-file ./retry_policy.example.json
|
|
249
|
+
|
|
239
250
|
# 组合选项
|
|
240
251
|
kdeploy execute deploy.rb deploy_web --limit web01 --parallel 3 --dry-run
|
|
241
252
|
```
|
|
@@ -509,6 +520,14 @@ sync "./app", "/var/www/app",
|
|
|
509
520
|
# 排除特定文件(与 ignore 相同,但语义更清晰)
|
|
510
521
|
sync "./config", "/etc/app",
|
|
511
522
|
exclude: ["*.example", "*.bak", ".env.local"]
|
|
523
|
+
|
|
524
|
+
# 启用快速同步(本地/远端均有 rsync 时优先使用)
|
|
525
|
+
sync "./app", "/var/www/app",
|
|
526
|
+
fast: true
|
|
527
|
+
|
|
528
|
+
# 设置同步并行度
|
|
529
|
+
sync "./app", "/var/www/app",
|
|
530
|
+
parallel: 4
|
|
512
531
|
```
|
|
513
532
|
|
|
514
533
|
**参数:**
|
|
@@ -517,6 +536,8 @@ sync "./config", "/etc/app",
|
|
|
517
536
|
- `ignore`: 要忽略的文件/目录模式数组(支持 .gitignore 风格的通配符)
|
|
518
537
|
- `exclude`: 与 `ignore` 相同,用于语义清晰
|
|
519
538
|
- `delete`: 布尔值,是否删除远程目录中不存在于源目录的文件(默认: false)
|
|
539
|
+
- `fast`: 布尔值,启用快速同步路径(优先 rsync,默认: false)
|
|
540
|
+
- `parallel`: 上传并行度(默认: 1)
|
|
520
541
|
|
|
521
542
|
**忽略模式支持:**
|
|
522
543
|
- `*.log` - 匹配所有 .log 文件
|
|
@@ -690,10 +711,24 @@ export KDEPLOY_SSH_TIMEOUT=60
|
|
|
690
711
|
parallel: 5
|
|
691
712
|
ssh_timeout: 60
|
|
692
713
|
verify_host_key: true
|
|
714
|
+
retries: 2
|
|
715
|
+
retry_delay: 1
|
|
716
|
+
retry_on_nonzero: false
|
|
717
|
+
step_timeout: 30
|
|
718
|
+
sync_fast: false
|
|
719
|
+
sync_parallel: 4
|
|
720
|
+
retry_policy:
|
|
721
|
+
run:
|
|
722
|
+
retries: 2
|
|
723
|
+
retry_on_exit_codes: [2, 255]
|
|
724
|
+
upload:
|
|
725
|
+
retries: 0
|
|
693
726
|
```
|
|
694
727
|
|
|
695
728
|
配置文件会自动从当前目录向上查找,直到找到 `.kdeploy.yml` 文件。
|
|
696
729
|
|
|
730
|
+
**重试策略示例文件**: `retry_policy.example.json`
|
|
731
|
+
|
|
697
732
|
## 🔧 高级用法
|
|
698
733
|
|
|
699
734
|
### 条件执行
|
|
@@ -710,6 +745,16 @@ task :deploy do
|
|
|
710
745
|
end
|
|
711
746
|
```
|
|
712
747
|
|
|
748
|
+
### 重试策略示例
|
|
749
|
+
|
|
750
|
+
你可以通过文件覆盖重试策略:
|
|
751
|
+
|
|
752
|
+
```bash
|
|
753
|
+
kdeploy execute deploy.rb deploy_web --retry-policy-file ./retry_policy.example.json
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
示例文件见:`retry_policy.example.json` / `retry_policy.example.yml`
|
|
757
|
+
|
|
713
758
|
### 循环主机
|
|
714
759
|
|
|
715
760
|
```ruby
|
data/README_EN.md
CHANGED
|
@@ -208,6 +208,9 @@ kdeploy execute deploy.rb deploy_web
|
|
|
208
208
|
- `--retry-delay SECONDS`: Delay between retries in seconds (default `1`)
|
|
209
209
|
- `--retry-on-nonzero`: Retry commands on nonzero exit status (default `false`)
|
|
210
210
|
- `--timeout SECONDS`: Per-host execution timeout in seconds (default: none)
|
|
211
|
+
- `--step-timeout SECONDS`: Per-step execution timeout in seconds (default: none)
|
|
212
|
+
- `--retry-policy JSON`: Retry policy JSON (overrides `.kdeploy.yml`)
|
|
213
|
+
- `--retry-policy-file PATH`: Retry policy JSON file (overrides `.kdeploy.yml`)
|
|
211
214
|
|
|
212
215
|
**Examples:**
|
|
213
216
|
```bash
|
|
@@ -235,6 +238,15 @@ kdeploy execute deploy.rb deploy_web --retries 2 --retry-on-nonzero
|
|
|
235
238
|
# Set per-host timeout (seconds)
|
|
236
239
|
kdeploy execute deploy.rb deploy_web --timeout 120
|
|
237
240
|
|
|
241
|
+
# Set per-step timeout (seconds)
|
|
242
|
+
kdeploy execute deploy.rb deploy_web --step-timeout 30
|
|
243
|
+
|
|
244
|
+
# Override retry policy via CLI JSON
|
|
245
|
+
kdeploy execute deploy.rb deploy_web --retry-policy '{"run":{"retries":2,"retry_on_exit_codes":[2]}}'
|
|
246
|
+
|
|
247
|
+
# Override retry policy via file
|
|
248
|
+
kdeploy execute deploy.rb deploy_web --retry-policy-file ./retry_policy.example.json
|
|
249
|
+
|
|
238
250
|
# Combine options
|
|
239
251
|
kdeploy execute deploy.rb deploy_web --limit web01 --parallel 3 --dry-run
|
|
240
252
|
```
|
|
@@ -488,6 +500,45 @@ upload_template "./config/nginx.conf.erb", "/etc/nginx/nginx.conf",
|
|
|
488
500
|
- `destination`: Remote file path
|
|
489
501
|
- `variables`: Hash of variables for template rendering
|
|
490
502
|
|
|
503
|
+
#### `sync` - Directory Sync
|
|
504
|
+
|
|
505
|
+
Recursively sync a local directory to a remote server with filtering options.
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
# Basic sync
|
|
509
|
+
sync "./app", "/var/www/app"
|
|
510
|
+
|
|
511
|
+
# Ignore specific files/dirs
|
|
512
|
+
sync "./app", "/var/www/app",
|
|
513
|
+
ignore: [".git", "*.log", "node_modules", "*.tmp"]
|
|
514
|
+
|
|
515
|
+
# Delete remote files not present locally
|
|
516
|
+
sync "./app", "/var/www/app",
|
|
517
|
+
ignore: [".git", "*.log"],
|
|
518
|
+
delete: true
|
|
519
|
+
|
|
520
|
+
# Exclude files (alias of ignore)
|
|
521
|
+
sync "./config", "/etc/app",
|
|
522
|
+
exclude: ["*.example", "*.bak", ".env.local"]
|
|
523
|
+
|
|
524
|
+
# Fast sync (prefer rsync when available)
|
|
525
|
+
sync "./app", "/var/www/app",
|
|
526
|
+
fast: true
|
|
527
|
+
|
|
528
|
+
# Parallel sync uploads
|
|
529
|
+
sync "./app", "/var/www/app",
|
|
530
|
+
parallel: 4
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Parameters:**
|
|
534
|
+
- `source`: Local source directory path
|
|
535
|
+
- `destination`: Remote destination directory path
|
|
536
|
+
- `ignore`: Patterns to ignore (gitignore-style)
|
|
537
|
+
- `exclude`: Same as `ignore` for clarity
|
|
538
|
+
- `delete`: Delete remote files not present locally (default: false)
|
|
539
|
+
- `fast`: Enable fast sync path (prefer rsync when available, default: false)
|
|
540
|
+
- `parallel`: Upload concurrency for sync (default: 1)
|
|
541
|
+
|
|
491
542
|
### Chef-Style Resource DSL
|
|
492
543
|
|
|
493
544
|
Kdeploy provides a declarative resource DSL similar to Chef, which can replace or mix with low-level primitives (`run`, `upload`, `upload_template`).
|
|
@@ -647,7 +698,21 @@ For project-specific configuration, create a `.kdeploy.yml`:
|
|
|
647
698
|
parallel: 5
|
|
648
699
|
ssh_timeout: 60
|
|
649
700
|
verify_host_key: true
|
|
650
|
-
|
|
701
|
+
retries: 2
|
|
702
|
+
retry_delay: 1
|
|
703
|
+
retry_on_nonzero: false
|
|
704
|
+
step_timeout: 30
|
|
705
|
+
sync_fast: false
|
|
706
|
+
sync_parallel: 4
|
|
707
|
+
retry_policy:
|
|
708
|
+
run:
|
|
709
|
+
retries: 2
|
|
710
|
+
retry_on_exit_codes: [2, 255]
|
|
711
|
+
upload:
|
|
712
|
+
retries: 0
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
**Retry policy example file**: `retry_policy.example.json`
|
|
651
716
|
|
|
652
717
|
## 🔧 Advanced Usage
|
|
653
718
|
|
|
@@ -665,6 +730,16 @@ task :deploy do
|
|
|
665
730
|
end
|
|
666
731
|
```
|
|
667
732
|
|
|
733
|
+
### Retry Policy Example
|
|
734
|
+
|
|
735
|
+
You can override retry policy via file:
|
|
736
|
+
|
|
737
|
+
```bash
|
|
738
|
+
kdeploy execute deploy.rb deploy_web --retry-policy-file ./retry_policy.example.json
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Example files: `retry_policy.example.json` / `retry_policy.example.yml`
|
|
742
|
+
|
|
668
743
|
### Looping Over Hosts
|
|
669
744
|
|
|
670
745
|
```ruby
|
|
@@ -1131,7 +1206,8 @@ end
|
|
|
1131
1206
|
task :deploy_app, roles: :web do
|
|
1132
1207
|
sync "./app", "/var/www/app",
|
|
1133
1208
|
ignore: [".git", "*.log", "node_modules", ".env.local", "*.tmp"],
|
|
1134
|
-
delete: true
|
|
1209
|
+
delete: true,
|
|
1210
|
+
fast: true # prefer rsync when available
|
|
1135
1211
|
sync "./config", "/etc/app", exclude: ["*.example", "*.bak"]
|
|
1136
1212
|
service "app", action: :restart
|
|
1137
1213
|
end
|
|
@@ -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,6 +54,9 @@ 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
61
|
load_config_file
|
|
58
62
|
show_banner_once
|
|
@@ -77,7 +81,7 @@ module Kdeploy
|
|
|
77
81
|
|
|
78
82
|
def load_task_file(file)
|
|
79
83
|
validate_task_file(file)
|
|
80
|
-
#
|
|
84
|
+
# Use module_eval with top-level binding to keep heredoc compatible
|
|
81
85
|
self.class.module_eval(File.read(file), file)
|
|
82
86
|
rescue StandardError => e
|
|
83
87
|
raise FileNotFoundError, file if e.message.include?('not found')
|
|
@@ -272,6 +276,8 @@ module Kdeploy
|
|
|
272
276
|
retry_on_nonzero =
|
|
273
277
|
options[:retry_on_nonzero].nil? ? Configuration.default_retry_on_nonzero : options[:retry_on_nonzero]
|
|
274
278
|
host_timeout = options[:timeout].nil? ? Configuration.default_host_timeout : options[:timeout]
|
|
279
|
+
step_timeout = options[:step_timeout].nil? ? Configuration.default_step_timeout : options[:step_timeout]
|
|
280
|
+
retry_policy = resolve_retry_policy
|
|
275
281
|
base_dir = @task_file_dir
|
|
276
282
|
runner = Runner.new(
|
|
277
283
|
hosts, self.class.kdeploy_tasks,
|
|
@@ -282,7 +288,9 @@ module Kdeploy
|
|
|
282
288
|
retries: retries,
|
|
283
289
|
retry_delay: retry_delay,
|
|
284
290
|
retry_on_nonzero: retry_on_nonzero,
|
|
285
|
-
host_timeout: host_timeout
|
|
291
|
+
host_timeout: host_timeout,
|
|
292
|
+
step_timeout: step_timeout,
|
|
293
|
+
retry_policy: retry_policy
|
|
286
294
|
)
|
|
287
295
|
results = runner.run(task)
|
|
288
296
|
if options[:format] == 'json'
|
|
@@ -370,15 +378,13 @@ module Kdeploy
|
|
|
370
378
|
duration: step[:duration]
|
|
371
379
|
}
|
|
372
380
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
out[:result] = step[:result] if step[:type] == :sync
|
|
376
|
-
|
|
377
|
-
if options[:debug] && step[:type] == :run && step[:output].is_a?(Hash)
|
|
381
|
+
if step[:output].is_a?(Hash)
|
|
378
382
|
out[:stdout] = step[:output][:stdout]
|
|
379
383
|
out[:stderr] = step[:output][:stderr]
|
|
384
|
+
out[:exit_status] = step[:output][:exit_status]
|
|
380
385
|
end
|
|
381
386
|
|
|
387
|
+
out[:result] = step[:result] if step[:type] == :sync
|
|
382
388
|
out
|
|
383
389
|
end
|
|
384
390
|
|
|
@@ -397,7 +403,9 @@ module Kdeploy
|
|
|
397
403
|
destination: cmd[:destination],
|
|
398
404
|
ignore: cmd[:ignore] || [],
|
|
399
405
|
exclude: cmd[:exclude] || [],
|
|
400
|
-
delete: cmd[:delete] || false
|
|
406
|
+
delete: cmd[:delete] || false,
|
|
407
|
+
fast: cmd[:fast],
|
|
408
|
+
parallel: cmd[:parallel]
|
|
401
409
|
}
|
|
402
410
|
else
|
|
403
411
|
{ type: cmd[:type].to_s }
|
|
@@ -409,11 +417,61 @@ module Kdeploy
|
|
|
409
417
|
|
|
410
418
|
if result[:output].is_a?(Array)
|
|
411
419
|
result[:output].map do |o|
|
|
412
|
-
|
|
420
|
+
next unless o.is_a?(Hash)
|
|
421
|
+
|
|
422
|
+
err = o[:output]
|
|
423
|
+
next unless err.is_a?(Hash)
|
|
424
|
+
|
|
425
|
+
pieces = []
|
|
426
|
+
pieces << "command=#{err[:command]}" if err[:command]
|
|
427
|
+
pieces << "exit_status=#{err[:exit_status]}" if err[:exit_status]
|
|
428
|
+
pieces << "stderr=#{err[:stderr]}" if err[:stderr]
|
|
429
|
+
pieces.join(' ')
|
|
413
430
|
end.compact.join("\n")
|
|
414
431
|
else
|
|
415
432
|
result[:output].to_s
|
|
416
433
|
end
|
|
417
434
|
end
|
|
435
|
+
|
|
436
|
+
def parse_retry_policy(raw)
|
|
437
|
+
policy = JSON.parse(raw)
|
|
438
|
+
raise ArgumentError, 'retry_policy must be a JSON object' unless policy.is_a?(Hash)
|
|
439
|
+
|
|
440
|
+
policy
|
|
441
|
+
rescue JSON::ParserError => e
|
|
442
|
+
raise ArgumentError, "retry_policy JSON parse error: #{e.message}"
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def resolve_retry_policy
|
|
446
|
+
if options[:retry_policy_file]
|
|
447
|
+
path = options[:retry_policy_file]
|
|
448
|
+
unless File.exist?(path)
|
|
449
|
+
raise ArgumentError,
|
|
450
|
+
"retry_policy file not found: #{path} (examples: retry_policy.example.json / retry_policy.example.yml)"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
return parse_retry_policy_file(path)
|
|
454
|
+
end
|
|
455
|
+
return parse_retry_policy(options[:retry_policy]) if options[:retry_policy]
|
|
456
|
+
|
|
457
|
+
Configuration.default_retry_policy
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def parse_retry_policy_file(path)
|
|
461
|
+
ext = File.extname(path).downcase
|
|
462
|
+
raw = File.read(path)
|
|
463
|
+
policy =
|
|
464
|
+
case ext
|
|
465
|
+
when '.yml', '.yaml'
|
|
466
|
+
YAML.safe_load(raw) || {}
|
|
467
|
+
else
|
|
468
|
+
JSON.parse(raw)
|
|
469
|
+
end
|
|
470
|
+
raise ArgumentError, 'retry_policy must be a JSON/YAML object' unless policy.is_a?(Hash)
|
|
471
|
+
|
|
472
|
+
policy
|
|
473
|
+
rescue JSON::ParserError, Psych::SyntaxError => e
|
|
474
|
+
raise ArgumentError, "retry_policy parse error: #{e.message}"
|
|
475
|
+
end
|
|
418
476
|
end
|
|
419
477
|
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)
|
|
@@ -72,6 +84,10 @@ module Kdeploy
|
|
|
72
84
|
@default_retry_delay = config['retry_delay'] if config.key?('retry_delay')
|
|
73
85
|
@default_host_timeout = config['host_timeout'] if config.key?('host_timeout')
|
|
74
86
|
@default_retry_on_nonzero = config['retry_on_nonzero'] if config.key?('retry_on_nonzero')
|
|
87
|
+
@default_sync_fast = config['sync_fast'] if config.key?('sync_fast')
|
|
88
|
+
@default_step_timeout = config['step_timeout'] if config.key?('step_timeout')
|
|
89
|
+
@default_retry_policy = config['retry_policy'] if config.key?('retry_policy')
|
|
90
|
+
@default_sync_parallel = config['sync_parallel'] if config.key?('sync_parallel')
|
|
75
91
|
end
|
|
76
92
|
|
|
77
93
|
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
|
|
@@ -5,6 +5,8 @@ require 'net/scp'
|
|
|
5
5
|
require 'pathname'
|
|
6
6
|
require 'find'
|
|
7
7
|
require 'shellwords'
|
|
8
|
+
require 'tempfile'
|
|
9
|
+
require 'concurrent'
|
|
8
10
|
require_relative 'file_filter'
|
|
9
11
|
|
|
10
12
|
module Kdeploy
|
|
@@ -16,7 +18,7 @@ module Kdeploy
|
|
|
16
18
|
@ip = host_config[:ip]
|
|
17
19
|
@password = host_config[:password]
|
|
18
20
|
@key = host_config[:key]
|
|
19
|
-
@port = host_config[:port] #
|
|
21
|
+
@port = host_config[:port] # Added custom port support
|
|
20
22
|
@use_sudo = host_config[:use_sudo] || false
|
|
21
23
|
@sudo_password = host_config[:sudo_password]
|
|
22
24
|
@base_dir = host_config[:base_dir] # Base directory for resolving relative paths
|
|
@@ -114,7 +116,8 @@ module Kdeploy
|
|
|
114
116
|
raise TemplateError.new("Template upload failed: #{e.message}", e)
|
|
115
117
|
end
|
|
116
118
|
|
|
117
|
-
def sync_directory(source, destination, ignore: [], exclude: [], delete: false,
|
|
119
|
+
def sync_directory(source, destination, ignore: [], exclude: [], delete: false, fast: nil, parallel: nil,
|
|
120
|
+
use_sudo: nil)
|
|
118
121
|
use_sudo = @use_sudo if use_sudo.nil?
|
|
119
122
|
|
|
120
123
|
# Resolve relative paths relative to base_dir
|
|
@@ -127,24 +130,29 @@ module Kdeploy
|
|
|
127
130
|
all_patterns = ignore + exclude
|
|
128
131
|
filter = FileFilter.new(ignore_patterns: all_patterns)
|
|
129
132
|
|
|
133
|
+
if fast
|
|
134
|
+
rsync_result = sync_with_rsync(
|
|
135
|
+
resolved_source,
|
|
136
|
+
destination,
|
|
137
|
+
ignore: ignore,
|
|
138
|
+
exclude: exclude,
|
|
139
|
+
delete: delete,
|
|
140
|
+
use_sudo: use_sudo
|
|
141
|
+
)
|
|
142
|
+
return rsync_result if rsync_result
|
|
143
|
+
end
|
|
144
|
+
|
|
130
145
|
# Collect files to sync
|
|
131
146
|
files_to_sync = collect_files_to_sync(resolved_source, filter)
|
|
132
147
|
|
|
133
148
|
# Upload files
|
|
134
|
-
uploaded_count =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
remote_dir = File.dirname(remote_path)
|
|
142
|
-
ensure_remote_directory(remote_dir, use_sudo: use_sudo)
|
|
143
|
-
|
|
144
|
-
# Upload file
|
|
145
|
-
upload(file_path, remote_path, use_sudo: use_sudo)
|
|
146
|
-
uploaded_count += 1
|
|
147
|
-
end
|
|
149
|
+
uploaded_count = upload_files(
|
|
150
|
+
files_to_sync,
|
|
151
|
+
resolved_source,
|
|
152
|
+
destination,
|
|
153
|
+
parallel: parallel,
|
|
154
|
+
use_sudo: use_sudo
|
|
155
|
+
)
|
|
148
156
|
|
|
149
157
|
# Delete extra files if requested
|
|
150
158
|
deleted_count = 0
|
|
@@ -196,6 +204,80 @@ module Kdeploy
|
|
|
196
204
|
end
|
|
197
205
|
end
|
|
198
206
|
|
|
207
|
+
def sync_with_rsync(source, destination, ignore:, exclude:, delete:, use_sudo:)
|
|
208
|
+
return nil unless system('command -v rsync >/dev/null 2>&1')
|
|
209
|
+
|
|
210
|
+
exclude_file = build_rsync_excludes(ignore + exclude)
|
|
211
|
+
|
|
212
|
+
unless remote_rsync_available?
|
|
213
|
+
File.delete(exclude_file) if exclude_file && File.exist?(exclude_file)
|
|
214
|
+
return nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
begin
|
|
218
|
+
rsync_cmd = build_rsync_command(source, destination, exclude_file, delete: delete, use_sudo: use_sudo)
|
|
219
|
+
return nil unless system(rsync_cmd)
|
|
220
|
+
|
|
221
|
+
{
|
|
222
|
+
uploaded: 0,
|
|
223
|
+
deleted: 0,
|
|
224
|
+
total: 0,
|
|
225
|
+
fast_path: 'rsync'
|
|
226
|
+
}
|
|
227
|
+
ensure
|
|
228
|
+
File.delete(exclude_file) if exclude_file && File.exist?(exclude_file)
|
|
229
|
+
end
|
|
230
|
+
rescue StandardError
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def build_rsync_excludes(patterns)
|
|
235
|
+
patterns = Array(patterns).compact
|
|
236
|
+
return nil if patterns.empty?
|
|
237
|
+
|
|
238
|
+
file = Tempfile.new('kdeploy_rsync_excludes')
|
|
239
|
+
patterns.each { |pattern| file.write("#{pattern}\n") }
|
|
240
|
+
file.close
|
|
241
|
+
file.path
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def build_rsync_command(source, destination, exclude_file, delete:, use_sudo:)
|
|
245
|
+
delete_flag = delete ? '--delete' : ''
|
|
246
|
+
exclude_flag = exclude_file ? "--exclude-from='#{exclude_file}'" : ''
|
|
247
|
+
ssh_cmd = build_rsync_ssh_command
|
|
248
|
+
sudo_flag = if use_sudo || requires_sudo?(destination)
|
|
249
|
+
'--rsync-path="sudo rsync"'
|
|
250
|
+
else
|
|
251
|
+
''
|
|
252
|
+
end
|
|
253
|
+
parts = [
|
|
254
|
+
'rsync -az',
|
|
255
|
+
delete_flag,
|
|
256
|
+
exclude_flag,
|
|
257
|
+
sudo_flag,
|
|
258
|
+
"-e \"#{ssh_cmd}\"",
|
|
259
|
+
"#{Shellwords.escape(source)}/",
|
|
260
|
+
"#{@user}@#{@ip}:#{Shellwords.escape(destination)}/"
|
|
261
|
+
].reject(&:empty?)
|
|
262
|
+
parts.join(' ')
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def build_rsync_ssh_command
|
|
266
|
+
ssh = ['ssh']
|
|
267
|
+
ssh << "-p #{@port}" if @port
|
|
268
|
+
ssh << "-i #{Shellwords.escape(@key)}" if @key
|
|
269
|
+
ssh.join(' ')
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def remote_rsync_available?
|
|
273
|
+
Net::SSH.start(@ip, @user, ssh_options) do |ssh|
|
|
274
|
+
output = ssh.exec!('command -v rsync 2>/dev/null || true')
|
|
275
|
+
return !output.to_s.strip.empty?
|
|
276
|
+
end
|
|
277
|
+
rescue StandardError
|
|
278
|
+
false
|
|
279
|
+
end
|
|
280
|
+
|
|
199
281
|
def requires_sudo?(path)
|
|
200
282
|
# Check if path is in system directories that typically require sudo
|
|
201
283
|
system_dirs = %w[/etc /usr /var /opt /sbin /bin /lib /lib64 /root]
|
|
@@ -229,16 +311,16 @@ module Kdeploy
|
|
|
229
311
|
end
|
|
230
312
|
|
|
231
313
|
def wrap_with_sudo(command)
|
|
232
|
-
#
|
|
314
|
+
# Do not add sudo again if command already starts with sudo
|
|
233
315
|
return command if command.strip.start_with?('sudo')
|
|
234
316
|
|
|
235
|
-
#
|
|
317
|
+
# Wrap multi-line/control/compound commands so sudo applies to the full block
|
|
236
318
|
needs_wrap = command.include?("\n") ||
|
|
237
319
|
command.match?(/\b(if|for|while|case|function)\b/) ||
|
|
238
320
|
command.match?(/\s(&&|\|\||;)\s/)
|
|
239
321
|
|
|
240
322
|
if needs_wrap
|
|
241
|
-
#
|
|
323
|
+
# Escape single quotes in command, then execute via bash -c
|
|
242
324
|
escaped_command = command.gsub("'", "'\"'\"'")
|
|
243
325
|
if @sudo_password
|
|
244
326
|
escaped_password = @sudo_password.gsub('\'', "'\"'\"'").gsub('$', '\\$').gsub('`', '\\`')
|
|
@@ -247,7 +329,7 @@ module Kdeploy
|
|
|
247
329
|
"sudo bash -c '#{escaped_command}'"
|
|
248
330
|
end
|
|
249
331
|
elsif @sudo_password
|
|
250
|
-
#
|
|
332
|
+
# Wrap single-line command directly
|
|
251
333
|
escaped_password = @sudo_password.gsub('\'', "'\"'\"'").gsub('$', '\\$').gsub('`', '\\`')
|
|
252
334
|
"echo '#{escaped_password}' | sudo -S #{command}"
|
|
253
335
|
else
|
|
@@ -271,6 +353,55 @@ module Kdeploy
|
|
|
271
353
|
files
|
|
272
354
|
end
|
|
273
355
|
|
|
356
|
+
def upload_files(files_to_sync, source_dir, destination, parallel:, use_sudo:)
|
|
357
|
+
count = Concurrent::AtomicFixnum.new(0)
|
|
358
|
+
source_path = Pathname.new(source_dir)
|
|
359
|
+
parallel = normalize_parallel(parallel)
|
|
360
|
+
return upload_files_sequential(files_to_sync, source_path, destination, use_sudo, count) if parallel <= 1
|
|
361
|
+
|
|
362
|
+
queue = Queue.new
|
|
363
|
+
files_to_sync.each { |path| queue << path }
|
|
364
|
+
workers = Array.new(parallel) do
|
|
365
|
+
Thread.new do
|
|
366
|
+
until queue.empty?
|
|
367
|
+
file_path = begin
|
|
368
|
+
queue.pop(true)
|
|
369
|
+
rescue StandardError
|
|
370
|
+
nil
|
|
371
|
+
end
|
|
372
|
+
next unless file_path
|
|
373
|
+
|
|
374
|
+
upload_single_file(file_path, source_path, destination, use_sudo)
|
|
375
|
+
count.increment
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
workers.each(&:join)
|
|
380
|
+
count.value
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def upload_files_sequential(files_to_sync, source_path, destination, use_sudo, count)
|
|
384
|
+
files_to_sync.each do |file_path|
|
|
385
|
+
upload_single_file(file_path, source_path, destination, use_sudo)
|
|
386
|
+
count.increment
|
|
387
|
+
end
|
|
388
|
+
count.value
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def upload_single_file(file_path, source_path, destination, use_sudo)
|
|
392
|
+
relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
|
|
393
|
+
remote_path = File.join(destination, relative_path).gsub(%r{/+}, '/')
|
|
394
|
+
remote_dir = File.dirname(remote_path)
|
|
395
|
+
ensure_remote_directory(remote_dir, use_sudo: use_sudo)
|
|
396
|
+
upload(file_path, remote_path, use_sudo: use_sudo)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def normalize_parallel(parallel)
|
|
400
|
+
value = parallel.nil? ? Configuration.default_sync_parallel : parallel
|
|
401
|
+
value = value.to_i
|
|
402
|
+
value.positive? ? value : 1
|
|
403
|
+
end
|
|
404
|
+
|
|
274
405
|
def ensure_remote_directory(remote_dir, use_sudo: nil)
|
|
275
406
|
use_sudo = @use_sudo if use_sudo.nil?
|
|
276
407
|
return if remote_dir.nil? || remote_dir.empty? || remote_dir == '.' || remote_dir == '/'
|
|
@@ -261,6 +261,7 @@ module Kdeploy
|
|
|
261
261
|
stats = []
|
|
262
262
|
stats << @pastel.green("#{uploaded} uploaded") if uploaded.positive?
|
|
263
263
|
stats << @pastel.yellow("#{deleted} deleted") if deleted.positive?
|
|
264
|
+
stats << @pastel.cyan("fast: #{result[:fast_path]}") if result[:fast_path]
|
|
264
265
|
stats_str = stats.any? ? " (#{stats.join(', ')})" : " (#{total} files)"
|
|
265
266
|
|
|
266
267
|
@pastel.dim(' ') + @pastel.cyan(display_path) + @pastel.dim(stats_str) + duration_str + " #{status_str}"
|
|
@@ -9,7 +9,10 @@ module Kdeploy
|
|
|
9
9
|
debug: false, base_dir: nil, retries: Configuration.default_retries,
|
|
10
10
|
retry_delay: Configuration.default_retry_delay,
|
|
11
11
|
retry_on_nonzero: Configuration.default_retry_on_nonzero,
|
|
12
|
-
host_timeout: Configuration.default_host_timeout
|
|
12
|
+
host_timeout: Configuration.default_host_timeout,
|
|
13
|
+
step_timeout: Configuration.default_step_timeout,
|
|
14
|
+
retry_policy: Configuration.default_retry_policy,
|
|
15
|
+
on_step: nil)
|
|
13
16
|
@hosts = hosts
|
|
14
17
|
@tasks = tasks
|
|
15
18
|
@parallel = parallel
|
|
@@ -20,6 +23,9 @@ module Kdeploy
|
|
|
20
23
|
@retry_delay = retry_delay
|
|
21
24
|
@retry_on_nonzero = retry_on_nonzero
|
|
22
25
|
@host_timeout = normalize_timeout(host_timeout)
|
|
26
|
+
@step_timeout = normalize_timeout(step_timeout)
|
|
27
|
+
@retry_policy = retry_policy
|
|
28
|
+
@on_step = on_step
|
|
23
29
|
@pool = Concurrent::FixedThreadPool.new(@parallel)
|
|
24
30
|
@results = Concurrent::Hash.new
|
|
25
31
|
end
|
|
@@ -102,7 +108,9 @@ module Kdeploy
|
|
|
102
108
|
debug: @debug,
|
|
103
109
|
retries: @retries,
|
|
104
110
|
retry_delay: @retry_delay,
|
|
105
|
-
retry_on_nonzero: @retry_on_nonzero
|
|
111
|
+
retry_on_nonzero: @retry_on_nonzero,
|
|
112
|
+
step_timeout: @step_timeout,
|
|
113
|
+
retry_policy: @retry_policy
|
|
106
114
|
)
|
|
107
115
|
result = { status: :success, output: [] }
|
|
108
116
|
|
|
@@ -124,6 +132,7 @@ module Kdeploy
|
|
|
124
132
|
commands.each do |command|
|
|
125
133
|
step_result = execute_command(command_executor, command, name)
|
|
126
134
|
result[:output] << step_result
|
|
135
|
+
emit_step(name, step_result, result, task_name)
|
|
127
136
|
rescue StandardError => e
|
|
128
137
|
step = step_description(command)
|
|
129
138
|
result[:status] = :failed
|
|
@@ -135,6 +144,7 @@ module Kdeploy
|
|
|
135
144
|
error: "#{e.class}: #{e.message}",
|
|
136
145
|
output: error_output_for_step(e)
|
|
137
146
|
}
|
|
147
|
+
emit_step(name, result[:output].last, result, task_name)
|
|
138
148
|
break
|
|
139
149
|
end
|
|
140
150
|
end
|
|
@@ -208,6 +218,15 @@ module Kdeploy
|
|
|
208
218
|
end
|
|
209
219
|
end
|
|
210
220
|
|
|
221
|
+
def emit_step(host_name, step_result, result, task_name)
|
|
222
|
+
return unless @on_step
|
|
223
|
+
|
|
224
|
+
@on_step.call(host_name, step_result, result, task_name)
|
|
225
|
+
rescue StandardError
|
|
226
|
+
# Best-effort only; never fail execution because of streaming hooks.
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
|
|
211
230
|
def execute_command(command_executor, command, host_name)
|
|
212
231
|
case command[:type]
|
|
213
232
|
when :run
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require 'erb'
|
|
4
4
|
require 'tempfile'
|
|
5
|
+
require 'set'
|
|
5
6
|
|
|
6
7
|
module Kdeploy
|
|
7
8
|
# ERB template rendering and upload handler
|
|
8
9
|
class Template
|
|
9
10
|
def self.render(template_path, variables = {})
|
|
10
11
|
template_content = File.read(template_path)
|
|
12
|
+
validate_template_variables(template_content, variables)
|
|
11
13
|
context = create_template_context(variables)
|
|
12
14
|
ERB.new(template_content).result(context.instance_eval { binding })
|
|
13
15
|
end
|
|
@@ -24,17 +26,38 @@ module Kdeploy
|
|
|
24
26
|
def self.render_and_upload(executor, template_path, destination, variables = {})
|
|
25
27
|
rendered_content = render(template_path, variables)
|
|
26
28
|
|
|
27
|
-
#
|
|
29
|
+
# Create temporary file
|
|
28
30
|
temp_file = Tempfile.new('kdeploy')
|
|
29
31
|
begin
|
|
30
32
|
temp_file.write(rendered_content)
|
|
31
33
|
temp_file.close
|
|
32
34
|
|
|
33
|
-
#
|
|
35
|
+
# Upload rendered file
|
|
34
36
|
executor.upload(temp_file.path, destination)
|
|
35
37
|
ensure
|
|
36
38
|
temp_file.unlink
|
|
37
39
|
end
|
|
38
40
|
end
|
|
41
|
+
|
|
42
|
+
def self.validate_template_variables(template_content, variables)
|
|
43
|
+
required = extract_template_identifiers(template_content)
|
|
44
|
+
return if required.empty?
|
|
45
|
+
|
|
46
|
+
provided = variables.keys.to_set(&:to_s)
|
|
47
|
+
missing = required.reject { |name| provided.include?(name) }
|
|
48
|
+
return if missing.empty?
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, "Missing template variables: #{missing.sort.join(', ')}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.extract_template_identifiers(template_content)
|
|
54
|
+
identifiers = template_content.scan(/<%=\s*([a-zA-Z_]\w*)/).flatten
|
|
55
|
+
identifiers.uniq - ruby_keywords
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.ruby_keywords
|
|
59
|
+
%w[alias and begin break case class def defined? do else elsif end ensure false for if in module next nil not
|
|
60
|
+
or redo rescue retry return self super then true undef unless until when while yield]
|
|
61
|
+
end
|
|
39
62
|
end
|
|
40
63
|
end
|
data/lib/kdeploy/version.rb
CHANGED
data/lib/kdeploy.rb
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'kdeploy/version'
|
|
4
4
|
require_relative 'kdeploy/errors'
|
|
5
|
-
require_relative 'kdeploy/configuration'
|
|
6
|
-
require_relative 'kdeploy/output'
|
|
5
|
+
require_relative 'kdeploy/config/configuration'
|
|
6
|
+
require_relative 'kdeploy/output/output'
|
|
7
7
|
require_relative 'kdeploy/banner'
|
|
8
|
-
require_relative 'kdeploy/file_filter'
|
|
9
|
-
require_relative 'kdeploy/dsl'
|
|
10
|
-
require_relative 'kdeploy/executor'
|
|
11
|
-
require_relative 'kdeploy/command_executor'
|
|
12
|
-
require_relative 'kdeploy/output_formatter'
|
|
13
|
-
require_relative 'kdeploy/help_formatter'
|
|
14
|
-
require_relative 'kdeploy/runner'
|
|
15
|
-
require_relative 'kdeploy/initializer'
|
|
16
|
-
require_relative 'kdeploy/template'
|
|
8
|
+
require_relative 'kdeploy/executor/file_filter'
|
|
9
|
+
require_relative 'kdeploy/dsl/dsl'
|
|
10
|
+
require_relative 'kdeploy/executor/executor'
|
|
11
|
+
require_relative 'kdeploy/executor/command_executor'
|
|
12
|
+
require_relative 'kdeploy/output/output_formatter'
|
|
13
|
+
require_relative 'kdeploy/cli/help_formatter'
|
|
14
|
+
require_relative 'kdeploy/runner/runner'
|
|
15
|
+
require_relative 'kdeploy/dsl/initializer'
|
|
16
|
+
require_relative 'kdeploy/template/template'
|
|
17
17
|
require_relative 'kdeploy/post_install'
|
|
18
|
-
require_relative 'kdeploy/cli'
|
|
18
|
+
require_relative 'kdeploy/cli/cli'
|
|
19
19
|
|
|
20
20
|
# Kdeploy - A lightweight agentless deployment automation tool
|
|
21
21
|
module Kdeploy
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kdeploy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kk
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-04-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bcrypt_pbkdf
|
|
@@ -167,22 +167,22 @@ files:
|
|
|
167
167
|
- ext/mkrf_conf.rb
|
|
168
168
|
- lib/kdeploy.rb
|
|
169
169
|
- lib/kdeploy/banner.rb
|
|
170
|
-
- lib/kdeploy/cli.rb
|
|
171
|
-
- lib/kdeploy/
|
|
170
|
+
- lib/kdeploy/cli/cli.rb
|
|
171
|
+
- lib/kdeploy/cli/help_formatter.rb
|
|
172
172
|
- lib/kdeploy/completions/kdeploy.bash
|
|
173
173
|
- lib/kdeploy/completions/kdeploy.zsh
|
|
174
|
-
- lib/kdeploy/configuration.rb
|
|
175
|
-
- lib/kdeploy/dsl.rb
|
|
174
|
+
- lib/kdeploy/config/configuration.rb
|
|
175
|
+
- lib/kdeploy/dsl/dsl.rb
|
|
176
|
+
- lib/kdeploy/dsl/initializer.rb
|
|
176
177
|
- lib/kdeploy/errors.rb
|
|
177
|
-
- lib/kdeploy/executor.rb
|
|
178
|
-
- lib/kdeploy/
|
|
179
|
-
- lib/kdeploy/
|
|
180
|
-
- lib/kdeploy/
|
|
181
|
-
- lib/kdeploy/output.rb
|
|
182
|
-
- lib/kdeploy/output_formatter.rb
|
|
178
|
+
- lib/kdeploy/executor/command_executor.rb
|
|
179
|
+
- lib/kdeploy/executor/executor.rb
|
|
180
|
+
- lib/kdeploy/executor/file_filter.rb
|
|
181
|
+
- lib/kdeploy/output/output.rb
|
|
182
|
+
- lib/kdeploy/output/output_formatter.rb
|
|
183
183
|
- lib/kdeploy/post_install.rb
|
|
184
|
-
- lib/kdeploy/runner.rb
|
|
185
|
-
- lib/kdeploy/template.rb
|
|
184
|
+
- lib/kdeploy/runner/runner.rb
|
|
185
|
+
- lib/kdeploy/template/template.rb
|
|
186
186
|
- lib/kdeploy/version.rb
|
|
187
187
|
homepage: https://github.com/kevin197011/kdeploy
|
|
188
188
|
licenses:
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Kdeploy
|
|
4
|
-
# Executes a single command and records execution time
|
|
5
|
-
class CommandExecutor
|
|
6
|
-
def initialize(executor, output, debug: false, retries: 0, retry_delay: 1, retry_on_nonzero: false)
|
|
7
|
-
@executor = executor
|
|
8
|
-
@output = output
|
|
9
|
-
@debug = debug
|
|
10
|
-
@retries = retries.to_i
|
|
11
|
-
@retry_delay = retry_delay.to_f
|
|
12
|
-
@retry_on_nonzero = retry_on_nonzero
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def execute_run(command, _host_name)
|
|
16
|
-
cmd = command[:command]
|
|
17
|
-
use_sudo = command[:sudo]
|
|
18
|
-
|
|
19
|
-
result, duration = measure_time do
|
|
20
|
-
with_retries { @executor.execute(cmd, use_sudo: use_sudo) }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
{ command: cmd, output: result, duration: duration, type: :run }
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def execute_upload(command, _host_name)
|
|
27
|
-
_result, duration = measure_time do
|
|
28
|
-
with_retries { @executor.upload(command[:source], command[:destination]) }
|
|
29
|
-
end
|
|
30
|
-
{
|
|
31
|
-
command: "upload: #{command[:source]} -> #{command[:destination]}",
|
|
32
|
-
duration: duration,
|
|
33
|
-
type: :upload
|
|
34
|
-
}
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def execute_upload_template(command, _host_name)
|
|
38
|
-
_result, duration = measure_time do
|
|
39
|
-
with_retries do
|
|
40
|
-
@executor.upload_template(command[:source], command[:destination], command[:variables])
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
{
|
|
44
|
-
command: "upload_template: #{command[:source]} -> #{command[:destination]}",
|
|
45
|
-
duration: duration,
|
|
46
|
-
type: :upload_template
|
|
47
|
-
}
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def execute_sync(command, _host_name)
|
|
51
|
-
source = command[:source]
|
|
52
|
-
destination = command[:destination]
|
|
53
|
-
|
|
54
|
-
result, duration = measure_time do
|
|
55
|
-
with_retries do
|
|
56
|
-
@executor.sync_directory(
|
|
57
|
-
source,
|
|
58
|
-
destination,
|
|
59
|
-
ignore: command[:ignore] || [],
|
|
60
|
-
exclude: command[:exclude] || [],
|
|
61
|
-
delete: command[:delete] || false
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
build_sync_result(source, destination, result, duration)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
def build_sync_result(source, destination, result, duration)
|
|
72
|
-
{
|
|
73
|
-
command: "sync: #{source} -> #{destination}",
|
|
74
|
-
duration: duration,
|
|
75
|
-
type: :sync,
|
|
76
|
-
result: result,
|
|
77
|
-
uploaded: result[:uploaded],
|
|
78
|
-
deleted: result[:deleted],
|
|
79
|
-
total: result[:total]
|
|
80
|
-
}
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def measure_time
|
|
84
|
-
start_time = Time.now
|
|
85
|
-
result = yield
|
|
86
|
-
duration = Time.now - start_time
|
|
87
|
-
[result, duration]
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def with_retries
|
|
91
|
-
attempts = 0
|
|
92
|
-
begin
|
|
93
|
-
attempts += 1
|
|
94
|
-
yield
|
|
95
|
-
rescue SSHError, SCPError, TemplateError => e
|
|
96
|
-
raise if e.is_a?(SSHError) && e.exit_status && !@retry_on_nonzero
|
|
97
|
-
raise if attempts > (@retries + 1)
|
|
98
|
-
|
|
99
|
-
sleep(@retry_delay) if @retry_delay.positive?
|
|
100
|
-
retry
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
File without changes
|
|
File without changes
|