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
@@ -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
|
data/lib/kdeploy/runner.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
def
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
data/lib/kdeploy/template.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
31
|
-
|
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
|
-
#
|
37
|
-
|
17
|
+
# 创建临时文件
|
18
|
+
temp_file = Tempfile.new('kdeploy')
|
19
|
+
begin
|
20
|
+
temp_file.write(rendered_content)
|
21
|
+
temp_file.close
|
38
22
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|