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.
@@ -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