kdeploy 1.2.1 â 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.
- checksums.yaml +4 -4
- data/OPTIMIZATION_COMPLETE.md +211 -0
- data/REFACTORING_SUMMARY.md +174 -0
- data/lib/kdeploy/cli.rb +134 -190
- data/lib/kdeploy/command_executor.rb +28 -11
- data/lib/kdeploy/dsl.rb +40 -21
- data/lib/kdeploy/executor.rb +48 -25
- data/lib/kdeploy/help_formatter.rb +67 -0
- data/lib/kdeploy/output.rb +2 -0
- data/lib/kdeploy/output_formatter.rb +194 -0
- data/lib/kdeploy/runner.rb +36 -17
- data/lib/kdeploy/template.rb +10 -2
- data/lib/kdeploy/version.rb +1 -1
- data/lib/kdeploy.rb +2 -0
- metadata +5 -1
|
@@ -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
|
data/lib/kdeploy/output.rb
CHANGED
|
@@ -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
|
data/lib/kdeploy/runner.rb
CHANGED
|
@@ -15,19 +15,31 @@ module Kdeploy
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
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)
|
|
18
25
|
task = @tasks[task_name]
|
|
19
26
|
raise TaskNotFoundError, task_name unless task
|
|
20
27
|
|
|
21
|
-
|
|
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|
|
|
22
39
|
Concurrent::Future.execute(executor: @pool) do
|
|
23
40
|
execute_task_for_host(name, config, task)
|
|
24
41
|
end
|
|
25
42
|
end
|
|
26
|
-
|
|
27
|
-
futures.each(&:wait)
|
|
28
|
-
@results
|
|
29
|
-
ensure
|
|
30
|
-
@pool.shutdown
|
|
31
43
|
end
|
|
32
44
|
|
|
33
45
|
private
|
|
@@ -37,23 +49,30 @@ module Kdeploy
|
|
|
37
49
|
command_executor = CommandExecutor.new(executor, @output)
|
|
38
50
|
result = { status: :success, output: [] }
|
|
39
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)
|
|
40
59
|
commands = task[:block].call
|
|
41
60
|
grouped_commands = CommandGrouper.group(commands)
|
|
42
61
|
|
|
43
62
|
grouped_commands.each_value do |command_group|
|
|
44
|
-
|
|
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
|
|
63
|
+
execute_command_group(command_group, command_executor, name, result)
|
|
52
64
|
end
|
|
65
|
+
end
|
|
53
66
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
57
76
|
end
|
|
58
77
|
|
|
59
78
|
def execute_command(command_executor, command, host_name)
|
data/lib/kdeploy/template.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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
|
|
@@ -9,10 +8,19 @@ module Kdeploy
|
|
|
9
8
|
class Template
|
|
10
9
|
def self.render(template_path, variables = {})
|
|
11
10
|
template_content = File.read(template_path)
|
|
12
|
-
context =
|
|
11
|
+
context = create_template_context(variables)
|
|
13
12
|
ERB.new(template_content).result(context.instance_eval { binding })
|
|
14
13
|
end
|
|
15
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
|
+
|
|
16
24
|
def self.render_and_upload(executor, template_path, destination, variables = {})
|
|
17
25
|
rendered_content = render(template_path, variables)
|
|
18
26
|
|
data/lib/kdeploy/version.rb
CHANGED
data/lib/kdeploy.rb
CHANGED
|
@@ -9,6 +9,8 @@ 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'
|
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.
|
|
4
|
+
version: 1.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kk
|
|
@@ -132,7 +132,9 @@ extensions:
|
|
|
132
132
|
- ext/mkrf_conf.rb
|
|
133
133
|
extra_rdoc_files: []
|
|
134
134
|
files:
|
|
135
|
+
- OPTIMIZATION_COMPLETE.md
|
|
135
136
|
- README.md
|
|
137
|
+
- REFACTORING_SUMMARY.md
|
|
136
138
|
- exe/kdeploy
|
|
137
139
|
- ext/mkrf_conf.rb
|
|
138
140
|
- lib/kdeploy.rb
|
|
@@ -146,8 +148,10 @@ files:
|
|
|
146
148
|
- lib/kdeploy/dsl.rb
|
|
147
149
|
- lib/kdeploy/errors.rb
|
|
148
150
|
- lib/kdeploy/executor.rb
|
|
151
|
+
- lib/kdeploy/help_formatter.rb
|
|
149
152
|
- lib/kdeploy/initializer.rb
|
|
150
153
|
- lib/kdeploy/output.rb
|
|
154
|
+
- lib/kdeploy/output_formatter.rb
|
|
151
155
|
- lib/kdeploy/post_install.rb
|
|
152
156
|
- lib/kdeploy/runner.rb
|
|
153
157
|
- lib/kdeploy/template.rb
|