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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +485 -1
  3. data/LICENSE.txt +25 -7
  4. data/README.md +12 -3
  5. data/lib/odysseus/builder/client.rb +15 -15
  6. data/lib/odysseus/caddy/client.rb +119 -42
  7. data/lib/odysseus/command_redaction.rb +38 -0
  8. data/lib/odysseus/config/parser.rb +43 -18
  9. data/lib/odysseus/core/deploy_versioning.rb +42 -0
  10. data/lib/odysseus/core/environment.rb +63 -0
  11. data/lib/odysseus/core/version.rb +1 -1
  12. data/lib/odysseus/core/volume_namespacer.rb +0 -0
  13. data/lib/odysseus/core.rb +2 -2
  14. data/lib/odysseus/deploy_log.rb +120 -0
  15. data/lib/odysseus/deploy_version.rb +9 -0
  16. data/lib/odysseus/deployer/dependency_manager.rb +121 -0
  17. data/lib/odysseus/deployer/executor.rb +213 -161
  18. data/lib/odysseus/deployer/retention_sweeper.rb +94 -0
  19. data/lib/odysseus/deployer/ssh.rb +92 -18
  20. data/lib/odysseus/docker/client.rb +240 -48
  21. data/lib/odysseus/docker/labels.rb +43 -0
  22. data/lib/odysseus/errors.rb +10 -0
  23. data/lib/odysseus/git.rb +70 -0
  24. data/lib/odysseus/host_paths.rb +79 -0
  25. data/lib/odysseus/host_providers/base.rb +1 -1
  26. data/lib/odysseus/host_providers/static.rb +1 -1
  27. data/lib/odysseus/host_providers.rb +3 -4
  28. data/lib/odysseus/host_verifier.rb +156 -0
  29. data/lib/odysseus/host_versions.rb +59 -0
  30. data/lib/odysseus/orchestrator/{accessory_deploy.rb → dependency_deploy.rb} +84 -88
  31. data/lib/odysseus/orchestrator/job_deploy.rb +31 -43
  32. data/lib/odysseus/orchestrator/web_deploy.rb +61 -55
  33. data/lib/odysseus/plugins.rb +72 -0
  34. data/lib/odysseus/retention_plan.rb +14 -0
  35. data/lib/odysseus/retention_planner.rb +58 -0
  36. data/lib/odysseus/rollback_plan.rb +20 -0
  37. data/lib/odysseus/rollback_planner.rb +107 -0
  38. data/lib/odysseus/sails.rb +0 -0
  39. data/lib/odysseus/secrets/encrypted_file.rb +5 -7
  40. data/lib/odysseus/secrets/loader.rb +1 -3
  41. data/lib/odysseus/setup/docker_apt.rb +178 -0
  42. data/lib/odysseus/setup/escalation.rb +73 -0
  43. data/lib/odysseus/setup/preparer.rb +372 -0
  44. data/lib/odysseus/setup/public_key.rb +127 -0
  45. data/lib/odysseus/validators/config.rb +38 -25
  46. data/lib/odysseus/version_resolver.rb +80 -0
  47. data/lib/odysseus.rb +0 -0
  48. data/sig/odysseus/core.rbs +0 -0
  49. metadata +48 -27
  50. data/Rakefile +0 -12
  51. data/lib/odysseus/version.rb +0 -5
@@ -1,10 +1,10 @@
1
- # lib/odysseus/orchestrator/accessory_deploy.rb
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 AccessoryDeploy
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 accessory is running
24
- # @param name [Symbol] accessory name
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
- accessory_config = @config[:accessories][name]
28
- raise Odysseus::ConfigError, "Accessory '#{name}' not found in config" unless accessory_config
27
+ dependency_config = @config[:dependencies][name]
28
+ raise Odysseus::ConfigError, "Dependency '#{name}' not found in config" unless dependency_config
29
29
 
