kdeploy 0.1.0 → 0.3.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.
@@ -1,249 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Pipeline class for managing deployment tasks and hosts
5
- class Pipeline
6
- attr_reader :name, :hosts, :tasks, :variables
7
-
8
- def initialize(name = 'default')
9
- @name = name
10
- @hosts = []
11
- @tasks = []
12
- @variables = {}
13
- end
14
-
15
- # Add host to pipeline
16
- # @param hostname [String] Hostname or IP address
17
- # @param user [String] SSH user
18
- # @param port [Integer] SSH port
19
- # @param ssh_options [Hash] SSH options
20
- # @param roles [Array] Host roles
21
- # @param vars [Hash] Host variables
22
- # @return [Host] Created host
23
- def add_host(hostname, user: nil, port: nil, ssh_options: {}, roles: [], vars: {})
24
- host = Host.new(
25
- hostname,
26
- user: user,
27
- port: port,
28
- ssh_options: ssh_options,
29
- roles: roles,
30
- vars: vars
31
- )
32
- @hosts << host unless @hosts.include?(host)
33
- host
34
- end
35
-
36
- # Add multiple hosts from hash
37
- # @param hosts_config [Hash] Hosts configuration
38
- def add_hosts(hosts_config)
39
- hosts_config.each do |hostname, config|
40
- config ||= {}
41
- add_host_from_config(hostname, config)
42
- end
43
- end
44
-
45
- # Get hosts by role
46
- # @param role [String, Symbol] Role to filter by
47
- # @return [Array<Host>] Hosts with specified role
48
- def hosts_with_role(role)
49
- @hosts.select { |host| host.has_role?(role) }
50
- end
51
-
52
- # Add task to pipeline
53
- # @param name [String] Task name
54
- # @param hosts [Array<Host>] Target hosts (default: all hosts)
55
- # @param options [Hash] Task options
56
- # @return [Task] Created task
57
- def add_task(name, hosts: nil, **options)
58
- target_hosts = hosts || @hosts
59
- task = create_task(name, target_hosts, options)
60
- @tasks << task
61
- task
62
- end
63
-
64
- # Set global variable
65
- # @param key [String, Symbol] Variable key
66
- # @param value [Object] Variable value
67
- def set_variable(key, value)
68
- @variables[key.to_s] = value
69
- end
70
-
71
- # Get global variable
72
- # @param key [String, Symbol] Variable key
73
- # @return [Object] Variable value
74
- def get_variable(key)
75
- @variables[key.to_s] || @variables[key.to_sym]
76
- end
77
-
78
- # Execute all tasks in pipeline
79
- # @return [Hash] Execution results
80
- def execute
81
- return empty_execution_result if @tasks.empty?
82
-
83
- log_pipeline_start
84
- start_time = Time.now
85
- results = execute_tasks
86
- duration = Time.now - start_time
87
- success_count = count_successful_tasks(results)
88
-
89
- log_pipeline_completion(duration, success_count)
90
- build_execution_result(results, duration, success_count)
91
- end
92
-
93
- # Get pipeline summary
94
- # @return [Hash] Pipeline summary
95
- def summary
96
- {
97
- name: @name,
98
- hosts_count: @hosts.size,
99
- tasks_count: @tasks.size,
100
- hosts: @hosts.map(&:hostname),
101
- tasks: @tasks.map(&:name)
102
- }
103
- end
104
-
105
- # Validate pipeline configuration
106
- # @return [Array<String>] Validation errors
107
- def validate
108
- errors = []
109
- errors.concat(validate_pipeline_structure)
110
- errors.concat(validate_hosts)
111
- errors.concat(validate_tasks)
112
- errors
113
- end
114
-
115
- # Check if pipeline is valid
116
- # @return [Boolean] True if pipeline is valid
117
- def valid?
118
- validate.empty?
119
- end
120
-
121
- private
122
-
123
- def add_host_from_config(hostname, config)
124
- add_host(
125
- hostname,
126
- user: config['user'] || config[:user],
127
- port: config['port'] || config[:port],
128
- ssh_options: config['ssh_options'] || config[:ssh_options] || {},
129
- roles: config['roles'] || config[:roles] || [],
130
- vars: config['vars'] || config[:vars] || {}
131
- )
132
- end
133
-
134
- def create_task(name, target_hosts, options)
135
- task = Task.new(name, target_hosts, options)
136
- task.global_variables = @variables
137
- task
138
- end
139
-
140
- def empty_execution_result
141
- { success: true, results: [], duration: 0 }
142
- end
143
-
144
- def log_pipeline_start
145
- KdeployLogger.info(
146
- "Starting pipeline '#{@name}' with #{@tasks.size} task(s) on #{@hosts.size} host(s)"
147
- )
148
- end
149
-
150
- def execute_tasks
151
- results = []
152
- overall_success = true
153
-
154
- @tasks.each_with_index do |task, index|
155
- log_task_execution(task, index)
156
- result = execute_task(task)
157
- results << result
158
- overall_success = false unless result[:success]
159
- end
160
-
161
- results
162
- end
163
-
164
- def log_task_execution(task, index)
165
- KdeployLogger.info("Executing task #{index + 1}/#{@tasks.size}: '#{task.name}'")
166
- end
167
-
168
- def execute_task(task)
169
- result = task.execute
170
- log_task_failure(task) unless result[:success]
171
-
172
- {
173
- task_name: task.name,
174
- **result
175
- }
176
- end
177
-
178
- def log_task_failure(task)
179
- KdeployLogger.error("Task '#{task.name}' failed, pipeline execution continuing...")
180
- end
181
-
182
- def count_successful_tasks(results)
183
- results.count { |r| r[:success] }
184
- end
185
-
186
- def log_pipeline_completion(duration, success_count)
187
- KdeployLogger.info(
188
- "Pipeline '#{@name}' completed in #{duration.round(2)}s: " \
189
- "#{success_count}/#{@tasks.size} tasks successful"
190
- )
191
- end
192
-
193
- def build_execution_result(results, duration, success_count)
194
- {
195
- success: results.all? { |r| r[:success] },
196
- results: results,
197
- duration: duration,
198
- tasks_count: @tasks.size,
199
- success_count: success_count
200
- }
201
- end
202
-
203
- def validate_pipeline_structure
204
- errors = []
205
- errors << 'No hosts defined' if @hosts.empty?
206
- errors << 'No tasks defined' if @tasks.empty?
207
- errors
208
- end
209
-
210
- def validate_hosts
211
- @hosts.each_with_object([]) do |host, errors|
212
- errors.concat(validate_host(host))
213
- end
214
- end
215
-
216
- def validate_host(host)
217
- errors = []
218
- errors << "Invalid hostname: #{host.hostname}" if invalid_hostname?(host)
219
- errors << "Invalid user: #{host.user}" if invalid_user?(host)
220
- errors << "Invalid port: #{host.port}" if invalid_port?(host)
221
- errors
222
- end
223
-
224
- def invalid_hostname?(host)
225
- host.hostname.nil? || host.hostname.empty?
226
- end
227
-
228
- def invalid_user?(host)
229
- host.user.nil? || host.user.empty?
230
- end
231
-
232
- def invalid_port?(host)
233
- !host.port.is_a?(Integer) || !host.port.positive?
234
- end
235
-
236
- def validate_tasks
237
- @tasks.each_with_object([]) do |task, errors|
238
- errors.concat(validate_task(task))
239
- end
240
- end
241
-
242
- def validate_task(task)
243
- errors = []
244
- errors << "Task '#{task.name}' has no commands" if task.commands.empty?
245
- errors << "Task '#{task.name}' has no hosts" if task.hosts.empty?
246
- errors
247
- end
248
- end
249
- end
@@ -1,187 +0,0 @@
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