kdeploy 1.2.0 → 1.2.2

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.
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+
5
+ module Kdeploy
6
+ # Formats help text for CLI
7
+ class HelpFormatter
8
+ def initialize
9
+ @pastel = Pastel.new
10
+ end
11
+
12
+ def format_help
13
+ <<~HELP
14
+ #{@pastel.bright_white('📖 Available Commands:')}
15
+
16
+ #{format_commands}
17
+
18
+ #{format_examples}
19
+
20
+ #{format_documentation}
21
+ HELP
22
+ end
23
+
24
+ private
25
+
26
+ def format_commands
27
+ <<~COMMANDS
28
+ #{@pastel.bright_yellow('🚀')} #{@pastel.bright_white('execute TASK_FILE [TASK]')} Execute deployment tasks from file
29
+ #{@pastel.dim(' --limit HOSTS')} Limit to specific hosts (comma-separated)
30
+ #{@pastel.dim(' --parallel NUM')} Number of parallel executions (default: 5)
31
+ #{@pastel.dim(' --dry-run')} Show what would be done without executing
32
+
33
+ #{@pastel.bright_yellow('🆕')} #{@pastel.bright_white('init [DIR]')} Initialize new deployment project
34
+ #{@pastel.bright_yellow('â„šī¸')} #{@pastel.bright_white('version')} Show version information
35
+ #{@pastel.bright_yellow('❓')} #{@pastel.bright_white('help [COMMAND]')} Show help information
36
+ COMMANDS
37
+ end
38
+
39
+ def format_examples
40
+ <<~EXAMPLES
41
+ #{@pastel.bright_white('💡 Examples:')}
42
+
43
+ #{@pastel.dim('# Initialize a new project')}
44
+ #{@pastel.bright_cyan('kdeploy init my-deployment')}
45
+
46
+ #{@pastel.dim('# Deploy to web servers')}
47
+ #{@pastel.bright_cyan('kdeploy execute deploy.rb deploy_web')}
48
+
49
+ #{@pastel.dim('# Backup database')}
50
+ #{@pastel.bright_cyan('kdeploy execute deploy.rb backup_db')}
51
+
52
+ #{@pastel.dim('# Run maintenance on specific hosts')}
53
+ #{@pastel.bright_cyan('kdeploy execute deploy.rb maintenance --limit web01')}
54
+
55
+ #{@pastel.dim('# Preview deployment')}
56
+ #{@pastel.bright_cyan('kdeploy execute deploy.rb deploy_web --dry-run')}
57
+ EXAMPLES
58
+ end
59
+
60
+ def format_documentation
61
+ <<~DOCS
62
+ #{@pastel.bright_white('📚 Documentation:')}
63
+ #{@pastel.bright_cyan('https://github.com/kevin197011/kdeploy')}
64
+ DOCS
65
+ end
66
+ end
67
+ end
@@ -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)
@@ -21,6 +21,7 @@ module Kdeploy
21
21
  # Console output implementation
22
22
  class ConsoleOutput < Output
23
23
  def initialize
24
+ super
24
25
  @pastel = Pastel.new
25
26
  end
26
27
 
@@ -44,6 +45,7 @@ module Kdeploy
44
45
  attr_reader :messages, :errors
45
46
 
46
47
  def initialize
48
+ super
47
49
  @messages = []
48
50
  @errors = []
49
51
  end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+ require 'tty-box'
