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
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kdeploy
|
4
|
+
class Runner
|
5
|
+
attr_reader :pipeline
|
6
|
+
|
7
|
+
def initialize(pipeline)
|
8
|
+
@pipeline = pipeline
|
9
|
+
end
|
10
|
+
|
11
|
+
# Execute the deployment pipeline
|
12
|
+
# @return [Hash] Execution results
|
13
|
+
def execute
|
14
|
+
validate_pipeline!
|
15
|
+
setup_logging
|
16
|
+
|
17
|
+
KdeployLogger.info("Starting deployment: #{@pipeline.name}")
|
18
|
+
KdeployLogger.info("Pipeline summary: #{@pipeline.summary}")
|
19
|
+
|
20
|
+
start_time = Time.now
|
21
|
+
|
22
|
+
begin
|
23
|
+
result = @pipeline.execute
|
24
|
+
|
25
|
+
duration = Time.now - start_time
|
26
|
+
|
27
|
+
# Enhanced result with pipeline info for statistics
|
28
|
+
enhanced_result = result.merge(
|
29
|
+
pipeline_name: @pipeline.name,
|
30
|
+
hosts_count: @pipeline.hosts.size
|
31
|
+
)
|
32
|
+
|
33
|
+
# Record deployment statistics
|
34
|
+
Kdeploy.statistics.record_deployment(enhanced_result)
|
35
|
+
|
36
|
+
log_final_results(enhanced_result, duration)
|
37
|
+
|
38
|
+
enhanced_result
|
39
|
+
rescue StandardError => e
|
40
|
+
duration = Time.now - start_time
|
41
|
+
KdeployLogger.fatal("Deployment failed after #{duration.round(2)}s: #{e.message}")
|
42
|
+
KdeployLogger.debug("Error backtrace: #{e.backtrace.join("\n")}")
|
43
|
+
|
44
|
+
failed_result = {
|
45
|
+
success: false,
|
46
|
+
error: e.message,
|
47
|
+
duration: duration,
|
48
|
+
results: [],
|
49
|
+
pipeline_name: @pipeline.name,
|
50
|
+
hosts_count: @pipeline.hosts.size,
|
51
|
+
tasks_count: @pipeline.tasks.size,
|
52
|
+
success_count: 0
|
53
|
+
}
|
54
|
+
|
55
|
+
# Record failed deployment statistics
|
56
|
+
Kdeploy.statistics.record_deployment(failed_result)
|
57
|
+
|
58
|
+
failed_result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Dry run - validate and show what would be executed
|
63
|
+
# @return [Hash] Validation results and execution plan
|
64
|
+
def dry_run
|
65
|
+
KdeployLogger.info("Performing dry run for pipeline: #{@pipeline.name}")
|
66
|
+
|
67
|
+
validation_errors = @pipeline.validate
|
68
|
+
|
69
|
+
if validation_errors.any?
|
70
|
+
KdeployLogger.error('Pipeline validation failed:')
|
71
|
+
validation_errors.each { |error| KdeployLogger.error(" - #{error}") }
|
72
|
+
|
73
|
+
return {
|
74
|
+
success: false,
|
75
|
+
validation_errors: validation_errors,
|
76
|
+
execution_plan: nil
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
execution_plan = generate_execution_plan
|
81
|
+
|
82
|
+
KdeployLogger.info('Dry run completed successfully')
|
83
|
+
log_execution_plan(execution_plan)
|
84
|
+
|
85
|
+
{
|
86
|
+
success: true,
|
87
|
+
validation_errors: [],
|
88
|
+
execution_plan: execution_plan
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def validate_pipeline!
|
95
|
+
validation_errors = @pipeline.validate
|
96
|
+
|
97
|
+
return if validation_errors.empty?
|
98
|
+
|
99
|
+
error_message = "Pipeline validation failed:\n#{validation_errors.map { |e| " - #{e}" }.join("\n")}"
|
100
|
+
raise ConfigurationError, error_message
|
101
|
+
end
|
102
|
+
|
103
|
+
def setup_logging
|
104
|
+
config = Kdeploy.configuration
|
105
|
+
return unless config
|
106
|
+
|
107
|
+
KdeployLogger.setup(
|
108
|
+
level: config.log_level,
|
109
|
+
file: config.log_file
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
def log_final_results(result, duration)
|
114
|
+
if result[:success]
|
115
|
+
KdeployLogger.info("✅ Deployment completed successfully in #{duration.round(2)}s")
|
116
|
+
KdeployLogger.info("📊 Summary: #{result[:success_count]}/#{result[:tasks_count]} tasks successful")
|
117
|
+
else
|
118
|
+
KdeployLogger.error("❌ Deployment failed in #{duration.round(2)}s")
|
119
|
+
KdeployLogger.error("📊 Summary: #{result[:success_count]}/#{result[:tasks_count]} tasks successful")
|
120
|
+
end
|
121
|
+
|
122
|
+
# Log task details
|
123
|
+
if result[:results].is_a?(Array)
|
124
|
+
result[:results].each do |task_result|
|
125
|
+
status = task_result[:success] ? '✅' : '❌'
|
126
|
+
success_info = "#{task_result[:success_count]}/#{task_result[:hosts_count]} hosts successful"
|
127
|
+
KdeployLogger.info("#{status} Task '#{task_result[:task_name]}': #{success_info}")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Log statistics summary
|
132
|
+
global_stats = Kdeploy.statistics.global_summary
|
133
|
+
KdeployLogger.info("📈 Global Stats: #{global_stats[:total_deployments]} deployments, " \
|
134
|
+
"#{global_stats[:successful_deployments]} successful, " \
|
135
|
+
"#{global_stats[:failed_deployments]} failed")
|
136
|
+
end
|
137
|
+
|
138
|
+
def generate_execution_plan
|
139
|
+
plan = {
|
140
|
+
pipeline_name: @pipeline.name,
|
141
|
+
total_hosts: @pipeline.hosts.size,
|
142
|
+
total_tasks: @pipeline.tasks.size,
|
143
|
+
hosts: @pipeline.hosts.map(&:hostname),
|
144
|
+
tasks: []
|
145
|
+
}
|
146
|
+
|
147
|
+
@pipeline.tasks.each do |task|
|
148
|
+
task_plan = {
|
149
|
+
name: task.name,
|
150
|
+
target_hosts: task.hosts.map(&:hostname),
|
151
|
+
commands: task.commands.map do |command|
|
152
|
+
{
|
153
|
+
name: command.name,
|
154
|
+
command: command.command,
|
155
|
+
options: command.options
|
156
|
+
}
|
157
|
+
end,
|
158
|
+
options: task.options
|
159
|
+
}
|
160
|
+
|
161
|
+
plan[:tasks] << task_plan
|
162
|
+
end
|
163
|
+
|
164
|
+
plan
|
165
|
+
end
|
166
|
+
|
167
|
+
def log_execution_plan(plan)
|
168
|
+
KdeployLogger.info('📋 Execution Plan:')
|
169
|
+
KdeployLogger.info(" Pipeline: #{plan[:pipeline_name]}")
|
170
|
+
KdeployLogger.info(" Hosts: #{plan[:total_hosts]} (#{plan[:hosts].join(', ')})")
|
171
|
+
KdeployLogger.info(" Tasks: #{plan[:total_tasks]}")
|
172
|
+
|
173
|
+
plan[:tasks].each_with_index do |task, index|
|
174
|
+
KdeployLogger.info(" #{index + 1}. #{task[:name]}")
|
175
|
+
KdeployLogger.info(" Targets: #{task[:target_hosts].join(', ')}")
|
176
|
+
KdeployLogger.info(" Commands: #{task[:commands].size}")
|
177
|
+
|
178
|
+
task[:commands].each_with_index do |command, cmd_index|
|
179
|
+
KdeployLogger.info(" #{cmd_index + 1}. #{command[:name]}: #{command[:command]}")
|
180
|
+
end
|
181
|
+
|
182
|
+
if task[:options][:parallel]
|
183
|
+
KdeployLogger.info(" Execution: Parallel (max: #{task[:options][:max_concurrent] || 'unlimited'})")
|
184
|
+
else
|
185
|
+
KdeployLogger.info(' Execution: Sequential')
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kdeploy
|
4
|
+
# SSHConnection class for managing SSH connections to remote hosts
|
5
|
+
class SSHConnection
|
6
|
+
attr_reader :host, :session
|
7
|
+
|
8
|
+
def initialize(host)
|
9
|
+
@host = host
|
10
|
+
@session = nil
|
11
|
+
@connected = false
|
12
|
+
end
|
13
|
+
|
14
|
+
# Establish SSH connection
|
15
|
+
# @return [Boolean] True if connection successful
|
16
|
+
# @raise [ConnectionError] If connection fails
|
17
|
+
def connect
|
18
|
+
return true if connected?
|
19
|
+
|
20
|
+
KdeployLogger.debug("Connecting to #{@host}")
|
21
|
+
establish_connection
|
22
|
+
KdeployLogger.debug("Connected to #{@host}")
|
23
|
+
true
|
24
|
+
rescue Net::SSH::Exception => e
|
25
|
+
handle_connection_error(e)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Check if connection is active
|
29
|
+
# @return [Boolean] True if connected
|
30
|
+
def connected?
|
31
|
+
@connected && @session && !@session.closed?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Execute command on remote host
|
35
|
+
# @param command [String] Command to execute
|
36
|
+
# @param timeout [Integer] Command timeout in seconds
|
37
|
+
# @return [Hash] Result with stdout, stderr, exit_code, and success
|
38
|
+
def execute(command, timeout: nil)
|
39
|
+
ensure_connected
|
40
|
+
|
41
|
+
result = initialize_result
|
42
|
+
timeout ||= Kdeploy.configuration&.command_timeout || 300
|
43
|
+
|
44
|
+
KdeployLogger.debug("Executing on #{@host}: #{command}")
|
45
|
+
execute_command(command, result)
|
46
|
+
KdeployLogger.debug("Command completed on #{@host}: exit_code=#{result[:exit_code]}")
|
47
|
+
|
48
|
+
result
|
49
|
+
rescue Net::SSH::Exception => e
|
50
|
+
handle_execution_error(e)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Upload file to remote host
|
54
|
+
# @param local_path [String] Local file path
|
55
|
+
# @param remote_path [String] Remote file path
|
56
|
+
# @return [Boolean] True if upload successful
|
57
|
+
def upload(local_path, remote_path)
|
58
|
+
ensure_connected
|
59
|
+
|
60
|
+
KdeployLogger.debug("Uploading #{local_path} to #{@host}:#{remote_path}")
|
61
|
+
perform_upload(local_path, remote_path)
|
62
|
+
KdeployLogger.debug("Upload completed: #{local_path} -> #{@host}:#{remote_path}")
|
63
|
+
true
|
64
|
+
rescue Net::SCP::Error => e
|
65
|
+
handle_upload_error(e, local_path, remote_path)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Download file from remote host
|
69
|
+
# @param remote_path [String] Remote file path
|
70
|
+
# @param local_path [String] Local file path
|
71
|
+
# @return [Boolean] True if download successful
|
72
|
+
def download(remote_path, local_path)
|
73
|
+
ensure_connected
|
74
|
+
|
75
|
+
KdeployLogger.debug("Downloading #{@host}:#{remote_path} to #{local_path}")
|
76
|
+
perform_download(remote_path, local_path)
|
77
|
+
KdeployLogger.debug("Download completed: #{@host}:#{remote_path} -> #{local_path}")
|
78
|
+
true
|
79
|
+
rescue Net::SCP::Error => e
|
80
|
+
handle_download_error(e, remote_path, local_path)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Close SSH connection
|
84
|
+
def disconnect
|
85
|
+
return unless @session
|
86
|
+
|
87
|
+
@session.close unless @session.closed?
|
88
|
+
@session = nil
|
89
|
+
@connected = false
|
90
|
+
KdeployLogger.debug("Disconnected from #{@host}")
|
91
|
+
end
|
92
|
+
|
93
|
+
# Clean up connection
|
94
|
+
def cleanup
|
95
|
+
disconnect
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def establish_connection
|
101
|
+
@session = Net::SSH.start(
|
102
|
+
@host.hostname,
|
103
|
+
@host.user,
|
104
|
+
port: @host.port,
|
105
|
+
**@host.connection_options
|
106
|
+
)
|
107
|
+
@connected = true
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_connection_error(error)
|
111
|
+
KdeployLogger.error("Failed to connect to #{@host}: #{error.message}")
|
112
|
+
raise ConnectionError, "Failed to connect to #{@host}: #{error.message}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def ensure_connected
|
116
|
+
connect unless connected?
|
117
|
+
raise ConnectionError, "Not connected to #{@host}" unless connected?
|
118
|
+
end
|
119
|
+
|
120
|
+
def initialize_result
|
121
|
+
{
|
122
|
+
stdout: '',
|
123
|
+
stderr: '',
|
124
|
+
exit_code: nil,
|
125
|
+
success: false
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def execute_command(command, result)
|
130
|
+
channel = create_command_channel(command, result)
|
131
|
+
channel.wait
|
132
|
+
result[:success] = result[:exit_code]&.zero? || false
|
133
|
+
end
|
134
|
+
|
135
|
+
def create_command_channel(command, result)
|
136
|
+
@session.open_channel do |ch|
|
137
|
+
ch.exec(command) do |ch, success|
|
138
|
+
handle_command_execution(ch, success, result)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def handle_command_execution(channel, success, result)
|
144
|
+
unless success
|
145
|
+
result[:stderr] = 'Failed to execute command'
|
146
|
+
result[:exit_code] = 1
|
147
|
+
return
|
148
|
+
end
|
149
|
+
|
150
|
+
setup_command_callbacks(channel, result)
|
151
|
+
end
|
152
|
+
|
153
|
+
def setup_command_callbacks(channel, result)
|
154
|
+
channel.on_data { |_ch, data| result[:stdout] += data }
|
155
|
+
channel.on_extended_data { |_ch, _type, data| result[:stderr] += data }
|
156
|
+
channel.on_request('exit-status') { |_ch, data| result[:exit_code] = data.read_long }
|
157
|
+
end
|
158
|
+
|
159
|
+
def handle_execution_error(error)
|
160
|
+
KdeployLogger.error("SSH execution error on #{@host}: #{error.message}")
|
161
|
+
{
|
162
|
+
stdout: '',
|
163
|
+
stderr: error.message,
|
164
|
+
exit_code: 1,
|
165
|
+
success: false
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def perform_upload(local_path, remote_path)
|
170
|
+
@session.scp.upload!(local_path, remote_path)
|
171
|
+
end
|
172
|
+
|
173
|
+
def handle_upload_error(error, local_path, remote_path)
|
174
|
+
KdeployLogger.error("Upload failed #{local_path} -> #{@host}:#{remote_path}: #{error.message}")
|
175
|
+
false
|
176
|
+
end
|
177
|
+
|
178
|
+
def perform_download(remote_path, local_path)
|
179
|
+
@session.scp.download!(remote_path, local_path)
|
180
|
+
end
|
181
|
+
|
182
|
+
def handle_download_error(error, remote_path, local_path)
|
183
|
+
KdeployLogger.error("Download failed #{@host}:#{remote_path} -> #{local_path}: #{error.message}")
|
184
|
+
false
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|