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.
- checksums.yaml +4 -4
- data/README.md +101 -936
- data/exe/kdeploy +6 -0
- data/k.md +149 -0
- data/lib/kdeploy/banner.rb +44 -14
- data/lib/kdeploy/cli.rb +138 -1389
- data/lib/kdeploy/dsl.rb +66 -530
- data/lib/kdeploy/executor.rb +73 -0
- data/lib/kdeploy/initializer.rb +229 -0
- data/lib/kdeploy/runner.rb +40 -180
- data/lib/kdeploy/template.rb +18 -161
- data/lib/kdeploy/version.rb +1 -2
- data/lib/kdeploy.rb +9 -100
- metadata +75 -52
- data/.editorconfig +0 -12
- data/.rspec +0 -3
- data/.rubocop.yml +0 -100
- data/LICENSE +0 -21
- data/Rakefile +0 -45
- data/bin/kdeploy +0 -7
- data/kdeploy.gemspec +0 -49
- data/lib/kdeploy/command.rb +0 -182
- data/lib/kdeploy/configuration.rb +0 -83
- data/lib/kdeploy/host.rb +0 -85
- data/lib/kdeploy/inventory.rb +0 -243
- data/lib/kdeploy/logger.rb +0 -100
- data/lib/kdeploy/pipeline.rb +0 -249
- data/lib/kdeploy/ssh_connection.rb +0 -187
- data/lib/kdeploy/statistics.rb +0 -439
- data/lib/kdeploy/task.rb +0 -240
- data/scripts/common_tasks.rb +0 -218
- data/scripts/deploy.rb +0 -50
data/lib/kdeploy/pipeline.rb
DELETED
@@ -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
|