5
+
6
+ module Kdeploy
7
+ # Formats and displays execution results
8
+ class OutputFormatter
9
+ def initialize
10
+ @pastel = Pastel.new
11
+ end
12
+
13
+ def format_task_header(task_name)
14
+ @pastel.cyan("\nPLAY [#{task_name}] " + ('*' * 64))
15
+ end
16
+
17
+ def format_host_status(host, status)
18
+ status_str = case status
19
+ when :success then @pastel.green('ok')
20
+ when :changed then @pastel.yellow('changed')
21
+ else @pastel.red('failed')
22
+ end
23
+ @pastel.bright_white("\n#{host.ljust(24)} : #{status_str}")
24
+ end
25
+
26
+ def format_upload_steps(steps, shown)
27
+ format_file_steps(steps, shown, :upload, @pastel.green(' === Upload ==='), 'upload: ')
28
+ end
29
+
30
+ def format_template_steps(steps, shown)
31
+ format_file_steps(steps, shown, :upload_template, @pastel.yellow(' === Template ==='), 'upload_template: ')
32
+ end
33
+
34
+ def format_file_steps(steps, shown, type, header, prefix)
35
+ output = [header]
36
+ steps.each do |step|
37
+ next if step_already_shown?(step, type, shown)
38
+
39
+ mark_step_as_shown(step, type, shown)
40
+ output << format_file_step(step, type, prefix)
41
+ end
42
+ output
43
+ end
44
+
45
+ def format_file_step(step, type, prefix)
46
+ duration_str = format_duration(step[:duration])
47
+ type == :upload ? @pastel.green : @pastel.yellow
48
+ label = type == :upload ? '[upload]' : '[template]'
49
+ color(" #{label} #{step[:command].sub(prefix, '')}#{duration_str}")
50
+ end
51
+
52
+ def format_run_steps(steps, shown)
53
+ output = []
54
+ output << @pastel.cyan(' === Run ===')
55
+ steps.each do |step|
56
+ next if step_already_shown?(step, :run, shown)
57
+
58
+ mark_step_as_shown(step, :run, shown)
59
+ output.concat(format_single_run_step(step))
60
+ end
61
+ output
62
+ end
63
+
64
+ def format_single_run_step(step)
65
+ output = []
66
+ duration_str = format_duration(step[:duration])
67
+ command_line = step[:command].to_s.lines.first.strip
68
+ output << @pastel.cyan(" [run] #{command_line}#{duration_str}")
69
+ output.concat(format_multiline_command(step[:command]))
70
+ output.concat(format_command_output(step[:output]))
71
+ output
72
+ end
73
+
74
+ def format_error(error_message)
75
+ @pastel.red(" ERROR: #{error_message}")
76
+ end
77
+
78
+ def format_summary_header
79
+ @pastel.cyan("\nPLAY RECAP #{'*' * 64}")
80
+ end
81
+
82
+ def format_summary_line(host, result, max_host_len)
83
+ counts = calculate_summary_counts(result)
84
+ line = build_summary_line(host, counts, max_host_len)
85
+ colorize_summary_line(line, counts)
86
+ end
87
+
88
+ def calculate_summary_counts(result)
89
+ ok = %i[success changed].include?(result[:status]) ? result[:output].size : 0
90
+ failed = result[:status] == :failed ? 1 : 0
91
+ changed = result[:status] == :changed ? result[:output].size : 0
92
+ { ok: ok, failed: failed, changed: changed }
93
+ end
94
+
95
+ def build_summary_line(host, counts, max_host_len)
96
+ ok_w = 7
97
+ changed_w = 11
98
+ failed_w = 10
99
+
100
+ ok_str = @pastel.green("ok=#{counts[:ok].to_s.ljust(ok_w - 3)}")
101
+ changed_str = @pastel.yellow("changed=#{counts[:changed].to_s.ljust(changed_w - 8)}")
102
+ failed_str = @pastel.red("failed=#{counts[:failed].to_s.ljust(failed_w - 7)}")
103
+ "#{host.ljust(max_host_len)} : #{ok_str} #{changed_str} #{failed_str}"
104
+ end
105
+
106
+ def colorize_summary_line(line, counts)
107
+ if counts[:failed].positive?
108
+ @pastel.red(line)
109
+ elsif counts[:ok].positive? && counts[:failed].zero?
110
+ @pastel.green(line)
111
+ else
112
+ line
113
+ end
114
+ end
115
+
116
+ def format_dry_run_box(title, content)
117
+ TTY::Box.frame(
118
+ content,
119
+ title: { top_left: " #{title} " },
120
+ style: {
121
+ border: {
122
+ fg: :yellow
123
+ }
124
+ }
125
+ )
126
+ end
127
+
128
+ def format_dry_run_header
129
+ TTY::Box.frame(
130
+ 'Showing what would be done without executing any commands',
131
+ title: { top_left: ' Dry Run Mode ', bottom_right: ' Kdeploy ' },
132
+ style: {
133
+ border: {
134
+ fg: :blue
135
+ }
136
+ }
137
+ )
138
+ end
139
+
140
+ def format_command_for_dry_run(command)
141
+ case command[:type]
142
+ when :run
143
+ "#{@pastel.green('>')} #{command[:command]}"
144
+ when :upload
145
+ "#{@pastel.blue('>')} Upload: #{command[:source]} -> #{command[:destination]}"
146
+ when :upload_template
147
+ "#{@pastel.blue('>')} Template: #{command[:source]} -> #{command[:destination]}"
148
+ else
149
+ "#{@pastel.blue('>')} #{command[:type]}: #{command}"
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ def format_duration(duration)
156
+ duration ? @pastel.dim(" [#{format('%.2f', duration)}s]") : ''
157
+ end
158
+
159
+ def format_multiline_command(command)
160
+ output = []
161
+ cmd_lines = command.to_s.lines[1..].map(&:strip).reject(&:empty?)
162
+ cmd_lines.each { |line| output << @pastel.cyan(" > #{line}") } if cmd_lines.any?
163
+ output
164
+ end
165
+
166
+ def format_command_output(output)
167
+ result = []
168
+ return result unless output
169
+
170
+ if output.is_a?(Hash) && output[:stdout]
171
+ format_stdout_lines(output[:stdout], result)
172
+ elsif output.is_a?(String)
173
+ format_stdout_lines(output, result)
174
+ end
175
+ result
176
+ end
177
+
178
+ def format_stdout_lines(stdout, result)
179
+ stdout.each_line do |line|
180
+ result << @pastel.green(" #{line.rstrip}") unless line.strip.empty?
181
+ end
182
+ end
183
+
184
+ def step_already_shown?(step, type, shown)
185
+ key = [step[:command], type].hash
186
+ shown[key]
187
+ end
188
+
189
+ def mark_step_as_shown(step, type, shown)
190
+ key = [step[:command], type].hash
191
+ shown[key] = true
192
+ end
193
+ end
194
+ 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,6 +3,7 @@
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
8
  def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new)
