kdeploy 1.1.9 → 1.2.1

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.
data/exe/kdeploy CHANGED
@@ -2,17 +2,17 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # 自动修复常见 gem 扩展
5
- %w[debug rbs].zip(%w[1.7.1 2.8.2]).each do |gem_name, version|
6
- require gem_name
7
- rescue LoadError
8
- warn "[Kdeploy] 自动修复 #{gem_name}-#{version} ..."
9
- system("gem pristine #{gem_name} --version #{version}")
10
- begin
11
- require gem_name
12
- rescue LoadError
13
- warn "[Kdeploy] 依然无法加载 #{gem_name}-#{version},请手动修复。"
14
- end
15
- end
5
+ # %w[debug rbs].zip(%w[1.7.1 2.8.2]).each do |gem_name, version|
6
+ # require gem_name
7
+ # rescue LoadError
8
+ # warn "[Kdeploy] 自动修复 #{gem_name}-#{version} ..."
9
+ # system("gem pristine #{gem_name} --version #{version}")
10
+ # begin
11
+ # require gem_name
12
+ # rescue LoadError
13
+ # warn "[Kdeploy] 依然无法加载 #{gem_name}-#{version},请手动修复。"
14
+ # end
15
+ # end
16
16
 
17
17
  require 'kdeploy'
18
18
 
@@ -3,11 +3,12 @@
3
3
  require 'pastel'
4
4
 
5
5
  module Kdeploy
6
+ # Banner display module for CLI output
6
7
  module Banner
7
8
  class << self
8
9
  def show
9
10
  pastel = Pastel.new
10
- <<~BANNER.freeze
11
+ <<~BANNER
11
12
  #{pastel.bright_blue(ASCII_LOGO)}
12
13
 
13
14
  #{pastel.bright_yellow('⚡')} #{pastel.bright_white('Lightweight Agentless Deployment Tool')}
data/lib/kdeploy/cli.rb CHANGED
@@ -7,6 +7,7 @@ require 'tty-box'
7
7
  require 'fileutils'
8
8
 
9
9
  module Kdeploy
10
+ # Command-line interface for Kdeploy
10
11
  class CLI < Thor
11
12
  extend DSL
12
13
 
@@ -108,7 +109,8 @@ module Kdeploy
108
109
  next
109
110
  end
110
111
 
111
- runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: options[:parallel])
112
+ output = ConsoleOutput.new
113
+ runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: options[:parallel], output: output)
112
114
  results = runner.run(task)
113
115
  print_results(results, task)
114
116
  end
@@ -127,6 +129,10 @@ module Kdeploy
127
129
 
128
130
  # 用 instance_eval 并传递顶层 binding,兼容 heredoc
129
131
  self.class.module_eval(File.read(file), file)
132
+ rescue StandardError => e
133
+ raise FileNotFoundError, file if e.message.include?('not found')
134
+
135
+ raise
130
136
  end
131
137
 
132
138
  def filter_hosts(limit, task_hosts)
@@ -199,7 +205,7 @@ module Kdeploy
199
205
  next if shown[key]
200
206
 
201
207
  shown[key] = true
202
- duration_str = step[:duration] ? pastel.dim(" [#{'%.2f' % step[:duration]}s]") : ''
208
+ duration_str = step[:duration] ? pastel.dim(" [#{format('%.2f', step[:duration])}s]") : ''
203
209
  puts pastel.green(" [upload] #{step[:command].sub('upload: ', '')}#{duration_str}")
204
210
  end
205
211
  when :upload_template
@@ -209,7 +215,7 @@ module Kdeploy
209
215
  next if shown[key]
210
216
 
211
217
  shown[key] = true
212
- duration_str = step[:duration] ? pastel.dim(" [#{'%.2f' % step[:duration]}s]") : ''
218
+ duration_str = step[:duration] ? pastel.dim(" [#{format('%.2f', step[:duration])}s]") : ''
213
219
  puts pastel.yellow(" [template] #{step[:command].sub('upload_template: ', '')}#{duration_str}")
214
220
  end
215
221
  when :run
@@ -219,7 +225,7 @@ module Kdeploy
219
225
  next if shown[key]
220
226
 
221
227
  shown[key] = true
222
- duration_str = step[:duration] ? pastel.dim(" [#{'%.2f' % step[:duration]}s]") : ''
228
+ duration_str = step[:duration] ? pastel.dim(" [#{format('%.2f', step[:duration])}s]") : ''
223
229
  puts pastel.cyan(" [run] #{step[:command].to_s.lines.first.strip}#{duration_str}")
