kdeploy 0.2.0 → 0.4.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.
data/Rakefile DELETED
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
- require 'time'
6
- require_relative 'lib/kdeploy/version'
7
-
8
- # Default task runs tests and RuboCop, then pushes code
9
- task default: %w[push]
10
-
11
- # Run RSpec tests
12
- RSpec::Core::RakeTask.new(:test) do |spec|
13
- spec.pattern = 'spec/**{,/*/**}/*_spec.rb'
14
- end
15
-
16
- # Run RuboCop
17
- task :rubocop do
18
- system 'bundle exec rubocop'
19
- end
20
-
21
- # Auto-commit and push changes
22
- task :push do
23
- system 'bundle exec rubocop -A'
24
- system 'git add .'
25
- system "git commit -m 'Update #{Time.now}'"
26
- system 'git pull'
27
- system 'git push'
28
- end
29
-
30
- # Documentation task
31
- task :doc do
32
- system 'bundle exec yard doc'
33
- end
34
-
35
- # Clean build artifacts
36
- task :clean do
37
- system 'rm -f *.gem'
38
- system 'rm -rf doc/'
39
- system 'rm -rf coverage/'
40
- end
41
-
42
- # Install gem locally
43
- task :install do
44
- system 'gem build kdeploy.gemspec'
45
- system "gem install kdeploy-#{Kdeploy::VERSION}.gem"
46
- end
data/bin/kdeploy DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require_relative '../lib/kdeploy'
5
- require_relative '../lib/kdeploy/cli'
6
-
7
- Kdeploy::CLI.start(ARGV)
data/kdeploy.gemspec DELETED
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'lib/kdeploy/version'
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = 'kdeploy'
7
- spec.version = Kdeploy::VERSION
8
- spec.authors = ['Kdeploy Team']
9
- spec.email = ['kevin197011@outlook.com']
10
-
11
- spec.summary = 'Lightweight agentless deployment tool with DSL, heredoc, and ERB template support'
12
- spec.description = <<~DESC
13
- Kdeploy is a lightweight, agentless deployment tool similar to Chef, Puppet, and Ansible.
14
- It uses Ruby DSL for defining deployment pipelines with support for inventory management,
15
- parallel execution, SSH-based remote operations, heredoc syntax for multi-line scripts,
16
- and ERB templates for dynamic configuration generation.
17
- DESC
18
- spec.homepage = 'https://github.com/kevin197011/kdeploy'
19
- spec.license = 'MIT'
20
- spec.required_ruby_version = '>= 3.0.0'
21
-
22
- spec.metadata['allowed_push_host'] = 'https://rubygems.org'
23
- spec.metadata['homepage_uri'] = spec.homepage
24
- spec.metadata['source_code_uri'] = 'https://github.com/kevin197011/kdeploy'
25
- spec.metadata['changelog_uri'] = 'https://github.com/kevin197011/kdeploy/blob/main/CHANGELOG.md'
26
- spec.metadata['rubygems_mfa_required'] = 'true'
27
-
28
- # Specify which files should be added to the gem when it is released.
29
- spec.files = Dir.chdir(__dir__) do
30
- `git ls-files -z`.split("\x0").reject do |f|
31
- (File.expand_path(f) == __FILE__) ||
32
- f.start_with?(*%w[exe/ test/ spec/ features/ .git .circleci appveyor Gemfile])
33
- end
34
- end
35
- spec.bindir = 'bin'
36
- spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
37
- spec.require_paths = ['lib']
38
-
39
- # Dependencies
40
- spec.add_dependency 'colorize', '~> 0.8'
41
- spec.add_dependency 'concurrent-ruby', '~> 1.2'
42
- spec.add_dependency 'net-scp', '~> 4.0'
43
- spec.add_dependency 'net-ssh', '~> 7.0'
44
- spec.add_dependency 'thor', '~> 1.3'
45
- spec.add_dependency 'tty-prompt', '~> 0.23'
46
- spec.add_dependency 'yaml', '~> 0.2'
47
-
48
- # NOTE: Development dependencies are managed in Gemfile
49
- end
@@ -1,182 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Command class for executing commands on remote hosts
5
- class Command
6
- attr_reader :name, :command, :options, :result
7
-
8
- def initialize(name, command, options = {})
9
- @name = name
10
- @command = command
11
- @options = default_options.merge(options)
12
- @global_variables = options.delete(:global_variables) || {}
13
- @result = nil
14
- end
15
-
16
- # Execute command on specified host
17
- # @param host [Host] Target host
18
- # @param connection [SSHConnection] SSH connection
19
- # @return [Boolean] True if command succeeded
20
- def execute(host, connection)
21
- start_time = Time.now
22
- processed_command = process_command_template(host)
23
-
24
- log_command_start(host, processed_command)
25
- @result = execute_with_retry(connection, processed_command)
26
- duration = Time.now - start_time
27
-
28
- log_result(host, duration)
29
- record_statistics(host.hostname, duration, @result[:success])
30
-
31
- @result[:success]
32
- rescue StandardError => e
33
- handle_execution_error(host, e, start_time)
34
- end
35
-
36
- # Check if command should run on host
37
- # @param host [Host] Target host
38
- # @return [Boolean] True if command should run
39
- def should_run_on?(host)
40
- return true unless @options[:only] || @options[:except]
41
-
42
- if @options[:only]
43
- roles = Array(@options[:only])
44
- return roles.any? { |role| host.has_role?(role) }
45
- end
46
-
47
- if @options[:except]
48
- roles = Array(@options[:except])
49
- return roles.none? { |role| host.has_role?(role) }
50
- end
51
-
52
- true
53
- end
54
-
55
- private
56
-
57
- def default_options
58
- {
59
- timeout: nil,
60
- retry_count: nil,
61
- retry_delay: nil,
62
- ignore_errors: false,
63
- only: nil,
64
- except: nil
65
- }
66
- end
67
-
68
- def process_command_template(host)
69
- processed = @command.dup
70
- process_global_variables(processed)
71
- process_host_variables(processed, host)
72
- process_host_info(processed, host)
73
- end
74
-
75
- def process_global_variables(command)
76
- @global_variables.each_with_object(command) do |(key, value), cmd|
77
- cmd.gsub!("{{#{key}}}", value.to_s)
78
- cmd.gsub!("${#{key}}", value.to_s)
79
- end
80
- end
81
-
82
- def process_host_variables(command, host)
83
- host.vars.each_with_object(command) do |(key, value), cmd|
84
- cmd.gsub!("{{#{key}}}", value.to_s)
85
- cmd.gsub!("${#{key}}", value.to_s)
86
- end
87
- end
88
-
89
- def process_host_info(command, host)
90
- command.gsub('{{hostname}}', host.hostname)
91
- .gsub('{{user}}', host.user)
92
- .gsub('{{port}}', host.port.to_s)
93
- end
94
-
95
- def execute_with_retry(connection, command)
96
- retry_count = @options[:retry_count] || Kdeploy.configuration&.retry_count || 0
97
- retry_delay = @options[:retry_delay] || Kdeploy.configuration&.retry_delay || 1
98
-
99
- result = nil
100
- attempts = 0
101
-
102
- loop do
103
- attempts += 1
104
- result = connection.execute(command, timeout: @options[:timeout])
105
-
106
- break if result[:success] || attempts > retry_count
107
-
108
- if attempts <= retry_count
109
- log_retry_attempt(attempts, retry_count, retry_delay)
110
- sleep(retry_delay)
111
- end
112
- end
113
-
114
- result
115
- end
116
-
117
- def log_command_start(host, command)
118
- KdeployLogger.info("🚀 Executing '#{@name}' on #{host}")
119
- KdeployLogger.debug(" Command: #{command}")
120
- end
121
-
122
- def log_retry_attempt(attempts, retry_count, retry_delay)
123
- KdeployLogger.warn(
124
- "Command '#{@name}' failed (attempt #{attempts}/#{retry_count + 1}), " \
125
- "retrying in #{retry_delay}s..."
126
- )
127
- end
128
-
129
- def log_result(host, duration)
130
- if @result[:success]
131
- log_success(host, duration)
132
- else
133
- log_failure(host, duration)
134
- end
135
- end
136
-
137
- def log_success(host, duration)
138
- KdeployLogger.info("✅ Command '#{@name}' completed on #{host} in #{duration.round(2)}s")
139
- return if @result[:stdout].strip.empty?
140
-
141
- KdeployLogger.info('📤 Output:')
142
- @result[:stdout].strip.split("\n").each do |line|
143
- KdeployLogger.info(" #{line}")
144
- end
145
- end
146
-
147
- def log_failure(host, duration)
148
- level = @options[:ignore_errors] ? :warn : :error
149
- icon = @options[:ignore_errors] ? '⚠️' : '❌'
150
-
151
- KdeployLogger.send(
152
- level,
153
- "#{icon} Command '#{@name}' failed on #{host} in #{duration.round(2)}s " \
154
- "(exit code: #{@result[:exit_code]})"
155
- )
156
-
157
- KdeployLogger.send(level, "📤 STDERR: #{@result[:stderr]}") unless @result[:stderr].empty?
158
- KdeployLogger.send(level, "📤 STDOUT: #{@result[:stdout]}") unless @result[:stdout].strip.empty?
159
- end
160
-
161
- def handle_execution_error(host, error, start_time)
162
- duration = Time.now - start_time
163
- KdeployLogger.error(
164
- "Command '#{@name}' failed on #{host} after #{duration.round(2)}s: #{error.message}"
165
- )
166
-
167
- @result = {
168
- stdout: '',
169
- stderr: error.message,
170
- exit_code: 1,
171
- success: false
172
- }
173
-
174
- record_statistics(host.hostname, duration, false)
175
- false
176
- end
177
-
178
- def record_statistics(hostname, duration, success)
179
- Kdeploy.statistics.record_command(@name, hostname, success, duration)
180
- end
181
- end
182
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Configuration class for managing global settings
5
- class Configuration
6
- attr_accessor :max_concurrent_tasks, :ssh_timeout, :command_timeout,
7
- :retry_count, :retry_delay, :log_level, :log_file,
8
- :default_user, :default_port, :ssh_options, :inventory_file,
9
- :template_dir
10
-
11
- def initialize
12
- set_default_values
13
- end
14
-
15
- # Load configuration from YAML file
16
- # @param config_file [String] Path to configuration file
17
- # @return [void]
18
- def load_from_file(config_file)
19
- return unless File.exist?(config_file)
20
-
21
- config = YAML.load_file(config_file)
22
- return unless config.is_a?(Hash)
23
-
24
- apply_configuration(config)
25
- end
26
-
27
- # Merge SSH options with defaults
28
- # @param options [Hash] SSH options to merge
29
- # @return [Hash] Merged SSH options
30
- def merged_ssh_options(options = {})
31
- ssh_options.merge(options)
32
- end
33
-
34
- private
35
-
36
- def set_default_values
37
- set_default_timeouts
38
- set_default_retry_settings
39
- set_default_logging
40
- set_default_ssh_settings
41
- set_default_paths
42
- end
43
-
44
- def set_default_timeouts
45
- @max_concurrent_tasks = 10
46
- @ssh_timeout = 30
47
- @command_timeout = 300
48
- end
49
-
50
- def set_default_retry_settings
51
- @retry_count = 3
52
- @retry_delay = 1
53
- end
54
-
55
- def set_default_logging
56
- @log_level = :info
57
- @log_file = nil
58
- end
59
-
60
- def set_default_ssh_settings
61
- @default_user = ENV.fetch('USER', 'root')
62
- @default_port = 22
63
- @ssh_options = {
64
- verify_host_key: :never,
65
- non_interactive: true,
66
- use_agent: true,
67
- forward_agent: false
68
- }
69
- end
70
-
71
- def set_default_paths
72
- @inventory_file = 'inventory.yml'
73
- @template_dir = 'templates'
74
- end
75
-
76
- def apply_configuration(config)
77
- config.each do |key, value|
78
- method_name = "#{key}="
79
- send(method_name, value) if respond_to?(method_name)
80
- end
81
- end
82
- end
83
- end
data/lib/kdeploy/host.rb DELETED
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Host class for managing remote host configuration and connection details
5
- class Host
6
- attr_reader :hostname, :user, :port, :ssh_options, :roles, :vars
7
-
8
- def initialize(hostname, user: nil, port: nil, ssh_options: {}, roles: [], vars: {})
9
- @hostname = hostname
10
- @user = user || Kdeploy.configuration&.default_user || ENV.fetch('USER', nil)
11
- @port = port || Kdeploy.configuration&.default_port || 22
12
- @ssh_options = ssh_options
13
- @roles = Array(roles)
14
- @vars = vars || {}
15
- end
16
-
17
- # Check if host has specific role
18
- # @param role [String, Symbol] Role to check
19
- # @return [Boolean] True if host has role
20
- def has_role?(role)
21
- @roles.include?(role.to_s) || @roles.include?(role.to_sym)
22
- end
23
-
24
- # Get variable value
25
- # @param key [String, Symbol] Variable key
26
- # @return [Object] Variable value
27
- def var(key)
28
- @vars[key.to_s] || @vars[key.to_sym]
29
- end
30
-
31
- # Set variable value
32
- # @param key [String, Symbol] Variable key
33
- # @param value [Object] Variable value
34
- # @return [Object] Set value
35
- def set_var(key, value)
36
- @vars[key.to_s] = value
37
- end
38
-
39
- # Get connection string for display
40
- # @return [String] Connection string
41
- def connection_string
42
- "#{@user}@#{@hostname}:#{@port}"
43
- end
44
-
45
- # Get SSH connection options
46
- # @return [Hash] SSH options
47
- def connection_options
48
- base_options = Kdeploy.configuration&.merged_ssh_options(@ssh_options) || @ssh_options
49
- base_options.merge(
50
- timeout: Kdeploy.configuration&.ssh_timeout || 30
51
- )
52
- end
53
-
54
- # String representation of the host
55
- # @return [String] Connection string
56
- def to_s
57
- connection_string
58
- end
59
-
60
- # Detailed string representation of the host
61
- # @return [String] Host details
62
- def inspect
63
- "#<Kdeploy::Host #{connection_string} roles=#{@roles} vars=#{@vars.keys}>"
64
- end
65
-
66
- # Compare hosts for equality
67
- # @param other [Host] Host to compare with
68
- # @return [Boolean] True if hosts are equal
69
- def ==(other)
70
- return false unless other.is_a?(Host)
71
-
72
- hostname == other.hostname &&
73
- user == other.user &&
74
- port == other.port
75
- end
76
-
77
- alias eql? ==
78
-
79
- # Generate hash code for host
80
- # @return [Integer] Hash code
81
- def hash
82
- [hostname, user, port].hash
83
- end
84
- end
85
- end
@@ -1,243 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Inventory class for managing host inventory and configuration
5
- class Inventory
6
- attr_reader :hosts, :groups, :vars
7
-
8
- def initialize(inventory_file = nil)
9
- @hosts = {}
10
- @groups = {}
11
- @vars = {}
12
- load_from_file(inventory_file) if inventory_file && File.exist?(inventory_file)
13
- end
14
-
15
- # Load inventory from YAML file
16
- # @param inventory_file [String] Path to inventory file
17
- # @raise [ConfigurationError] If inventory file is invalid
18
- def load_from_file(inventory_file)
19
- inventory_data = YAML.load_file(inventory_file)
20
- parse_inventory(inventory_data)
21
- rescue Psych::SyntaxError => e
22
- raise ConfigurationError, "Invalid YAML syntax in inventory file: #{e.message}"
23
- rescue StandardError => e
24
- raise ConfigurationError, "Failed to load inventory file: #{e.message}"
25
- end
26
-
27
- # Get all hosts in a group
28
- # @param group_name [String, Symbol] Group name
29
- # @return [Array<Host>] Hosts in the group
30
- def hosts_in_group(group_name)
31
- group_name = group_name.to_s
32
- return [] unless @groups[group_name]
33
-
34
- @groups[group_name][:hosts].map { |hostname| @hosts[hostname] }.compact
35
- end
36
-
37
- # Get all hosts with specific role
38
- # @param role [String, Symbol] Role name
39
- # @return [Array<Host>] Hosts with the role
40
- def hosts_with_role(role)
41
- @hosts.values.select { |host| host.has_role?(role) }
42
- end
43
-
44
- # Get all hosts
45
- # @return [Array<Host>] All hosts
46
- def all_hosts
47
- @hosts.values
48
- end
49
-
50
- # Get host by hostname
51
- # @param hostname [String] Hostname
52
- # @return [Host, nil] Host object or nil
53
- def host(hostname)
54
- @hosts[hostname]
55
- end
56
-
57
- # Get group variable
58
- # @param group_name [String, Symbol] Group name
59
- # @param var_name [String, Symbol] Variable name
60
- # @return [Object] Variable value
61
- def group_var(group_name, var_name)
62
- group_name = group_name.to_s
63
- return nil unless @groups[group_name]
64
-
65
- @groups[group_name][:vars][var_name.to_s] || @groups[group_name][:vars][var_name.to_sym]
66
- end
67
-
68
- # Get global variable
69
- # @param var_name [String, Symbol] Variable name
70
- # @return [Object] Variable value
71
- def global_var(var_name)
72
- @vars[var_name.to_s] || @vars[var_name.to_sym]
73
- end
74
-
75
- # Export inventory summary
76
- # @return [Hash] Inventory summary
77
- def summary
78
- {
79
- total_hosts: @hosts.size,
80
- total_groups: @groups.size,
81
- hosts: @hosts.keys,
82
- groups: @groups.keys
83
- }
84
- end
85
-
86
- private
87
-
88
- def parse_inventory(inventory_data)
89
- return unless inventory_data.is_a?(Hash)
90
-
91
- @vars = extract_vars(inventory_data)
92
- parse_groups(inventory_data)
93
- parse_hosts(inventory_data)
94
- apply_group_variables
95
- end
96
-
97
- def extract_vars(data)
98
- data['vars'] || data[:vars] || {}
99
- end
100
-
101
- def parse_groups(inventory_data)
102
- groups_data = inventory_data['groups'] || inventory_data[:groups] || {}
103
-
104
- groups_data.each do |group_name, group_config|
105
- process_group(group_name.to_s, group_config || {})
106
- end
107
-
108
- resolve_group_children
109
- end
110
-
111
- def process_group(group_name, group_config)
112
- @groups[group_name] = {
113
- hosts: extract_group_hosts(group_config),
114
- vars: extract_group_vars(group_config),
115
- children: extract_group_children(group_config)
116
- }
117
- end
118
-
119
- def extract_group_hosts(config)
120
- Array(config['hosts'] || config[:hosts] || [])
121
- end
122
-
123
- def extract_group_vars(config)
124
- config['vars'] || config[:vars] || {}
125
- end
126
-
127
- def extract_group_children(config)
128
- Array(config['children'] || config[:children] || [])
129
- end
130
-
131
- def parse_hosts(inventory_data)
132
- hosts_data = inventory_data['hosts'] || inventory_data[:hosts] || {}
133
-
134
- hosts_data.each do |hostname, host_config|
135
- process_host(hostname, host_config || {})
136
- end
137
- end
138
-
139
- def process_host(hostname, host_config)
140
- host_groups = find_host_groups(hostname)
141
- host_roles = Array(host_config['roles'] || host_config[:roles] || host_groups)
142
-
143
- @hosts[hostname] = create_host(hostname, host_config, host_roles)
144
- end
145
-
146
- def create_host(hostname, config, roles)
147
- Host.new(
148
- hostname,
149
- user: config['user'] || config[:user],
150
- port: config['port'] || config[:port],
151
- ssh_options: parse_ssh_options(config),
152
- roles: roles,
153
- vars: config['vars'] || config[:vars] || {}
154
- )
155
- end
156
-
157
- def parse_ssh_options(host_config)
158
- ssh_config = host_config['ssh'] || host_config[:ssh] || {}
159
- options = {}
160
-
161
- process_ssh_key_options(ssh_config, options)
162
- process_ssh_auth_options(ssh_config, options)
163
- process_ssh_verification_options(ssh_config, options)
164
- process_ssh_timeout_option(ssh_config, options)
165
-
166
- options
167
- end
168
-
169
- def process_ssh_key_options(ssh_config, options)
170
- if ssh_config['key_file'] || ssh_config[:key_file]
171
- key_file = ssh_config['key_file'] || ssh_config[:key_file]
172
- options[:keys] = [File.expand_path(key_file)]
173
- end
174
-
175
- return unless ssh_config['key_data'] || ssh_config[:key_data]
176
-
177
- options[:key_data] = Array(ssh_config['key_data'] || ssh_config[:key_data])
178
- end
179
-
180
- def process_ssh_auth_options(ssh_config, options)
181
- options[:password] = ssh_config['password'] || ssh_config[:password] if ssh_config['password'] || ssh_config[:password]
182
- return unless ssh_config['passphrase'] || ssh_config[:passphrase]
183
-
184
- options[:passphrase] = ssh_config['passphrase'] || ssh_config[:passphrase]
185
- end
186
-
187
- def process_ssh_verification_options(ssh_config, options)
188
- return unless ssh_config.key?('verify_host_key') || ssh_config.key?(:verify_host_key)
189
-
190
- verify_host_key = ssh_config['verify_host_key'] || ssh_config[:verify_host_key]
191
- options[:verify_host_key] = verify_host_key ? :always : :never
192
- end
193
-
194
- def process_ssh_timeout_option(ssh_config, options)
195
- options[:timeout] = ssh_config['timeout'] || ssh_config[:timeout] if ssh_config['timeout'] || ssh_config[:timeout]
196
- end
197
-
198
- def find_host_groups(hostname)
199
- @groups.each_with_object([]) do |(group_name, group_config), groups|
200
- groups << group_name if group_config[:hosts].include?(hostname)
201
- end
202
- end
203
-
204
- def resolve_group_children
205
- @groups.each do |group_name, group_config|
206
- process_group_children(group_name, group_config)
207
- end
208
- end
209
-
210
- def process_group_children(group_name, group_config)
211
- group_config[:children].each do |child_group|
212
- next unless @groups[child_group]
213
-
214
- @groups[group_name][:hosts].concat(@groups[child_group][:hosts])
215
- end
216
-
217
- @groups[group_name][:hosts].uniq!
218
- end
219
-
220
- def apply_group_variables
221
- @hosts.each do |hostname, host|
222
- host_groups = find_host_groups(hostname)
223
- apply_group_vars_to_host(host, host_groups)
224
- apply_global_vars_to_host(host)
225
- end
226
- end
227
-
228
- def apply_group_vars_to_host(host, host_groups)
229
- host_groups.each do |group_name|
230
- group_vars = @groups[group_name][:vars] || {}
231
- group_vars.each do |key, value|
232
- host.set_var(key, value) unless host.var(key)
233
- end
234
- end
235
- end
236
-
237
- def apply_global_vars_to_host(host)
238
- @vars.each do |key, value|
239
- host.set_var(key, value) unless host.var(key)
240
- end
241
- end
242
- end
243
- end