8
9
  @hosts = hosts
@@ -14,19 +15,31 @@ module Kdeploy
14
15
  end
15
16
 
16
17
  def run(task_name)
18
+ task = find_task(task_name)
19
+ execute_concurrent_tasks(task)
20
+ ensure
21
+ @pool.shutdown
22
+ end
23
+
24
+ def find_task(task_name)
17
25
  task = @tasks[task_name]
18
26
  raise TaskNotFoundError, task_name unless task
19
27
 
20
- futures = @hosts.map do |name, config|
28
+ task
29
+ end
30
+
31
+ def execute_concurrent_tasks(task)
32
+ futures = create_task_futures(task)
33
+ futures.each(&:wait)
34
+ @results
35
+ end
36
+
37
+ def create_task_futures(task)
38
+ @hosts.map do |name, config|
21
39
  Concurrent::Future.execute(executor: @pool) do
22
40
  execute_task_for_host(name, config, task)
23
41
  end
24
42
  end
25
-
26
- futures.each(&:wait)
27
- @results
28
- ensure
29
- @pool.shutdown
30
43
  end
31
44
 
32
45
  private
@@ -36,23 +49,30 @@ module Kdeploy
36
49
  command_executor = CommandExecutor.new(executor, @output)
37
50
  result = { status: :success, output: [] }
38
51
 
52
+ execute_grouped_commands(task, command_executor, name, result)
53
+ @results[name] = result
54
+ rescue StandardError => e
55
+ @results[name] = { status: :failed, error: e.message }
56
+ end
57
+
58
+ def execute_grouped_commands(task, command_executor, name, result)
39
59
  commands = task[:block].call
40
60
  grouped_commands = CommandGrouper.group(commands)
41
61
 
42
62
  grouped_commands.each_value do |command_group|
43
- first_cmd = command_group.first
44
- task_desc = CommandGrouper.task_description(first_cmd)
45
- show_task_header(task_desc)
46
-
47
- command_group.each do |command|
48
- step_result = execute_command(command_executor, command, name)
49
- result[:output] << step_result
50
- end
63
+ execute_command_group(command_group, command_executor, name, result)
51
64
  end
65
+ end
52
66
 
