kdeploy 0.2.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.
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Kdeploy
6
+ class Initializer
7
+ def initialize(target_dir = '.')
8
+ @target_dir = File.expand_path(target_dir)
9
+ end
10
+
11
+ def run
12
+ create_directory_structure
13
+ create_deploy_file
14
+ create_config_files
15
+ create_readme
16
+ show_success_message
17
+ end
18
+
19
+ private
20
+
21
+ def create_directory_structure
22
+ FileUtils.mkdir_p(@target_dir) unless @target_dir == '.'
23
+ FileUtils.mkdir_p(File.join(@target_dir, 'config'))
24
+ end
25
+
26
+ def create_deploy_file
27
+ File.write(File.join(@target_dir, 'deploy.rb'), <<~RUBY)
28
+ # frozen_string_literal: true
29
+
30
+ # Define hosts
31
+ host "web01", user: "ubuntu", ip: "10.0.0.1", key: "~/.ssh/id_rsa"
32
+ host "web02", user: "ubuntu", ip: "10.0.0.2", key: "~/.ssh/id_rsa"
33
+
34
+ # Define roles
35
+ role :web, %w[web01 web02]
36
+ role :db, %w[db01]
37
+
38
+ # Define inventory
39
+ inventory do
40
+ host "db01", user: "root", ip: "10.0.0.3", key: "~/.ssh/id_rsa"
41
+ end
42
+
43
+ # Define deployment task for web servers
44
+ task :deploy_web, roles: :web do
45
+ # Stop service
46
+ run "sudo systemctl stop nginx"
47
+
48
+ # Upload configuration using ERB template
49
+ upload_template "./config/nginx.conf.erb", "/etc/nginx/nginx.conf",
50
+ domain_name: "example.com",
51
+ port: 3000,
52
+ worker_processes: 4,
53
+ worker_connections: 2048
54
+
55
+ # Upload static configuration
56
+ upload "./config/app.conf", "/etc/nginx/conf.d/app.conf"
57
+
58
+ # Restart service
59
+ run "sudo systemctl start nginx"
60
+
61
+ # Check status
62
+ run "sudo systemctl status nginx"
63
+ end
64
+
65
+ # Define backup task for database servers
66
+ task :backup_db, roles: :db do
67
+ run "tar -czf /tmp/backup.tar.gz /var/lib/postgresql/data"
68
+ run "aws s3 cp /tmp/backup.tar.gz s3://my-backups/"
69
+ run "rm /tmp/backup.tar.gz"
70
+ end
71
+
72
+ # Define task for specific hosts
73
+ task :maintenance, on: %w[web01] do
74
+ run "sudo systemctl stop nginx"
75
+ run "sudo apt-get update && sudo apt-get upgrade -y"
76
+ run "sudo systemctl start nginx"
77
+ end
78
+
79
+ # Define task for all hosts
80
+ task :update do
81
+ run "sudo apt-get update && sudo apt-get upgrade -y"
82
+ end
83
+ RUBY
84
+ end
85
+
86
+ def create_config_files
87
+ # 创建配置目录
88
+ config_dir = File.join(@target_dir, 'config')
89
+ FileUtils.mkdir_p(config_dir)
90
+
91
+ # 创建 Nginx ERB 模板
92
+ File.write(File.join(config_dir, 'nginx.conf.erb'), <<~CONF)
93
+ user nginx;
94
+ worker_processes <%= worker_processes %>;
95
+ error_log /var/log/nginx/error.log;
96
+ pid /run/nginx.pid;
97
+
98
+ events {
99
+ worker_connections <%= worker_connections %>;
100
+ }
101
+
102
+ http {
103
+ include /etc/nginx/mime.types;
104
+ default_type application/octet-stream;
105
+
106
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
107
+ '$status $body_bytes_sent "$http_referer" '
108
+ '"$http_user_agent" "$http_x_forwarded_for"';
109
+
110
+ access_log /var/log/nginx/access.log main;
111
+
112
+ sendfile on;
113
+ tcp_nopush on;
114
+ tcp_nodelay on;
115
+ keepalive_timeout 65;
116
+ types_hash_max_size 2048;
117
+
118
+ upstream app_servers {
119
+ server 127.0.0.1:<%= port %>;
120
+ }
121
+
122
+ server {
123
+ listen 80;
124
+ server_name <%= domain_name %>;
125
+
126
+ location / {
127
+ proxy_pass http://app_servers;
128
+ proxy_http_version 1.1;
129
+ proxy_set_header Upgrade $http_upgrade;
130
+ proxy_set_header Connection 'upgrade';
131
+ proxy_set_header Host $host;
132
+ proxy_cache_bypass $http_upgrade;
133
+ }
134
+
135
+ error_page 500 502 503 504 /50x.html;
136
+ location = /50x.html {
137
+ root /usr/share/nginx/html;
138
+ }
139
+ }
140
+ }
141
+ CONF
142
+
143
+ # 创建静态配置文件示例
144
+ File.write(File.join(config_dir, 'app.conf'), <<~CONF)
145
+ location /api {
146
+ proxy_pass http://localhost:3000;
147
+ proxy_http_version 1.1;
148
+ proxy_set_header Upgrade $http_upgrade;
149
+ proxy_set_header Connection 'upgrade';
150
+ proxy_set_header Host $host;
151
+ proxy_cache_bypass $http_upgrade;
152
+ }
153
+ CONF
154
+ end
155
+
156
+ def create_readme
157
+ File.write(File.join(@target_dir, 'README.md'), <<~MD)
158
+ # Deployment Project
159
+
160
+ This is a deployment project created with Kdeploy.
161
+
162
+ ## Structure
163
+
164
+ ```
165
+ .
166
+ ├── deploy.rb # Deployment tasks
167
+ ├── config/ # Configuration files
168
+ │ ├── nginx.conf.erb # Nginx configuration template
169
+ │ └── app.conf # Static configuration
170
+ └── README.md # This file
171
+ ```
172
+
173
+ ## Configuration Templates
174
+
175
+ The project uses ERB templates for dynamic configuration. For example, in `nginx.conf.erb`:
176
+
177
+ ```erb
178
+ worker_processes <%= worker_processes %>;
179
+ server_name <%= domain_name %>;
180
+ ```
181
+
182
+ Variables are passed when uploading the template:
183
+
184
+ ```ruby
185
+ upload_template "./config/nginx.conf.erb", "/etc/nginx/nginx.conf",
186
+ domain_name: "example.com",
187
+ worker_processes: 4
188
+ ```
189
+
190
+ ## Usage
191
+
192
+ ```bash
193
+ # Show what would be done
194
+ kdeploy execute deploy.rb --dry-run
195
+
196
+ # Deploy to web servers
197
+ kdeploy execute deploy.rb deploy_web
198
+
199
+ # Backup database
200
+ kdeploy execute deploy.rb backup_db
201
+
202
+ # Run maintenance on web01
203
+ kdeploy execute deploy.rb maintenance
204
+
205
+ # Update all hosts
206
+ kdeploy execute deploy.rb update
207
+
208
+ # Deploy to specific hosts
209
+ kdeploy execute deploy.rb deploy_web --limit web01,web02
210
+ ```
211
+ MD
212
+ end
213
+
214
+ def show_success_message
215
+ pastel = Pastel.new
216
+ puts Kdeploy::Banner.show_success("Project initialized in #{@target_dir}")
217
+ puts <<~INFO
218
+ #{pastel.bright_white('Created files:')}
219
+ #{pastel.dim(" #{File.join(@target_dir, 'deploy.rb')}")}
220
+ #{pastel.dim(" #{File.join(@target_dir, 'config/nginx.conf.erb')}")}
221
+ #{pastel.dim(" #{File.join(@target_dir, 'config/app.conf')}")}
222
+ #{pastel.dim(" #{File.join(@target_dir, 'README.md')}")}
223
+
224
+ #{pastel.bright_white('Try running:')}
225
+ #{pastel.bright_cyan(" kdeploy execute #{File.join(@target_dir, 'deploy.rb')} deploy_web --dry-run")}
226
+ INFO
227
+ end
228
+ end
229
+ end
@@ -1,190 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'concurrent'
4
+
3
5
  module Kdeploy
