odysseus-core 0.3.2 → 0.9.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/CHANGELOG.md +485 -1
- data/LICENSE.txt +25 -7
- data/README.md +12 -3
- data/lib/odysseus/builder/client.rb +15 -15
- data/lib/odysseus/caddy/client.rb +119 -42
- data/lib/odysseus/command_redaction.rb +38 -0
- data/lib/odysseus/config/parser.rb +43 -18
- data/lib/odysseus/core/deploy_versioning.rb +42 -0
- data/lib/odysseus/core/environment.rb +63 -0
- data/lib/odysseus/core/version.rb +1 -1
- data/lib/odysseus/core/volume_namespacer.rb +0 -0
- data/lib/odysseus/core.rb +2 -2
- data/lib/odysseus/deploy_log.rb +120 -0
- data/lib/odysseus/deploy_version.rb +9 -0
- data/lib/odysseus/deployer/dependency_manager.rb +121 -0
- data/lib/odysseus/deployer/executor.rb +213 -161
- data/lib/odysseus/deployer/retention_sweeper.rb +94 -0
- data/lib/odysseus/deployer/ssh.rb +92 -18
- data/lib/odysseus/docker/client.rb +240 -48
- data/lib/odysseus/docker/labels.rb +43 -0
- data/lib/odysseus/errors.rb +10 -0
- data/lib/odysseus/git.rb +70 -0
- data/lib/odysseus/host_paths.rb +79 -0
- data/lib/odysseus/host_providers/base.rb +1 -1
- data/lib/odysseus/host_providers/static.rb +1 -1
- data/lib/odysseus/host_providers.rb +3 -4
- data/lib/odysseus/host_verifier.rb +156 -0
- data/lib/odysseus/host_versions.rb +59 -0
- data/lib/odysseus/orchestrator/{accessory_deploy.rb → dependency_deploy.rb} +84 -88
- data/lib/odysseus/orchestrator/job_deploy.rb +31 -43
- data/lib/odysseus/orchestrator/web_deploy.rb +61 -55
- data/lib/odysseus/plugins.rb +72 -0
- data/lib/odysseus/retention_plan.rb +14 -0
- data/lib/odysseus/retention_planner.rb +58 -0
- data/lib/odysseus/rollback_plan.rb +20 -0
- data/lib/odysseus/rollback_planner.rb +107 -0
- data/lib/odysseus/sails.rb +0 -0
- data/lib/odysseus/secrets/encrypted_file.rb +5 -7
- data/lib/odysseus/secrets/loader.rb +1 -3
- data/lib/odysseus/setup/docker_apt.rb +178 -0
- data/lib/odysseus/setup/escalation.rb +73 -0
- data/lib/odysseus/setup/preparer.rb +372 -0
- data/lib/odysseus/setup/public_key.rb +127 -0
- data/lib/odysseus/validators/config.rb +38 -25
- data/lib/odysseus/version_resolver.rb +80 -0
- data/lib/odysseus.rb +0 -0
- data/sig/odysseus/core.rbs +0 -0
- metadata +48 -27
- data/Rakefile +0 -12
- data/lib/odysseus/version.rb +0 -5
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# lib/odysseus/orchestrator/
|
|
1
|
+
# lib/odysseus/orchestrator/dependency_deploy.rb
|
|
2
2
|
|
|
3
3
|
require_relative '../core/volume_namespacer'
|
|
4
4
|
|
|
5
5
|
module Odysseus
|
|
6
6
|
module Orchestrator
|
|
7
|
-
class
|
|
7
|
+
class DependencyDeploy
|
|
8
8
|
include Odysseus::Core::VolumeNamespacer
|
|
9
9
|
|
|
10
10
|
# @param ssh [Odysseus::Deployer::SSH] SSH connection
|
|
@@ -20,64 +20,64 @@ module Odysseus
|
|
|
20
20
|
@caddy = Odysseus::Caddy::Client.new(ssh: ssh, docker: @docker)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Deploy/ensure an
|
|
24
|
-
# @param name [Symbol]
|
|
23
|
+
# Deploy/ensure an dependency is running
|
|
24
|
+
# @param name [Symbol] dependency name
|
|
25
25
|
# @return [Hash] deploy result
|
|
26
26
|
def deploy(name:)
|
|
27
|
-
|
|
28
|
-
raise Odysseus::ConfigError, "
|
|
27
|
+
dependency_config = @config[:dependencies][name]
|
|
28
|
+
raise Odysseus::ConfigError, "Dependency '#{name}' not found in config" unless dependency_config
|
|
29
29
|
|
|
30
|
-
service_name =
|
|
31
|
-
image =
|
|
30
|
+
service_name = dependency_name(name)
|
|
31
|
+
image = dependency_config[:image]
|
|
32
32
|
|
|
33
|
-
log "Deploying
|
|
33
|
+
log "Deploying dependency: #{service_name}"
|
|
34
34
|
log " Image: #{image}"
|
|
35
35
|
|
|
36
|
-
# Ensure the Docker network exists (
|
|
36
|
+
# Ensure the Docker network exists (dependencies may boot before any service deploy)
|
|
37
37
|
ensure_network!
|
|
38
38
|
|
|
39
|
-
# Check if
|
|
39
|
+
# Check if dependency is already running
|
|
40
40
|
existing = @docker.list(service: service_name)
|
|
41
41
|
if existing.any? { |c| c['State'] == 'running' }
|
|
42
|
-
log
|
|
42
|
+
log ' Already running — skipping'
|
|
43
43
|
return { success: true, already_running: true, service: service_name }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
# Start the
|
|
46
|
+
# Start the dependency
|
|
47
47
|
log "Starting #{service_name}..."
|
|
48
|
-
container_id =
|
|
48
|
+
container_id = start_dependency(name: name, config: dependency_config)
|
|
49
49
|
log " Container started: #{container_id[0..11]}"
|
|
50
50
|
|
|
51
51
|
# Wait for healthy if healthcheck configured
|
|
52
|
-
if
|
|
53
|
-
hc =
|
|
52
|
+
if dependency_config[:healthcheck]
|
|
53
|
+
hc = dependency_config[:healthcheck]
|
|
54
54
|
log "Waiting for health check... (cmd: #{hc[:cmd]}, interval: #{hc[:interval]}s)"
|
|
55
55
|
unless @docker.wait_healthy(container_id, timeout: 120)
|
|
56
56
|
log_health_failure(container_id)
|
|
57
57
|
@docker.stop(container_id)
|
|
58
58
|
@docker.remove(container_id, force: true)
|
|
59
|
-
raise Odysseus::DeployError,
|
|
59
|
+
raise Odysseus::DeployError, 'Dependency failed health checks'
|
|
60
60
|
end
|
|
61
|
-
log
|
|
61
|
+
log ' Health check passed'
|
|
62
62
|
else
|
|
63
|
-
log
|
|
63
|
+
log 'No health check configured, waiting 3s for startup...'
|
|
64
64
|
sleep 3
|
|
65
65
|
unless @docker.running?(container_id)
|
|
66
66
|
log_health_failure(container_id)
|
|
67
|
-
raise Odysseus::DeployError,
|
|
67
|
+
raise Odysseus::DeployError, 'Dependency failed to start'
|
|
68
68
|
end
|
|
69
|
-
log
|
|
69
|
+
log ' Container is running'
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Add to Caddy if proxy config is present
|
|
73
|
-
if
|
|
74
|
-
proxy_hosts =
|
|
73
|
+
if dependency_config[:proxy]
|
|
74
|
+
proxy_hosts = dependency_config[:proxy][:hosts]&.join(', ')
|
|
75
75
|
log "Configuring proxy (hosts: #{proxy_hosts})..."
|
|
76
|
-
add_to_caddy(name: name, container_id: container_id, config:
|
|
77
|
-
log
|
|
76
|
+
add_to_caddy(name: name, container_id: container_id, config: dependency_config)
|
|
77
|
+
log ' Proxy configured'
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
log "
|
|
80
|
+
log "Dependency #{service_name} deployed"
|
|
81
81
|
|
|
82
82
|
{
|
|
83
83
|
success: true,
|
|
@@ -86,25 +86,25 @@ module Odysseus
|
|
|
86
86
|
image: image
|
|
87
87
|
}
|
|
88
88
|
rescue StandardError => e
|
|
89
|
-
log "
|
|
89
|
+
log "Dependency deploy FAILED: #{e.message}", :error
|
|
90
90
|
raise
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
# Stop and remove an
|
|
94
|
-
# @param name [Symbol]
|
|
93
|
+
# Stop and remove an dependency
|
|
94
|
+
# @param name [Symbol] dependency name
|
|
95
95
|
def remove(name:)
|
|
96
|
-
|
|
97
|
-
raise Odysseus::ConfigError, "
|
|
96
|
+
dependency_config = @config[:dependencies][name]
|
|
97
|
+
raise Odysseus::ConfigError, "Dependency '#{name}' not found in config" unless dependency_config
|
|
98
98
|
|
|
99
|
-
service_name =
|
|
100
|
-
log "Removing
|
|
99
|
+
service_name = dependency_name(name)
|
|
100
|
+
log "Removing dependency: #{service_name}"
|
|
101
101
|
|
|
102
102
|
# Remove from Caddy if proxy configured
|
|
103
|
-
if
|
|
103
|
+
if dependency_config[:proxy]
|
|
104
104
|
containers = @docker.list(service: service_name)
|
|
105
105
|
containers.each do |c|
|
|
106
106
|
container_name = c['Names'].delete_prefix('/')
|
|
107
|
-
port =
|
|
107
|
+
port = dependency_config[:proxy][:app_port]
|
|
108
108
|
@caddy.drain_upstream(service: service_name, upstream: "#{container_name}:#{port}")
|
|
109
109
|
end
|
|
110
110
|
end
|
|
@@ -116,49 +116,49 @@ module Odysseus
|
|
|
116
116
|
@docker.remove(c['ID'], force: true)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
log "
|
|
119
|
+
log "Dependency #{service_name} removed!"
|
|
120
120
|
{ success: true, service: service_name }
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
-
# Restart an
|
|
124
|
-
# @param name [Symbol]
|
|
123
|
+
# Restart an dependency (remove and redeploy)
|
|
124
|
+
# @param name [Symbol] dependency name
|
|
125
125
|
def restart(name:)
|
|
126
126
|
remove(name: name)
|
|
127
127
|
deploy(name: name)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
# Upgrade an
|
|
131
|
-
# @param name [Symbol]
|
|
130
|
+
# Upgrade an dependency to a new image version (preserves volumes)
|
|
131
|
+
# @param name [Symbol] dependency name
|
|
132
132
|
# @return [Hash] upgrade result
|
|
133
133
|
def upgrade(name:)
|
|
134
|
-
|
|
135
|
-
raise Odysseus::ConfigError, "
|
|
134
|
+
dependency_config = @config[:dependencies][name]
|
|
135
|
+
raise Odysseus::ConfigError, "Dependency '#{name}' not found in config" unless dependency_config
|
|
136
136
|
|
|
137
|
-
service_name =
|
|
138
|
-
image =
|
|
137
|
+
service_name = dependency_name(name)
|
|
138
|
+
image = dependency_config[:image]
|
|
139
139
|
|
|
140
|
-
log "Upgrading
|
|
140
|
+
log "Upgrading dependency: #{service_name}"
|
|
141
141
|
log " Image: #{image}"
|
|
142
142
|
|
|
143
143
|
# Ensure the Docker network exists
|
|
144
144
|
ensure_network!
|
|
145
145
|
|
|
146
146
|
# Pull the new image first (before stopping anything)
|
|
147
|
-
log
|
|
147
|
+
log 'Pulling new image...'
|
|
148
148
|
@docker.pull(image)
|
|
149
|
-
log
|
|
149
|
+
log ' Image pulled'
|
|
150
150
|
|
|
151
151
|
# Check for existing container
|
|
152
152
|
existing = @docker.list(service: service_name, all: true)
|
|
153
153
|
old_container = existing.first
|
|
154
154
|
|
|
155
155
|
# Remove from Caddy if proxy configured (before stopping)
|
|
156
|
-
if
|
|
156
|
+
if dependency_config[:proxy] && old_container && old_container['State'] == 'running'
|
|
157
157
|
container_name = old_container['Names'].delete_prefix('/')
|
|
158
|
-
port =
|
|
159
|
-
log
|
|
158
|
+
port = dependency_config[:proxy][:app_port]
|
|
159
|
+
log 'Draining from proxy...'
|
|
160
160
|
@caddy.drain_upstream(service: service_name, upstream: "#{container_name}:#{port}")
|
|
161
|
-
log
|
|
161
|
+
log ' Drained from proxy'
|
|
162
162
|
end
|
|
163
163
|
|
|
164
164
|
# Stop and remove old container if exists
|
|
@@ -166,44 +166,44 @@ module Odysseus
|
|
|
166
166
|
log "Stopping old container #{old_container['ID'][0..11]} (30s grace period)..."
|
|
167
167
|
@docker.stop(old_container['ID'], timeout: 30) if old_container['State'] == 'running'
|
|
168
168
|
@docker.remove(old_container['ID'], force: true)
|
|
169
|
-
log
|
|
169
|
+
log ' Old container removed'
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
# Start the
|
|
173
|
-
log
|
|
174
|
-
container_id =
|
|
172
|
+
# Start the dependency with the new image (volumes are preserved on host)
|
|
173
|
+
log 'Starting new container...'
|
|
174
|
+
container_id = start_dependency(name: name, config: dependency_config)
|
|
175
175
|
log " Container started: #{container_id[0..11]}"
|
|
176
176
|
|
|
177
177
|
# Wait for healthy if healthcheck configured
|
|
178
|
-
if
|
|
179
|
-
hc =
|
|
178
|
+
if dependency_config[:healthcheck]
|
|
179
|
+
hc = dependency_config[:healthcheck]
|
|
180
180
|
log "Waiting for health check... (cmd: #{hc[:cmd]}, interval: #{hc[:interval]}s)"
|
|
181
181
|
unless @docker.wait_healthy(container_id, timeout: 120)
|
|
182
182
|
log_health_failure(container_id)
|
|
183
183
|
@docker.stop(container_id)
|
|
184
184
|
@docker.remove(container_id, force: true)
|
|
185
|
-
raise Odysseus::DeployError,
|
|
185
|
+
raise Odysseus::DeployError, 'Dependency failed health checks after upgrade'
|
|
186
186
|
end
|
|
187
|
-
log
|
|
187
|
+
log ' Health check passed'
|
|
188
188
|
else
|
|
189
|
-
log
|
|
189
|
+
log 'No health check configured, waiting 3s for startup...'
|
|
190
190
|
sleep 3
|
|
191
191
|
unless @docker.running?(container_id)
|
|
192
192
|
log_health_failure(container_id)
|
|
193
|
-
raise Odysseus::DeployError,
|
|
193
|
+
raise Odysseus::DeployError, 'Dependency failed to start after upgrade'
|
|
194
194
|
end
|
|
195
|
-
log
|
|
195
|
+
log ' Container is running'
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
# Add to Caddy if proxy config is present
|
|
199
|
-
if
|
|
200
|
-
proxy_hosts =
|
|
199
|
+
if dependency_config[:proxy]
|
|
200
|
+
proxy_hosts = dependency_config[:proxy][:hosts]&.join(', ')
|
|
201
201
|
log "Configuring proxy (hosts: #{proxy_hosts})..."
|
|
202
|
-
add_to_caddy(name: name, container_id: container_id, config:
|
|
203
|
-
log
|
|
202
|
+
add_to_caddy(name: name, container_id: container_id, config: dependency_config)
|
|
203
|
+
log ' Proxy configured'
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
-
log "
|
|
206
|
+
log "Dependency #{service_name} upgraded"
|
|
207
207
|
|
|
208
208
|
{
|
|
209
209
|
success: true,
|
|
@@ -213,17 +213,17 @@ module Odysseus
|
|
|
213
213
|
upgraded: true
|
|
214
214
|
}
|
|
215
215
|
rescue StandardError => e
|
|
216
|
-
log "
|
|
216
|
+
log "Dependency upgrade failed: #{e.message}", :error
|
|
217
217
|
raise
|
|
218
218
|
end
|
|
219
219
|
|
|
220
|
-
# List status of all
|
|
221
|
-
# @return [Array<Hash>]
|
|
220
|
+
# List status of all dependencies
|
|
221
|
+
# @return [Array<Hash>] dependency statuses
|
|
222
222
|
def list_status
|
|
223
|
-
return [] unless @config[:
|
|
223
|
+
return [] unless @config[:dependencies]
|
|
224
224
|
|
|
225
|
-
@config[:
|
|
226
|
-
service_name =
|
|
225
|
+
@config[:dependencies].map do |name, config|
|
|
226
|
+
service_name = dependency_name(name)
|
|
227
227
|
containers = @docker.list(service: service_name, all: true)
|
|
228
228
|
running = containers.find { |c| c['State'] == 'running' }
|
|
229
229
|
|
|
@@ -240,29 +240,25 @@ module Odysseus
|
|
|
240
240
|
|
|
241
241
|
private
|
|
242
242
|
|
|
243
|
-
def
|
|
243
|
+
def dependency_name(name)
|
|
244
244
|
"#{@config[:service]}-#{name}"
|
|
245
245
|
end
|
|
246
246
|
|
|
247
247
|
def ensure_network!
|
|
248
|
-
log
|
|
248
|
+
log 'Ensuring Docker network exists...'
|
|
249
249
|
@docker.ensure_network('odysseus', labels: { 'odysseus.managed' => 'true' })
|
|
250
250
|
end
|
|
251
251
|
|
|
252
|
-
def
|
|
253
|
-
service_name =
|
|
252
|
+
def start_dependency(name:, config:)
|
|
253
|
+
service_name = dependency_name(name)
|
|
254
254
|
|
|
255
255
|
env = build_environment(config[:env])
|
|
256
256
|
log " Environment: #{env.size} variable(s) injected" if env.any?
|
|
257
257
|
|
|
258
258
|
volumes = namespace_volumes(config[:volumes], service: service_name)
|
|
259
|
-
if volumes&.any?
|
|
260
|
-
log " Volumes: #{volumes.join(', ')}"
|
|
261
|
-
end
|
|
259
|
+
log " Volumes: #{volumes.join(', ')}" if volumes&.any?
|
|
262
260
|
|
|
263
|
-
if config[:ports]&.any?
|
|
264
|
-
log " Ports: #{config[:ports].join(', ')}"
|
|
265
|
-
end
|
|
261
|
+
log " Ports: #{config[:ports].join(', ')}" if config[:ports]&.any?
|
|
266
262
|
|
|
267
263
|
@docker.run(
|
|
268
264
|
name: service_name,
|
|
@@ -329,7 +325,7 @@ module Odysseus
|
|
|
329
325
|
upstream = "#{container_name}:#{port}"
|
|
330
326
|
|
|
331
327
|
@caddy.add_upstream(
|
|
332
|
-
service:
|
|
328
|
+
service: dependency_name(name),
|
|
333
329
|
hosts: proxy_config[:hosts],
|
|
334
330
|
upstream: upstream,
|
|
335
331
|
ssl: proxy_config[:ssl],
|
|
@@ -343,7 +339,7 @@ module Odysseus
|
|
|
343
339
|
begin
|
|
344
340
|
recent_logs = @docker.logs(container_id, tail: 30)
|
|
345
341
|
unless recent_logs.strip.empty?
|
|
346
|
-
log
|
|
342
|
+
log ' Container logs (last 30 lines):', :error
|
|
347
343
|
recent_logs.each_line { |line| log " #{line.rstrip}", :error }
|
|
348
344
|
end
|
|
349
345
|
rescue StandardError => e
|
|
@@ -360,9 +356,9 @@ module Odysseus
|
|
|
360
356
|
|
|
361
357
|
def default_logger
|
|
362
358
|
@default_logger ||= Object.new.tap do |l|
|
|
363
|
-
def l.info(msg)
|
|
364
|
-
def l.warn(msg)
|
|
365
|
-
def l.error(msg)
|
|
359
|
+
def l.info(msg) = puts(msg)
|
|
360
|
+
def l.warn(msg) = puts("[WARN] #{msg}")
|
|
361
|
+
def l.error(msg) = puts("[ERROR] #{msg}")
|
|
366
362
|
end
|
|
367
363
|
end
|
|
368
364
|
|
|
@@ -6,6 +6,7 @@ module Odysseus
|
|
|
6
6
|
module Orchestrator
|
|
7
7
|
class JobDeploy
|
|
8
8
|
include Odysseus::Core::VolumeNamespacer
|
|
9
|
+
include Odysseus::Core::DeployVersioning
|
|
9
10
|
|
|
10
11
|
# @param ssh [Odysseus::Deployer::SSH] SSH connection
|
|
11
12
|
# @param config [Hash] parsed deploy config
|
|
@@ -39,7 +40,7 @@ module Odysseus
|
|
|
39
40
|
log " Found #{old_containers.size} existing container(s)"
|
|
40
41
|
|
|
41
42
|
# Step 2: Start new container
|
|
42
|
-
log
|
|
43
|
+
log 'Starting new container...'
|
|
43
44
|
new_container_id = start_new_container(image: image, role: role)
|
|
44
45
|
log " Container started: #{new_container_id[0..11]}"
|
|
45
46
|
|
|
@@ -50,25 +51,25 @@ module Odysseus
|
|
|
50
51
|
unless wait_for_healthy(new_container_id)
|
|
51
52
|
log_health_failure(new_container_id)
|
|
52
53
|
handle_failed_deploy(new_container_id)
|
|
53
|
-
raise Odysseus::DeployError,
|
|
54
|
+
raise Odysseus::DeployError, 'Container failed health checks'
|
|
54
55
|
end
|
|
55
|
-
log
|
|
56
|
+
log ' Health check passed'
|
|
56
57
|
else
|
|
57
|
-
log
|
|
58
|
+
log 'No health check configured, waiting 5s for startup...'
|
|
58
59
|
sleep 5
|
|
59
60
|
unless @docker.running?(new_container_id)
|
|
60
61
|
log_health_failure(new_container_id)
|
|
61
62
|
handle_failed_deploy(new_container_id)
|
|
62
|
-
raise Odysseus::DeployError,
|
|
63
|
+
raise Odysseus::DeployError, 'Container failed to start'
|
|
63
64
|
end
|
|
64
|
-
log
|
|
65
|
+
log ' Container is running'
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
# Step 4: Stop old containers gracefully
|
|
68
69
|
old_containers.each do |old|
|
|
69
70
|
log "Stopping old container #{old['ID'][0..11]} (30s grace period)..."
|
|
70
71
|
graceful_stop(old['ID'])
|
|
71
|
-
log
|
|
72
|
+
log ' Old container removed'
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
# Step 5: Cleanup old stopped containers
|
|
@@ -93,19 +94,22 @@ module Odysseus
|
|
|
93
94
|
def start_new_container(image:, role:)
|
|
94
95
|
service = @config[:service]
|
|
95
96
|
role_name = "#{service}-#{role}"
|
|
96
|
-
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
|
97
|
-
container_name = "#{role_name}-#{timestamp}"
|
|
97
|
+
timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
|
|
98
|
+
container_name = "#{role_name}-#{deploy_version_tag(image)}-#{timestamp}"
|
|
98
99
|
|
|
99
100
|
server_config = @config[:servers][role] || {}
|
|
100
101
|
options = server_config[:options] || {}
|
|
101
102
|
|
|
103
|
+
# A jobs-only deploy may be the first thing ever run on this host, so
|
|
104
|
+
# the network can't be assumed to exist (a web role gets it as a side
|
|
105
|
+
# effect of ensure_caddy!, and DependencyDeploy has its own copy).
|
|
106
|
+
ensure_network!
|
|
107
|
+
|
|
102
108
|
env = build_environment
|
|
103
109
|
log " Environment: #{env.size} variable(s) injected"
|
|
104
110
|
|
|
105
111
|
volumes = namespace_volumes(server_config[:volumes], service: role_name)
|
|
106
|
-
if volumes&.any?
|
|
107
|
-
log " Volumes: #{volumes.join(', ')}"
|
|
108
|
-
end
|
|
112
|
+
log " Volumes: #{volumes.join(', ')}" if volumes&.any?
|
|
109
113
|
|
|
110
114
|
if options[:memory] || options[:cpus]
|
|
111
115
|
log " Resources: memory=#{options[:memory] || 'default'}, cpus=#{options[:cpus] || 'default'}"
|
|
@@ -116,7 +120,8 @@ module Odysseus
|
|
|
116
120
|
image: image,
|
|
117
121
|
options: {
|
|
118
122
|
service: role_name,
|
|
119
|
-
version:
|
|
123
|
+
version: deploy_version_tag(image),
|
|
124
|
+
labels: version_labels,
|
|
120
125
|
env: env,
|
|
121
126
|
volumes: volumes,
|
|
122
127
|
memory: options[:memory],
|
|
@@ -130,31 +135,14 @@ module Odysseus
|
|
|
130
135
|
)
|
|
131
136
|
end
|
|
132
137
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
@config[:env][:clear]&.each do |key, value|
|
|
138
|
-
env[key.to_s] = value.to_s
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Secret env vars (from encrypted file or server environment)
|
|
142
|
-
@config[:env][:secret]&.each do |key|
|
|
143
|
-
# Try encrypted secrets file first
|
|
144
|
-
if @secrets_loader&.configured?
|
|
145
|
-
value = @secrets_loader.get(key)
|
|
146
|
-
if value
|
|
147
|
-
env[key.to_s] = value.to_s
|
|
148
|
-
next
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Fall back to server's environment
|
|
153
|
-
value = @ssh.execute("echo $#{key}").strip
|
|
154
|
-
env[key.to_s] = value unless value.empty?
|
|
155
|
-
end
|
|
138
|
+
def ensure_network!
|
|
139
|
+
log 'Ensuring Docker network exists...'
|
|
140
|
+
@docker.ensure_network('odysseus', labels: { 'odysseus.managed' => 'true' })
|
|
141
|
+
end
|
|
156
142
|
|
|
157
|
-
|
|
143
|
+
# Same environment a web container gets — see Core::Environment.
|
|
144
|
+
def build_environment
|
|
145
|
+
Odysseus::Core::Environment.new(config: @config, secrets_loader: @secrets_loader, ssh: @ssh).build
|
|
158
146
|
end
|
|
159
147
|
|
|
160
148
|
def build_healthcheck(hc_config)
|
|
@@ -179,10 +167,10 @@ module Odysseus
|
|
|
179
167
|
end
|
|
180
168
|
|
|
181
169
|
def handle_failed_deploy(new_container_id)
|
|
182
|
-
log
|
|
170
|
+
log 'Rolling back failed deploy...', :warn
|
|
183
171
|
@docker.stop(new_container_id)
|
|
184
172
|
@docker.remove(new_container_id, force: true)
|
|
185
|
-
log
|
|
173
|
+
log 'Rollback complete — failed container removed'
|
|
186
174
|
end
|
|
187
175
|
|
|
188
176
|
def log_health_failure(container_id)
|
|
@@ -191,7 +179,7 @@ module Odysseus
|
|
|
191
179
|
begin
|
|
192
180
|
recent_logs = @docker.logs(container_id, tail: 30)
|
|
193
181
|
unless recent_logs.strip.empty?
|
|
194
|
-
log
|
|
182
|
+
log ' Container logs (last 30 lines):', :error
|
|
195
183
|
recent_logs.each_line { |line| log " #{line.rstrip}", :error }
|
|
196
184
|
end
|
|
197
185
|
rescue StandardError => e
|
|
@@ -208,9 +196,9 @@ module Odysseus
|
|
|
208
196
|
|
|
209
197
|
def default_logger
|
|
210
198
|
@default_logger ||= Object.new.tap do |l|
|
|
211
|
-
def l.info(msg)
|
|
212
|
-
def l.warn(msg)
|
|
213
|
-
def l.error(msg)
|
|
199
|
+
def l.info(msg) = puts(msg)
|
|
200
|
+
def l.warn(msg) = puts("[WARN] #{msg}")
|
|
201
|
+
def l.error(msg) = puts("[ERROR] #{msg}")
|
|
214
202
|
end
|
|
215
203
|
end
|
|
216
204
|
|