224
230
  # 多行命令内容高亮
225
231
  cmd_lines = step[:command].to_s.lines[1..].map(&:strip).reject(&:empty?)
@@ -0,0 +1,88 @@
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)
7
+ @executor = executor
8
+ @output = output
9
+ end
10
+
11
+ def execute_run(command, host_name)
12
+ show_command_header(host_name, :run, command[:command])
13
+ result, duration = measure_time do
14
+ @executor.execute(command[:command])
15
+ end
16
+ show_command_output(result)
17
+ { command: command[:command], output: result, duration: duration, type: :run }
18
+ end
19
+
20
+ def execute_upload(command, host_name)
21
+ show_command_header(host_name, :upload, "#{command[:source]} -> #{command[:destination]}")
22
+ _result, duration = measure_time do
23
+ @executor.upload(command[:source], command[:destination])
24
+ end
25
+ {
26
+ command: "upload: #{command[:source]} -> #{command[:destination]}",
27
+ duration: duration,
28
+ type: :upload
29
+ }
30
+ end
31
+
32
+ def execute_upload_template(command, host_name)
33
+ show_command_header(host_name, :upload_template, "#{command[:source]} -> #{command[:destination]}")
34
+ _result, duration = measure_time do
35
+ @executor.upload_template(command[:source], command[:destination], command[:variables])
36
+ end
37
+ {
38
+ command: "upload_template: #{command[:source]} -> #{command[:destination]}",
39
+ duration: duration,
40
+ type: :upload_template
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ def measure_time
47
+ start_time = Time.now
48
+ result = yield
49
+ duration = Time.now - start_time
50
+ [result, duration]
51
+ end
52
+
53
+ def show_command_output(output)
54
+ return unless output.is_a?(Hash)
55
+
56
+ pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
57
+
58
+ if output[:stdout] && !output[:stdout].empty?
59
+ output[:stdout].each_line do |line|
60
+ @output.write_line(" #{line.rstrip}") unless line.strip.empty?
61
+ end
62
+ end
63
+
64
+ return unless output[:stderr] && !output[:stderr].empty?
65
+
66
+ output[:stderr].each_line do |line|
67
+ @output.write_line(pastel.green(" #{line.rstrip}")) unless line.strip.empty?
68
+ end
69
+ end
70
+
71
+ def show_command_header(host_name, type, description)
72
+ pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
73
+ @output.write_line(pastel.bright_white("\n#{host_name.ljust(24)} : "))
74
+
75
+ case type
76
+ when :run
77
+ @output.write_line(pastel.cyan(" [run] #{description.lines.first.strip}"))
78
+ description.lines[1..].each do |line|
79
+ @output.write_line(" > #{line.strip}") unless line.strip.empty?
80
+ end
81
+ when :upload
82
+ @output.write_line(pastel.green(" [upload] #{description}"))
83
+ when :upload_template
84
+ @output.write_line(pastel.yellow(" [template] #{description}"))
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kdeploy
4
+ # Groups commands by type and generates task descriptions
5
+ class CommandGrouper
6
+ def self.group(commands)
7
+ commands.group_by do |cmd|
8
+ group_key_for(cmd)
9
+ end
10
+ end
11
+
12
+ def self.group_key_for(cmd)
13
+ case cmd[:type]
14
+ when :upload, :upload_template
15
+ "#{cmd[:type]}_#{cmd[:source]}"
16
+ when :run
17
+ "#{cmd[:type]}_#{cmd[:command].to_s.lines.first.strip}"
18
+ else
19
+ cmd[:type].to_s
20
+ end
21
+ end
22
+
23
+ def self.task_description(command)
24
+ case command[:type]
25
+ when :upload
26
+ "upload #{command[:source]}"
27
+ when :upload_template
28
+ "template #{command[:source]}"
29
+ when :run
30
+ command[:command].to_s.lines.first.strip
31
+ else
32
+ command[:type].to_s
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kdeploy
4
+ # Configuration management for Kdeploy
5
+ class Configuration
6
+ DEFAULT_PARALLEL = 10
7
+ DEFAULT_SSH_TIMEOUT = 30
8
+ DEFAULT_VERIFY_HOST_KEY = :never
9
+
10
+ class << self
11
+ attr_accessor :default_parallel,
12
+ :default_ssh_timeout,
13
+ :default_verify_host_key
14
+
15
+ def reset
16
+ @default_parallel = DEFAULT_PARALLEL
17
+ @default_ssh_timeout = DEFAULT_SSH_TIMEOUT
18
+ @default_verify_host_key = DEFAULT_VERIFY_HOST_KEY
19
+ end
20
+ end
21
+
22
+ # Initialize with defaults
23
+ reset
24
+ end
25
+ end
data/lib/kdeploy/dsl.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  module Kdeploy
4
6
  module DSL
5
7
  def self.extended(base)
@@ -68,8 +70,8 @@ module Kdeploy
68
70
  }