4
6
  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}")
7
+ def initialize(hosts, tasks, parallel: 5)
8
+ @hosts = hosts
9
+ @tasks = tasks
10
+ @parallel = parallel
11
+ @pool = Concurrent::FixedThreadPool.new(@parallel)
12
+ @results = Concurrent::Hash.new
13
+ end
14
+
15
+ def run(task_name)
16
+ task = @tasks[task_name]
17
+ raise "Task not found: #{task_name}" unless task
18
+
19
+ futures = @hosts.map do |name, config|
20
+ Concurrent::Future.execute(executor: @pool) do
21
+ executor = Executor.new(config)
22
+ result = { status: :success, output: [] }
23
+
24
+ task[:block].call.each do |command|
25
+ case command[:type]
26
+ when :run
27
+ output = executor.execute(command[:command])
28
+ result[:output] << { command: command[:command], output: output }
29
+ when :upload
30
+ executor.upload(command[:source], command[:destination])
31
+ result[:output] << { command: "upload: #{command[:source]} -> #{command[:destination]}" }
32
+ when :upload_template
33
+ executor.upload_template(command[:source], command[:destination], command[:variables])
34
+ result[:output] << { command: "upload_template: #{command[:source]} -> #{command[:destination]}" }
35
+ end
36
+ end
37
+
38
+ @results[name] = result
39
+ rescue StandardError => e
40
+ @results[name] = { status: :failed, error: e.message }
128
41
  end