30
- service_name = accessory_name(name)
31
- image = accessory_config[:image]
30
+ service_name = dependency_name(name)
31
+ image = dependency_config[:image]
32
32
 
33
- log "Deploying accessory: #{service_name}"
33
+ log "Deploying dependency: #{service_name}"
34
34
  log " Image: #{image}"
35
35
 
36
- # Ensure the Docker network exists (accessories may boot before any service deploy)
36
+ # Ensure the Docker network exists (dependencies may boot before any service deploy)
37
37
  ensure_network!
38
38
 
39
- # Check if accessory is already running
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 " Already running — skipping"
42
+ log ' Already running — skipping'
43
43
  return { success: true, already_running: true, service: service_name }
44
44
  end
45
45
 
46
- # Start the accessory
46
+ # Start the dependency
47
47
  log "Starting #{service_name}..."
48
- container_id = start_accessory(name: name, config: accessory_config)
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 accessory_config[:healthcheck]
53
- hc = accessory_config[:healthcheck]
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, "Accessory failed health checks"
59
+ raise Odysseus::DeployError, 'Dependency failed health checks'
60
60
  end
61
- log " Health check passed"
61
+ log ' Health check passed'
62
62
  else
63
- log "No health check configured, waiting 3s for startup..."
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, "Accessory failed to start"
67
+ raise Odysseus::DeployError, 'Dependency failed to start'
68
68
  end
69
- log " Container is running"
69
+ log ' Container is running'
70
70
  end
71
71
 
72
72
  # Add to Caddy if proxy config is present
73
- if accessory_config[:proxy]
74
- proxy_hosts = accessory_config[:proxy][:hosts]&.join(', ')
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: accessory_config)
77
- log " Proxy configured"
76
+ add_to_caddy(name: name, container_id: container_id, config: dependency_config)
77
+ log ' Proxy configured'
78
78
  end
79
79
 
80
- log "Accessory #{service_name} deployed"
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 "Accessory deploy FAILED: #{e.message}", :error
89
+ log "Dependency deploy FAILED: #{e.message}", :error
90
90
  raise
91
91
  end
92
92
 
93
- # Stop and remove an accessory
94
- # @param name [Symbol] accessory name
93
+ # Stop and remove an dependency
94
+ # @param name [Symbol] dependency name
95
95
  def remove(name:)
96
- accessory_config = @config[:accessories][name]
97
- raise Odysseus::ConfigError, "Accessory '#{name}' not found in config" unless accessory_config
96
+ dependency_config = @config[:dependencies][name]
97
+ raise Odysseus::ConfigError, "Dependency '#{name}' not found in config" unless dependency_config
98
98
 
99
- service_name = accessory_name(name)
100
- log "Removing accessory: #{service_name}"
99
+ service_name = dependency_name(name)
100
+ log "Removing dependency: #{service_name}"
101
101
 
102
102
  # Remove from Caddy if proxy configured