53
- @results[name] = result
54
- rescue StandardError => e
55
- @results[name] = { status: :failed, error: e.message }
67
+ def execute_command_group(command_group, command_executor, name, result)
68
+ first_cmd = command_group.first
69
+ task_desc = CommandGrouper.task_description(first_cmd)
70
+ show_task_header(task_desc)
71
+
72
+ command_group.each do |command|
73
+ step_result = execute_command(command_executor, command, name)
74
+ result[:output] << step_result
75
+ end
56
76
  end
57
77
 
58
78
  def execute_command(command_executor, command, host_name)
@@ -1,17 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'erb'
4
- require 'ostruct'
5
4
  require 'tempfile'
6
5
 
7
6
  module Kdeploy
7
+ # ERB template rendering and upload handler
8
8
  class Template
9
9
  def self.render(template_path, variables = {})
10
10
  template_content = File.read(template_path)
11
- context = OpenStruct.new(variables)
11
+ context = create_template_context(variables)
12
12
  ERB.new(template_content).result(context.instance_eval { binding })
13
13
  end
14
14
 
15
+ def self.create_template_context(variables)
16
+ # Use a simple class instead of OpenStruct for better performance
17
+ context_class = Class.new
18
+ variables.each do |key, value|
19
+ context_class.define_method(key) { value }
20
+ end
21
+ context_class.new
22
+ end
23
+
15
24
  def self.render_and_upload(executor, template_path, destination, variables = {})
16
25
  rendered_content = render(template_path, variables)
17
26
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kdeploy
4
- VERSION = '1.2.0'
4
+ VERSION = '1.2.2'
5
5
  end
data/lib/kdeploy.rb CHANGED
@@ -9,12 +9,15 @@ require_relative 'kdeploy/dsl'
9
9
  require_relative 'kdeploy/executor'
10
10
  require_relative 'kdeploy/command_grouper'
11
11
  require_relative 'kdeploy/command_executor'
12
+ require_relative 'kdeploy/output_formatter'
13
+ require_relative 'kdeploy/help_formatter'
12
14
  require_relative 'kdeploy/runner'
13
15
  require_relative 'kdeploy/initializer'
14
16
  require_relative 'kdeploy/template'
15
17
  require_relative 'kdeploy/post_install'
16
18
  require_relative 'kdeploy/cli'
17
19
 
20
+ # Kdeploy - A lightweight agentless deployment automation tool
18
21
  module Kdeploy
19
22
  # Your code goes here...
20
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kdeploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kk
@@ -122,48 +122,6 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0.12'
125
- - !ruby/object:Gem::Dependency
126
- name: rake
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - "~>"
130
- - !ruby/object:Gem::Version
131
- version: '13.0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - "~>"
137
- - !ruby/object:Gem::Version
138
- version: '13.0'
139
- - !ruby/object:Gem::Dependency
140
- name: rspec
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - "~>"
144
- - !ruby/object:Gem::Version
145
- version: '3.0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - "~>"
151
- - !ruby/object:Gem::Version
152
- version: '3.0'
153
- - !ruby/object:Gem::Dependency
154
- name: rubocop
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - "~>"
158
- - !ruby/object:Gem::Version
159
- version: '1.21'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - "~>"
165
- - !ruby/object:Gem::Version
166
- version: '1.21'
167
125
  description: Kdeploy is a Ruby-based deployment automation tool that provides agentless
168
126
  remote deployment solutions with an elegant DSL
169
127
  email:
@@ -174,7 +132,9 @@ extensions:
174
132
  - ext/mkrf_conf.rb
175
133
  extra_rdoc_files: []
176
134
  files:
135
+ - OPTIMIZATION_COMPLETE.md
177
136
  - README.md
137
+ - REFACTORING_SUMMARY.md
178
138
  - exe/kdeploy
179
139
  - ext/mkrf_conf.rb
180
140
  - lib/kdeploy.rb
@@ -188,8 +148,10 @@ files:
188
148
  - lib/kdeploy/dsl.rb
189
149
  - lib/kdeploy/errors.rb
190
150
  - lib/kdeploy/executor.rb
151
+ - lib/kdeploy/help_formatter.rb
191
152
  - lib/kdeploy/initializer.rb
192
153
  - lib/kdeploy/output.rb
154
+ - lib/kdeploy/output_formatter.rb
193
155
  - lib/kdeploy/post_install.rb
194
156
  - lib/kdeploy/runner.rb
195
157
  - lib/kdeploy/template.rb