69
71
  end
70
72
 
71
- def inventory(&)
72
- instance_eval(&) if block_given?
73
+ def inventory(&block)
74
+ instance_eval(&block) if block_given?
73
75
  end
74
76
 
75
77
  def get_task_hosts(task_name)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kdeploy
4
+ # Base error class for all Kdeploy errors
5
+ class Error < StandardError; end
6
+
7
+ # Raised when a task is not found
8
+ class TaskNotFoundError < Error
9
+ def initialize(task_name)
10
+ super("Task not found: #{task_name}")
11
+ end
12
+ end
13
+
14
+ # Raised when a host is not found
15
+ class HostNotFoundError < Error
16
+ def initialize(host_name)
17
+ super("Host not found: #{host_name}")
18
+ end
19
+ end
20
+
21
+ # Raised when SSH operation fails
22
+ class SSHError < Error
23
+ def initialize(message, original_error = nil)
24
+ super("SSH operation failed: #{message}")
25
+ @original_error = original_error
26
+ end
27
+
28
+ attr_reader :original_error
29
+ end
30
+
31
+ # Raised when SCP operation fails
32
+ class SCPError < Error
33
+ def initialize(message, original_error = nil)
34
+ super("SCP upload failed: #{message}")
35
+ @original_error = original_error
36
+ end
37
+
38
+ attr_reader :original_error
39
+ end
40
+
41
+ # Raised when template operation fails
42
+ class TemplateError < Error
43
+ def initialize(message, original_error = nil)
44
+ super("Template operation failed: #{message}")
45
+ @original_error = original_error
46
+ end
47
+
48
+ attr_reader :original_error
49
+ end
50
+
51
+ # Raised when configuration is invalid
52
+ class ConfigurationError < Error
53
+ def initialize(message)
54
+ super("Configuration error: #{message}")
55
+ end
56
+ end
57
+
58
+ # Raised when file operation fails
59
+ class FileNotFoundError < Error
60
+ def initialize(file_path)
61
+ super("File not found: #{file_path}")
62
+ end
63
+ end
64
+ end
@@ -4,6 +4,7 @@ require 'net/ssh'
4
4
  require 'net/scp'
5
5
 
6
6
  module Kdeploy
7
+ # SSH/SCP executor for remote command execution and file operations
7
8
  class Executor
8
9
  def initialize(host_config)
9
10
  @host = host_config[:name]
@@ -21,7 +22,7 @@ module Kdeploy
21
22
 
22
23
  ssh.open_channel do |channel|
23
24
  channel.exec(command) do |_ch, success|
24
- raise "Could not execute command: #{command}" unless success
25
+ raise SSHError, "Could not execute command: #{command}" unless success
25
26
 
26
27
  channel.on_data do |_ch, data|
27
28
  stdout << data
@@ -40,9 +41,9 @@ module Kdeploy
40
41
  }
41
42
  end
42
43
  rescue Net::SSH::AuthenticationFailed => e
43
- raise "SSH authentication failed: #{e.message}"
44
+ raise SSHError.new("SSH authentication failed: #{e.message}", e)
44
45
  rescue StandardError => e
45
- raise "SSH execution failed: #{e.message}"
46
+ raise SSHError.new("SSH execution failed: #{e.message}", e)
46
47
  end
47
48
 
48
49
  def upload(source, destination)
@@ -50,13 +51,13 @@ module Kdeploy
50
51
  scp.upload!(source, destination)
51
52
  end
52
53
  rescue StandardError => e
53
- raise "SCP upload failed: #{e.message}"
54
+ raise SCPError.new("SCP upload failed: #{e.message}", e)
54
55
  end
55
56
 
56
57
  def upload_template(source, destination, variables = {})
57
58
  Template.render_and_upload(self, source, destination, variables)
58
59
  rescue StandardError => e
59
- raise "Template upload failed: #{e.message}"
60
+ raise TemplateError.new("Template upload failed: #{e.message}", e)
60
61
  end
61
62
 
62
63
  private
@@ -3,6 +3,7 @@
3
3
  require 'fileutils'
