gjallarhorn 0.1.0.alpha → 0.1.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/.yardopts +12 -0
- data/README.md +116 -9
- data/examples/zero-downtime-deployment.rb +104 -0
- data/lib/gjallarhorn/adapter/aws.rb +652 -0
- data/lib/gjallarhorn/adapter/base.rb +125 -0
- data/lib/gjallarhorn/cli.rb +90 -4
- data/lib/gjallarhorn/configuration.rb +64 -8
- data/lib/gjallarhorn/deployer.rb +179 -9
- data/lib/gjallarhorn/deployment/basic.rb +171 -0
- data/lib/gjallarhorn/deployment/legacy.rb +40 -0
- data/lib/gjallarhorn/deployment/strategy.rb +189 -0
- data/lib/gjallarhorn/deployment/zero_downtime.rb +276 -0
- data/lib/gjallarhorn/history.rb +164 -0
- data/lib/gjallarhorn/proxy/kamal_proxy_manager.rb +36 -0
- data/lib/gjallarhorn/proxy/manager.rb +186 -0
- data/lib/gjallarhorn/proxy/nginx_manager.rb +362 -0
- data/lib/gjallarhorn/proxy/traefik_manager.rb +36 -0
- data/lib/gjallarhorn/version.rb +1 -1
- data/lib/gjallarhorn.rb +16 -0
- metadata +101 -6
- data/lib/gjallarhorn/adapters/aws.rb +0 -96
- data/lib/gjallarhorn/adapters/base.rb +0 -56
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require_relative "manager"
|
|
5
|
+
|
|
6
|
+
module Gjallarhorn
|
|
7
|
+
module Proxy
|
|
8
|
+
# Nginx proxy manager for zero-downtime deployments
|
|
9
|
+
#
|
|
10
|
+
# Manages nginx configuration updates and reloads to enable
|
|
11
|
+
# zero-downtime deployments by switching upstream servers.
|
|
12
|
+
#
|
|
13
|
+
# @since 0.1.0
|
|
14
|
+
class NginxManager < Manager
|
|
15
|
+
# Default nginx configuration paths
|
|
16
|
+
DEFAULT_NGINX_CONF_DIR = "/etc/nginx/conf.d"
|
|
17
|
+
DEFAULT_NGINX_BIN = "nginx"
|
|
18
|
+
|
|
19
|
+
# Initialize nginx proxy manager
|
|
20
|
+
#
|
|
21
|
+
# @param config [Hash] Nginx configuration
|
|
22
|
+
# @param logger [Logger] Logger instance
|
|
23
|
+
def initialize(config, logger = nil)
|
|
24
|
+
super
|
|
25
|
+
@nginx_conf_dir = config[:conf_dir] || DEFAULT_NGINX_CONF_DIR
|
|
26
|
+
@nginx_bin = config[:nginx_bin] || DEFAULT_NGINX_BIN
|
|
27
|
+
@domain = config[:domain] || config[:host]
|
|
28
|
+
@ssl_enabled = config[:ssl] || false
|
|
29
|
+
@app_port = config[:app_port] || 3000
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Switch traffic from old containers to new container
|
|
33
|
+
#
|
|
34
|
+
# @param service_name [String] Service name
|
|
35
|
+
# @param from_containers [Array<Hash>] Containers to switch traffic from (unused in nginx implementation)
|
|
36
|
+
# @param to_container [Hash] Container to switch traffic to
|
|
37
|
+
# @return [void]
|
|
38
|
+
def switch_traffic(service_name:, to_container:, from_containers: nil)
|
|
39
|
+
@logger.info "Switching nginx traffic for #{service_name} to #{to_container[:name]}"
|
|
40
|
+
|
|
41
|
+
# Generate new nginx configuration
|
|
42
|
+
new_config = generate_service_config(service_name, [to_container])
|
|
43
|
+
|
|
44
|
+
# Write configuration to file
|
|
45
|
+
write_nginx_config(service_name, new_config)
|
|
46
|
+
|
|
47
|
+
# Test nginx configuration
|
|
48
|
+
test_nginx_config
|
|
49
|
+
|
|
50
|
+
# Reload nginx gracefully
|
|
51
|
+
reload_nginx
|
|
52
|
+
|
|
53
|
+
# Verify traffic is flowing to new container
|
|
54
|
+
if verify_traffic_switch(service_name, to_container)
|
|
55
|
+
@logger.info "Successfully switched traffic to #{to_container[:name]}"
|
|
56
|
+
else
|
|
57
|
+
@logger.warn "Traffic switch completed but verification failed"
|
|
58
|
+
end
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
@logger.error "Failed to switch nginx traffic: #{e.message}"
|
|
61
|
+
raise ProxyError, "Nginx traffic switch failed: #{e.message}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get nginx proxy status
|
|
65
|
+
#
|
|
66
|
+
# @return [Hash] Nginx status information
|
|
67
|
+
def status
|
|
68
|
+
{
|
|
69
|
+
type: "nginx",
|
|
70
|
+
status: nginx_running? ? "running" : "stopped",
|
|
71
|
+
config_dir: @nginx_conf_dir,
|
|
72
|
+
upstreams: configured_upstreams,
|
|
73
|
+
last_reload: last_reload_time
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Restart nginx service
|
|
78
|
+
#
|
|
79
|
+
# @return [Boolean] True if restart successful
|
|
80
|
+
def restart
|
|
81
|
+
@logger.info "Restarting nginx..."
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
execute_nginx_command("restart")
|
|
85
|
+
@logger.info "Nginx restarted successfully"
|
|
86
|
+
true
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
@logger.error "Failed to restart nginx: #{e.message}"
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if nginx is healthy
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] True if nginx is responding
|
|
96
|
+
def healthy?
|
|
97
|
+
nginx_running? && config_syntax_valid?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Generate nginx configuration for a service
|
|
103
|
+
#
|
|
104
|
+
# @param service_name [String] Service name
|
|
105
|
+
# @param containers [Array<Hash>] Container information
|
|
106
|
+
# @return [String] Nginx configuration
|
|
107
|
+
def generate_service_config(service_name, containers)
|
|
108
|
+
upstream_config = generate_upstream_config(service_name, containers)
|
|
109
|
+
server_config = generate_server_config(service_name)
|
|
110
|
+
|
|
111
|
+
<<~NGINX
|
|
112
|
+
# Generated by Gjallarhorn for #{service_name}
|
|
113
|
+
# Generated at: #{Time.now.utc.iso8601}
|
|
114
|
+
|
|
115
|
+
#{upstream_config}
|
|
116
|
+
|
|
117
|
+
#{server_config}
|
|
118
|
+
NGINX
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Generate nginx server configuration block
|
|
122
|
+
#
|
|
123
|
+
# @param service_name [String] Service name
|
|
124
|
+
# @return [String] Server configuration block
|
|
125
|
+
def generate_server_config(service_name)
|
|
126
|
+
ssl_config = generate_ssl_config
|
|
127
|
+
security_headers = generate_security_headers
|
|
128
|
+
health_check_config = generate_health_check_config(service_name)
|
|
129
|
+
location_config = generate_location_config(service_name)
|
|
130
|
+
|
|
131
|
+
<<~SERVER
|
|
132
|
+
server {
|
|
133
|
+
listen 80;
|
|
134
|
+
#{ssl_config}
|
|
135
|
+
server_name #{@domain};
|
|
136
|
+
|
|
137
|
+
#{security_headers}
|
|
138
|
+
|
|
139
|
+
#{health_check_config}
|
|
140
|
+
|
|
141
|
+
#{location_config}
|
|
142
|
+
}
|
|
143
|
+
SERVER
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Generate health check configuration
|
|
147
|
+
#
|
|
148
|
+
# @param service_name [String] Service name
|
|
149
|
+
# @return [String] Health check location block
|
|
150
|
+
def generate_health_check_config(service_name)
|
|
151
|
+
health_path = @config[:health_check_path] || "/health"
|
|
152
|
+
|
|
153
|
+
<<~HEALTH
|
|
154
|
+
# Health check endpoint with container identification
|
|
155
|
+
location #{health_path} {
|
|
156
|
+
proxy_pass http://#{service_name}#{health_path};
|
|
157
|
+
proxy_set_header Host $host;
|
|
158
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
159
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
160
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
161
|
+
#{" "}
|
|
162
|
+
# Add headers for traffic verification
|
|
163
|
+
add_header X-Proxy-Backend $upstream_addr always;
|
|
164
|
+
add_header X-Proxy-Status $upstream_status always;
|
|
165
|
+
}
|
|
166
|
+
HEALTH
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Write nginx configuration to file
|
|
170
|
+
#
|
|
171
|
+
# @param service_name [String] Service name
|
|
172
|
+
# @param config_content [String] Configuration content
|
|
173
|
+
# @return [String] Path to written configuration file
|
|
174
|
+
def write_nginx_config(service_name, config_content)
|
|
175
|
+
config_file = File.join(@nginx_conf_dir, "gjallarhorn-#{service_name}.conf")
|
|
176
|
+
|
|
177
|
+
# Create backup of existing config if it exists
|
|
178
|
+
if File.exist?(config_file)
|
|
179
|
+
backup_file = "#{config_file}.backup-#{Time.now.strftime("%Y%m%d-%H%M%S")}"
|
|
180
|
+
FileUtils.cp(config_file, backup_file)
|
|
181
|
+
@logger.debug "Backed up existing config to #{backup_file}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Write new configuration
|
|
185
|
+
File.write(config_file, config_content)
|
|
186
|
+
@logger.debug "Wrote nginx config to #{config_file}"
|
|
187
|
+
|
|
188
|
+
config_file
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Test nginx configuration syntax
|
|
192
|
+
#
|
|
193
|
+
# @raise [ProxyError] If configuration is invalid
|
|
194
|
+
# @return [void]
|
|
195
|
+
def test_nginx_config
|
|
196
|
+
@logger.debug "Testing nginx configuration syntax..."
|
|
197
|
+
|
|
198
|
+
result = execute_nginx_command("configtest")
|
|
199
|
+
raise ProxyError, "Invalid nginx configuration: #{result[:error]}" unless result[:success]
|
|
200
|
+
|
|
201
|
+
@logger.debug "Nginx configuration syntax is valid"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Reload nginx gracefully
|
|
205
|
+
#
|
|
206
|
+
# @raise [ProxyError] If reload fails
|
|
207
|
+
# @return [void]
|
|
208
|
+
def reload_nginx
|
|
209
|
+
@logger.info "Reloading nginx configuration..."
|
|
210
|
+
|
|
211
|
+
result = execute_nginx_command("reload")
|
|
212
|
+
raise ProxyError, "Failed to reload nginx: #{result[:error]}" unless result[:success]
|
|
213
|
+
|
|
214
|
+
@logger.info "Nginx reloaded successfully"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Execute nginx command
|
|
218
|
+
#
|
|
219
|
+
# @param action [String] Action to perform (reload, restart, configtest)
|
|
220
|
+
# @return [Hash] Execution result
|
|
221
|
+
def execute_nginx_command(action)
|
|
222
|
+
case action
|
|
223
|
+
when "reload"
|
|
224
|
+
command = "#{@nginx_bin} -s reload"
|
|
225
|
+
when "restart"
|
|
226
|
+
command = "systemctl restart nginx"
|
|
227
|
+
when "configtest"
|
|
228
|
+
command = "#{@nginx_bin} -t"
|
|
229
|
+
else
|
|
230
|
+
raise ArgumentError, "Unknown nginx action: #{action}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
@logger.debug "Executing: #{command}"
|
|
234
|
+
|
|
235
|
+
result = system(command)
|
|
236
|
+
{
|
|
237
|
+
success: result,
|
|
238
|
+
error: result ? nil : "Command failed with exit code: #{$CHILD_STATUS.exitstatus}"
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Check if nginx is running
|
|
243
|
+
#
|
|
244
|
+
# @return [Boolean] True if nginx process is running
|
|
245
|
+
def nginx_running?
|
|
246
|
+
system("pgrep nginx > /dev/null 2>&1")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Check if nginx configuration syntax is valid
|
|
250
|
+
#
|
|
251
|
+
# @return [Boolean] True if configuration is valid
|
|
252
|
+
def config_syntax_valid?
|
|
253
|
+
result = execute_nginx_command("configtest")
|
|
254
|
+
result[:success]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Get configured upstream servers
|
|
258
|
+
#
|
|
259
|
+
# @return [Array<String>] List of configured upstreams
|
|
260
|
+
def configured_upstreams
|
|
261
|
+
config_files = Dir.glob(File.join(@nginx_conf_dir, "gjallarhorn-*.conf"))
|
|
262
|
+
upstreams = []
|
|
263
|
+
|
|
264
|
+
config_files.each do |file|
|
|
265
|
+
content = File.read(file)
|
|
266
|
+
upstreams.concat(content.scan(/upstream\s+(\w+)\s*{/).flatten)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
upstreams
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Get last nginx reload time
|
|
273
|
+
#
|
|
274
|
+
# @return [Time, nil] Last reload time or nil if unknown
|
|
275
|
+
def last_reload_time
|
|
276
|
+
# Try to get nginx master process start time as proxy for last reload
|
|
277
|
+
if nginx_running?
|
|
278
|
+
pid = `pgrep -f "nginx: master process"`.strip
|
|
279
|
+
unless pid.empty?
|
|
280
|
+
stat_file = "/proc/#{pid}/stat"
|
|
281
|
+
if File.exist?(stat_file)
|
|
282
|
+
boot_time = File.read("/proc/stat").match(/btime (\d+)/)[1].to_i
|
|
283
|
+
start_time_ticks = File.read(stat_file).split[21].to_i
|
|
284
|
+
clock_ticks = 100 # Typical value for USER_HZ
|
|
285
|
+
Time.at(boot_time + start_time_ticks / clock_ticks)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
rescue StandardError
|
|
290
|
+
nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Generate SSL configuration block
|
|
294
|
+
#
|
|
295
|
+
# @return [String] SSL configuration
|
|
296
|
+
def generate_ssl_config
|
|
297
|
+
return "" unless @ssl_enabled
|
|
298
|
+
|
|
299
|
+
<<~SSL
|
|
300
|
+
listen 443 ssl http2;
|
|
301
|
+
ssl_certificate /etc/letsencrypt/live/#{@domain}/fullchain.pem;
|
|
302
|
+
ssl_certificate_key /etc/letsencrypt/live/#{@domain}/privkey.pem;
|
|
303
|
+
ssl_session_timeout 1d;
|
|
304
|
+
ssl_session_cache shared:SSL:50m;
|
|
305
|
+
ssl_stapling on;
|
|
306
|
+
ssl_stapling_verify on;
|
|
307
|
+
SSL
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Generate security headers configuration
|
|
311
|
+
#
|
|
312
|
+
# @return [String] Security headers configuration
|
|
313
|
+
def generate_security_headers
|
|
314
|
+
<<~HEADERS
|
|
315
|
+
# Security headers
|
|
316
|
+
add_header X-Frame-Options DENY always;
|
|
317
|
+
add_header X-Content-Type-Options nosniff always;
|
|
318
|
+
add_header X-XSS-Protection "1; mode=block" always;
|
|
319
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
320
|
+
|
|
321
|
+
# Gjallarhorn identification headers
|
|
322
|
+
add_header X-Proxy-Type "nginx" always;
|
|
323
|
+
add_header X-Managed-By "gjallarhorn" always;
|
|
324
|
+
HEADERS
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Generate location configuration for main proxy
|
|
328
|
+
#
|
|
329
|
+
# @param service_name [String] Service name
|
|
330
|
+
# @return [String] Location configuration
|
|
331
|
+
def generate_location_config(service_name)
|
|
332
|
+
<<~LOCATION
|
|
333
|
+
location / {
|
|
334
|
+
proxy_pass http://#{service_name};
|
|
335
|
+
proxy_set_header Host $host;
|
|
336
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
337
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
338
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
339
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
340
|
+
proxy_set_header X-Forwarded-Port $server_port;
|
|
341
|
+
#{" "}
|
|
342
|
+
# Timeouts
|
|
343
|
+
proxy_connect_timeout 5s;
|
|
344
|
+
proxy_send_timeout 60s;
|
|
345
|
+
proxy_read_timeout 60s;
|
|
346
|
+
#{" "}
|
|
347
|
+
# Buffer settings
|
|
348
|
+
proxy_buffering on;
|
|
349
|
+
proxy_buffer_size 4k;
|
|
350
|
+
proxy_buffers 8 4k;
|
|
351
|
+
#{" "}
|
|
352
|
+
# Health check support
|
|
353
|
+
proxy_next_upstream error timeout http_502 http_503 http_504;
|
|
354
|
+
}
|
|
355
|
+
LOCATION
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Raised when proxy operations fail
|
|
360
|
+
class ProxyError < Error; end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "manager"
|
|
4
|
+
|
|
5
|
+
module Gjallarhorn
|
|
6
|
+
module Proxy
|
|
7
|
+
# Traefik proxy manager for zero-downtime deployments
|
|
8
|
+
#
|
|
9
|
+
# This is a placeholder implementation for Traefik support.
|
|
10
|
+
# Full implementation will be added in a future release.
|
|
11
|
+
#
|
|
12
|
+
# @since 0.1.0
|
|
13
|
+
class TraefikManager < Manager
|
|
14
|
+
# Switch traffic from old containers to new container
|
|
15
|
+
#
|
|
16
|
+
# @param service_name [String] Service name
|
|
17
|
+
# @param from_containers [Array<Hash>] Containers to switch traffic from
|
|
18
|
+
# @param to_container [Hash] Container to switch traffic to
|
|
19
|
+
# @return [void]
|
|
20
|
+
def switch_traffic(service_name:, from_containers:, to_container:)
|
|
21
|
+
raise NotImplementedError, "Traefik proxy support not yet implemented"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get traefik proxy status
|
|
25
|
+
#
|
|
26
|
+
# @return [Hash] Traefik status information
|
|
27
|
+
def status
|
|
28
|
+
{
|
|
29
|
+
type: "traefik",
|
|
30
|
+
status: "not_implemented",
|
|
31
|
+
upstreams: []
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/gjallarhorn/version.rb
CHANGED
data/lib/gjallarhorn.rb
CHANGED
|
@@ -2,10 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "gjallarhorn/version"
|
|
4
4
|
|
|
5
|
+
# Gjallarhorn - Multi-cloud deployment guardian
|
|
6
|
+
#
|
|
7
|
+
# Gjallarhorn provides a unified interface for deploying containerized applications
|
|
8
|
+
# across different cloud providers using their native APIs. Named after Heimdall's horn
|
|
9
|
+
# in Norse mythology that sounds across all realms, Gjallarhorn enables secure,
|
|
10
|
+
# API-first deployments beyond traditional SSH-based tools.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# deployer = Gjallarhorn::Deployer.new('deploy.yml')
|
|
14
|
+
# deployer.deploy('production', 'myapp:v1.0.0')
|
|
15
|
+
# deployer.status('production')
|
|
16
|
+
#
|
|
17
|
+
# @author Ken C. Demanawa
|
|
18
|
+
# @since 0.1.0
|
|
5
19
|
module Gjallarhorn
|
|
20
|
+
# Base error class for all Gjallarhorn-specific errors
|
|
6
21
|
class Error < StandardError; end
|
|
7
22
|
end
|
|
8
23
|
|
|
9
24
|
require_relative "gjallarhorn/configuration"
|
|
10
25
|
require_relative "gjallarhorn/deployer"
|
|
11
26
|
require_relative "gjallarhorn/cli"
|
|
27
|
+
require_relative "gjallarhorn/history"
|
metadata
CHANGED
|
@@ -1,15 +1,57 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gjallarhorn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-07-
|
|
11
|
+
date: 2025-07-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: thor
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: aws-sdk-ssm
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: aws-sdk-ec2
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.0'
|
|
13
55
|
- !ruby/object:Gem::Dependency
|
|
14
56
|
name: irb
|
|
15
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,6 +108,48 @@ dependencies:
|
|
|
66
108
|
- - "~>"
|
|
67
109
|
- !ruby/object:Gem::Version
|
|
68
110
|
version: '1.21'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rubocop-minitest
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: 0.38.1
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: 0.38.1
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: rubocop-rake
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: 0.7.1
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: 0.7.1
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: simplecov
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0.22'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0.22'
|
|
69
153
|
description: A Ruby gem that sounds across all cloud realms with secure, API-first
|
|
70
154
|
deployments beyond SSH.
|
|
71
155
|
email:
|
|
@@ -75,6 +159,7 @@ executables:
|
|
|
75
159
|
extensions: []
|
|
76
160
|
extra_rdoc_files: []
|
|
77
161
|
files:
|
|
162
|
+
- ".yardopts"
|
|
78
163
|
- CODE_OF_CONDUCT.md
|
|
79
164
|
- LICENSE.txt
|
|
80
165
|
- README.md
|
|
@@ -82,13 +167,23 @@ files:
|
|
|
82
167
|
- Rakefile
|
|
83
168
|
- examples/deploy.yml
|
|
84
169
|
- examples/dream-deploy.yml
|
|
170
|
+
- examples/zero-downtime-deployment.rb
|
|
85
171
|
- exe/gjallarhorn
|
|
86
172
|
- lib/gjallarhorn.rb
|
|
87
|
-
- lib/gjallarhorn/
|
|
88
|
-
- lib/gjallarhorn/
|
|
173
|
+
- lib/gjallarhorn/adapter/aws.rb
|
|
174
|
+
- lib/gjallarhorn/adapter/base.rb
|
|
89
175
|
- lib/gjallarhorn/cli.rb
|
|
90
176
|
- lib/gjallarhorn/configuration.rb
|
|
91
177
|
- lib/gjallarhorn/deployer.rb
|
|
178
|
+
- lib/gjallarhorn/deployment/basic.rb
|
|
179
|
+
- lib/gjallarhorn/deployment/legacy.rb
|
|
180
|
+
- lib/gjallarhorn/deployment/strategy.rb
|
|
181
|
+
- lib/gjallarhorn/deployment/zero_downtime.rb
|
|
182
|
+
- lib/gjallarhorn/history.rb
|
|
183
|
+
- lib/gjallarhorn/proxy/kamal_proxy_manager.rb
|
|
184
|
+
- lib/gjallarhorn/proxy/manager.rb
|
|
185
|
+
- lib/gjallarhorn/proxy/nginx_manager.rb
|
|
186
|
+
- lib/gjallarhorn/proxy/traefik_manager.rb
|
|
92
187
|
- lib/gjallarhorn/version.rb
|
|
93
188
|
- sig/bifrost.rbs
|
|
94
189
|
homepage: https://github.com/kanutocd/gjallarhorn
|
|
@@ -109,9 +204,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
109
204
|
version: 3.1.0
|
|
110
205
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
206
|
requirements:
|
|
112
|
-
- - "
|
|
207
|
+
- - ">="
|
|
113
208
|
- !ruby/object:Gem::Version
|
|
114
|
-
version:
|
|
209
|
+
version: '0'
|
|
115
210
|
requirements: []
|
|
116
211
|
rubygems_version: 3.3.27
|
|
117
212
|
signing_key:
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# AWS SSM Adapter
|
|
4
|
-
module Gjallarhorn
|
|
5
|
-
module Adapters
|
|
6
|
-
class AWSAdapter < Base
|
|
7
|
-
def initialize(config)
|
|
8
|
-
super
|
|
9
|
-
require "aws-sdk-ssm"
|
|
10
|
-
require "aws-sdk-ec2"
|
|
11
|
-
@ssm = Aws::SSM::Client.new(region: config[:region])
|
|
12
|
-
@ec2 = Aws::EC2::Client.new(region: config[:region])
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def deploy(image:, environment:, services: [])
|
|
16
|
-
instances = get_instances_by_tags(environment)
|
|
17
|
-
|
|
18
|
-
commands = build_deployment_commands(image, services)
|
|
19
|
-
|
|
20
|
-
logger.info "Deploying #{image} to #{instances.size} AWS instances"
|
|
21
|
-
|
|
22
|
-
response = @ssm.send_command(
|
|
23
|
-
instance_ids: instances,
|
|
24
|
-
document_name: "AWS-RunShellScript",
|
|
25
|
-
parameters: {
|
|
26
|
-
"commands" => commands,
|
|
27
|
-
"executionTimeout" => ["3600"]
|
|
28
|
-
},
|
|
29
|
-
comment: "Deploy #{image} via Gjallarhorn"
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
wait_for_command_completion(response.command.command_id, instances)
|
|
33
|
-
|
|
34
|
-
# Verify health across all instances
|
|
35
|
-
services.each do |service|
|
|
36
|
-
wait_for_health(service)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
logger.info "Deployment completed successfully"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def rollback(version:)
|
|
43
|
-
# Similar implementation for rollback
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def status
|
|
47
|
-
instances = get_instances_by_tags(config[:environment])
|
|
48
|
-
instances.map do |instance_id|
|
|
49
|
-
{
|
|
50
|
-
instance: instance_id,
|
|
51
|
-
status: get_instance_status(instance_id)
|
|
52
|
-
}
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def health_check(*)
|
|
57
|
-
# Implement health check via SSM command
|
|
58
|
-
true # Simplified
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def get_instances_by_tags(environment)
|
|
64
|
-
resp = @ec2.describe_instances(
|
|
65
|
-
filters: [
|
|
66
|
-
{ name: "tag:Environment", values: [environment] },
|
|
67
|
-
{ name: "tag:Role", values: %w[web app] },
|
|
68
|
-
{ name: "instance-state-name", values: ["running"] }
|
|
69
|
-
]
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
resp.reservations.flat_map(&:instances).map(&:instance_id)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def build_deployment_commands(image, services)
|
|
76
|
-
[
|
|
77
|
-
"docker pull #{image}",
|
|
78
|
-
*services.map { |svc| "docker stop #{svc[:name]} || true" },
|
|
79
|
-
*services.map do |svc|
|
|
80
|
-
"docker run -d --name #{svc[:name]} " \
|
|
81
|
-
"#{svc[:ports].map { |p| "-p #{p}" }.join(" ")} " \
|
|
82
|
-
"#{svc[:env].map { |k, v| "-e #{k}=#{v}" }.join(" ")} " \
|
|
83
|
-
"#{image}"
|
|
84
|
-
end
|
|
85
|
-
]
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def wait_for_command_completion(command_id, _instances)
|
|
89
|
-
@ssm.wait_until(:command_executed, command_id: command_id) do |w|
|
|
90
|
-
w.max_attempts = 60
|
|
91
|
-
w.delay = 5
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|