kdeploy 0.1.0
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 +7 -0
- data/.editorconfig +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +100 -0
- data/LICENSE +21 -0
- data/README.md +1030 -0
- data/Rakefile +45 -0
- data/bin/kdeploy +7 -0
- data/kdeploy.gemspec +49 -0
- data/lib/kdeploy/banner.rb +28 -0
- data/lib/kdeploy/cli.rb +1452 -0
- data/lib/kdeploy/command.rb +182 -0
- data/lib/kdeploy/configuration.rb +83 -0
- data/lib/kdeploy/dsl.rb +566 -0
- data/lib/kdeploy/host.rb +85 -0
- data/lib/kdeploy/inventory.rb +243 -0
- data/lib/kdeploy/logger.rb +100 -0
- data/lib/kdeploy/pipeline.rb +249 -0
- data/lib/kdeploy/runner.rb +190 -0
- data/lib/kdeploy/ssh_connection.rb +187 -0
- data/lib/kdeploy/statistics.rb +439 -0
- data/lib/kdeploy/task.rb +240 -0
- data/lib/kdeploy/template.rb +173 -0
- data/lib/kdeploy/version.rb +6 -0
- data/lib/kdeploy.rb +106 -0
- data/scripts/common_tasks.rb +218 -0
- data/scripts/deploy.rb +50 -0
- metadata +178 -0
data/lib/kdeploy/cli.rb
ADDED
@@ -0,0 +1,1452 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add String truncate method if not available
|
4
|
+
class String
|
5
|
+
def truncate(length)
|
6
|
+
return self if size <= length
|
7
|
+
|
8
|
+
"#{self[0, length - 3]}..."
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Kdeploy
|
13
|
+
# Command Line Interface for kdeploy
|
14
|
+
class CLI < Thor
|
15
|
+
# Fix Thor deprecation warning
|
16
|
+
def self.exit_on_failure?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
# Common options for commands that execute scripts
|
21
|
+
SCRIPT_OPTIONS = {
|
22
|
+
config: { aliases: '-c', desc: 'Configuration file path' },
|
23
|
+
inventory: { aliases: '-i', desc: 'Inventory file path' },
|
24
|
+
dry_run: { aliases: '-d', type: :boolean, desc: 'Perform dry run without executing' },
|
25
|
+
verbose: { aliases: '-v', type: :boolean, desc: 'Enable verbose output' },
|
26
|
+
log_file: { aliases: '-l', desc: 'Log file path' }
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
desc 'execute SCRIPT', 'Execute deployment script'
|
30
|
+
SCRIPT_OPTIONS.each { |name, opts| option(name, opts) }
|
31
|
+
def execute(script_file)
|
32
|
+
setup_configuration
|
33
|
+
setup_logging
|
34
|
+
|
35
|
+
validate_script_file(script_file)
|
36
|
+
|
37
|
+
begin
|
38
|
+
options[:dry_run] ? perform_dry_run(script_file) : execute_script(script_file)
|
39
|
+
rescue Kdeploy::Error => e
|
40
|
+
handle_deployment_error(e)
|
41
|
+
rescue StandardError => e
|
42
|
+
handle_unexpected_error(e)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
desc 'deploy SCRIPT', 'Execute deployment script (alias for execute)'
|
47
|
+
SCRIPT_OPTIONS.each { |name, opts| option(name, opts) }
|
48
|
+
def deploy(script_file)
|
49
|
+
execute(script_file)
|
50
|
+
end
|
51
|
+
|
52
|
+
desc 'init [PROJECT_NAME]', 'Initialize new deployment project'
|
53
|
+
option :name, aliases: '-n', desc: 'Specify project name'
|
54
|
+
def init(project_name = nil)
|
55
|
+
show_kdeploy_banner
|
56
|
+
project_name = determine_project_name(project_name)
|
57
|
+
|
58
|
+
display_init_header(project_name)
|
59
|
+
create_project_structure(project_name)
|
60
|
+
display_init_success(project_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'validate SCRIPT', 'Validate deployment script'
|
64
|
+
option :config, aliases: '-c', desc: 'Configuration file path'
|
65
|
+
option :inventory, aliases: '-i', desc: 'Inventory file path'
|
66
|
+
def validate(script_file)
|
67
|
+
show_kdeploy_banner
|
68
|
+
setup_configuration
|
69
|
+
|
70
|
+
display_validation_header(script_file)
|
71
|
+
validate_script_file(script_file)
|
72
|
+
|
73
|
+
begin
|
74
|
+
pipeline = Kdeploy.load_script(script_file)
|
75
|
+
validation_errors = pipeline.validate
|
76
|
+
|
77
|
+
if validation_errors.empty?
|
78
|
+
display_validation_success(pipeline)
|
79
|
+
else
|
80
|
+
display_validation_errors(validation_errors)
|
81
|
+
end
|
82
|
+
rescue Kdeploy::Error => e
|
83
|
+
display_validation_failure(e)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
desc 'config', 'Show configuration'
|
88
|
+
option :config, aliases: '-c', desc: 'Configuration file path'
|
89
|
+
option :inventory, aliases: '-i', desc: 'Inventory file path'
|
90
|
+
option :verbose, aliases: '-v', type: :boolean, desc: 'Enable verbose output'
|
91
|
+
option :log_file, aliases: '-l', desc: 'Log file path'
|
92
|
+
def config
|
93
|
+
show_kdeploy_banner
|
94
|
+
setup_configuration
|
95
|
+
|
96
|
+
display_configuration(Kdeploy.configuration)
|
97
|
+
end
|
98
|
+
|
99
|
+
desc 'version', 'Show version'
|
100
|
+
def version
|
101
|
+
show_kdeploy_banner
|
102
|
+
display_version_info
|
103
|
+
end
|
104
|
+
|
105
|
+
desc 'help [COMMAND]', 'Describe available commands or one specific command'
|
106
|
+
def help(command = nil)
|
107
|
+
show_kdeploy_banner
|
108
|
+
puts ''
|
109
|
+
puts 'š Available Commands:'.colorize(:yellow)
|
110
|
+
puts ''
|
111
|
+
|
112
|
+
if command
|
113
|
+
super
|
114
|
+
else
|
115
|
+
display_available_commands
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
desc 'stats [COMMAND]', 'Show deployment statistics'
|
120
|
+
option :days, aliases: '-d', type: :numeric, default: 30, desc: 'Number of days to analyze'
|
121
|
+
option :format, aliases: '-f', type: :string, default: 'text', desc: 'Output format (text, json, csv)'
|
122
|
+
option :output, aliases: '-o', type: :string, desc: 'Output file path'
|
123
|
+
def stats(command = 'summary')
|
124
|
+
case command.downcase
|
125
|
+
when 'summary'
|
126
|
+
show_summary_stats
|
127
|
+
when 'deployments'
|
128
|
+
show_deployment_stats
|
129
|
+
when 'tasks'
|
130
|
+
show_task_stats
|
131
|
+
when 'failures'
|
132
|
+
show_failure_stats
|
133
|
+
when 'trends'
|
134
|
+
show_trend_stats
|
135
|
+
when 'global'
|
136
|
+
show_global_stats
|
137
|
+
when 'clear'
|
138
|
+
clear_statistics
|
139
|
+
when 'export'
|
140
|
+
export_statistics
|
141
|
+
else
|
142
|
+
error "Unknown stats command: #{command}"
|
143
|
+
puts ''
|
144
|
+
puts 'Available commands: summary, deployments, tasks, failures, trends, global, clear, export'
|
145
|
+
exit 1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def display_configuration(config)
|
152
|
+
puts ''
|
153
|
+
puts 'āļø Current Configuration'.colorize(:yellow)
|
154
|
+
puts '=' * 50
|
155
|
+
|
156
|
+
display_execution_settings(config)
|
157
|
+
display_network_settings(config)
|
158
|
+
display_file_settings(config)
|
159
|
+
display_logging_settings(config)
|
160
|
+
display_ssh_options(config) if config.respond_to?(:ssh_options) && config.ssh_options&.any?
|
161
|
+
|
162
|
+
puts ''
|
163
|
+
puts 'š” Use --config FILE to load custom configuration'.colorize(:yellow)
|
164
|
+
puts ''
|
165
|
+
end
|
166
|
+
|
167
|
+
def display_execution_settings(config)
|
168
|
+
puts ''
|
169
|
+
puts 'š§ Execution Settings'.colorize(:cyan)
|
170
|
+
puts " Max Concurrent Tasks: #{config.max_concurrent_tasks}".colorize(:light_blue)
|
171
|
+
puts " Retry Count: #{config.retry_count}".colorize(:light_blue)
|
172
|
+
puts " Retry Delay: #{config.retry_delay}s".colorize(:light_blue)
|
173
|
+
end
|
174
|
+
|
175
|
+
def display_network_settings(config)
|
176
|
+
puts ''
|
177
|
+
puts 'š Network & SSH Settings'.colorize(:cyan)
|
178
|
+
puts " SSH Timeout: #{config.ssh_timeout}s".colorize(:light_blue)
|
179
|
+
puts " Command Timeout: #{config.command_timeout}s".colorize(:light_blue)
|
180
|
+
puts " Default User: #{config.default_user}".colorize(:light_blue)
|
181
|
+
puts " Default Port: #{config.default_port}".colorize(:light_blue)
|
182
|
+
end
|
183
|
+
|
184
|
+
def display_file_settings(config)
|
185
|
+
puts ''
|
186
|
+
puts 'š File & Directory Settings'.colorize(:cyan)
|
187
|
+
puts " Inventory File: #{config.inventory_file || 'not specified'}".colorize(:light_blue)
|
188
|
+
puts " Template Directory: #{config.template_dir}".colorize(:light_blue)
|
189
|
+
end
|
190
|
+
|
191
|
+
def display_logging_settings(config)
|
192
|
+
puts ''
|
193
|
+
puts 'š Logging Settings'.colorize(:cyan)
|
194
|
+
puts " Log Level: #{config.log_level}".colorize(:light_blue)
|
195
|
+
puts " Log File: #{config.log_file || 'stdout'}".colorize(:light_blue)
|
196
|
+
end
|
197
|
+
|
198
|
+
def display_ssh_options(config)
|
199
|
+
puts ''
|
200
|
+
puts 'š SSH Options'.colorize(:cyan)
|
201
|
+
config.ssh_options.each do |key, value|
|
202
|
+
puts " #{key.to_s.capitalize.gsub('_', ' ')}: #{value}".colorize(:light_blue)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def display_version_info
|
207
|
+
puts ''
|
208
|
+
puts "š Version: #{Kdeploy::VERSION}".colorize(:green)
|
209
|
+
puts 'š
Released: 2025'.colorize(:light_blue)
|
210
|
+
puts 'š Homepage: https://github.com/kevin197011/kdeploy'.colorize(:light_blue)
|
211
|
+
puts 'š Documentation: https://github.com/kevin197011/kdeploy/wiki'.colorize(:light_blue)
|
212
|
+
puts ''
|
213
|
+
end
|
214
|
+
|
215
|
+
def display_available_commands
|
216
|
+
puts " š #{'deploy SCRIPT'.ljust(25)} Execute deployment script"
|
217
|
+
puts " ā” #{'execute SCRIPT'.ljust(25)} Execute deployment script (alias for deploy)"
|
218
|
+
puts " š #{'init [PROJECT_NAME]'.ljust(25)} Initialize new deployment project"
|
219
|
+
puts " ā
#{'validate SCRIPT'.ljust(25)} Validate deployment script"
|
220
|
+
puts " āļø #{'config'.ljust(25)} Show configuration"
|
221
|
+
puts " š #{'stats [COMMAND]'.ljust(25)} Show deployment statistics"
|
222
|
+
puts " š #{'version'.ljust(25)} Show version"
|
223
|
+
puts ''
|
224
|
+
puts 'š” For more details on a command:'.colorize(:yellow)
|
225
|
+
puts ' kdeploy help COMMAND'.colorize(:light_blue)
|
226
|
+
puts ''
|
227
|
+
end
|
228
|
+
|
229
|
+
def setup_configuration
|
230
|
+
Kdeploy.configure do |config|
|
231
|
+
config.config_file = options[:config] if options[:config]
|
232
|
+
config.inventory_file = options[:inventory] if options[:inventory]
|
233
|
+
config.log_level = options[:verbose] ? :debug : :info
|
234
|
+
config.log_file = options[:log_file] if options[:log_file]
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def setup_logging
|
239
|
+
config = Kdeploy.configuration
|
240
|
+
KdeployLogger.setup(
|
241
|
+
level: config.log_level,
|
242
|
+
log_file: config.log_file
|
243
|
+
)
|
244
|
+
end
|
245
|
+
|
246
|
+
def perform_dry_run(script_file)
|
247
|
+
info 'š Performing dry run...'
|
248
|
+
pipeline = Kdeploy.load_script(script_file)
|
249
|
+
|
250
|
+
puts ''
|
251
|
+
puts 'š Pipeline Summary:'.colorize(:cyan)
|
252
|
+
puts " Name: #{pipeline.name}".colorize(:light_blue)
|
253
|
+
puts " Hosts: #{pipeline.hosts.size}".colorize(:light_blue)
|
254
|
+
puts " Tasks: #{pipeline.tasks.size}".colorize(:light_blue)
|
255
|
+
|
256
|
+
display_target_hosts(pipeline.hosts) if pipeline.hosts.any?
|
257
|
+
display_tasks(pipeline.tasks) if pipeline.tasks.any?
|
258
|
+
|
259
|
+
success 'ā
Dry run completed'
|
260
|
+
end
|
261
|
+
|
262
|
+
def execute_script(script_file)
|
263
|
+
show_kdeploy_banner
|
264
|
+
info "š Executing deployment script: #{script_file}"
|
265
|
+
|
266
|
+
pipeline = Kdeploy.load_script(script_file)
|
267
|
+
result = pipeline.execute
|
268
|
+
|
269
|
+
if result[:success]
|
270
|
+
success 'ā
Deployment completed successfully'
|
271
|
+
else
|
272
|
+
error 'ā Deployment failed'
|
273
|
+
exit 1
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def show_kdeploy_banner
|
278
|
+
Banner.show
|
279
|
+
end
|
280
|
+
|
281
|
+
def info(message)
|
282
|
+
puts message.colorize(:blue)
|
283
|
+
end
|
284
|
+
|
285
|
+
def success(message)
|
286
|
+
puts message.colorize(:green)
|
287
|
+
end
|
288
|
+
|
289
|
+
def error(message)
|
290
|
+
puts message.colorize(:red)
|
291
|
+
end
|
292
|
+
|
293
|
+
def create_project_structure(project_name)
|
294
|
+
create_project_directories(project_name)
|
295
|
+
create_sample_files(project_name)
|
296
|
+
create_sample_scripts(project_name)
|
297
|
+
create_sample_templates(project_name)
|
298
|
+
end
|
299
|
+
|
300
|
+
def create_project_directories(project_name)
|
301
|
+
dirs = [
|
302
|
+
project_name,
|
303
|
+
"#{project_name}/config",
|
304
|
+
"#{project_name}/scripts",
|
305
|
+
"#{project_name}/templates"
|
306
|
+
]
|
307
|
+
|
308
|
+
dirs.each do |dir|
|
309
|
+
FileUtils.mkdir_p(dir)
|
310
|
+
info "Created directory: #{dir}"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def create_sample_files(project_name)
|
315
|
+
create_deploy_script(project_name)
|
316
|
+
create_inventory_file(project_name)
|
317
|
+
create_config_file(project_name)
|
318
|
+
end
|
319
|
+
|
320
|
+
def create_deploy_script(project_name)
|
321
|
+
content = <<~RUBY
|
322
|
+
# frozen_string_literal: true
|
323
|
+
|
324
|
+
# Main deployment script
|
325
|
+
pipeline 'main' do
|
326
|
+
# Define target hosts
|
327
|
+
host 'app1.example.com', roles: [:app, :web]
|
328
|
+
host 'app2.example.com', roles: [:app, :web]
|
329
|
+
host 'db.example.com', roles: [:db]
|
330
|
+
|
331
|
+
# Set global variables
|
332
|
+
set :app_name, 'my_app'
|
333
|
+
set :deploy_to, '/var/www/${app_name}'
|
334
|
+
set :keep_releases, 5
|
335
|
+
|
336
|
+
# Define tasks
|
337
|
+
task :check_requirements do
|
338
|
+
run 'ruby -v'
|
339
|
+
run 'node -v'
|
340
|
+
run 'git --version'
|
341
|
+
end
|
342
|
+
|
343
|
+
task :setup_directories do
|
344
|
+
run "mkdir -p ${deploy_to}"
|
345
|
+
run "mkdir -p ${deploy_to}/releases"
|
346
|
+
run "mkdir -p ${deploy_to}/shared"
|
347
|
+
end
|
348
|
+
|
349
|
+
task :deploy do
|
350
|
+
depends_on :check_requirements, :setup_directories
|
351
|
+
|
352
|
+
run 'git clone https://github.com/user/repo.git ${deploy_to}/releases/$(date +%Y%m%d%H%M%S)'
|
353
|
+
run 'ln -sfn ${deploy_to}/releases/$(ls -t ${deploy_to}/releases | head -n1) ${deploy_to}/current'
|
354
|
+
end
|
355
|
+
|
356
|
+
task :restart_services do
|
357
|
+
run 'sudo systemctl restart nginx'
|
358
|
+
run 'sudo systemctl restart app'
|
359
|
+
end
|
360
|
+
|
361
|
+
task :cleanup do
|
362
|
+
run "cd ${deploy_to}/releases && ls -t | tail -n +${keep_releases + 1} | xargs rm -rf"
|
363
|
+
end
|
364
|
+
end
|
365
|
+
RUBY
|
366
|
+
|
367
|
+
write_file("#{project_name}/deploy.rb", content)
|
368
|
+
info "Created deployment script: #{project_name}/deploy.rb"
|
369
|
+
end
|
370
|
+
|
371
|
+
def create_inventory_file(project_name)
|
372
|
+
content = <<~YAML
|
373
|
+
# Server inventory configuration
|
374
|
+
groups:
|
375
|
+
production:
|
376
|
+
hosts:
|
377
|
+
- hostname: app1.example.com
|
378
|
+
roles: [app, web]
|
379
|
+
user: deploy
|
380
|
+
port: 22
|
381
|
+
- hostname: app2.example.com
|
382
|
+
roles: [app, web]
|
383
|
+
user: deploy
|
384
|
+
port: 22
|
385
|
+
- hostname: db.example.com
|
386
|
+
roles: [db]
|
387
|
+
user: deploy
|
388
|
+
port: 22
|
389
|
+
vars:
|
390
|
+
db_name: production_db
|
391
|
+
db_user: app_user
|
392
|
+
|
393
|
+
staging:
|
394
|
+
hosts:
|
395
|
+
- hostname: staging.example.com
|
396
|
+
roles: [app, web, db]
|
397
|
+
user: deploy
|
398
|
+
port: 22
|
399
|
+
vars:
|
400
|
+
rails_env: staging
|
401
|
+
node_env: staging
|
402
|
+
YAML
|
403
|
+
|
404
|
+
write_file("#{project_name}/inventory.yml", content)
|
405
|
+
info "Created inventory file: #{project_name}/inventory.yml"
|
406
|
+
end
|
407
|
+
|
408
|
+
def create_config_file(project_name)
|
409
|
+
content = <<~YAML
|
410
|
+
# Deployment configuration
|
411
|
+
max_concurrent_tasks: 5
|
412
|
+
retry_count: 3
|
413
|
+
retry_delay: 5
|
414
|
+
ssh_timeout: 30
|
415
|
+
command_timeout: 300
|
416
|
+
|
417
|
+
default_user: deploy
|
418
|
+
default_port: 22
|
419
|
+
|
420
|
+
template_dir: templates
|
421
|
+
inventory_file: inventory.yml
|
422
|
+
|
423
|
+
ssh_options:
|
424
|
+
forward_agent: true
|
425
|
+
verify_host_key: true
|
426
|
+
keepalive: true
|
427
|
+
keepalive_interval: 30
|
428
|
+
|
429
|
+
logging:
|
430
|
+
level: info
|
431
|
+
file: kdeploy.log
|
432
|
+
YAML
|
433
|
+
|
434
|
+
write_file("#{project_name}/config/kdeploy.yml", content)
|
435
|
+
info "Created configuration file: #{project_name}/config/kdeploy.yml"
|
436
|
+
end
|
437
|
+
|
438
|
+
def create_sample_scripts(project_name)
|
439
|
+
create_setup_script(project_name)
|
440
|
+
create_database_script(project_name)
|
441
|
+
create_backup_script(project_name)
|
442
|
+
create_monitoring_script(project_name)
|
443
|
+
create_rollback_script(project_name)
|
444
|
+
create_cleanup_script(project_name)
|
445
|
+
end
|
446
|
+
|
447
|
+
def create_setup_script(project_name)
|
448
|
+
content = <<~RUBY
|
449
|
+
# frozen_string_literal: true
|
450
|
+
|
451
|
+
# Server setup script
|
452
|
+
pipeline 'setup' do
|
453
|
+
# Target all hosts
|
454
|
+
host 'app1.example.com', roles: [:app, :web]
|
455
|
+
host 'app2.example.com', roles: [:app, :web]
|
456
|
+
host 'db.example.com', roles: [:db]
|
457
|
+
|
458
|
+
# Set global variables
|
459
|
+
set :ruby_version, '3.2.0'
|
460
|
+
set :node_version, '18.x'
|
461
|
+
|
462
|
+
task :install_dependencies do
|
463
|
+
run <<~BASH
|
464
|
+
# Update package lists
|
465
|
+
sudo apt-get update
|
466
|
+
|
467
|
+
# Install essential packages
|
468
|
+
sudo apt-get install -y build-essential git curl
|
469
|
+
sudo apt-get install -y nginx redis-server
|
470
|
+
BASH
|
471
|
+
end
|
472
|
+
|
473
|
+
task :setup_ruby do
|
474
|
+
run <<~BASH
|
475
|
+
# Install rbenv
|
476
|
+
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
|
477
|
+
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
|
478
|
+
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
479
|
+
source ~/.bashrc
|
480
|
+
|
481
|
+
# Install ruby-build
|
482
|
+
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
483
|
+
|
484
|
+
# Install Ruby
|
485
|
+
rbenv install ${ruby_version}
|
486
|
+
rbenv global ${ruby_version}
|
487
|
+
BASH
|
488
|
+
end
|
489
|
+
|
490
|
+
task :setup_node do
|
491
|
+
run <<~BASH
|
492
|
+
# Install Node.js
|
493
|
+
curl -fsSL https://deb.nodesource.com/setup_${node_version} | sudo -E bash -
|
494
|
+
sudo apt-get install -y nodejs
|
495
|
+
|
496
|
+
# Install Yarn
|
497
|
+
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
498
|
+
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
499
|
+
sudo apt-get update && sudo apt-get install -y yarn
|
500
|
+
BASH
|
501
|
+
end
|
502
|
+
|
503
|
+
task :setup_nginx do
|
504
|
+
# Upload nginx configuration
|
505
|
+
upload_template 'nginx.conf', '/etc/nginx/sites-available/app'
|
506
|
+
run 'sudo ln -sf /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app'
|
507
|
+
run 'sudo nginx -t && sudo systemctl restart nginx'
|
508
|
+
end
|
509
|
+
|
510
|
+
task :setup_app_service do
|
511
|
+
# Upload systemd service configuration
|
512
|
+
upload_template 'app.service', '/etc/systemd/system/app.service'
|
513
|
+
run 'sudo systemctl daemon-reload'
|
514
|
+
run 'sudo systemctl enable app'
|
515
|
+
end
|
516
|
+
|
517
|
+
task :setup do
|
518
|
+
depends_on :install_dependencies,
|
519
|
+
:setup_ruby,
|
520
|
+
:setup_node,
|
521
|
+
:setup_nginx,
|
522
|
+
:setup_app_service
|
523
|
+
end
|
524
|
+
end
|
525
|
+
RUBY
|
526
|
+
|
527
|
+
write_file("#{project_name}/scripts/setup.rb", content)
|
528
|
+
info "Created setup script: #{project_name}/scripts/setup.rb"
|
529
|
+
end
|
530
|
+
|
531
|
+
def create_database_script(project_name)
|
532
|
+
content = <<~RUBY
|
533
|
+
# frozen_string_literal: true
|
534
|
+
|
535
|
+
# Database management script
|
536
|
+
pipeline 'database' do
|
537
|
+
# Target database hosts
|
538
|
+
host 'db.example.com', roles: [:db]
|
539
|
+
|
540
|
+
# Set database configuration
|
541
|
+
set :db_name, 'app_production'
|
542
|
+
set :db_user, 'app_user'
|
543
|
+
set :db_password, ENV['DB_PASSWORD']
|
544
|
+
|
545
|
+
task :create_database do
|
546
|
+
run <<~SQL
|
547
|
+
psql -U postgres -c "CREATE USER ${db_user} WITH PASSWORD '${db_password}';"
|
548
|
+
psql -U postgres -c "CREATE DATABASE ${db_name} OWNER ${db_user};"
|
549
|
+
SQL
|
550
|
+
end
|
551
|
+
|
552
|
+
task :migrate do
|
553
|
+
run 'cd /var/www/app/current && RAILS_ENV=production bundle exec rake db:migrate'
|
554
|
+
end
|
555
|
+
|
556
|
+
task :seed do
|
557
|
+
run 'cd /var/www/app/current && RAILS_ENV=production bundle exec rake db:seed'
|
558
|
+
end
|
559
|
+
|
560
|
+
task :backup do
|
561
|
+
run <<~BASH
|
562
|
+
timestamp=$(date +%Y%m%d_%H%M%S)
|
563
|
+
pg_dump -U ${db_user} ${db_name} > /var/backups/${db_name}_${timestamp}.sql
|
564
|
+
gzip /var/backups/${db_name}_${timestamp}.sql
|
565
|
+
BASH
|
566
|
+
end
|
567
|
+
|
568
|
+
task :restore do
|
569
|
+
run <<~BASH
|
570
|
+
latest_backup=$(ls -t /var/backups/${db_name}_*.sql.gz | head -n1)
|
571
|
+
gunzip -c $latest_backup | psql -U ${db_user} ${db_name}
|
572
|
+
BASH
|
573
|
+
end
|
574
|
+
end
|
575
|
+
RUBY
|
576
|
+
|
577
|
+
write_file("#{project_name}/scripts/database.rb", content)
|
578
|
+
info "Created database script: #{project_name}/scripts/database.rb"
|
579
|
+
end
|
580
|
+
|
581
|
+
def create_backup_script(project_name)
|
582
|
+
content = <<~RUBY
|
583
|
+
# frozen_string_literal: true
|
584
|
+
|
585
|
+
# Backup operations script
|
586
|
+
pipeline 'backup' do
|
587
|
+
# Target all hosts
|
588
|
+
host 'app1.example.com', roles: [:app, :web]
|
589
|
+
host 'app2.example.com', roles: [:app, :web]
|
590
|
+
host 'db.example.com', roles: [:db]
|
591
|
+
|
592
|
+
# Set backup configuration
|
593
|
+
set :backup_dir, '/var/backups'
|
594
|
+
set :keep_backups, 10
|
595
|
+
|
596
|
+
task :backup_database do
|
597
|
+
only :db
|
598
|
+
run <<~BASH
|
599
|
+
timestamp=$(date +%Y%m%d_%H%M%S)
|
600
|
+
pg_dump -U ${db_user} ${db_name} > ${backup_dir}/${db_name}_${timestamp}.sql
|
601
|
+
gzip ${backup_dir}/${db_name}_${timestamp}.sql
|
602
|
+
BASH
|
603
|
+
end
|
604
|
+
|
605
|
+
task :backup_uploads do
|
606
|
+
only [:app, :web]
|
607
|
+
run <<~BASH
|
608
|
+
timestamp=$(date +%Y%m%d_%H%M%S)
|
609
|
+
tar -czf ${backup_dir}/uploads_${timestamp}.tar.gz /var/www/app/shared/public/uploads
|
610
|
+
BASH
|
611
|
+
end
|
612
|
+
|
613
|
+
task :backup_logs do
|
614
|
+
run <<~BASH
|
615
|
+
timestamp=$(date +%Y%m%d_%H%M%S)
|
616
|
+
tar -czf ${backup_dir}/logs_${timestamp}.tar.gz /var/log/app
|
617
|
+
BASH
|
618
|
+
end
|
619
|
+
|
620
|
+
task :cleanup_old_backups do
|
621
|
+
run <<~BASH
|
622
|
+
cd ${backup_dir}
|
623
|
+
ls -t *.sql.gz | tail -n +${keep_backups + 1} | xargs rm -f
|
624
|
+
ls -t *.tar.gz | tail -n +${keep_backups + 1} | xargs rm -f
|
625
|
+
BASH
|
626
|
+
end
|
627
|
+
|
628
|
+
task :backup do
|
629
|
+
depends_on :backup_database,
|
630
|
+
:backup_uploads,
|
631
|
+
:backup_logs,
|
632
|
+
:cleanup_old_backups
|
633
|
+
end
|
634
|
+
end
|
635
|
+
RUBY
|
636
|
+
|
637
|
+
write_file("#{project_name}/scripts/backup.rb", content)
|
638
|
+
info "Created backup script: #{project_name}/scripts/backup.rb"
|
639
|
+
end
|
640
|
+
|
641
|
+
def create_monitoring_script(project_name)
|
642
|
+
content = <<~RUBY
|
643
|
+
# frozen_string_literal: true
|
644
|
+
|
645
|
+
# Health checks and monitoring script
|
646
|
+
pipeline 'monitoring' do
|
647
|
+
# Target all hosts
|
648
|
+
host 'app1.example.com', roles: [:app, :web]
|
649
|
+
host 'app2.example.com', roles: [:app, :web]
|
650
|
+
host 'db.example.com', roles: [:db]
|
651
|
+
|
652
|
+
task :check_system_resources do
|
653
|
+
run <<~BASH
|
654
|
+
echo "Memory Usage:"
|
655
|
+
free -h
|
656
|
+
echo "\\nDisk Usage:"
|
657
|
+
df -h
|
658
|
+
echo "\\nCPU Load:"
|
659
|
+
uptime
|
660
|
+
BASH
|
661
|
+
end
|
662
|
+
|
663
|
+
task :check_services do
|
664
|
+
run <<~BASH
|
665
|
+
echo "Nginx Status:"
|
666
|
+
sudo systemctl status nginx
|
667
|
+
echo "\\nApp Status:"
|
668
|
+
sudo systemctl status app
|
669
|
+
echo "\\nRedis Status:"
|
670
|
+
sudo systemctl status redis-server
|
671
|
+
BASH
|
672
|
+
end
|
673
|
+
|
674
|
+
task :check_logs do
|
675
|
+
run <<~BASH
|
676
|
+
echo "Last 50 lines of application log:"
|
677
|
+
tail -n 50 /var/www/app/current/log/production.log
|
678
|
+
echo "\\nLast 50 lines of nginx error log:"
|
679
|
+
sudo tail -n 50 /var/log/nginx/error.log
|
680
|
+
BASH
|
681
|
+
end
|
682
|
+
|
683
|
+
task :check_database do
|
684
|
+
only :db
|
685
|
+
run <<~BASH
|
686
|
+
echo "PostgreSQL Status:"
|
687
|
+
sudo systemctl status postgresql
|
688
|
+
echo "\\nDatabase Size:"
|
689
|
+
psql -U ${db_user} -d ${db_name} -c "\\l+"
|
690
|
+
BASH
|
691
|
+
end
|
692
|
+
|
693
|
+
task :monitor do
|
694
|
+
depends_on :check_system_resources,
|
695
|
+
:check_services,
|
696
|
+
:check_logs,
|
697
|
+
:check_database
|
698
|
+
end
|
699
|
+
end
|
700
|
+
RUBY
|
701
|
+
|
702
|
+
write_file("#{project_name}/scripts/monitoring.rb", content)
|
703
|
+
info "Created monitoring script: #{project_name}/scripts/monitoring.rb"
|
704
|
+
end
|
705
|
+
|
706
|
+
def create_rollback_script(project_name)
|
707
|
+
content = <<~RUBY
|
708
|
+
# frozen_string_literal: true
|
709
|
+
|
710
|
+
# Rollback operations script
|
711
|
+
pipeline 'rollback' do
|
712
|
+
# Target application hosts
|
713
|
+
host 'app1.example.com', roles: [:app, :web]
|
714
|
+
host 'app2.example.com', roles: [:app, :web]
|
715
|
+
|
716
|
+
# Set deployment configuration
|
717
|
+
set :app_name, 'my_app'
|
718
|
+
set :deploy_to, '/var/www/${app_name}'
|
719
|
+
|
720
|
+
task :list_releases do
|
721
|
+
run "ls -lt ${deploy_to}/releases"
|
722
|
+
end
|
723
|
+
|
724
|
+
task :rollback_code do
|
725
|
+
run <<~BASH
|
726
|
+
current_release=$(readlink ${deploy_to}/current)
|
727
|
+
previous_release=$(ls -t ${deploy_to}/releases | head -n 2 | tail -n 1)
|
728
|
+
ln -sfn ${deploy_to}/releases/$previous_release ${deploy_to}/current
|
729
|
+
BASH
|
730
|
+
end
|
731
|
+
|
732
|
+
task :rollback_database do
|
733
|
+
run 'cd ${deploy_to}/current && RAILS_ENV=production bundle exec rake db:rollback STEP=1'
|
734
|
+
end
|
735
|
+
|
736
|
+
task :restart_services do
|
737
|
+
run 'sudo systemctl restart app'
|
738
|
+
run 'sudo systemctl restart nginx'
|
739
|
+
end
|
740
|
+
|
741
|
+
task :rollback do
|
742
|
+
depends_on :list_releases,
|
743
|
+
:rollback_code,
|
744
|
+
:rollback_database,
|
745
|
+
:restart_services
|
746
|
+
end
|
747
|
+
end
|
748
|
+
RUBY
|
749
|
+
|
750
|
+
write_file("#{project_name}/scripts/rollback.rb", content)
|
751
|
+
info "Created rollback script: #{project_name}/scripts/rollback.rb"
|
752
|
+
end
|
753
|
+
|
754
|
+
def create_cleanup_script(project_name)
|
755
|
+
content = <<~RUBY
|
756
|
+
# frozen_string_literal: true
|
757
|
+
|
758
|
+
# Cleanup operations script
|
759
|
+
pipeline 'cleanup' do
|
760
|
+
# Target all hosts
|
761
|
+
host 'app1.example.com', roles: [:app, :web]
|
762
|
+
host 'app2.example.com', roles: [:app, :web]
|
763
|
+
host 'db.example.com', roles: [:db]
|
764
|
+
|
765
|
+
# Set cleanup configuration
|
766
|
+
set :app_name, 'my_app'
|
767
|
+
set :deploy_to, '/var/www/${app_name}'
|
768
|
+
set :keep_releases, 5
|
769
|
+
set :keep_logs, 7
|
770
|
+
|
771
|
+
task :cleanup_releases do
|
772
|
+
run <<~BASH
|
773
|
+
cd ${deploy_to}/releases
|
774
|
+
ls -t | tail -n +${keep_releases + 1} | xargs rm -rf
|
775
|
+
BASH
|
776
|
+
end
|
777
|
+
|
778
|
+
task :cleanup_logs do
|
779
|
+
run <<~BASH
|
780
|
+
find /var/www/app/current/log -name "*.log.*" -mtime +${keep_logs} -exec rm {} \\;
|
781
|
+
find /var/log/nginx -name "*.log.*" -mtime +${keep_logs} -exec sudo rm {} \\;
|
782
|
+
BASH
|
783
|
+
end
|
784
|
+
|
785
|
+
task :cleanup_temp do
|
786
|
+
run <<~BASH
|
787
|
+
find /tmp -name "#{app_name}-*" -mtime +1 -exec rm -rf {} \\;
|
788
|
+
find /var/tmp -name "#{app_name}-*" -mtime +1 -exec rm -rf {} \\;
|
789
|
+
BASH
|
790
|
+
end
|
791
|
+
|
792
|
+
task :cleanup do
|
793
|
+
depends_on :cleanup_releases,
|
794
|
+
:cleanup_logs,
|
795
|
+
:cleanup_temp
|
796
|
+
end
|
797
|
+
end
|
798
|
+
RUBY
|
799
|
+
|
800
|
+
write_file("#{project_name}/scripts/cleanup.rb", content)
|
801
|
+
info "Created cleanup script: #{project_name}/scripts/cleanup.rb"
|
802
|
+
end
|
803
|
+
|
804
|
+
def create_sample_templates(project_name)
|
805
|
+
create_nginx_template(project_name)
|
806
|
+
create_app_service_template(project_name)
|
807
|
+
create_deploy_script_template(project_name)
|
808
|
+
create_backup_script_template(project_name)
|
809
|
+
end
|
810
|
+
|
811
|
+
def create_nginx_template(project_name)
|
812
|
+
content = <<~ERB
|
813
|
+
# Nginx configuration for <%= app_name %>
|
814
|
+
upstream app_server {
|
815
|
+
server unix:/var/www/<%= app_name %>/shared/tmp/sockets/puma.sock fail_timeout=0;
|
816
|
+
}
|
817
|
+
|
818
|
+
server {
|
819
|
+
listen 80;
|
820
|
+
server_name <%= server_name %>;
|
821
|
+
root /var/www/<%= app_name %>/current/public;
|
822
|
+
|
823
|
+
location ^~ /assets/ {
|
824
|
+
gzip_static on;
|
825
|
+
expires max;
|
826
|
+
add_header Cache-Control public;
|
827
|
+
}
|
828
|
+
|
829
|
+
try_files $uri/index.html $uri @app;
|
830
|
+
|
831
|
+
location @app {
|
832
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
833
|
+
proxy_set_header Host $http_host;
|
834
|
+
proxy_redirect off;
|
835
|
+
proxy_pass http://app_server;
|
836
|
+
}
|
837
|
+
|
838
|
+
error_page 500 502 503 504 /500.html;
|
839
|
+
client_max_body_size 4G;
|
840
|
+
keepalive_timeout 10;
|
841
|
+
}
|
842
|
+
ERB
|
843
|
+
|
844
|
+
write_file("#{project_name}/templates/nginx.conf.erb", content)
|
845
|
+
info "Created nginx template: #{project_name}/templates/nginx.conf.erb"
|
846
|
+
end
|
847
|
+
|
848
|
+
def create_app_service_template(project_name)
|
849
|
+
content = <<~ERB
|
850
|
+
[Unit]
|
851
|
+
Description=<%= app_name %> application server
|
852
|
+
After=network.target
|
853
|
+
|
854
|
+
[Service]
|
855
|
+
Type=simple
|
856
|
+
User=<%= app_user %>
|
857
|
+
WorkingDirectory=/var/www/<%= app_name %>/current
|
858
|
+
Environment=RAILS_ENV=production
|
859
|
+
Environment=PATH=/home/<%= app_user %>/.rbenv/shims:/usr/local/bin:/usr/bin:/bin
|
860
|
+
ExecStart=/home/<%= app_user %>/.rbenv/shims/bundle exec puma -C config/puma.rb
|
861
|
+
Restart=always
|
862
|
+
RestartSec=1
|
863
|
+
|
864
|
+
[Install]
|
865
|
+
WantedBy=multi-user.target
|
866
|
+
ERB
|
867
|
+
|
868
|
+
write_file("#{project_name}/templates/app.service.erb", content)
|
869
|
+
info "Created app service template: #{project_name}/templates/app.service.erb"
|
870
|
+
end
|
871
|
+
|
872
|
+
def create_deploy_script_template(project_name)
|
873
|
+
content = <<~ERB
|
874
|
+
#!/bin/bash
|
875
|
+
# Deployment script for <%= app_name %>
|
876
|
+
|
877
|
+
set -e
|
878
|
+
|
879
|
+
APP_ROOT="/var/www/<%= app_name %>"
|
880
|
+
CURRENT="$APP_ROOT/current"
|
881
|
+
SHARED="$APP_ROOT/shared"
|
882
|
+
RELEASE="$APP_ROOT/releases/$(date +%Y%m%d%H%M%S)"
|
883
|
+
|
884
|
+
echo "Deploying <%= app_name %> to $RELEASE"
|
885
|
+
|
886
|
+
# Create release directory
|
887
|
+
mkdir -p $RELEASE
|
888
|
+
|
889
|
+
# Clone repository
|
890
|
+
git clone <%= repository_url %> $RELEASE
|
891
|
+
|
892
|
+
# Install dependencies
|
893
|
+
cd $RELEASE
|
894
|
+
bundle install --deployment --without development test
|
895
|
+
yarn install --production
|
896
|
+
|
897
|
+
# Compile assets
|
898
|
+
bundle exec rake assets:precompile RAILS_ENV=production
|
899
|
+
|
900
|
+
# Create symlinks
|
901
|
+
ln -s $SHARED/config/database.yml $RELEASE/config/database.yml
|
902
|
+
ln -s $SHARED/config/master.key $RELEASE/config/master.key
|
903
|
+
ln -s $SHARED/public/uploads $RELEASE/public/uploads
|
904
|
+
|
905
|
+
# Update current symlink
|
906
|
+
ln -sfn $RELEASE $CURRENT
|
907
|
+
|
908
|
+
# Restart application
|
909
|
+
sudo systemctl restart <%= app_name %>
|
910
|
+
|
911
|
+
echo "Deployment completed successfully!"
|
912
|
+
ERB
|
913
|
+
|
914
|
+
write_file("#{project_name}/templates/deploy.sh.erb", content)
|
915
|
+
info "Created deploy script template: #{project_name}/templates/deploy.sh.erb"
|
916
|
+
end
|
917
|
+
|
918
|
+
def create_backup_script_template(project_name)
|
919
|
+
content = <<~ERB
|
920
|
+
#!/bin/bash
|
921
|
+
# Backup script for <%= app_name %>
|
922
|
+
|
923
|
+
set -e
|
924
|
+
|
925
|
+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
926
|
+
BACKUP_DIR="<%= backup_dir %>"
|
927
|
+
DB_NAME="<%= db_name %>"
|
928
|
+
APP_ROOT="/var/www/<%= app_name %>"
|
929
|
+
|
930
|
+
echo "Starting backup for <%= app_name %>"
|
931
|
+
|
932
|
+
# Create backup directory
|
933
|
+
mkdir -p $BACKUP_DIR
|
934
|
+
|
935
|
+
# Backup database
|
936
|
+
echo "Backing up database..."
|
937
|
+
pg_dump -U <%= db_user %> $DB_NAME > $BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql
|
938
|
+
gzip $BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql
|
939
|
+
|
940
|
+
# Backup uploads
|
941
|
+
echo "Backing up uploads..."
|
942
|
+
tar -czf $BACKUP_DIR/uploads_${TIMESTAMP}.tar.gz $APP_ROOT/shared/public/uploads
|
943
|
+
|
944
|
+
# Backup configuration
|
945
|
+
echo "Backing up configuration..."
|
946
|
+
tar -czf $BACKUP_DIR/config_${TIMESTAMP}.tar.gz $APP_ROOT/shared/config
|
947
|
+
|
948
|
+
# Cleanup old backups
|
949
|
+
echo "Cleaning up old backups..."
|
950
|
+
find $BACKUP_DIR -name "*.sql.gz" -mtime +<%= keep_days %> -delete
|
951
|
+
find $BACKUP_DIR -name "*.tar.gz" -mtime +<%= keep_days %> -delete
|
952
|
+
|
953
|
+
echo "Backup completed successfully!"
|
954
|
+
ERB
|
955
|
+
|
956
|
+
write_file("#{project_name}/templates/backup.sh.erb", content)
|
957
|
+
info "Created backup script template: #{project_name}/templates/backup.sh.erb"
|
958
|
+
end
|
959
|
+
|
960
|
+
def write_file(path, content)
|
961
|
+
File.write(path, content)
|
962
|
+
end
|
963
|
+
|
964
|
+
def determine_project_name(project_name)
|
965
|
+
project_name || options[:name] || File.basename(Dir.pwd)
|
966
|
+
end
|
967
|
+
|
968
|
+
def display_init_header(project_name)
|
969
|
+
puts ''
|
970
|
+
puts "š Initializing kdeploy project: #{project_name}".colorize(:yellow)
|
971
|
+
puts ''
|
972
|
+
end
|
973
|
+
|
974
|
+
def display_init_success(project_name)
|
975
|
+
puts ''
|
976
|
+
puts 'ā
Project initialized successfully!'.colorize(:green)
|
977
|
+
puts ''
|
978
|
+
display_project_structure(project_name)
|
979
|
+
display_next_steps(project_name)
|
980
|
+
display_available_scripts
|
981
|
+
end
|
982
|
+
|
983
|
+
def display_project_structure(project_name)
|
984
|
+
puts 'š Project Structure Created:'.colorize(:cyan)
|
985
|
+
puts " #{project_name}/".colorize(:light_blue)
|
986
|
+
puts ' āāā deploy.rb # Main deployment script'.colorize(:light_blue)
|
987
|
+
puts ' āāā inventory.yml # Server inventory'.colorize(:light_blue)
|
988
|
+
puts ' āāā config/ # Configuration files'.colorize(:light_blue)
|
989
|
+
puts ' ā āāā kdeploy.yml # Deployment configuration'.colorize(:light_blue)
|
990
|
+
puts ' āāā scripts/ # Additional scripts'.colorize(:light_blue)
|
991
|
+
puts ' ā āāā setup.rb # Server setup script'.colorize(:light_blue)
|
992
|
+
puts ' ā āāā database.rb # Database management'.colorize(:light_blue)
|
993
|
+
puts ' ā āāā backup.rb # Backup operations'.colorize(:light_blue)
|
994
|
+
puts ' ā āāā monitoring.rb # Health checks & monitoring'.colorize(:light_blue)
|
995
|
+
puts ' ā āāā rollback.rb # Rollback operations'.colorize(:light_blue)
|
996
|
+
puts ' ā āāā cleanup.rb # Cleanup operations'.colorize(:light_blue)
|
997
|
+
puts ' āāā templates/ # Configuration templates'.colorize(:light_blue)
|
998
|
+
puts ' āāā nginx.conf.erb # Nginx configuration'.colorize(:light_blue)
|
999
|
+
puts ' āāā app.service.erb # Systemd service'.colorize(:light_blue)
|
1000
|
+
puts ' āāā deploy.sh.erb # Deployment script'.colorize(:light_blue)
|
1001
|
+
puts ' āāā backup.sh.erb # Backup script'.colorize(:light_blue)
|
1002
|
+
puts ''
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
def display_next_steps(project_name)
|
1006
|
+
puts 'š Next Steps:'.colorize(:cyan)
|
1007
|
+
puts " 1. cd #{project_name}".colorize(:light_blue)
|
1008
|
+
puts ' 2. Edit deploy.rb to configure your deployment'.colorize(:light_blue)
|
1009
|
+
puts ' 3. Update inventory.yml with your servers'.colorize(:light_blue)
|
1010
|
+
puts ' 4. Run: kdeploy deploy scripts/setup.rb # Setup servers'.colorize(:light_blue)
|
1011
|
+
puts ' 5. Run: kdeploy deploy deploy.rb # Deploy application'.colorize(:light_blue)
|
1012
|
+
puts ''
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
def display_available_scripts
|
1016
|
+
puts 'š” Available Scripts:'.colorize(:cyan)
|
1017
|
+
puts ' kdeploy deploy scripts/setup.rb # Initial server setup'.colorize(:light_blue)
|
1018
|
+
puts ' kdeploy deploy scripts/database.rb # Database operations'.colorize(:light_blue)
|
1019
|
+
puts ' kdeploy deploy scripts/backup.rb # Backup operations'.colorize(:light_blue)
|
1020
|
+
puts ' kdeploy deploy scripts/monitoring.rb # Health checks'.colorize(:light_blue)
|
1021
|
+
puts ' kdeploy deploy scripts/rollback.rb # Rollback operations'.colorize(:light_blue)
|
1022
|
+
puts ' kdeploy deploy scripts/cleanup.rb # Cleanup operations'.colorize(:light_blue)
|
1023
|
+
puts ''
|
1024
|
+
puts 'š” Need help? Run: kdeploy help deploy'.colorize(:yellow)
|
1025
|
+
puts ''
|
1026
|
+
end
|
1027
|
+
|
1028
|
+
def validate_script_file(script_file)
|
1029
|
+
return if File.exist?(script_file)
|
1030
|
+
|
1031
|
+
error "Script file not found: #{script_file}"
|
1032
|
+
exit 1
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
def display_validation_header(script_file)
|
1036
|
+
puts ''
|
1037
|
+
puts "š Validating deployment script: #{script_file}".colorize(:yellow)
|
1038
|
+
puts ''
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
def display_validation_success(pipeline)
|
1042
|
+
puts 'ā
Script validation passed'.colorize(:green)
|
1043
|
+
puts ''
|
1044
|
+
display_pipeline_summary(pipeline)
|
1045
|
+
display_target_hosts(pipeline.hosts) if pipeline.hosts.any?
|
1046
|
+
display_tasks(pipeline.tasks) if pipeline.tasks.any?
|
1047
|
+
puts ''
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
def display_pipeline_summary(pipeline)
|
1051
|
+
puts 'š Pipeline Summary:'.colorize(:cyan)
|
1052
|
+
puts " Name: #{pipeline.name}".colorize(:light_blue)
|
1053
|
+
puts " Hosts: #{pipeline.hosts.size}".colorize(:light_blue)
|
1054
|
+
puts " Tasks: #{pipeline.tasks.size}".colorize(:light_blue)
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
def display_target_hosts(hosts)
|
1058
|
+
puts ''
|
1059
|
+
puts 'š„ļø Target Hosts:'.colorize(:cyan)
|
1060
|
+
hosts.each do |host|
|
1061
|
+
puts " ⢠#{host.hostname}:#{host.port} (#{host.user})".colorize(:light_blue)
|
1062
|
+
end
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
def display_tasks(tasks)
|
1066
|
+
puts ''
|
1067
|
+
puts 'š§ Tasks to Execute:'.colorize(:cyan)
|
1068
|
+
tasks.each_with_index do |task, index|
|
1069
|
+
puts " #{index + 1}. #{task.name}".colorize(:light_blue)
|
1070
|
+
end
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
def display_validation_errors(errors)
|
1074
|
+
puts 'ā Script validation failed:'.colorize(:red)
|
1075
|
+
puts ''
|
1076
|
+
errors.each { |err| puts " ⢠#{err}".colorize(:red) }
|
1077
|
+
puts ''
|
1078
|
+
exit 1
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
def display_validation_failure(error)
|
1082
|
+
puts "ā Validation failed: #{error.message}".colorize(:red)
|
1083
|
+
puts ''
|
1084
|
+
exit 1
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
def handle_deployment_error(error)
|
1088
|
+
error "Deployment failed: #{error.message}"
|
1089
|
+
exit 1
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
def handle_unexpected_error(error)
|
1093
|
+
error "Unexpected error: #{error.message}"
|
1094
|
+
KdeployLogger.debug("Backtrace: #{error.backtrace.join("\n")}")
|
1095
|
+
exit 1
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
def show_summary_stats
|
1099
|
+
show_kdeploy_banner
|
1100
|
+
stats = Kdeploy.statistics
|
1101
|
+
deployment_summary = stats.deployment_summary(days: options[:days])
|
1102
|
+
task_summary = stats.task_summary(days: options[:days])
|
1103
|
+
global_summary = stats.global_summary
|
1104
|
+
|
1105
|
+
case options[:format].downcase
|
1106
|
+
when 'json'
|
1107
|
+
display_json_summary(deployment_summary, task_summary, global_summary)
|
1108
|
+
else
|
1109
|
+
print_summary_table(deployment_summary, task_summary, global_summary)
|
1110
|
+
end
|
1111
|
+
end
|
1112
|
+
|
1113
|
+
def show_deployment_stats
|
1114
|
+
show_kdeploy_banner
|
1115
|
+
stats = Kdeploy.statistics
|
1116
|
+
summary = stats.deployment_summary(days: options[:days])
|
1117
|
+
|
1118
|
+
case options[:format].downcase
|
1119
|
+
when 'json'
|
1120
|
+
puts JSON.pretty_generate(summary)
|
1121
|
+
else
|
1122
|
+
print_deployment_table(summary)
|
1123
|
+
end
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
def show_task_stats
|
1127
|
+
show_kdeploy_banner
|
1128
|
+
stats = Kdeploy.statistics
|
1129
|
+
summary = stats.task_summary(days: options[:days])
|
1130
|
+
|
1131
|
+
case options[:format].downcase
|
1132
|
+
when 'json'
|
1133
|
+
puts JSON.pretty_generate(summary)
|
1134
|
+
else
|
1135
|
+
print_task_table(summary)
|
1136
|
+
end
|
1137
|
+
end
|
1138
|
+
|
1139
|
+
def show_failure_stats
|
1140
|
+
show_kdeploy_banner
|
1141
|
+
stats = Kdeploy.statistics
|
1142
|
+
failed_tasks = stats.top_failed_tasks(limit: 10, days: options[:days])
|
1143
|
+
|
1144
|
+
case options[:format].downcase
|
1145
|
+
when 'json'
|
1146
|
+
puts JSON.pretty_generate(failed_tasks)
|
1147
|
+
else
|
1148
|
+
print_failure_table(failed_tasks)
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
|
1152
|
+
def show_trend_stats
|
1153
|
+
show_kdeploy_banner
|
1154
|
+
stats = Kdeploy.statistics
|
1155
|
+
trends = stats.performance_trends(days: options[:days])
|
1156
|
+
|
1157
|
+
case options[:format].downcase
|
1158
|
+
when 'json'
|
1159
|
+
puts JSON.pretty_generate(trends)
|
1160
|
+
else
|
1161
|
+
print_trend_table(trends)
|
1162
|
+
end
|
1163
|
+
end
|
1164
|
+
|
1165
|
+
def show_global_stats
|
1166
|
+
show_kdeploy_banner
|
1167
|
+
stats = Kdeploy.statistics
|
1168
|
+
global_summary = stats.global_summary
|
1169
|
+
|
1170
|
+
case options[:format].downcase
|
1171
|
+
when 'json'
|
1172
|
+
puts JSON.pretty_generate(global_summary)
|
1173
|
+
else
|
1174
|
+
print_global_table(global_summary)
|
1175
|
+
end
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
def clear_statistics
|
1179
|
+
show_kdeploy_banner
|
1180
|
+
prompt = TTY::Prompt.new
|
1181
|
+
if prompt.yes?('Are you sure you want to clear all statistics? This cannot be undone.')
|
1182
|
+
Kdeploy.statistics.clear_statistics!
|
1183
|
+
success 'Statistics cleared successfully'
|
1184
|
+
else
|
1185
|
+
info 'Statistics clearing cancelled'
|
1186
|
+
end
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
def export_statistics
|
1190
|
+
show_kdeploy_banner
|
1191
|
+
export_file = options[:output] || generate_export_filename
|
1192
|
+
format = determine_export_format(export_file)
|
1193
|
+
|
1194
|
+
Kdeploy.statistics.export_statistics(export_file, format: format)
|
1195
|
+
success "Statistics exported to #{export_file}"
|
1196
|
+
end
|
1197
|
+
|
1198
|
+
def display_json_summary(deployment_summary, task_summary, global_summary)
|
1199
|
+
puts JSON.pretty_generate(
|
1200
|
+
{
|
1201
|
+
deployment_summary: deployment_summary,
|
1202
|
+
task_summary: task_summary,
|
1203
|
+
global_summary: global_summary
|
1204
|
+
}
|
1205
|
+
)
|
1206
|
+
end
|
1207
|
+
|
1208
|
+
def print_summary_table(deployment_summary, task_summary, global_summary)
|
1209
|
+
puts "\nš Kdeploy Statistics Summary (Last #{options[:days]} days)".colorize(:cyan)
|
1210
|
+
puts '=' * 60
|
1211
|
+
|
1212
|
+
print_deployment_section(deployment_summary)
|
1213
|
+
print_task_section(task_summary)
|
1214
|
+
print_global_section(global_summary)
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
def print_deployment_section(summary)
|
1218
|
+
puts "\nš¦ Deployment Statistics".colorize(:yellow)
|
1219
|
+
if summary[:total_deployments].positive?
|
1220
|
+
puts " Total Deployments: #{summary[:total_deployments]}"
|
1221
|
+
puts " Successful: #{summary[:successful_deployments]} (#{summary[:success_rate]}%)"
|
1222
|
+
puts " Failed: #{summary[:failed_deployments]}"
|
1223
|
+
puts " Average Duration: #{summary[:avg_duration]}s"
|
1224
|
+
puts " Total Duration: #{format_duration(summary[:total_duration])}"
|
1225
|
+
else
|
1226
|
+
puts " No deployments in the last #{options[:days]} days"
|
1227
|
+
end
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
def print_task_section(summary)
|
1231
|
+
puts "\nš§ Task Statistics".colorize(:yellow)
|
1232
|
+
if summary[:total_task_executions].positive?
|
1233
|
+
puts " Total Task Executions: #{summary[:total_task_executions]}"
|
1234
|
+
puts " Unique Tasks: #{summary[:unique_tasks]}"
|
1235
|
+
print_top_tasks(summary[:tasks]) if summary[:tasks].any?
|
1236
|
+
else
|
1237
|
+
puts " No task executions in the last #{options[:days]} days"
|
1238
|
+
end
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
def print_top_tasks(tasks)
|
1242
|
+
puts ' Top Tasks:'
|
1243
|
+
sorted_tasks = tasks.sort_by { |_, stats| -stats[:total_executions] }.first(5)
|
1244
|
+
sorted_tasks.each do |name, stats|
|
1245
|
+
puts " #{name}: #{stats[:total_executions]} executions (#{stats[:success_rate]}% success)"
|
1246
|
+
end
|
1247
|
+
end
|
1248
|
+
|
1249
|
+
def print_global_section(summary)
|
1250
|
+
puts "\nš Global Statistics".colorize(:yellow)
|
1251
|
+
puts " Total Deployments: #{summary[:total_deployments]}"
|
1252
|
+
puts " Total Tasks: #{summary[:total_tasks]}"
|
1253
|
+
puts " Total Commands: #{summary[:total_commands]}"
|
1254
|
+
puts " Total Execution Time: #{format_duration(summary[:total_execution_time])}"
|
1255
|
+
puts " Session Duration: #{format_duration(summary[:session_duration])}"
|
1256
|
+
puts ''
|
1257
|
+
end
|
1258
|
+
|
1259
|
+
def print_deployment_table(summary)
|
1260
|
+
puts "\nš¦ Deployment Statistics (Last #{options[:days]} days)".colorize(:cyan)
|
1261
|
+
puts '=' * 60
|
1262
|
+
|
1263
|
+
if summary[:total_deployments].zero?
|
1264
|
+
puts 'No deployments found'
|
1265
|
+
return
|
1266
|
+
end
|
1267
|
+
|
1268
|
+
display_deployment_stats(summary)
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
def display_deployment_stats(summary)
|
1272
|
+
puts "Total Deployments: #{summary[:total_deployments]}"
|
1273
|
+
puts "Successful: #{summary[:successful_deployments]} (#{summary[:success_rate]}%)"
|
1274
|
+
puts "Failed: #{summary[:failed_deployments]}"
|
1275
|
+
puts "Average Duration: #{summary[:avg_duration]}s"
|
1276
|
+
puts "Min Duration: #{summary[:min_duration]}s"
|
1277
|
+
puts "Max Duration: #{summary[:max_duration]}s"
|
1278
|
+
puts "Total Duration: #{format_duration(summary[:total_duration])}"
|
1279
|
+
puts ''
|
1280
|
+
end
|
1281
|
+
|
1282
|
+
def print_task_table(summary)
|
1283
|
+
puts "\nš§ Task Statistics (Last #{options[:days]} days)".colorize(:cyan)
|
1284
|
+
puts '=' * 60
|
1285
|
+
|
1286
|
+
if summary[:total_task_executions].zero?
|
1287
|
+
puts 'No task executions found'
|
1288
|
+
return
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
display_task_summary(summary)
|
1292
|
+
display_task_details(summary[:tasks]) if summary[:tasks].any?
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
def display_task_summary(summary)
|
1296
|
+
puts "Total Executions: #{summary[:total_task_executions]}"
|
1297
|
+
puts "Unique Tasks: #{summary[:unique_tasks]}"
|
1298
|
+
puts ''
|
1299
|
+
end
|
1300
|
+
|
1301
|
+
def display_task_details(tasks)
|
1302
|
+
puts 'Task Details:'
|
1303
|
+
print_task_header
|
1304
|
+
tasks.each { |name, stats| print_task_row(name, stats) }
|
1305
|
+
puts ''
|
1306
|
+
end
|
1307
|
+
|
1308
|
+
def print_task_header
|
1309
|
+
printf "%-30s %10s %10s %10s %12s %12s\n",
|
1310
|
+
'Task Name', 'Executions', 'Success', 'Failed', 'Success Rate', 'Avg Duration'
|
1311
|
+
puts '-' * 95
|
1312
|
+
end
|
1313
|
+
|
1314
|
+
def print_task_row(name, stats)
|
1315
|
+
printf "%-30s %10d %10d %10d %11.1f%% %11.2fs\n",
|
1316
|
+
name.truncate(28),
|
1317
|
+
stats[:total_executions],
|
1318
|
+
stats[:successful],
|
1319
|
+
stats[:failed],
|
1320
|
+
stats[:success_rate],
|
1321
|
+
stats[:avg_duration]
|
1322
|
+
end
|
1323
|
+
|
1324
|
+
def print_failure_table(failed_tasks)
|
1325
|
+
puts "\nā Top Failed Tasks (Last #{options[:days]} days)".colorize(:red)
|
1326
|
+
puts '=' * 60
|
1327
|
+
|
1328
|
+
if failed_tasks.empty?
|
1329
|
+
puts 'No failed tasks found'
|
1330
|
+
return
|
1331
|
+
end
|
1332
|
+
|
1333
|
+
print_failure_header
|
1334
|
+
failed_tasks.each { |task| print_failure_row(task) }
|
1335
|
+
puts ''
|
1336
|
+
end
|
1337
|
+
|
1338
|
+
def print_failure_header
|
1339
|
+
printf "%-30s %10s %20s\n", 'Task Name', 'Failures', 'Last Failure'
|
1340
|
+
puts '-' * 62
|
1341
|
+
end
|
1342
|
+
|
1343
|
+
def print_failure_row(task)
|
1344
|
+
last_failure_time = Time.at(task[:last_failure][:timestamp]).strftime('%Y-%m-%d %H:%M:%S')
|
1345
|
+
printf "%-30s %10d %20s\n",
|
1346
|
+
task[:task_name].truncate(28),
|
1347
|
+
task[:failure_count],
|
1348
|
+
last_failure_time
|
1349
|
+
end
|
1350
|
+
|
1351
|
+
def print_trend_table(trends)
|
1352
|
+
puts "\nš Performance Trends (Last #{options[:days]} days)".colorize(:cyan)
|
1353
|
+
puts '=' * 80
|
1354
|
+
|
1355
|
+
if trends[:trends].empty?
|
1356
|
+
puts 'No trend data available'
|
1357
|
+
return
|
1358
|
+
end
|
1359
|
+
|
1360
|
+
print_trend_header
|
1361
|
+
trends[:trends].each { |date, stats| print_trend_row(date, stats) }
|
1362
|
+
puts ''
|
1363
|
+
end
|
1364
|
+
|
1365
|
+
def print_trend_header
|
1366
|
+
printf "%-12s %10s %10s %10s %12s %12s\n",
|
1367
|
+
'Date', 'Total', 'Success', 'Failed', 'Success Rate', 'Avg Duration'
|
1368
|
+
puts '-' * 78
|
1369
|
+
end
|
1370
|
+
|
1371
|
+
def print_trend_row(date, stats)
|
1372
|
+
printf "%-12s %10d %10d %10d %11.1f%% %11.2fs\n",
|
1373
|
+
date,
|
1374
|
+
stats[:total],
|
1375
|
+
stats[:successful],
|
1376
|
+
stats[:failed],
|
1377
|
+
stats[:success_rate],
|
1378
|
+
stats[:avg_duration]
|
1379
|
+
end
|
1380
|
+
|
1381
|
+
def print_global_table(global_summary)
|
1382
|
+
puts "\nš Global Statistics".colorize(:cyan)
|
1383
|
+
puts '=' * 60
|
1384
|
+
|
1385
|
+
print_global_deployment_stats(global_summary)
|
1386
|
+
print_global_task_stats(global_summary)
|
1387
|
+
print_global_command_stats(global_summary)
|
1388
|
+
print_global_execution_stats(global_summary)
|
1389
|
+
end
|
1390
|
+
|
1391
|
+
def print_global_deployment_stats(summary)
|
1392
|
+
puts 'Deployments:'
|
1393
|
+
puts " Total: #{summary[:total_deployments]}"
|
1394
|
+
puts " Successful: #{summary[:successful_deployments]}"
|
1395
|
+
puts " Failed: #{summary[:failed_deployments]}"
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
def print_global_task_stats(summary)
|
1399
|
+
puts "\nTasks:"
|
1400
|
+
puts " Total: #{summary[:total_tasks]}"
|
1401
|
+
puts " Successful: #{summary[:successful_tasks]}"
|
1402
|
+
puts " Failed: #{summary[:failed_tasks]}"
|
1403
|
+
end
|
1404
|
+
|
1405
|
+
def print_global_command_stats(summary)
|
1406
|
+
puts "\nCommands:"
|
1407
|
+
puts " Total: #{summary[:total_commands]}"
|
1408
|
+
puts " Successful: #{summary[:successful_commands]}"
|
1409
|
+
puts " Failed: #{summary[:failed_commands]}"
|
1410
|
+
end
|
1411
|
+
|
1412
|
+
def print_global_execution_stats(summary)
|
1413
|
+
puts "\nExecution Time:"
|
1414
|
+
puts " Total: #{format_duration(summary[:total_execution_time])}"
|
1415
|
+
puts " Session: #{format_duration(summary[:session_duration])}"
|
1416
|
+
puts " Session Started: #{summary[:session_start_time].strftime('%Y-%m-%d %H:%M:%S')}"
|
1417
|
+
puts ''
|
1418
|
+
end
|
1419
|
+
|
1420
|
+
def format_duration(seconds)
|
1421
|
+
return '0s' if seconds.nil? || seconds.zero?
|
1422
|
+
|
1423
|
+
if seconds < 60
|
1424
|
+
"#{seconds.round(1)}s"
|
1425
|
+
elsif seconds < 3600
|
1426
|
+
format_minutes(seconds)
|
1427
|
+
else
|
1428
|
+
format_hours(seconds)
|
1429
|
+
end
|
1430
|
+
end
|
1431
|
+
|
1432
|
+
def format_minutes(seconds)
|
1433
|
+
minutes = (seconds / 60).to_i
|
1434
|
+
remaining_seconds = (seconds % 60).to_i
|
1435
|
+
"#{minutes}m #{remaining_seconds}s"
|
1436
|
+
end
|
1437
|
+
|
1438
|
+
def format_hours(seconds)
|
1439
|
+
hours = (seconds / 3600).to_i
|
1440
|
+
remaining_minutes = ((seconds % 3600) / 60).to_i
|
1441
|
+
"#{hours}h #{remaining_minutes}m"
|
1442
|
+
end
|
1443
|
+
|
1444
|
+
def generate_export_filename
|
1445
|
+
"kdeploy_stats_#{Time.now.strftime('%Y%m%d_%H%M%S')}.json"
|
1446
|
+
end
|
1447
|
+
|
1448
|
+
def determine_export_format(filename)
|
1449
|
+
File.extname(filename) == '.csv' ? :csv : :json
|
1450
|
+
end
|
1451
|
+
end
|
1452
|
+
end
|