4
4
 
5
5
  module Kdeploy
6
+ # Project initializer for creating new deployment projects
6
7
  class Initializer
7
8
  def initialize(target_dir = '.')
8
9
  @target_dir = File.expand_path(target_dir)
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+
5
+ module Kdeploy
6
+ # Abstract output interface
7
+ class Output
8
+ def write(message)
9
+ raise NotImplementedError, 'Subclasses must implement write'
10
+ end
11
+
12
+ def write_line(message)
13
+ write("#{message}\n")
14
+ end
15
+
16
+ def write_error(message)
17
+ write_line(message)
18
+ end
19
+ end
20
+
21
+ # Console output implementation
22
+ class ConsoleOutput < Output
23
+ def initialize
24
+ @pastel = Pastel.new
25
+ end
26
+
27
+ def write(message)
28
+ print(message)
29
+ end
30
+
31
+ def write_line(message)
32
+ puts(message)
33
+ end
34
+
35
+ def write_error(message)
36
+ puts(@pastel.red(message))
37
+ end
38
+
39
+ attr_reader :pastel
40
+ end
41
+
42
+ # Silent output for testing
43
+ class SilentOutput < Output
44
+ attr_reader :messages, :errors
45
+
46
+ def initialize
47
+ @messages = []
48
+ @errors = []
49
+ end
50
+
51
+ def write(message)
52
+ @messages << message
53
+ end
54
+
55
+ def write_line(message)
56
+ @messages << "#{message}\n"
57
+ end
58
+
59
+ def write_error(message)
60
+ @errors << message
61
+ @messages << "#{message}\n"
62
+ end
63
+
64
+ def clear
65
+ @messages.clear
66
+ @errors.clear
67
+ end
68
+ end
69
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kdeploy
4
+ # Post-installation configuration handler
4
5
  class PostInstall
5
6
  class << self
6
7
  def run
@@ -3,90 +3,24 @@
3
3
  require 'concurrent'
4
4
 
5
5
  module Kdeploy
6
+ # Concurrent task runner for executing tasks across multiple hosts
6
7
  class Runner
7
- def initialize(hosts, tasks, parallel: 10)
8
+ def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new)
8
9
  @hosts = hosts
9
10
  @tasks = tasks
10
11
  @parallel = parallel
12
+ @output = output
11
13
  @pool = Concurrent::FixedThreadPool.new(@parallel)
12
14
  @results = Concurrent::Hash.new
13
15
  end
14
16
 
15
17
  def run(task_name)
16
18
  task = @tasks[task_name]
17
- raise "Task not found: #{task_name}" unless task
19
+ raise TaskNotFoundError, task_name unless task
18
20
 
19
21
  futures = @hosts.map do |name, config|
20
22
  Concurrent::Future.execute(executor: @pool) do
21
- executor = Executor.new(config)
22
- result = { status: :success, output: [] }
23
-
24
- task[:block].call.group_by do |cmd|
25
- cmd[:type].to_s + (if cmd[:type] == :upload
26
- cmd[:source].to_s
27
- else
28
- cmd[:type] == :run ? cmd[:command].to_s.lines.first.strip : ''
29
- end)
30
- end.each_value do |commands|
31
- # 生成 TASK 横幅
32
- pastel = Pastel.new
33
- first_cmd = commands.first
34
- task_desc = case first_cmd[:type]
35
- when :upload
36
- "upload #{first_cmd[:source]}"
37
- when :upload_template
38
- "template #{first_cmd[:source]}"
39
- when :run
40
- first_cmd[:command].to_s.lines.first.strip
41
- else
42
- first_cmd[:type].to_s
43
- end
44
- puts pastel.cyan("\nTASK [#{task_desc}] " + ('*' * 64))
45
- commands.each do |command|
46
- case command[:type]
47
- when :run
48
- pastel = Pastel.new
49
- puts pastel.bright_white("\n#{name.ljust(24)} : ")
50
- puts pastel.cyan(" [run] #{command[:command].lines.first.strip}")
51
- command[:command].lines[1..].each { |line| puts " > #{line.strip}" unless line.strip.empty? }
52
- t1 = Time.now
53
- output = executor.execute(command[:command])
54
- t2 = Time.now
55
- duration = t2 - t1
56
- # 统一输出命令结果
57
- if output[:stdout] && !output[:stdout].empty?
58
- output[:stdout].each_line { |line| puts " #{line.rstrip}" unless line.strip.empty? }
59
- end
60
- if output[:stderr] && !output[:stderr].empty?
61
- output[:stderr].each_line { |line| puts pastel.green(" #{line.rstrip}") unless line.strip.empty? }
62
- end
63
- result[:output] << { command: command[:command], output: output, duration: duration, type: :run }
64
- when :upload
65
- pastel = Pastel.new
66
- puts pastel.bright_white("\n#{name.ljust(24)} : ")
67
- puts pastel.green(" [upload] #{command[:source]} -> #{command[:destination]}")
68
- t1 = Time.now
69
- executor.upload(command[:source], command[:destination])
70
- t2 = Time.now
71
- duration = t2 - t1
72
- result[:output] << { command: "upload: #{command[:source]} -> #{command[:destination]}", duration: duration, type: :upload }
73
- when :upload_template
74
- pastel = Pastel.new
75
- puts pastel.bright_white("\n#{name.ljust(24)} : ")
76
- puts pastel.yellow(" [template] #{command[:source]} -> #{command[:destination]}")
77
- t1 = Time.now
78
- executor.upload_template(command[:source], command[:destination], command[:vars])
79
- t2 = Time.now
80
- duration = t2 - t1
81
- result[:output] << { command: "upload_template: #{command[:source]} -> #{command[:destination]}", duration: duration,
82
- type: :upload_template }
83
- end
84
- end
85
- end
86
-
87
- @results[name] = result
88
- rescue StandardError => e
89
- @results[name] = { status: :failed, error: e.message }
23
+ execute_task_for_host(name, config, task)
90
24
  end