129
42
  end
130
43
 
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
44
+ futures.each(&:wait)
45
+ @results
46
+ ensure
47
+ @pool.shutdown
188
48
  end
189
49
  end
190
50
  end
@@ -1,173 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'erb'
4
+ require 'ostruct'
5
+
3
6
  module Kdeploy
4
- # ERB template management class
5
7
  class Template
6
- attr_reader :path, :content, :variables
7
-
8
- def initialize(template_path, variables = {})
9
- @path = template_path
10
- @variables = variables
11
- @content = load_template
12
- end
13
-
14
- # Render template with variables
15
- # @param additional_vars [Hash] Additional variables to merge
16
- # @return [String] Rendered content
17
- def render(additional_vars = {})
18
- all_vars = @variables.merge(additional_vars)
19
-
20
- # Create binding with variables
21
- template_binding = create_binding(all_vars)
22
-
23
- # Render ERB template
24
- erb = ERB.new(@content, trim_mode: '-')
25
- erb.result(template_binding)
26
- rescue StandardError => e
27
- raise TemplateError, "Failed to render template #{@path}: #{e.message}"
8
+ def self.render(template_path, variables = {})
9
+ template_content = File.read(template_path)
10
+ context = OpenStruct.new(variables)
11
+ ERB.new(template_content).result(context.instance_eval { binding })
28
12
  end
29
13
 
30
- # Render and save to file
31
- # @param output_path [String] Output file path
32
- # @param additional_vars [Hash] Additional variables
33
- def render_to_file(output_path, additional_vars = {})
34
- rendered_content = render(additional_vars)
14
+ def self.render_and_upload(executor, template_path, destination, variables = {})
15
+ rendered_content = render(template_path, variables)
35
16
 
36
- # Ensure output directory exists
37
- FileUtils.mkdir_p(File.dirname(output_path))
17
+ # 创建临时文件
18
+ temp_file = Tempfile.new('kdeploy')
19
+ begin
20
+ temp_file.write(rendered_content)
21
+ temp_file.close
38
22
 