103
- if accessory_config[:proxy]
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 = accessory_config[:proxy][:app_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 "Accessory #{service_name} removed!"
119
+ log "Dependency #{service_name} removed!"
120
120
  { success: true, service: service_name }
121
121
  end
122
122
 
123
- # Restart an accessory (remove and redeploy)
124
- # @param name [Symbol] accessory name
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 accessory to a new image version (preserves volumes)
131
- # @param name [Symbol] accessory name
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
- accessory_config = @config[:accessories][name]
135
- raise Odysseus::ConfigError, "Accessory '#{name}' not found in config" unless accessory_config
134
+ dependency_config = @config[:dependencies][name]
135
+ raise Odysseus::ConfigError, "Dependency '#{name}' not found in config" unless dependency_config
136
136
 
137
- service_name = accessory_name(name)
138
- image = accessory_config[:image]
137
+ service_name = dependency_name(name)
138
+ image = dependency_config[:image]
139
139
 
140
- log "Upgrading accessory: #{service_name}"
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 "Pulling new image..."
147
+ log 'Pulling new image...'
148
148
  @docker.pull(image)
149
- log " Image pulled"
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 accessory_config[:proxy] && old_container && old_container['State'] == 'running'
156
+ if dependency_config[:proxy] && old_container && old_container['State'] == 'running'
157
157
  container_name = old_container['Names'].delete_prefix('/')
158
- port = accessory_config[:proxy][:app_port]
159
- log "Draining from proxy..."
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 " Drained from proxy"
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 " Old container removed"
169
+ log ' Old container removed'
170
170
  end
171
171
 
172
- # Start the accessory with the new image (volumes are preserved on host)
173
- log "Starting new container..."
174
- container_id = start_accessory(name: name, config: accessory_config)
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 accessory_config[:healthcheck]
179
- hc = accessory_config[:healthcheck]
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, "Accessory failed health checks after upgrade"
185
+ raise Odysseus::DeployError, 'Dependency failed health checks after upgrade'
186
186
  end
187
- log " Health check passed"
187
+ log ' Health check passed'
188
188
  else
189
- log "No health check configured, waiting 3s for startup..."
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, "Accessory failed to start after upgrade"
193
+ raise Odysseus::DeployError, 'Dependency failed to start after upgrade'
194
194
  end
195
- log " Container is running"
195
+ log ' Container is running'
196
196
  end
197
197
 
198
198
  # Add to Caddy if proxy config is present
199
- if accessory_config[:proxy]
200
- proxy_hosts = accessory_config[:proxy][:hosts]&.join(', ')
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: accessory_config)
203
- log " Proxy configured"
202
+ add_to_caddy(name: name, container_id: container_id, config: dependency_config)
203
+ log ' Proxy configured'
204
204
  end
205
205
 
206
- log "Accessory #{service_name} upgraded"
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 "Accessory upgrade failed: #{e.message}", :error
216
+ log "Dependency upgrade failed: #{e.message}", :error
217
217
  raise
218
218
  end
219
219
 
220
- # List status of all accessories
221
- # @return [Array<Hash>] accessory statuses
220
+ # List status of all dependencies
221
+ # @return [Array<Hash>] dependency statuses
222
222
  def list_status
223
- return [] unless @config[:accessories]
223
+ return [] unless @config[:dependencies]
224
224
 
225
- @config[:accessories].map do |name, config|
226
- service_name = accessory_name(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 accessory_name(name)
243
+ def dependency_name(name)
244
244
  "#{@config[:service]}-#{name}"
245
245
  end
246
246
 
247
247
  def ensure_network!
248
- log "Ensuring Docker network exists..."
248
+ log 'Ensuring Docker network exists...'
249
249
  @docker.ensure_network('odysseus', labels: { 'odysseus.managed' => 'true' })
250
250
  end
251
251
 
252
- def start_accessory(name:, config:)
253
- service_name = accessory_name(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: accessory_name(name),
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 " Container logs (last 30 lines):", :error
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); puts msg; end
364
- def l.warn(msg); puts "[WARN] #{msg}"; end
365
- def l.error(msg); puts "[ERROR] #{msg}"; end
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 "Starting new container..."
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, "Container failed health checks"
54
+ raise Odysseus::DeployError, 'Container failed health checks'
54
55
  end
55
- log " Health check passed"
56
+ log ' Health check passed'
56
57
  else
57
- log "No health check configured, waiting 5s for startup..."
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, "Container failed to start"
63
+ raise Odysseus::DeployError, 'Container failed to start'
63
64
  end
64
- log " Container is running"
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 " Old container removed"
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: timestamp,
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 build_environment
134
- env = {}
135
-
136
- # Clear env vars (hardcoded values)
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
- env
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 "Rolling back failed deploy...", :warn
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 "Rollback complete — failed container removed"
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 " Container logs (last 30 lines):", :error
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); puts msg; end
212
- def l.warn(msg); puts "[WARN] #{msg}"; end
213
- def l.error(msg); puts "[ERROR] #{msg}"; end
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