odysseus-core 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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +8 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/lib/odysseus/builder/client.rb +292 -0
- data/lib/odysseus/caddy/client.rb +338 -0
- data/lib/odysseus/config/parser.rb +225 -0
- data/lib/odysseus/core/version.rb +7 -0
- data/lib/odysseus/core.rb +10 -0
- data/lib/odysseus/deployer/executor.rb +389 -0
- data/lib/odysseus/deployer/ssh.rb +143 -0
- data/lib/odysseus/docker/client.rb +333 -0
- data/lib/odysseus/errors.rb +27 -0
- data/lib/odysseus/host_providers/aws_asg.rb +91 -0
- data/lib/odysseus/host_providers/base.rb +27 -0
- data/lib/odysseus/host_providers/static.rb +24 -0
- data/lib/odysseus/host_providers.rb +49 -0
- data/lib/odysseus/orchestrator/accessory_deploy.rb +309 -0
- data/lib/odysseus/orchestrator/job_deploy.rb +176 -0
- data/lib/odysseus/orchestrator/web_deploy.rb +253 -0
- data/lib/odysseus/secrets/encrypted_file.rb +125 -0
- data/lib/odysseus/secrets/loader.rb +56 -0
- data/lib/odysseus/validators/config.rb +85 -0
- data/lib/odysseus/version.rb +5 -0
- data/lib/odysseus.rb +26 -0
- data/sig/odysseus/core.rbs +6 -0
- metadata +127 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# lib/odysseus/orchestrator/accessory_deploy.rb
|
|
2
|
+
|
|
3
|
+
module Odysseus
|
|
4
|
+
module Orchestrator
|
|
5
|
+
class AccessoryDeploy
|
|
6
|
+
# @param ssh [Odysseus::Deployer::SSH] SSH connection
|
|
7
|
+
# @param config [Hash] parsed deploy config
|
|
8
|
+
# @param secrets_loader [Odysseus::Secrets::Loader] secrets loader (optional)
|
|
9
|
+
# @param logger [Object] logger (optional)
|
|
10
|
+
def initialize(ssh:, config:, secrets_loader: nil, logger: nil)
|
|
11
|
+
@ssh = ssh
|
|
12
|
+
@config = config
|
|
13
|
+
@secrets_loader = secrets_loader
|
|
14
|
+
@logger = logger || default_logger
|
|
15
|
+
@docker = Odysseus::Docker::Client.new(ssh)
|
|
16
|
+
@caddy = Odysseus::Caddy::Client.new(ssh: ssh, docker: @docker)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Deploy/ensure an accessory is running
|
|
20
|
+
# @param name [Symbol] accessory name
|
|
21
|
+
# @return [Hash] deploy result
|
|
22
|
+
def deploy(name:)
|
|
23
|
+
accessory_config = @config[:accessories][name]
|
|
24
|
+
raise Odysseus::ConfigError, "Accessory '#{name}' not found in config" unless accessory_config
|
|
25
|
+
|
|
26
|
+
service_name = accessory_name(name)
|
|
27
|
+
image = accessory_config[:image]
|
|
28
|
+
|
|
29
|
+
log "Deploying accessory: #{service_name}"
|
|
30
|
+
|
|
31
|
+
# Check if accessory is already running
|
|
32
|
+
existing = @docker.list(service: service_name)
|
|
33
|
+
if existing.any? { |c| c['State'] == 'running' }
|
|
34
|
+
log "Accessory #{service_name} is already running"
|
|
35
|
+
return { success: true, already_running: true, service: service_name }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Start the accessory
|
|
39
|
+
log "Starting #{service_name}..."
|
|
40
|
+
container_id = start_accessory(name: name, config: accessory_config)
|
|
41
|
+
log "Started container: #{container_id[0..11]}"
|
|
42
|
+
|
|
43
|
+
# Wait for healthy if healthcheck configured
|
|
44
|
+
if accessory_config[:healthcheck]
|
|
45
|
+
log "Waiting for container to be healthy..."
|
|
46
|
+
unless @docker.wait_healthy(container_id, timeout: 120)
|
|
47
|
+
@docker.stop(container_id)
|
|
48
|
+
@docker.remove(container_id, force: true)
|
|
49
|
+
raise Odysseus::DeployError, "Accessory failed health checks"
|
|
50
|
+
end
|
|
51
|
+
log "Container is healthy!"
|
|
52
|
+
else
|
|
53
|
+
sleep 3
|
|
54
|
+
unless @docker.running?(container_id)
|
|
55
|
+
raise Odysseus::DeployError, "Accessory failed to start"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Add to Caddy if proxy config is present
|
|
60
|
+
if accessory_config[:proxy]
|
|
61
|
+
log "Configuring proxy..."
|
|
62
|
+
add_to_caddy(name: name, container_id: container_id, config: accessory_config)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
log "Accessory #{service_name} deployed!"
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
success: true,
|
|
69
|
+
container_id: container_id,
|
|
70
|
+
service: service_name,
|
|
71
|
+
image: image
|
|
72
|
+
}
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
log "Accessory deploy failed: #{e.message}", :error
|
|
75
|
+
raise
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Stop and remove an accessory
|
|
79
|
+
# @param name [Symbol] accessory name
|
|
80
|
+
def remove(name:)
|
|
81
|
+
accessory_config = @config[:accessories][name]
|
|
82
|
+
raise Odysseus::ConfigError, "Accessory '#{name}' not found in config" unless accessory_config
|
|
83
|
+
|
|
84
|
+
service_name = accessory_name(name)
|
|
85
|
+
log "Removing accessory: #{service_name}"
|
|
86
|
+
|
|
87
|
+
# Remove from Caddy if proxy configured
|
|
88
|
+
if accessory_config[:proxy]
|
|
89
|
+
containers = @docker.list(service: service_name)
|
|
90
|
+
containers.each do |c|
|
|
91
|
+
container_name = c['Names'].delete_prefix('/')
|
|
92
|
+
port = accessory_config[:proxy][:app_port]
|
|
93
|
+
@caddy.drain_upstream(service: service_name, upstream: "#{container_name}:#{port}")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Stop and remove containers
|
|
98
|
+
containers = @docker.list(service: service_name, all: true)
|
|
99
|
+
containers.each do |c|
|
|
100
|
+
@docker.stop(c['ID']) if c['State'] == 'running'
|
|
101
|
+
@docker.remove(c['ID'], force: true)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
log "Accessory #{service_name} removed!"
|
|
105
|
+
{ success: true, service: service_name }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Restart an accessory (remove and redeploy)
|
|
109
|
+
# @param name [Symbol] accessory name
|
|
110
|
+
def restart(name:)
|
|
111
|
+
remove(name: name)
|
|
112
|
+
deploy(name: name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Upgrade an accessory to a new image version (preserves volumes)
|
|
116
|
+
# @param name [Symbol] accessory name
|
|
117
|
+
# @return [Hash] upgrade result
|
|
118
|
+
def upgrade(name:)
|
|
119
|
+
accessory_config = @config[:accessories][name]
|
|
120
|
+
raise Odysseus::ConfigError, "Accessory '#{name}' not found in config" unless accessory_config
|
|
121
|
+
|
|
122
|
+
service_name = accessory_name(name)
|
|
123
|
+
image = accessory_config[:image]
|
|
124
|
+
|
|
125
|
+
log "Upgrading accessory: #{service_name} to #{image}"
|
|
126
|
+
|
|
127
|
+
# Pull the new image first (before stopping anything)
|
|
128
|
+
log "Pulling new image: #{image}..."
|
|
129
|
+
@docker.pull(image)
|
|
130
|
+
log "Image pulled successfully"
|
|
131
|
+
|
|
132
|
+
# Check for existing container
|
|
133
|
+
existing = @docker.list(service: service_name, all: true)
|
|
134
|
+
old_container = existing.first
|
|
135
|
+
|
|
136
|
+
# Remove from Caddy if proxy configured (before stopping)
|
|
137
|
+
if accessory_config[:proxy] && old_container && old_container['State'] == 'running'
|
|
138
|
+
container_name = old_container['Names'].delete_prefix('/')
|
|
139
|
+
port = accessory_config[:proxy][:app_port]
|
|
140
|
+
log "Removing from proxy..."
|
|
141
|
+
@caddy.drain_upstream(service: service_name, upstream: "#{container_name}:#{port}")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Stop and remove old container if exists
|
|
145
|
+
if old_container
|
|
146
|
+
log "Stopping old container: #{old_container['ID'][0..11]}..."
|
|
147
|
+
@docker.stop(old_container['ID'], timeout: 30) if old_container['State'] == 'running'
|
|
148
|
+
@docker.remove(old_container['ID'], force: true)
|
|
149
|
+
log "Old container removed"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Start the accessory with the new image (volumes are preserved on host)
|
|
153
|
+
log "Starting new container with #{image}..."
|
|
154
|
+
container_id = start_accessory(name: name, config: accessory_config)
|
|
155
|
+
log "Started container: #{container_id[0..11]}"
|
|
156
|
+
|
|
157
|
+
# Wait for healthy if healthcheck configured
|
|
158
|
+
if accessory_config[:healthcheck]
|
|
159
|
+
log "Waiting for container to be healthy..."
|
|
160
|
+
unless @docker.wait_healthy(container_id, timeout: 120)
|
|
161
|
+
@docker.stop(container_id)
|
|
162
|
+
@docker.remove(container_id, force: true)
|
|
163
|
+
raise Odysseus::DeployError, "Accessory failed health checks after upgrade"
|
|
164
|
+
end
|
|
165
|
+
log "Container is healthy!"
|
|
166
|
+
else
|
|
167
|
+
sleep 3
|
|
168
|
+
unless @docker.running?(container_id)
|
|
169
|
+
raise Odysseus::DeployError, "Accessory failed to start after upgrade"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Add to Caddy if proxy config is present
|
|
174
|
+
if accessory_config[:proxy]
|
|
175
|
+
log "Configuring proxy..."
|
|
176
|
+
add_to_caddy(name: name, container_id: container_id, config: accessory_config)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
log "Accessory #{service_name} upgraded to #{image}!"
|
|
180
|
+
|
|
181
|
+
{
|
|
182
|
+
success: true,
|
|
183
|
+
container_id: container_id,
|
|
184
|
+
service: service_name,
|
|
185
|
+
image: image,
|
|
186
|
+
upgraded: true
|
|
187
|
+
}
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
log "Accessory upgrade failed: #{e.message}", :error
|
|
190
|
+
raise
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# List status of all accessories
|
|
194
|
+
# @return [Array<Hash>] accessory statuses
|
|
195
|
+
def list_status
|
|
196
|
+
return [] unless @config[:accessories]
|
|
197
|
+
|
|
198
|
+
@config[:accessories].map do |name, config|
|
|
199
|
+
service_name = accessory_name(name)
|
|
200
|
+
containers = @docker.list(service: service_name, all: true)
|
|
201
|
+
running = containers.find { |c| c['State'] == 'running' }
|
|
202
|
+
|
|
203
|
+
{
|
|
204
|
+
name: name,
|
|
205
|
+
service: service_name,
|
|
206
|
+
image: config[:image],
|
|
207
|
+
running: !running.nil?,
|
|
208
|
+
container_id: running&.dig('ID'),
|
|
209
|
+
has_proxy: !config[:proxy].nil?
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
def accessory_name(name)
|
|
217
|
+
"#{@config[:service]}-#{name}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def start_accessory(name:, config:)
|
|
221
|
+
service_name = accessory_name(name)
|
|
222
|
+
|
|
223
|
+
@docker.run(
|
|
224
|
+
name: service_name,
|
|
225
|
+
image: config[:image],
|
|
226
|
+
options: {
|
|
227
|
+
service: service_name,
|
|
228
|
+
env: build_environment(config[:env]),
|
|
229
|
+
ports: config[:ports],
|
|
230
|
+
volumes: config[:volumes],
|
|
231
|
+
network: 'odysseus',
|
|
232
|
+
restart: 'unless-stopped',
|
|
233
|
+
healthcheck: build_healthcheck(config[:healthcheck]),
|
|
234
|
+
cmd: config[:cmd]
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def build_environment(env_config)
|
|
240
|
+
return {} unless env_config
|
|
241
|
+
|
|
242
|
+
env = {}
|
|
243
|
+
|
|
244
|
+
# Clear env vars (hardcoded values)
|
|
245
|
+
env_config[:clear]&.each do |key, value|
|
|
246
|
+
env[key.to_s] = value.to_s
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Secret env vars - first try encrypted file, then server environment
|
|
250
|
+
env_config[:secret]&.each do |key|
|
|
251
|
+
# Try encrypted secrets file first
|
|
252
|
+
if @secrets_loader&.configured?
|
|
253
|
+
value = @secrets_loader.get(key)
|
|
254
|
+
if value
|
|
255
|
+
env[key.to_s] = value.to_s
|
|
256
|
+
next
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Fall back to server's environment
|
|
261
|
+
value = @ssh.execute("echo $#{key}").strip
|
|
262
|
+
env[key.to_s] = value unless value.empty?
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
env
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def build_healthcheck(hc_config)
|
|
269
|
+
return nil unless hc_config
|
|
270
|
+
|
|
271
|
+
{
|
|
272
|
+
cmd: hc_config[:cmd],
|
|
273
|
+
interval: hc_config[:interval] || 30,
|
|
274
|
+
timeout: hc_config[:timeout] || 10,
|
|
275
|
+
retries: hc_config[:retries] || 3
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def add_to_caddy(name:, container_id:, config:)
|
|
280
|
+
container_info = @ssh.execute("docker inspect --format '{{.Name}}' #{container_id}").strip
|
|
281
|
+
container_name = container_info.delete_prefix('/')
|
|
282
|
+
|
|
283
|
+
proxy_config = config[:proxy]
|
|
284
|
+
port = proxy_config[:app_port]
|
|
285
|
+
upstream = "#{container_name}:#{port}"
|
|
286
|
+
|
|
287
|
+
@caddy.add_upstream(
|
|
288
|
+
service: accessory_name(name),
|
|
289
|
+
hosts: proxy_config[:hosts],
|
|
290
|
+
upstream: upstream,
|
|
291
|
+
ssl: proxy_config[:ssl],
|
|
292
|
+
ssl_email: proxy_config[:ssl_email]
|
|
293
|
+
)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def default_logger
|
|
297
|
+
@default_logger ||= Object.new.tap do |l|
|
|
298
|
+
def l.info(msg); puts msg; end
|
|
299
|
+
def l.warn(msg); puts "[WARN] #{msg}"; end
|
|
300
|
+
def l.error(msg); puts "[ERROR] #{msg}"; end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def log(message, level = :info)
|
|
305
|
+
@logger.send(level, message)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# lib/odysseus/orchestrator/job_deploy.rb
|
|
2
|
+
|
|
3
|
+
module Odysseus
|
|
4
|
+
module Orchestrator
|
|
5
|
+
class JobDeploy
|
|
6
|
+
# @param ssh [Odysseus::Deployer::SSH] SSH connection
|
|
7
|
+
# @param config [Hash] parsed deploy config
|
|
8
|
+
# @param logger [Object] logger (optional)
|
|
9
|
+
# @param secrets_loader [Odysseus::Secrets::Loader] secrets loader (optional)
|
|
10
|
+
def initialize(ssh:, config:, logger: nil, secrets_loader: nil)
|
|
11
|
+
@ssh = ssh
|
|
12
|
+
@config = config
|
|
13
|
+
@logger = logger || default_logger
|
|
14
|
+
@secrets_loader = secrets_loader
|
|
15
|
+
@docker = Odysseus::Docker::Client.new(ssh)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Execute deploy for a job/worker service
|
|
19
|
+
# @param image_tag [String] image tag to deploy
|
|
20
|
+
# @param role [Symbol] server role (e.g., :jobs, :worker)
|
|
21
|
+
# @return [Hash] deploy result
|
|
22
|
+
def deploy(image_tag:, role:)
|
|
23
|
+
service = @config[:service]
|
|
24
|
+
role_name = "#{service}-#{role}"
|
|
25
|
+
image = "#{@config[:image]}:#{image_tag}"
|
|
26
|
+
|
|
27
|
+
log "Starting deploy of #{role_name} with #{image}"
|
|
28
|
+
|
|
29
|
+
# Step 1: Find existing containers for this role
|
|
30
|
+
log "Checking for existing containers..."
|
|
31
|
+
old_containers = @docker.list(service: role_name)
|
|
32
|
+
log "Found #{old_containers.size} existing container(s)"
|
|
33
|
+
|
|
34
|
+
# Step 2: Start new container
|
|
35
|
+
log "Starting new container..."
|
|
36
|
+
new_container_id = start_new_container(image: image, role: role)
|
|
37
|
+
log "Started container: #{new_container_id[0..11]}"
|
|
38
|
+
|
|
39
|
+
# Step 3: Wait for healthy (if healthcheck configured)
|
|
40
|
+
server_config = @config[:servers][role] || {}
|
|
41
|
+
if server_config[:healthcheck]
|
|
42
|
+
log "Waiting for container to be healthy..."
|
|
43
|
+
unless wait_for_healthy(new_container_id)
|
|
44
|
+
handle_failed_deploy(new_container_id)
|
|
45
|
+
raise Odysseus::DeployError, "Container failed health checks"
|
|
46
|
+
end
|
|
47
|
+
log "Container is healthy!"
|
|
48
|
+
else
|
|
49
|
+
# No healthcheck - just wait a few seconds for startup
|
|
50
|
+
log "No healthcheck configured, waiting for startup..."
|
|
51
|
+
sleep 5
|
|
52
|
+
unless @docker.running?(new_container_id)
|
|
53
|
+
handle_failed_deploy(new_container_id)
|
|
54
|
+
raise Odysseus::DeployError, "Container failed to start"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Step 4: Stop old containers gracefully
|
|
59
|
+
old_containers.each do |old|
|
|
60
|
+
log "Stopping old container: #{old['ID'][0..11]}..."
|
|
61
|
+
graceful_stop(old['ID'])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Step 5: Cleanup old stopped containers
|
|
65
|
+
log "Cleaning up old containers..."
|
|
66
|
+
@docker.cleanup_old_containers(service: role_name, keep: 2)
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
success: true,
|
|
70
|
+
container_id: new_container_id,
|
|
71
|
+
service: role_name,
|
|
72
|
+
image: image
|
|
73
|
+
}
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
log "Deploy failed: #{e.message}", :error
|
|
76
|
+
raise
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def start_new_container(image:, role:)
|
|
82
|
+
service = @config[:service]
|
|
83
|
+
role_name = "#{service}-#{role}"
|
|
84
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
|
85
|
+
container_name = "#{role_name}-#{timestamp}"
|
|
86
|
+
|
|
87
|
+
server_config = @config[:servers][role] || {}
|
|
88
|
+
options = server_config[:options] || {}
|
|
89
|
+
|
|
90
|
+
@docker.run(
|
|
91
|
+
name: container_name,
|
|
92
|
+
image: image,
|
|
93
|
+
options: {
|
|
94
|
+
service: role_name,
|
|
95
|
+
version: timestamp,
|
|
96
|
+
env: build_environment,
|
|
97
|
+
memory: options[:memory],
|
|
98
|
+
memory_reservation: options[:memory_reservation],
|
|
99
|
+
cpus: options[:cpus],
|
|
100
|
+
cpu_shares: options[:cpu_shares],
|
|
101
|
+
network: 'odysseus',
|
|
102
|
+
healthcheck: build_healthcheck(server_config[:healthcheck]),
|
|
103
|
+
cmd: server_config[:cmd]
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_environment
|
|
109
|
+
env = {}
|
|
110
|
+
|
|
111
|
+
# Clear env vars (hardcoded values)
|
|
112
|
+
@config[:env][:clear]&.each do |key, value|
|
|
113
|
+
env[key.to_s] = value.to_s
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Secret env vars (from encrypted file or server environment)
|
|
117
|
+
@config[:env][:secret]&.each do |key|
|
|
118
|
+
# Try encrypted secrets file first
|
|
119
|
+
if @secrets_loader&.configured?
|
|
120
|
+
value = @secrets_loader.get(key)
|
|
121
|
+
if value
|
|
122
|
+
env[key.to_s] = value.to_s
|
|
123
|
+
next
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Fall back to server's environment
|
|
128
|
+
value = @ssh.execute("echo $#{key}").strip
|
|
129
|
+
env[key.to_s] = value unless value.empty?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
env
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def build_healthcheck(hc_config)
|
|
136
|
+
return nil unless hc_config
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
cmd: hc_config[:cmd],
|
|
140
|
+
interval: hc_config[:interval] || 30,
|
|
141
|
+
timeout: hc_config[:timeout] || 10,
|
|
142
|
+
retries: hc_config[:retries] || 3
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def wait_for_healthy(container_id, timeout: 120)
|
|
147
|
+
@docker.wait_healthy(container_id, timeout: timeout)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def graceful_stop(container_id)
|
|
151
|
+
# Give workers time to finish current job (30 second timeout)
|
|
152
|
+
@docker.stop(container_id, timeout: 30)
|
|
153
|
+
@docker.remove(container_id)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def handle_failed_deploy(new_container_id)
|
|
157
|
+
log "Rolling back failed deploy...", :warn
|
|
158
|
+
@docker.stop(new_container_id)
|
|
159
|
+
@docker.remove(new_container_id, force: true)
|
|
160
|
+
log "Rollback complete"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def default_logger
|
|
164
|
+
@default_logger ||= Object.new.tap do |l|
|
|
165
|
+
def l.info(msg); puts msg; end
|
|
166
|
+
def l.warn(msg); puts "[WARN] #{msg}"; end
|
|
167
|
+
def l.error(msg); puts "[ERROR] #{msg}"; end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def log(message, level = :info)
|
|
172
|
+
@logger.send(level, message)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|