91
25
  end
92
26
 
@@ -95,5 +29,49 @@ module Kdeploy
95
29
  ensure
96
30
  @pool.shutdown
97
31
  end
32
+
33
+ private
34
+
35
+ def execute_task_for_host(name, config, task)
36
+ executor = Executor.new(config)
37
+ command_executor = CommandExecutor.new(executor, @output)
38
+ result = { status: :success, output: [] }
39
+
40
+ commands = task[:block].call
41
+ grouped_commands = CommandGrouper.group(commands)
42
+
43
+ grouped_commands.each_value do |command_group|
44
+ first_cmd = command_group.first
45
+ task_desc = CommandGrouper.task_description(first_cmd)
46
+ show_task_header(task_desc)
47
+
48
+ command_group.each do |command|
49
+ step_result = execute_command(command_executor, command, name)
50
+ result[:output] << step_result
51
+ end
52
+ end
53
+
54
+ @results[name] = result
55
+ rescue StandardError => e
56
+ @results[name] = { status: :failed, error: e.message }
57
+ end
58
+
59
+ def execute_command(command_executor, command, host_name)
60
+ case command[:type]
61
+ when :run
62
+ command_executor.execute_run(command, host_name)
63
+ when :upload
64
+ command_executor.execute_upload(command, host_name)
65
+ when :upload_template
66
+ command_executor.execute_upload_template(command, host_name)
67
+ else
68
+ raise ConfigurationError, "Unknown command type: #{command[:type]}"
69
+ end
70
+ end
71
+
72
+ def show_task_header(task_desc)
73
+ pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
74
+ @output.write_line(pastel.cyan("\nTASK [#{task_desc}] " + ('*' * 64)))
75
+ end
98
76
  end
99
77
  end
@@ -5,6 +5,7 @@ require 'ostruct'
5
5
  require 'tempfile'
6
6
 
7
7
  module Kdeploy
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kdeploy
4
- VERSION = '1.1.9'
4
+ VERSION = '1.2.1'
5
5
  end
data/lib/kdeploy.rb CHANGED
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'kdeploy/version'
4
+ require_relative 'kdeploy/errors'
5
+ require_relative 'kdeploy/configuration'
6
+ require_relative 'kdeploy/output'
4
7
  require_relative 'kdeploy/banner'
5
8
  require_relative 'kdeploy/dsl'
6
9
  require_relative 'kdeploy/executor'
10
+ require_relative 'kdeploy/command_grouper'
11
+ require_relative 'kdeploy/command_executor'
7
12
  require_relative 'kdeploy/runner'
8
13
  require_relative 'kdeploy/initializer'
9
14
  require_relative 'kdeploy/template'
10
15
  require_relative 'kdeploy/post_install'
11
16
  require_relative 'kdeploy/cli'
12
17
 
18
+ # Kdeploy - A lightweight agentless deployment automation tool
13
19
  module Kdeploy
14
- class Error < StandardError; end
15
20
  # Your code goes here...
16
21
  end