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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 782163100dc54c6588d66c1655434a9ab4f6a0cd3aa5d97583e4b9e5fb757bbd
4
- data.tar.gz: 38c8a9e92920dcdbfdca974a9ed4a51579cb618cc2a80fedcdaf19f0fe73fd80
3
+ metadata.gz: bd3c4b1e7ad1dc2062bfa0e732144b64f0c3c81655320dc96defeaed10816392
4
+ data.tar.gz: 2ac13e3f6b7093042d4a313c139ad997e59e06b1fa2d6dc7372dc33919f3eebc
5
5
  SHA512:
6
- metadata.gz: bcf659df1c65bfc50c0ebdb57a7b9f689a74c9db7107f7d3c48beb869b37633b2b887f02ca27f8f0b1707c5a593b8727e0416754221654c31faeaa1c1ddc2a3c
7
- data.tar.gz: 2fa594ddb0cf3196dc1abcfd9ae9fb54729b91907445912a0e10cd4aa5d7da83c42ac9dbab02ee33e8d824b38ef25408f35e7791108e9b26728a1b96507bd816
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
- # instance_eval 并传递顶层 binding,兼容 heredoc
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
- out[:exit_status] = step[:output][:exit_status] if step[:output].is_a?(Hash) && step[:output].key?(:exit_status)
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
- o[:output][:stderr] if o[:output].is_a?(Hash)
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
- # 安装系统包。默认 apt 平台;支持 platform: :yum
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
- # 管理系统服务(systemd)。action 支持 :start, :stop, :restart, :reload, :enable, :disable
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
- # 部署 ERB 模板到远程路径。支持 block 或关键字参数。
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
- # 确保远程目录存在。支持 mode 参数。
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
- # 创建 Nginx ERB 模板
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;
@@ -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, use_sudo: nil)
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 = 0
135
- source_path = Pathname.new(resolved_source)
136
- files_to_sync.each do |file_path|
137
- relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
138
- remote_path = File.join(destination, relative_path).gsub(%r{/+}, '/')
139
-
140
- # Ensure remote directory exists
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
- # 如果命令已经以 sudo 开头,不重复添加
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
- # 多行、shell 控制结构、或包含 && / || / ; 的复合命令需整段在 sudo 下执行
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
- # 转义命令中的单引号,然后用 bash -c 执行
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Kdeploy module for version management
4
4
  module Kdeploy
5
- VERSION = '1.3.1' unless const_defined?(:VERSION)
5
+ VERSION = '1.3.3' unless const_defined?(:VERSION)
6
6
  end
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.1
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-02-03 00:00:00.000000000 Z
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/command_executor.rb
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/file_filter.rb
179
- - lib/kdeploy/help_formatter.rb
180
- - lib/kdeploy/initializer.rb
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