kdeploy 1.3.1 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +42 -1072
- data/README_EN.md +42 -1080
- data/lib/kdeploy/{cli.rb → cli/cli.rb} +95 -21
- data/lib/kdeploy/{help_formatter.rb → cli/help_formatter.rb} +3 -0
- data/lib/kdeploy/{configuration.rb → config/configuration.rb} +41 -1
- data/lib/kdeploy/{dsl.rb → dsl/dsl.rb} +9 -7
- data/lib/kdeploy/{initializer.rb → dsl/initializer.rb} +3 -3
- data/lib/kdeploy/errors.rb +7 -0
- data/lib/kdeploy/executor/command_executor.rb +155 -0
- data/lib/kdeploy/{executor.rb → executor/executor.rb} +151 -20
- data/lib/kdeploy/{output_formatter.rb → output/output_formatter.rb} +4 -7
- data/lib/kdeploy/{runner.rb → runner/runner.rb} +9 -2
- data/lib/kdeploy/{template.rb → template/template.rb} +25 -2
- data/lib/kdeploy/version.rb +1 -1
- data/lib/kdeploy.rb +12 -12
- metadata +14 -15
- data/AGENTS.md +0 -18
- data/lib/kdeploy/command_executor.rb +0 -104
- /data/lib/kdeploy/{file_filter.rb → executor/file_filter.rb} +0 -0
- /data/lib/kdeploy/{output.rb → output/output.rb} +0 -0
|
@@ -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 == '/'
|
|
@@ -18,7 +18,6 @@ module Kdeploy
|
|
|
18
18
|
def format_host_status(host, status)
|
|
19
19
|
status_str = case status
|
|
20
20
|
when :success then @pastel.green('✓ ok')
|
|
21
|
-
when :changed then @pastel.yellow('~ changed')
|
|
22
21
|
else @pastel.red('✗ failed')
|
|
23
22
|
end
|
|
24
23
|
@pastel.bright_white(" #{host.ljust(20)} #{status_str}")
|
|
@@ -101,21 +100,18 @@ module Kdeploy
|
|
|
101
100
|
end
|
|
102
101
|
|
|
103
102
|
def calculate_summary_counts(result)
|
|
104
|
-
ok =
|
|
103
|
+
ok = result[:status] == :success ? result[:output].size : 0
|
|
105
104
|
failed = result[:status] == :failed ? 1 : 0
|
|
106
|
-
|
|
107
|
-
{ ok: ok, failed: failed, changed: changed }
|
|
105
|
+
{ ok: ok, failed: failed }
|
|
108
106
|
end
|
|
109
107
|
|
|
110
108
|
def build_summary_line(host, counts, max_host_len)
|
|
111
109
|
ok_w = 7
|
|
112
|
-
changed_w = 11
|
|
113
110
|
failed_w = 10
|
|
114
111
|
|
|
115
112
|
ok_str = @pastel.green("ok=#{counts[:ok].to_s.ljust(ok_w - 3)}")
|
|
116
|
-
changed_str = @pastel.yellow("changed=#{counts[:changed].to_s.ljust(changed_w - 8)}")
|
|
117
113
|
failed_str = @pastel.red("failed=#{counts[:failed].to_s.ljust(failed_w - 7)}")
|
|
118
|
-
"#{host.ljust(max_host_len)} : #{ok_str} #{
|
|
114
|
+
"#{host.ljust(max_host_len)} : #{ok_str} #{failed_str}"
|
|
119
115
|
end
|
|
120
116
|
|
|
121
117
|
def colorize_summary_line(line, counts)
|
|
@@ -261,6 +257,7 @@ module Kdeploy
|
|
|
261
257
|
stats = []
|
|
262
258
|
stats << @pastel.green("#{uploaded} uploaded") if uploaded.positive?
|
|
263
259
|
stats << @pastel.yellow("#{deleted} deleted") if deleted.positive?
|
|
260
|
+
stats << @pastel.cyan("fast: #{result[:fast_path]}") if result[:fast_path]
|
|
264
261
|
stats_str = stats.any? ? " (#{stats.join(', ')})" : " (#{total} files)"
|
|
265
262
|
|
|
266
263
|
@pastel.dim(' ') + @pastel.cyan(display_path) + @pastel.dim(stats_str) + duration_str + " #{status_str}"
|
|
@@ -9,7 +9,9 @@ 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)
|
|
13
15
|
@hosts = hosts
|
|
14
16
|
@tasks = tasks
|
|
15
17
|
@parallel = parallel
|
|
@@ -20,6 +22,8 @@ module Kdeploy
|
|
|
20
22
|
@retry_delay = retry_delay
|
|
21
23
|
@retry_on_nonzero = retry_on_nonzero
|
|
22
24
|
@host_timeout = normalize_timeout(host_timeout)
|
|
25
|
+
@step_timeout = normalize_timeout(step_timeout)
|
|
26
|
+
@retry_policy = retry_policy
|
|
23
27
|
@pool = Concurrent::FixedThreadPool.new(@parallel)
|
|
24
28
|
@results = Concurrent::Hash.new
|
|
25
29
|
end
|
|
@@ -61,6 +65,7 @@ module Kdeploy
|
|
|
61
65
|
pending.delete(future)
|
|
62
66
|
progressed = true
|
|
63
67
|
elsif timeout_exceeded?(started_at, now)
|
|
68
|
+
future.cancel if future.respond_to?(:cancel)
|
|
64
69
|
@results[host_name] ||= {
|
|
65
70
|
status: :failed,
|
|
66
71
|
error: "execution timeout after #{@host_timeout}s",
|
|
@@ -102,7 +107,9 @@ module Kdeploy
|
|
|
102
107
|
debug: @debug,
|
|
103
108
|
retries: @retries,
|
|
104
109
|
retry_delay: @retry_delay,
|
|
105
|
-
retry_on_nonzero: @retry_on_nonzero
|
|
110
|
+
retry_on_nonzero: @retry_on_nonzero,
|
|
111
|
+
step_timeout: @step_timeout,
|
|
112
|
+
retry_policy: @retry_policy
|
|
106
113
|
)
|
|
107
114
|
result = { status: :success, output: [] }
|
|
108
115
|
|
|
@@ -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.4
|
|
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-07-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bcrypt_pbkdf
|
|
@@ -160,29 +160,28 @@ extensions:
|
|
|
160
160
|
- ext/mkrf_conf.rb
|
|
161
161
|
extra_rdoc_files: []
|
|
162
162
|
files:
|
|
163
|
-
- AGENTS.md
|
|
164
163
|
- README.md
|
|
165
164
|
- README_EN.md
|
|
166
165
|
- exe/kdeploy
|
|
167
166
|
- ext/mkrf_conf.rb
|
|
168
167
|
- lib/kdeploy.rb
|
|
169
168
|
- lib/kdeploy/banner.rb
|
|
170
|
-
- lib/kdeploy/cli.rb
|
|
171
|
-
- lib/kdeploy/
|
|
169
|
+
- lib/kdeploy/cli/cli.rb
|
|
170
|
+
- lib/kdeploy/cli/help_formatter.rb
|
|
172
171
|
- lib/kdeploy/completions/kdeploy.bash
|
|
173
172
|
- lib/kdeploy/completions/kdeploy.zsh
|
|
174
|
-
- lib/kdeploy/configuration.rb
|
|
175
|
-
- lib/kdeploy/dsl.rb
|
|
173
|
+
- lib/kdeploy/config/configuration.rb
|
|
174
|
+
- lib/kdeploy/dsl/dsl.rb
|
|
175
|
+
- lib/kdeploy/dsl/initializer.rb
|
|
176
176
|
- 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
|
|
177
|
+
- lib/kdeploy/executor/command_executor.rb
|
|
178
|
+
- lib/kdeploy/executor/executor.rb
|
|
179
|
+
- lib/kdeploy/executor/file_filter.rb
|
|
180
|
+
- lib/kdeploy/output/output.rb
|
|
181
|
+
- lib/kdeploy/output/output_formatter.rb
|
|
183
182
|
- lib/kdeploy/post_install.rb
|
|
184
|
-
- lib/kdeploy/runner.rb
|
|
185
|
-
- lib/kdeploy/template.rb
|
|
183
|
+
- lib/kdeploy/runner/runner.rb
|
|
184
|
+
- lib/kdeploy/template/template.rb
|
|
186
185
|
- lib/kdeploy/version.rb
|
|
187
186
|
homepage: https://github.com/kevin197011/kdeploy
|
|
188
187
|
licenses:
|
data/AGENTS.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<!-- OPENSPEC:START -->
|
|
2
|
-
# OpenSpec Instructions
|
|
3
|
-
|
|
4
|
-
These instructions are for AI assistants working in this project.
|
|
5
|
-
|
|
6
|
-
Always open `@/openspec/AGENTS.md` when the request:
|
|
7
|
-
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
|
8
|
-
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
|
9
|
-
- Sounds ambiguous and you need the authoritative spec before coding
|
|
10
|
-
|
|
11
|
-
Use `@/openspec/AGENTS.md` to learn:
|
|
12
|
-
- How to create and apply change proposals
|
|
13
|
-
- Spec format and conventions
|
|
14
|
-
- Project structure and guidelines
|
|
15
|
-
|
|
16
|
-
Keep this managed block so 'openspec update' can refresh the instructions.
|
|
17
|
-
|
|
18
|
-
<!-- OPENSPEC:END -->
|
|
@@ -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
|