39
- # Write rendered content
40
- File.write(output_path, rendered_content)
41
-
42
- KdeployLogger.info("Template rendered to: #{output_path}")
43
- output_path
44
- end
45
-
46
- # Check if template file exists
47
- # @return [Boolean] True if template exists
48
- def exist?
49
- File.exist?(@path)
50
- end
51
-
52
- # Get template modification time
53
- # @return [Time] Template file mtime
54
- def mtime
55
- File.mtime(@path) if exist?
56
- end
57
-
58
- private
59
-
60
- # Load template content from file
61
- # @return [String] Template content
62
- def load_template
63
- raise TemplateError, "Template file not found: #{@path}" unless File.exist?(@path)
64
-
65
- File.read(@path)
66
- rescue StandardError => e
67
- raise TemplateError, "Failed to load template #{@path}: #{e.message}"
68
- end
69
-
70
- # Create binding with variables
71
- # @param vars [Hash] Variables hash
72
- # @return [Binding] Binding object with variables
73
- def create_binding(vars)
74
- # Create a clean binding
75
- template_binding = binding
76
-
77
- # Define variables in the binding
78
- vars.each do |key, value|
79
- template_binding.local_variable_set(key.to_sym, value)
23
+ # 上传渲染后的文件
24
+ executor.upload(temp_file.path, destination)
25
+ ensure
26
+ temp_file.unlink
80
27
  end
81
-
82
- # Define helper methods
83
- template_binding.local_variable_set(:hostname, vars[:hostname] || vars['hostname'])
84
- template_binding.local_variable_set(:user, vars[:user] || vars['user'])
85
- template_binding.local_variable_set(:port, vars[:port] || vars['port'])
86
-
87
- template_binding
88
28
  end
89
29
  end
90
-
91
- # Template manager for handling multiple templates
92
- class TemplateManager
93
- attr_reader :template_dir, :global_variables
94
-
95
- def initialize(template_dir = 'templates', global_variables = {})
96
- @template_dir = template_dir
97
- @global_variables = global_variables
98
- @templates = {}
99
- end
100
-
101
- # Load template by name
102
- # @param template_name [String] Template name (without .erb extension)
103
- # @param variables [Hash] Template variables
104
- # @return [Template] Template object
105
- def load_template(template_name, variables = {})
106
- template_path = resolve_template_path(template_name)
107
- all_variables = @global_variables.merge(variables)
108
-
109
- @templates[template_name] = Template.new(template_path, all_variables)
110
- end
111
-
112
- # Render template by name
113
- # @param template_name [String] Template name
114
- # @param variables [Hash] Additional variables
115
- # @return [String] Rendered content
116
- def render(template_name, variables = {})
117
- template = @templates[template_name] || load_template(template_name, variables)
118
- template.render(variables)
119
- end
120
-
121
- # Render template to file
122
- # @param template_name [String] Template name
123
- # @param output_path [String] Output file path
124
- # @param variables [Hash] Additional variables
125
- # @return [String] Output file path
126
- def render_to_file(template_name, output_path, variables = {})
127
- template = @templates[template_name] || load_template(template_name, variables)
128
- template.render_to_file(output_path, variables)
129
- end
130
-
131
- # List available templates
132
- # @return [Array<String>] Template names
133
- def list_templates
134
- return [] unless Dir.exist?(@template_dir)
135
-
136
- Dir.glob("#{@template_dir}/**/*.erb").map do |path|
137
- File.basename(path, '.erb')
138
- end
139
- end
140
-
141
- # Set global variables
142
- # @param variables [Hash] Global variables
143
- def global_variables=(variables)
144
- @global_variables.merge!(variables)
145
- end
146
-
147
- private
148
-
149
- # Resolve template file path
150
- # @param template_name [String] Template name
151
- # @return [String] Full template path
152
- def resolve_template_path(template_name)
153
- # Add .erb extension if not present
154
- template_name += '.erb' unless template_name.end_with?('.erb')
155
-
156
- # Try template directory first
157
- template_path = File.join(@template_dir, template_name)
158
- return template_path if File.exist?(template_path)
159
-
160
- # Try relative to current directory
161
- return template_name if File.exist?(template_name)
162
-
163
- # Try absolute path
164
- return template_name if File.absolute_path?(template_name) && File.exist?(template_name)
165
-
166
- # Default to template directory path (will be checked by Template class)
167
- template_path
168
- end
169
- end
170
-
171
- # Template-related errors
172
- class TemplateError < StandardError; end
173
30
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kdeploy
4
- # Current version of Kdeploy
5
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
6
5
  end