odysseus-cli 0.3.0 → 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 +332 -0
- data/LICENSE.txt +26 -0
- data/README.md +472 -88
- data/bin/odysseus +176 -118
- data/lib/odysseus/cli/cli.rb +293 -229
- data/lib/odysseus/cli/doctor_commands.rb +93 -0
- data/lib/odysseus/cli/interactive_commands.rb +198 -0
- data/lib/odysseus/cli/rollback_commands.rb +107 -0
- data/lib/odysseus/cli/setup_commands.rb +176 -0
- data/lib/odysseus/cli/ui.rb +49 -39
- data/lib/odysseus/cli/version.rb +7 -0
- metadata +18 -49
data/lib/odysseus/cli/cli.rb
CHANGED
|
@@ -4,10 +4,19 @@ require 'odysseus'
|
|
|
4
4
|
require 'yaml'
|
|
5
5
|
require 'tempfile'
|
|
6
6
|
require_relative 'ui'
|
|
7
|
+
require_relative 'rollback_commands'
|
|
8
|
+
require_relative 'interactive_commands'
|
|
9
|
+
require_relative 'doctor_commands'
|
|
10
|
+
require_relative 'setup_commands'
|
|
7
11
|
|
|
8
12
|
module Odysseus
|
|
9
13
|
module CLI
|
|
10
14
|
class CLI
|
|
15
|
+
include RollbackCommands
|
|
16
|
+
include InteractiveCommands
|
|
17
|
+
include DoctorCommands
|
|
18
|
+
include SetupCommands
|
|
19
|
+
|
|
11
20
|
def initialize(debug: false)
|
|
12
21
|
@ui = UI.new(debug: debug)
|
|
13
22
|
end
|
|
@@ -15,7 +24,7 @@ module Odysseus
|
|
|
15
24
|
# Deploy command
|
|
16
25
|
def deploy(options = {})
|
|
17
26
|
config_file = options[:config] || 'deploy.yml'
|
|
18
|
-
image_tag = options[:image]
|
|
27
|
+
image_tag = options[:image]
|
|
19
28
|
should_build = options[:build] || false
|
|
20
29
|
dry_run = options[:'dry-run'] || false
|
|
21
30
|
verbose = options[:verbose] || @ui.debug?
|
|
@@ -23,41 +32,37 @@ module Odysseus
|
|
|
23
32
|
config = load_config(config_file)
|
|
24
33
|
uses_registry = config[:registry] && config[:registry][:server]
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
executor = Odysseus::Deployer::Executor.new(config_file, verbose: verbose)
|
|
36
|
+
resolved = executor.deploy_version(image_tag)
|
|
37
|
+
|
|
38
|
+
distribution = uses_registry ? "registry (#{config[:registry][:server]})" : 'pussh (SSH)'
|
|
27
39
|
@ui.deploy_header(
|
|
28
40
|
service: config[:service],
|
|
29
41
|
image: config[:image],
|
|
30
|
-
image_tag:
|
|
42
|
+
image_tag: resolved.version,
|
|
31
43
|
build: should_build,
|
|
32
44
|
distribution: distribution
|
|
33
45
|
)
|
|
34
46
|
|
|
35
|
-
executor = Odysseus::Deployer::Executor.new(config_file, verbose: verbose)
|
|
36
47
|
start_time = Time.now
|
|
37
48
|
|
|
38
49
|
if should_build
|
|
39
50
|
# Build phase — captured as streaming steps
|
|
40
|
-
@ui.stream_steps(title:
|
|
51
|
+
@ui.stream_steps(title: 'Building and distributing') do
|
|
41
52
|
build_result = executor.build_and_distribute(image_tag: image_tag)
|
|
42
53
|
|
|
43
|
-
unless build_result[:build][:success]
|
|
44
|
-
raise Odysseus::BuildError, "Build failed: #{build_result[:build][:error]}"
|
|
45
|
-
end
|
|
54
|
+
raise Odysseus::BuildError, "Build failed: #{build_result[:build][:error]}" unless build_result[:build][:success]
|
|
46
55
|
|
|
47
56
|
if uses_registry
|
|
48
|
-
unless build_result[:push][:success]
|
|
49
|
-
raise Odysseus::BuildError, "Push to registry failed"
|
|
50
|
-
end
|
|
57
|
+
raise Odysseus::BuildError, 'Push to registry failed' unless build_result[:push][:success]
|
|
51
58
|
else
|
|
52
|
-
unless build_result[:pussh][:success]
|
|
53
|
-
raise Odysseus::BuildError, "Image distribution failed"
|
|
54
|
-
end
|
|
59
|
+
raise Odysseus::BuildError, 'Image distribution failed' unless build_result[:pussh][:success]
|
|
55
60
|
end
|
|
56
61
|
end
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
# Deploy phase — each orchestrator log line becomes a sub-step
|
|
60
|
-
@ui.stream_steps(title:
|
|
65
|
+
@ui.stream_steps(title: 'Deploying service') do
|
|
61
66
|
executor.deploy_all(image_tag: image_tag, dry_run: dry_run)
|
|
62
67
|
end
|
|
63
68
|
|
|
@@ -71,27 +76,28 @@ module Odysseus
|
|
|
71
76
|
# Build command
|
|
72
77
|
def build(options = {})
|
|
73
78
|
config_file = options[:config] || 'deploy.yml'
|
|
74
|
-
image_tag = options[:image]
|
|
79
|
+
image_tag = options[:image]
|
|
75
80
|
push = options[:push] || false
|
|
76
81
|
context_path = options[:context]
|
|
77
82
|
verbose = options[:verbose] || @ui.debug?
|
|
78
83
|
|
|
79
84
|
config = load_config(config_file)
|
|
80
85
|
|
|
81
|
-
@ui.header "Odysseus Build"
|
|
82
|
-
@ui.info "Image", "#{config[:image]}:#{image_tag}"
|
|
83
|
-
@ui.info "Strategy", (config.dig(:builder, :strategy) || :local).to_s
|
|
84
|
-
@ui.blank
|
|
85
|
-
|
|
86
86
|
executor = Odysseus::Deployer::Executor.new(config_file, verbose: verbose)
|
|
87
|
+
resolved = executor.deploy_version(image_tag)
|
|
88
|
+
|
|
89
|
+
@ui.header 'Odysseus Build'
|
|
90
|
+
@ui.info 'Image', "#{config[:image]}:#{resolved.version}"
|
|
91
|
+
@ui.info 'Strategy', (config.dig(:builder, :strategy) || :local).to_s
|
|
92
|
+
@ui.blank
|
|
87
93
|
|
|
88
|
-
result = @ui.spin_step("Building image #{config[:image]}:#{
|
|
94
|
+
result = @ui.spin_step("Building image #{config[:image]}:#{resolved.version}") do
|
|
89
95
|
executor.build(image_tag: image_tag, push: push, context_path: context_path)
|
|
90
96
|
end
|
|
91
97
|
|
|
92
98
|
if result[:success]
|
|
93
|
-
@ui.step_ok
|
|
94
|
-
@ui.step_ok
|
|
99
|
+
@ui.step_ok 'Pushed to registry' if result[:pushed]
|
|
100
|
+
@ui.step_ok 'Build complete'
|
|
95
101
|
else
|
|
96
102
|
@ui.step_fail "Build failed: #{result[:error]}"
|
|
97
103
|
exit 1
|
|
@@ -104,20 +110,21 @@ module Odysseus
|
|
|
104
110
|
# Pussh command
|
|
105
111
|
def pussh(options = {})
|
|
106
112
|
config_file = options[:config] || 'deploy.yml'
|
|
107
|
-
image_tag = options[:image]
|
|
113
|
+
image_tag = options[:image]
|
|
108
114
|
should_build = options[:build] || false
|
|
109
115
|
verbose = options[:verbose] || @ui.debug?
|
|
110
116
|
|
|
111
117
|
config = load_config(config_file)
|
|
112
118
|
|
|
113
|
-
@ui.header "Odysseus Pussh"
|
|
114
|
-
@ui.info "Image", "#{config[:image]}:#{image_tag}"
|
|
115
|
-
@ui.blank
|
|
116
|
-
|
|
117
119
|
executor = Odysseus::Deployer::Executor.new(config_file, verbose: verbose)
|
|
120
|
+
resolved = executor.deploy_version(image_tag)
|
|
121
|
+
|
|
122
|
+
@ui.header 'Odysseus Pussh'
|
|
123
|
+
@ui.info 'Image', "#{config[:image]}:#{resolved.version}"
|
|
124
|
+
@ui.blank
|
|
118
125
|
|
|
119
126
|
if should_build
|
|
120
|
-
result = @ui.spin_step("Building image #{config[:image]}:#{
|
|
127
|
+
result = @ui.spin_step("Building image #{config[:image]}:#{resolved.version}") do
|
|
121
128
|
executor.build_and_pussh(image_tag: image_tag)
|
|
122
129
|
end
|
|
123
130
|
|
|
@@ -127,7 +134,7 @@ module Odysseus
|
|
|
127
134
|
end
|
|
128
135
|
pussh_result = result[:pussh]
|
|
129
136
|
else
|
|
130
|
-
pussh_result = @ui.spin_step(
|
|
137
|
+
pussh_result = @ui.spin_step('Pushing image via SSH...') do
|
|
131
138
|
executor.pussh(image_tag: image_tag)
|
|
132
139
|
end
|
|
133
140
|
end
|
|
@@ -135,7 +142,7 @@ module Odysseus
|
|
|
135
142
|
if pussh_result[:success]
|
|
136
143
|
@ui.step_ok "Pussh complete (#{pussh_result[:results]&.size || 0} host(s))"
|
|
137
144
|
else
|
|
138
|
-
@ui.step_fail
|
|
145
|
+
@ui.step_fail 'Pussh failed'
|
|
139
146
|
exit 1
|
|
140
147
|
end
|
|
141
148
|
rescue Odysseus::Error => e
|
|
@@ -149,9 +156,9 @@ module Odysseus
|
|
|
149
156
|
config = load_config(config_file)
|
|
150
157
|
service_name = config[:service]
|
|
151
158
|
|
|
152
|
-
@ui.header
|
|
153
|
-
@ui.info
|
|
154
|
-
@ui.info
|
|
159
|
+
@ui.header 'Odysseus Status'
|
|
160
|
+
@ui.info 'Service', service_name
|
|
161
|
+
@ui.info 'Server', server
|
|
155
162
|
@ui.blank
|
|
156
163
|
|
|
157
164
|
ssh = connect_to_server(server, config)
|
|
@@ -161,24 +168,21 @@ module Odysseus
|
|
|
161
168
|
caddy = Odysseus::Caddy::Client.new(ssh: ssh, docker: docker)
|
|
162
169
|
|
|
163
170
|
# Web containers
|
|
164
|
-
@ui.section
|
|
171
|
+
@ui.section 'Web'
|
|
165
172
|
web_containers = docker.list(service: service_name)
|
|
166
173
|
if web_containers.empty?
|
|
167
|
-
@ui.step
|
|
174
|
+
@ui.step '(no containers running)'
|
|
168
175
|
else
|
|
169
|
-
rows = web_containers.map
|
|
170
|
-
|
|
171
|
-
[c['Names'], c['State'], c['Image'], health]
|
|
172
|
-
end
|
|
173
|
-
@ui.table(headers: ['Name', 'State', 'Image', 'Health'], rows: rows)
|
|
176
|
+
rows = web_containers.map { |c| web_container_row(c) }
|
|
177
|
+
@ui.table(headers: %w[Version Ref Deployed Image State Health], rows: rows)
|
|
174
178
|
end
|
|
175
179
|
|
|
176
180
|
caddy_status = caddy.status
|
|
177
181
|
if caddy_status[:running]
|
|
178
182
|
svc_route = caddy_status[:services].find { |s| s[:service] == service_name }
|
|
179
183
|
if svc_route
|
|
180
|
-
@ui.info
|
|
181
|
-
@ui.info
|
|
184
|
+
@ui.info 'Proxy', svc_route[:hosts].join(', ')
|
|
185
|
+
@ui.info 'Upstreams', svc_route[:upstreams].join(', ')
|
|
182
186
|
end
|
|
183
187
|
end
|
|
184
188
|
@ui.blank
|
|
@@ -186,7 +190,7 @@ module Odysseus
|
|
|
186
190
|
# Workers
|
|
187
191
|
non_web_roles = config[:servers].keys.reject { |r| r == :web }
|
|
188
192
|
if non_web_roles.any?
|
|
189
|
-
@ui.section
|
|
193
|
+
@ui.section 'Workers'
|
|
190
194
|
rows = []
|
|
191
195
|
non_web_roles.each do |role|
|
|
192
196
|
role_service = "#{service_name}-#{role}"
|
|
@@ -199,14 +203,14 @@ module Odysseus
|
|
|
199
203
|
end
|
|
200
204
|
end
|
|
201
205
|
end
|
|
202
|
-
@ui.table(headers: [
|
|
206
|
+
@ui.table(headers: %w[Role State Name Image], rows: rows)
|
|
203
207
|
@ui.blank
|
|
204
208
|
end
|
|
205
209
|
|
|
206
|
-
#
|
|
207
|
-
if config[:
|
|
208
|
-
@ui.section
|
|
209
|
-
rows = config[:
|
|
210
|
+
# Dependencies
|
|
211
|
+
if config[:dependencies]&.any?
|
|
212
|
+
@ui.section 'Dependencies'
|
|
213
|
+
rows = config[:dependencies].map do |name, acc_config|
|
|
210
214
|
acc_service = "#{service_name}-#{name}"
|
|
211
215
|
containers = docker.list(service: acc_service, all: true)
|
|
212
216
|
running = containers.find { |c| c['State'] == 'running' }
|
|
@@ -217,27 +221,27 @@ module Odysseus
|
|
|
217
221
|
[name.to_s, 'stopped', acc_config[:image], '-', '']
|
|
218
222
|
end
|
|
219
223
|
end
|
|
220
|
-
@ui.table(headers: [
|
|
224
|
+
@ui.table(headers: %w[Name State Image Container Health], rows: rows)
|
|
221
225
|
@ui.blank
|
|
222
226
|
end
|
|
223
227
|
|
|
224
228
|
# TLS
|
|
225
229
|
if config[:proxy][:hosts]&.any?
|
|
226
|
-
@ui.section
|
|
230
|
+
@ui.section 'TLS'
|
|
227
231
|
if caddy_status[:running] && caddy_status[:tls][:enabled]
|
|
228
232
|
service_hosts = config[:proxy][:hosts]
|
|
229
233
|
relevant_policy = caddy_status[:tls][:policies].find do |p|
|
|
230
|
-
|
|
234
|
+
p[:subjects].intersect?(service_hosts)
|
|
231
235
|
end
|
|
232
236
|
if relevant_policy
|
|
233
|
-
@ui.info
|
|
234
|
-
@ui.info
|
|
235
|
-
@ui.info
|
|
237
|
+
@ui.info 'Domains', (relevant_policy[:subjects] & service_hosts).join(', ')
|
|
238
|
+
@ui.info 'Issuer', relevant_policy[:issuer]
|
|
239
|
+
@ui.info 'Email', relevant_policy[:email] || '(not set)'
|
|
236
240
|
else
|
|
237
|
-
@ui.step
|
|
241
|
+
@ui.step '(not configured for this service)'
|
|
238
242
|
end
|
|
239
243
|
else
|
|
240
|
-
@ui.step
|
|
244
|
+
@ui.step '(Caddy not running or TLS not configured)'
|
|
241
245
|
end
|
|
242
246
|
end
|
|
243
247
|
ensure
|
|
@@ -252,8 +256,8 @@ module Odysseus
|
|
|
252
256
|
def containers(server, options = {})
|
|
253
257
|
config_file = options[:config] || 'deploy.yml'
|
|
254
258
|
|
|
255
|
-
@ui.header
|
|
256
|
-
@ui.info
|
|
259
|
+
@ui.header 'Odysseus Containers'
|
|
260
|
+
@ui.info 'Server', server
|
|
257
261
|
@ui.blank
|
|
258
262
|
|
|
259
263
|
config = load_config(config_file)
|
|
@@ -270,7 +274,7 @@ module Odysseus
|
|
|
270
274
|
rows = containers.map do |c|
|
|
271
275
|
[c['ID'][0..11], c['State'], c['Names'], c['Image'], c['Status']]
|
|
272
276
|
end
|
|
273
|
-
@ui.table(headers: [
|
|
277
|
+
@ui.table(headers: %w[ID State Name Image Status], rows: rows)
|
|
274
278
|
end
|
|
275
279
|
ensure
|
|
276
280
|
ssh.close
|
|
@@ -287,107 +291,107 @@ module Odysseus
|
|
|
287
291
|
@ui.header "Validating #{config_file}"
|
|
288
292
|
config = load_config(config_file)
|
|
289
293
|
|
|
290
|
-
@ui.success
|
|
291
|
-
@ui.info
|
|
292
|
-
@ui.info
|
|
293
|
-
@ui.info
|
|
294
|
-
@ui.info
|
|
295
|
-
@ui.info
|
|
294
|
+
@ui.success 'Configuration is valid'
|
|
295
|
+
@ui.info 'Service', config[:service]
|
|
296
|
+
@ui.info 'Image', config[:image]
|
|
297
|
+
@ui.info 'Servers', config[:servers].keys.join(', ')
|
|
298
|
+
@ui.info 'Proxy', config[:proxy][:hosts]&.join(', ')
|
|
299
|
+
@ui.info 'Dependencies', config[:dependencies].keys.join(', ') if config[:dependencies]&.any?
|
|
296
300
|
rescue Odysseus::Error => e
|
|
297
301
|
@ui.error "Validation failed: #{e.message}"
|
|
298
302
|
exit 1
|
|
299
303
|
end
|
|
300
304
|
|
|
301
|
-
#
|
|
302
|
-
def
|
|
305
|
+
# Dependency commands
|
|
306
|
+
def dependency_boot(options = {})
|
|
303
307
|
config_file = options[:config] || 'deploy.yml'
|
|
304
308
|
name = require_name!(options)
|
|
305
309
|
|
|
306
|
-
@ui.header
|
|
307
|
-
@ui.info
|
|
310
|
+
@ui.header 'Dependency Boot'
|
|
311
|
+
@ui.info 'Dependency', name
|
|
308
312
|
@ui.blank
|
|
309
313
|
|
|
310
314
|
executor = Odysseus::Deployer::Executor.new(config_file)
|
|
311
|
-
@ui.spin_step("Booting #{name}...") { executor.
|
|
312
|
-
@ui.step_ok "
|
|
315
|
+
@ui.spin_step("Booting #{name}...") { executor.deploy_dependency(name: name) }
|
|
316
|
+
@ui.step_ok "Dependency #{name} deployed"
|
|
313
317
|
rescue Odysseus::Error => e
|
|
314
318
|
@ui.step_fail e.message
|
|
315
319
|
exit 1
|
|
316
320
|
end
|
|
317
321
|
|
|
318
|
-
def
|
|
322
|
+
def dependency_boot_all(options = {})
|
|
319
323
|
config_file = options[:config] || 'deploy.yml'
|
|
320
324
|
|
|
321
|
-
@ui.header
|
|
325
|
+
@ui.header 'Dependency Boot All'
|
|
322
326
|
@ui.blank
|
|
323
327
|
|
|
324
328
|
executor = Odysseus::Deployer::Executor.new(config_file)
|
|
325
|
-
@ui.spin_step(
|
|
326
|
-
@ui.step_ok
|
|
329
|
+
@ui.spin_step('Booting all dependencies...') { executor.boot_dependencies }
|
|
330
|
+
@ui.step_ok 'All dependencies deployed'
|
|
327
331
|
rescue Odysseus::Error => e
|
|
328
332
|
@ui.step_fail e.message
|
|
329
333
|
exit 1
|
|
330
334
|
end
|
|
331
335
|
|
|
332
|
-
def
|
|
336
|
+
def dependency_remove(options = {})
|
|
333
337
|
config_file = options[:config] || 'deploy.yml'
|
|
334
338
|
name = require_name!(options)
|
|
335
339
|
|
|
336
|
-
@ui.header
|
|
337
|
-
@ui.info
|
|
340
|
+
@ui.header 'Dependency Remove'
|
|
341
|
+
@ui.info 'Dependency', name
|
|
338
342
|
@ui.blank
|
|
339
343
|
|
|
340
344
|
executor = Odysseus::Deployer::Executor.new(config_file)
|
|
341
|
-
@ui.spin_step("Removing #{name}...") { executor.
|
|
342
|
-
@ui.step_ok "
|
|
345
|
+
@ui.spin_step("Removing #{name}...") { executor.remove_dependency(name: name) }
|
|
346
|
+
@ui.step_ok "Dependency #{name} removed"
|
|
343
347
|
rescue Odysseus::Error => e
|
|
344
348
|
@ui.step_fail e.message
|
|
345
349
|
exit 1
|
|
346
350
|
end
|
|
347
351
|
|
|
348
|
-
def
|
|
352
|
+
def dependency_restart(options = {})
|
|
349
353
|
config_file = options[:config] || 'deploy.yml'
|
|
350
354
|
name = require_name!(options)
|
|
351
355
|
|
|
352
|
-
@ui.header
|
|
353
|
-
@ui.info
|
|
356
|
+
@ui.header 'Dependency Restart'
|
|
357
|
+
@ui.info 'Dependency', name
|
|
354
358
|
@ui.blank
|
|
355
359
|
|
|
356
360
|
executor = Odysseus::Deployer::Executor.new(config_file)
|
|
357
|
-
@ui.spin_step("Restarting #{name}...") { executor.
|
|
358
|
-
@ui.step_ok "
|
|
361
|
+
@ui.spin_step("Restarting #{name}...") { executor.restart_dependency(name: name) }
|
|
362
|
+
@ui.step_ok "Dependency #{name} restarted"
|
|
359
363
|
rescue Odysseus::Error => e
|
|
360
364
|
@ui.step_fail e.message
|
|
361
365
|
exit 1
|
|
362
366
|
end
|
|
363
367
|
|
|
364
|
-
def
|
|
368
|
+
def dependency_upgrade(options = {})
|
|
365
369
|
config_file = options[:config] || 'deploy.yml'
|
|
366
370
|
name = require_name!(options)
|
|
367
371
|
|
|
368
|
-
@ui.header
|
|
369
|
-
@ui.info
|
|
372
|
+
@ui.header 'Dependency Upgrade'
|
|
373
|
+
@ui.info 'Dependency', name
|
|
370
374
|
@ui.blank
|
|
371
375
|
|
|
372
376
|
executor = Odysseus::Deployer::Executor.new(config_file)
|
|
373
|
-
@ui.spin_step("Upgrading #{name}...") { executor.
|
|
374
|
-
@ui.step_ok "
|
|
377
|
+
@ui.spin_step("Upgrading #{name}...") { executor.upgrade_dependency(name: name) }
|
|
378
|
+
@ui.step_ok "Dependency #{name} upgraded"
|
|
375
379
|
rescue Odysseus::Error => e
|
|
376
380
|
@ui.step_fail e.message
|
|
377
381
|
exit 1
|
|
378
382
|
end
|
|
379
383
|
|
|
380
|
-
def
|
|
384
|
+
def dependency_status(options = {})
|
|
381
385
|
config_file = options[:config] || 'deploy.yml'
|
|
382
386
|
|
|
383
|
-
@ui.header
|
|
387
|
+
@ui.header 'Dependency Status'
|
|
384
388
|
@ui.blank
|
|
385
389
|
|
|
386
390
|
executor = Odysseus::Deployer::Executor.new(config_file)
|
|
387
|
-
statuses = executor.
|
|
391
|
+
statuses = executor.dependency_status
|
|
388
392
|
|
|
389
393
|
if statuses.empty?
|
|
390
|
-
@ui.step
|
|
394
|
+
@ui.step 'No dependencies configured'
|
|
391
395
|
else
|
|
392
396
|
rows = statuses.map do |s|
|
|
393
397
|
state = s[:running] ? 'running' : 'stopped'
|
|
@@ -395,7 +399,7 @@ module Odysseus
|
|
|
395
399
|
proxy = s[:has_proxy] ? '✓' : ''
|
|
396
400
|
[s[:name].to_s, s[:host], state, s[:image], container, proxy]
|
|
397
401
|
end
|
|
398
|
-
@ui.table(headers: [
|
|
402
|
+
@ui.table(headers: %w[Name Host State Image Container Proxy], rows: rows)
|
|
399
403
|
end
|
|
400
404
|
rescue Odysseus::Error => e
|
|
401
405
|
@ui.error e.message
|
|
@@ -411,27 +415,20 @@ module Odysseus
|
|
|
411
415
|
since = options[:since]
|
|
412
416
|
|
|
413
417
|
config = load_config(config_file)
|
|
414
|
-
service_name =
|
|
418
|
+
service_name = Odysseus::Docker::Labels.service_for(service: config[:service], role: role)
|
|
415
419
|
|
|
416
420
|
@ui.header "Logs: #{service_name}"
|
|
417
|
-
@ui.info
|
|
421
|
+
@ui.info 'Server', server
|
|
418
422
|
@ui.blank
|
|
419
423
|
|
|
420
424
|
ssh = connect_to_server(server, config)
|
|
421
425
|
|
|
422
426
|
begin
|
|
423
427
|
docker = Odysseus::Docker::Client.new(ssh)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if containers.empty?
|
|
427
|
-
@ui.warn "No running containers found for #{service_name}"
|
|
428
|
-
return
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
container_id = containers.first['ID']
|
|
428
|
+
container_id = log_container_id!(docker, service_name, server, config, role: role)
|
|
432
429
|
|
|
433
430
|
if follow
|
|
434
|
-
@ui.step
|
|
431
|
+
@ui.step 'Following logs (Ctrl+C to stop)...'
|
|
435
432
|
@ui.blank
|
|
436
433
|
docker.logs(container_id, follow: true, tail: lines, since: since) { |line| print line }
|
|
437
434
|
else
|
|
@@ -445,8 +442,8 @@ module Odysseus
|
|
|
445
442
|
exit 1
|
|
446
443
|
end
|
|
447
444
|
|
|
448
|
-
#
|
|
449
|
-
def
|
|
445
|
+
# Dependency logs
|
|
446
|
+
def dependency_logs(server, options = {})
|
|
450
447
|
config_file = options[:config] || 'deploy.yml'
|
|
451
448
|
name = require_name!(options)
|
|
452
449
|
follow = options[:follow] || false
|
|
@@ -456,25 +453,18 @@ module Odysseus
|
|
|
456
453
|
config = load_config(config_file)
|
|
457
454
|
service_name = "#{config[:service]}-#{name}"
|
|
458
455
|
|
|
459
|
-
@ui.header "
|
|
460
|
-
@ui.info
|
|
456
|
+
@ui.header "Dependency Logs: #{service_name}"
|
|
457
|
+
@ui.info 'Server', server
|
|
461
458
|
@ui.blank
|
|
462
459
|
|
|
463
460
|
ssh = connect_to_server(server, config)
|
|
464
461
|
|
|
465
462
|
begin
|
|
466
463
|
docker = Odysseus::Docker::Client.new(ssh)
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if containers.empty?
|
|
470
|
-
@ui.warn "No running containers found for #{service_name}"
|
|
471
|
-
return
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
container_id = containers.first['ID']
|
|
464
|
+
container_id = log_container_id!(docker, service_name, server, config)
|
|
475
465
|
|
|
476
466
|
if follow
|
|
477
|
-
@ui.step
|
|
467
|
+
@ui.step 'Following logs (Ctrl+C to stop)...'
|
|
478
468
|
@ui.blank
|
|
479
469
|
docker.logs(container_id, follow: true, tail: lines, since: since) { |line| print line }
|
|
480
470
|
else
|
|
@@ -491,27 +481,28 @@ module Odysseus
|
|
|
491
481
|
# App exec
|
|
492
482
|
def app_exec(server, options = {})
|
|
493
483
|
config_file = options[:config] || 'deploy.yml'
|
|
484
|
+
role = (options[:role] || 'web').to_sym
|
|
494
485
|
command = options[:command]
|
|
495
486
|
|
|
496
487
|
unless command
|
|
497
|
-
@ui.error
|
|
488
|
+
@ui.error 'Command required (--command)'
|
|
498
489
|
exit 1
|
|
499
490
|
end
|
|
500
491
|
|
|
501
492
|
config = load_config(config_file)
|
|
502
|
-
image =
|
|
493
|
+
image = running_image(server, config, role)
|
|
503
494
|
|
|
504
|
-
@ui.header
|
|
505
|
-
@ui.info
|
|
506
|
-
@ui.info
|
|
495
|
+
@ui.header 'App Exec'
|
|
496
|
+
@ui.info 'Server', server
|
|
497
|
+
@ui.info 'Role', role
|
|
498
|
+
@ui.info 'Command', command
|
|
507
499
|
@ui.blank
|
|
508
500
|
|
|
509
501
|
ssh = connect_to_server(server, config)
|
|
510
502
|
|
|
511
503
|
begin
|
|
512
504
|
docker = Odysseus::Docker::Client.new(ssh)
|
|
513
|
-
env =
|
|
514
|
-
config[:env][:clear]&.each { |k, v| env[k.to_s] = v.to_s }
|
|
505
|
+
env = build_environment(config, config_file, ssh)
|
|
515
506
|
|
|
516
507
|
puts docker.run_once(image: image, command: command, options: { env: env, network: 'odysseus' })
|
|
517
508
|
ensure
|
|
@@ -522,84 +513,23 @@ module Odysseus
|
|
|
522
513
|
exit 1
|
|
523
514
|
end
|
|
524
515
|
|
|
525
|
-
#
|
|
526
|
-
def
|
|
527
|
-
config_file = options[:config] || 'deploy.yml'
|
|
528
|
-
config = load_config(config_file)
|
|
529
|
-
image = "#{config[:image]}:latest"
|
|
530
|
-
|
|
531
|
-
ssh_keys = config[:ssh][:keys].map { |k| "-i #{File.expand_path(k)}" }.join(' ')
|
|
532
|
-
env_flags = config[:env][:clear]&.map { |k, v| "-e #{k}=#{v}" }&.join(' ') || ''
|
|
533
|
-
|
|
534
|
-
system("ssh #{ssh_keys} -t #{config[:ssh][:user]}@#{server} 'docker run -it --rm --network odysseus #{env_flags} #{image} /bin/sh'")
|
|
535
|
-
rescue Odysseus::Error => e
|
|
536
|
-
@ui.error e.message
|
|
537
|
-
exit 1
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
# App console
|
|
541
|
-
def app_console(server, options = {})
|
|
542
|
-
config_file = options[:config] || 'deploy.yml'
|
|
543
|
-
console_cmd = options[:cmd] || '/bin/sh'
|
|
544
|
-
config = load_config(config_file)
|
|
545
|
-
image = "#{config[:image]}:latest"
|
|
546
|
-
|
|
547
|
-
ssh_keys = config[:ssh][:keys].map { |k| "-i #{File.expand_path(k)}" }.join(' ')
|
|
548
|
-
env_flags = config[:env][:clear]&.map { |k, v| "-e #{k}=#{v}" }&.join(' ') || ''
|
|
549
|
-
|
|
550
|
-
system("ssh #{ssh_keys} -t #{config[:ssh][:user]}@#{server} 'docker run -it --rm --network odysseus #{env_flags} #{image} #{console_cmd}'")
|
|
551
|
-
rescue Odysseus::Error => e
|
|
552
|
-
@ui.error e.message
|
|
553
|
-
exit 1
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
# Accessory shell
|
|
557
|
-
def accessory_shell(server, options = {})
|
|
558
|
-
config_file = options[:config] || 'deploy.yml'
|
|
559
|
-
name = require_name!(options)
|
|
560
|
-
|
|
561
|
-
config = load_config(config_file)
|
|
562
|
-
service_name = "#{config[:service]}-#{name}"
|
|
563
|
-
|
|
564
|
-
ssh = connect_to_server(server, config)
|
|
565
|
-
begin
|
|
566
|
-
docker = Odysseus::Docker::Client.new(ssh)
|
|
567
|
-
containers = docker.list(service: service_name)
|
|
568
|
-
|
|
569
|
-
if containers.empty?
|
|
570
|
-
@ui.error "No running containers found for #{service_name}"
|
|
571
|
-
exit 1
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
container_id = containers.first['ID']
|
|
575
|
-
ensure
|
|
576
|
-
ssh.close
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
ssh_keys = config[:ssh][:keys].map { |k| "-i #{File.expand_path(k)}" }.join(' ')
|
|
580
|
-
system("ssh #{ssh_keys} -t #{config[:ssh][:user]}@#{server} 'docker exec -it #{container_id} /bin/sh'")
|
|
581
|
-
rescue Odysseus::Error => e
|
|
582
|
-
@ui.error e.message
|
|
583
|
-
exit 1
|
|
584
|
-
end
|
|
585
|
-
|
|
586
|
-
# Accessory exec
|
|
587
|
-
def accessory_exec(server, options = {})
|
|
516
|
+
# Dependency exec
|
|
517
|
+
def dependency_exec(server, options = {})
|
|
588
518
|
config_file = options[:config] || 'deploy.yml'
|
|
589
519
|
name = require_name!(options)
|
|
590
520
|
command = options[:command]
|
|
591
521
|
|
|
592
522
|
unless command
|
|
593
|
-
@ui.error
|
|
523
|
+
@ui.error 'Command required (--command)'
|
|
594
524
|
exit 1
|
|
595
525
|
end
|
|
596
526
|
|
|
597
527
|
config = load_config(config_file)
|
|
598
528
|
service_name = "#{config[:service]}-#{name}"
|
|
599
529
|
|
|
600
|
-
@ui.header
|
|
601
|
-
@ui.info
|
|
602
|
-
@ui.info
|
|
530
|
+
@ui.header 'Dependency Exec'
|
|
531
|
+
@ui.info 'Dependency', name
|
|
532
|
+
@ui.info 'Command', command
|
|
603
533
|
@ui.blank
|
|
604
534
|
|
|
605
535
|
ssh = connect_to_server(server, config)
|
|
@@ -630,9 +560,9 @@ module Odysseus
|
|
|
630
560
|
config = load_config(config_file)
|
|
631
561
|
service_name = config[:service]
|
|
632
562
|
|
|
633
|
-
@ui.header
|
|
634
|
-
@ui.info
|
|
635
|
-
@ui.info
|
|
563
|
+
@ui.header 'Odysseus Cleanup'
|
|
564
|
+
@ui.info 'Service', service_name
|
|
565
|
+
@ui.info 'Server', server
|
|
636
566
|
@ui.blank
|
|
637
567
|
|
|
638
568
|
ssh = connect_to_server(server, config)
|
|
@@ -642,14 +572,14 @@ module Odysseus
|
|
|
642
572
|
caddy = Odysseus::Caddy::Client.new(ssh: ssh, docker: docker)
|
|
643
573
|
|
|
644
574
|
if @ui.debug?
|
|
645
|
-
@ui.section
|
|
575
|
+
@ui.section 'Disk usage (before)'
|
|
646
576
|
puts docker.disk_usage
|
|
647
577
|
@ui.blank
|
|
648
578
|
end
|
|
649
579
|
|
|
650
580
|
service_names = [service_name]
|
|
651
581
|
config[:servers].keys.reject { |r| r == :web }.each { |role| service_names << "#{service_name}-#{role}" }
|
|
652
|
-
config[:
|
|
582
|
+
config[:dependencies]&.each_key { |name| service_names << "#{service_name}-#{name}" }
|
|
653
583
|
|
|
654
584
|
total_removed = 0
|
|
655
585
|
service_names.each do |svc|
|
|
@@ -665,30 +595,46 @@ module Odysseus
|
|
|
665
595
|
|
|
666
596
|
# Clean Caddy routes
|
|
667
597
|
if caddy.running?
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
598
|
+
begin
|
|
599
|
+
caddy.remove_upstream(service: service_name, upstream: nil)
|
|
600
|
+
rescue StandardError
|
|
601
|
+
nil
|
|
602
|
+
end
|
|
603
|
+
config[:dependencies]&.each do |name, acc_config|
|
|
604
|
+
next unless acc_config[:proxy]
|
|
605
|
+
|
|
606
|
+
begin
|
|
607
|
+
caddy.remove_upstream(service: "#{service_name}-#{name}", upstream: nil)
|
|
608
|
+
rescue StandardError
|
|
609
|
+
nil
|
|
672
610
|
end
|
|
673
611
|
end
|
|
674
612
|
|
|
675
613
|
remaining = caddy.list_services.reject { |s| s[:service] == 'unknown' || s[:service].empty? }
|
|
676
614
|
if remaining.empty?
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
615
|
+
begin
|
|
616
|
+
docker.stop('odysseus-caddy', timeout: 10)
|
|
617
|
+
rescue StandardError
|
|
618
|
+
nil
|
|
619
|
+
end
|
|
620
|
+
begin
|
|
621
|
+
docker.remove('odysseus-caddy', force: true)
|
|
622
|
+
rescue StandardError
|
|
623
|
+
nil
|
|
624
|
+
end
|
|
625
|
+
@ui.step 'Caddy removed (no other services)'
|
|
680
626
|
else
|
|
681
627
|
@ui.step "Caddy kept (#{remaining.size} other service(s))"
|
|
682
628
|
end
|
|
683
629
|
end
|
|
684
630
|
|
|
685
631
|
if prune_images
|
|
686
|
-
@ui.step
|
|
632
|
+
@ui.step 'Pruning dangling images...'
|
|
687
633
|
docker.prune(containers: false, images: true, volumes: false, networks: false)
|
|
688
634
|
end
|
|
689
635
|
|
|
690
636
|
@ui.blank
|
|
691
|
-
@ui.success
|
|
637
|
+
@ui.success 'Cleanup complete'
|
|
692
638
|
ensure
|
|
693
639
|
ssh.close
|
|
694
640
|
end
|
|
@@ -699,16 +645,16 @@ module Odysseus
|
|
|
699
645
|
|
|
700
646
|
# Secrets commands
|
|
701
647
|
def secrets_generate_key(_options = {})
|
|
702
|
-
@ui.header
|
|
648
|
+
@ui.header 'Secrets: Generate Key'
|
|
703
649
|
@ui.blank
|
|
704
650
|
|
|
705
651
|
key = Odysseus::Secrets::EncryptedFile.generate_key
|
|
706
|
-
@ui.success
|
|
652
|
+
@ui.success 'Generated master key:'
|
|
707
653
|
@ui.blank
|
|
708
654
|
puts " #{key}"
|
|
709
655
|
@ui.blank
|
|
710
|
-
@ui.warn
|
|
711
|
-
@ui.step
|
|
656
|
+
@ui.warn 'Save this key securely!'
|
|
657
|
+
@ui.step 'Set as ODYSSEUS_MASTER_KEY environment variable'
|
|
712
658
|
end
|
|
713
659
|
|
|
714
660
|
def secrets_encrypt(options = {})
|
|
@@ -716,7 +662,7 @@ module Odysseus
|
|
|
716
662
|
output_file = options[:file] || 'secrets.yml.enc'
|
|
717
663
|
|
|
718
664
|
unless input_file
|
|
719
|
-
@ui.error
|
|
665
|
+
@ui.error 'Input file required (--input)'
|
|
720
666
|
exit 1
|
|
721
667
|
end
|
|
722
668
|
|
|
@@ -725,9 +671,9 @@ module Odysseus
|
|
|
725
671
|
exit 1
|
|
726
672
|
end
|
|
727
673
|
|
|
728
|
-
@ui.header
|
|
729
|
-
@ui.info
|
|
730
|
-
@ui.info
|
|
674
|
+
@ui.header 'Secrets: Encrypt'
|
|
675
|
+
@ui.info 'Input', input_file
|
|
676
|
+
@ui.info 'Output', output_file
|
|
731
677
|
@ui.blank
|
|
732
678
|
|
|
733
679
|
secrets = YAML.load_file(input_file)
|
|
@@ -737,7 +683,7 @@ module Odysseus
|
|
|
737
683
|
@ui.success "Secrets encrypted to #{output_file}"
|
|
738
684
|
rescue Odysseus::Secrets::EncryptedFile::MissingKeyError => e
|
|
739
685
|
@ui.error e.message
|
|
740
|
-
@ui.step
|
|
686
|
+
@ui.step 'Generate a key with: odysseus secrets generate-key'
|
|
741
687
|
exit 1
|
|
742
688
|
rescue Odysseus::Error => e
|
|
743
689
|
@ui.error e.message
|
|
@@ -752,8 +698,8 @@ module Odysseus
|
|
|
752
698
|
exit 1
|
|
753
699
|
end
|
|
754
700
|
|
|
755
|
-
@ui.header
|
|
756
|
-
@ui.info
|
|
701
|
+
@ui.header 'Secrets: Decrypt'
|
|
702
|
+
@ui.info 'File', secrets_file
|
|
757
703
|
@ui.blank
|
|
758
704
|
|
|
759
705
|
encrypted_file = Odysseus::Secrets::EncryptedFile.new(secrets_file)
|
|
@@ -764,7 +710,7 @@ module Odysseus
|
|
|
764
710
|
@ui.info key, masked
|
|
765
711
|
end
|
|
766
712
|
@ui.blank
|
|
767
|
-
@ui.step
|
|
713
|
+
@ui.step '(values masked)'
|
|
768
714
|
rescue Odysseus::Secrets::EncryptedFile::MissingKeyError, Odysseus::Secrets::EncryptedFile::DecryptionError => e
|
|
769
715
|
@ui.error e.message
|
|
770
716
|
exit 1
|
|
@@ -774,9 +720,9 @@ module Odysseus
|
|
|
774
720
|
secrets_file = options[:file] || 'secrets.yml.enc'
|
|
775
721
|
editor = ENV['EDITOR'] || 'vi'
|
|
776
722
|
|
|
777
|
-
@ui.header
|
|
778
|
-
@ui.info
|
|
779
|
-
@ui.info
|
|
723
|
+
@ui.header 'Secrets: Edit'
|
|
724
|
+
@ui.info 'File', secrets_file
|
|
725
|
+
@ui.info 'Editor', editor
|
|
780
726
|
@ui.blank
|
|
781
727
|
|
|
782
728
|
encrypted_file = Odysseus::Secrets::EncryptedFile.new(secrets_file)
|
|
@@ -792,13 +738,13 @@ module Odysseus
|
|
|
792
738
|
edited_secrets = YAML.load_file(temp_file.path)
|
|
793
739
|
encrypted_file.write(edited_secrets)
|
|
794
740
|
|
|
795
|
-
@ui.success
|
|
741
|
+
@ui.success 'Secrets updated and encrypted'
|
|
796
742
|
ensure
|
|
797
743
|
temp_file.unlink
|
|
798
744
|
end
|
|
799
745
|
rescue Odysseus::Secrets::EncryptedFile::MissingKeyError => e
|
|
800
746
|
@ui.error e.message
|
|
801
|
-
@ui.step
|
|
747
|
+
@ui.step 'Generate a key with: odysseus secrets generate-key'
|
|
802
748
|
exit 1
|
|
803
749
|
rescue Odysseus::Error => e
|
|
804
750
|
@ui.error e.message
|
|
@@ -812,19 +758,137 @@ module Odysseus
|
|
|
812
758
|
parser.parse
|
|
813
759
|
end
|
|
814
760
|
|
|
815
|
-
def
|
|
761
|
+
def web_container_row(container)
|
|
762
|
+
labels = Odysseus::Docker::Labels.parse(container['Labels'])
|
|
763
|
+
version = labels['odysseus.version'] || '(unlabelled)'
|
|
764
|
+
ref = labels['odysseus.git_ref'] || '-'
|
|
765
|
+
deployed_at = labels['odysseus.deployed_at'] || '-'
|
|
766
|
+
health = container['Status'].include?('(healthy)') ? '✓' : ''
|
|
767
|
+
[version, ref, deployed_at, container['Image'], container['State'], health]
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
# The image reference that is actually serving, so a one-off container runs
|
|
771
|
+
# the same code as the deployed one. Prefers the container's own Image
|
|
772
|
+
# field, which docker ps reports directly, over reconstructing a tag from
|
|
773
|
+
# the odysseus.version label: a container deployed before this branch
|
|
774
|
+
# carries a deploy timestamp in that label and was built from an image
|
|
775
|
+
# tagged `latest`, so reconstruction would name a tag that was never
|
|
776
|
+
# pushed. Falling back to reconstruction only covers the case where
|
|
777
|
+
# Image is somehow absent from docker's own output.
|
|
778
|
+
#
|
|
779
|
+
# The lookup goes through Labels.service_for because only the web role is
|
|
780
|
+
# labelled with the bare service name. Asking for that name on a jobs host
|
|
781
|
+
# matches nothing, and on a service with no web role it matches nothing
|
|
782
|
+
# anywhere — which is what these commands did before they took a role.
|
|
783
|
+
def running_image(server, config, role)
|
|
784
|
+
service_name = Odysseus::Docker::Labels.service_for(service: config[:service], role: role)
|
|
785
|
+
ssh = connect_to_server(server, config)
|
|
786
|
+
|
|
787
|
+
begin
|
|
788
|
+
docker = Odysseus::Docker::Client.new(ssh)
|
|
789
|
+
container = docker.list(service: service_name).first
|
|
790
|
+
|
|
791
|
+
# No search of other roles: running a command against a role other
|
|
792
|
+
# than the one asked for is worse than being told what to type.
|
|
793
|
+
no_container!(server, config, service_name, role: role, state: 'running') unless container
|
|
794
|
+
|
|
795
|
+
container['Image'] || "#{config[:image]}:#{Odysseus::Docker::Labels.version_of(container)}"
|
|
796
|
+
ensure
|
|
797
|
+
ssh.close
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
# The container to read logs from, chosen from everything carrying the
|
|
802
|
+
# service label — stopped containers included. `docker ps` without -a
|
|
803
|
+
# hides the container that has just exited, which is precisely the one
|
|
804
|
+
# whose logs you came for.
|
|
805
|
+
#
|
|
806
|
+
# Note what that container is NOT: a previous version. A deploy stops AND
|
|
807
|
+
# removes the container it replaces (web_deploy.rb:252-253), and the
|
|
808
|
+
# keep: 2 sweep selects only containers already in state `exited`
|
|
809
|
+
# (docker/client.rb:299-301) — so what it retains is crashes, not
|
|
810
|
+
# history. A stopped container here means something exited on its own,
|
|
811
|
+
# which is exactly the case worth reading logs for. `status` and
|
|
812
|
+
# `cleanup` already read with all: true.
|
|
813
|
+
#
|
|
814
|
+
# Finding nothing is a failed request for logs, not a success, so it
|
|
815
|
+
# exits non-zero. And when the only match is stopped, say so: otherwise
|
|
816
|
+
# the log just ends and the reader has no way to know why.
|
|
817
|
+
#
|
|
818
|
+
# That notice goes to stderr. This command's stdout is a log stream —
|
|
819
|
+
# `odysseus logs web1 > app.log`, or a pipe into something that parses it
|
|
820
|
+
# — so a line about the logs must not arrive inside them, while still
|
|
821
|
+
# reaching the terminal of whoever ran the command.
|
|
822
|
+
#
|
|
823
|
+
# @param role [Symbol, nil] the role the label came from, or nil for a
|
|
824
|
+
# dependency, which is named with --name rather than --role
|
|
825
|
+
def log_container_id!(docker, service_name, server, config, role: nil)
|
|
826
|
+
containers = docker.list(service: service_name, all: true)
|
|
827
|
+
no_container!(server, config, service_name, role: role, state: 'running or stopped') if containers.empty?
|
|
828
|
+
|
|
829
|
+
container = containers.find { |c| c['State'] == 'running' } || containers.first
|
|
830
|
+
unless container['State'] == 'running'
|
|
831
|
+
@ui.warn "No running container for #{service_name}: showing logs from " \
|
|
832
|
+
"#{container['State']} container #{container['ID'][0..11]}", io: $stderr
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
container['ID']
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
# What a container lookup that found nothing says, for every command that
|
|
839
|
+
# finds one by its odysseus.service label. A mistyped --role is the
|
|
840
|
+
# ordinary reason nothing matches, so the message names the role, the
|
|
841
|
+
# exact label searched, the option that changes it and the roles this
|
|
842
|
+
# config declares. `logs` used to say only `No containers found for
|
|
843
|
+
# myapp-jbos on w1 (stopped ones included)`, naming a label the reader
|
|
844
|
+
# never typed: one job, two messages, and only one of them any use.
|
|
845
|
+
#
|
|
846
|
+
# A dependency passes no role. It is chosen with --name, and advising
|
|
847
|
+
# --role would send the reader to an option that command does not have.
|
|
848
|
+
def no_container!(server, config, service_name, role: nil, state: 'running')
|
|
849
|
+
subject = role ? "role '#{role}'" : service_name
|
|
850
|
+
@ui.error "No #{state} container for #{subject} on #{server} " \
|
|
851
|
+
"(nothing labelled odysseus.service=#{service_name})", io: $stderr
|
|
852
|
+
if role
|
|
853
|
+
@ui.step "Name the role with --role. Roles in this config: #{config[:servers].keys.join(', ')}",
|
|
854
|
+
io: $stderr
|
|
855
|
+
end
|
|
856
|
+
exit 1
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# The environment a one-off container starts with, built by the same class
|
|
860
|
+
# the deploy paths use. These commands used to inject env.clear alone, so
|
|
861
|
+
# `app exec … --command "rails db:migrate"` ran against a container with
|
|
862
|
+
# no DATABASE_URL while the container deployed seconds earlier had one.
|
|
863
|
+
#
|
|
864
|
+
# The secrets loader resolves a relative secrets_file against the
|
|
865
|
+
# directory holding deploy.yml rather than the working directory, which is
|
|
866
|
+
# what Executor does and what `--config ../other/deploy.yml` needs.
|
|
867
|
+
def build_environment(config, config_file, ssh)
|
|
868
|
+
loader = Odysseus::Secrets::Loader.new(config, config_dir: File.dirname(config_file))
|
|
869
|
+
Odysseus::Core::Environment.new(config: config, secrets_loader: loader, ssh: ssh).build
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
# --debug (or ODYSSEUS_DEBUG=1) reaches the SSH layer here rather than
|
|
873
|
+
# only the UI, so every command built on this connection can show the
|
|
874
|
+
# commands it actually sent. Deploy, build and pussh already did this
|
|
875
|
+
# via `options[:verbose] || @ui.debug?`; the connection builder itself
|
|
876
|
+
# did not, which left doctor, status, containers and logs unable to
|
|
877
|
+
# show anything under --debug however loudly it was asked.
|
|
878
|
+
def connect_to_server(server, config, verbose: @ui.debug?)
|
|
816
879
|
Odysseus::Deployer::SSH.new(
|
|
817
880
|
host: server,
|
|
818
881
|
user: config[:ssh][:user],
|
|
819
882
|
keys: config[:ssh][:keys],
|
|
820
|
-
use_tailscale: true
|
|
883
|
+
use_tailscale: true,
|
|
884
|
+
verbose: verbose
|
|
821
885
|
)
|
|
822
886
|
end
|
|
823
887
|
|
|
824
888
|
def require_name!(options)
|
|
825
889
|
name = options[:name]
|
|
826
890
|
unless name
|
|
827
|
-
@ui.error
|
|
891
|
+
@ui.error 'Dependency name required (--name)'
|
|
828
892
|
exit 1
|
|
829
893
|
end
|
|
830
894
|
name
|