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/bin/odysseus
CHANGED
|
@@ -3,17 +3,30 @@
|
|
|
3
3
|
|
|
4
4
|
require 'odysseus'
|
|
5
5
|
require 'odysseus/cli/cli'
|
|
6
|
+
require 'odysseus/cli/version'
|
|
6
7
|
require 'optparse'
|
|
7
8
|
|
|
9
|
+
# `dependency` is the name; `dep` is shorthand; `accessory` is the former
|
|
10
|
+
# name, kept for one release.
|
|
11
|
+
DEPENDENCY_ALIASES = %w[dependency dep accessory].freeze
|
|
12
|
+
|
|
8
13
|
def main
|
|
9
14
|
# Debug mode: verbose output with no spinners
|
|
10
15
|
debug_mode = ENV['ODYSSEUS_DEBUG'] == '1' || ARGV.include?('--debug')
|
|
11
16
|
ARGV.delete('--debug')
|
|
12
17
|
|
|
18
|
+
# Answered before anything else so it works without a deploy.yml or a server.
|
|
19
|
+
# Note -v is taken by --verbose, so the flag spelling is --version only.
|
|
20
|
+
if ARGV[0] == 'version' || ARGV.include?('--version')
|
|
21
|
+
print_version
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
13
25
|
cli = Odysseus::CLI::CLI.new(debug: debug_mode)
|
|
14
26
|
|
|
15
27
|
commands = {
|
|
16
28
|
'deploy' => { method: :deploy, needs_server: false },
|
|
29
|
+
'rollback' => { method: :rollback, needs_server: false },
|
|
17
30
|
'build' => { method: :build, needs_server: false },
|
|
18
31
|
'pussh' => { method: :pussh, needs_server: false },
|
|
19
32
|
'status' => { method: :status, needs_server: true },
|
|
@@ -21,17 +34,24 @@ def main
|
|
|
21
34
|
'logs' => { method: :logs, needs_server: true },
|
|
22
35
|
'cleanup' => { method: :cleanup, needs_server: true },
|
|
23
36
|
'validate' => { method: :validate, needs_server: false },
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'secrets' => { method: :secrets_dispatch, needs_server: false }
|
|
37
|
+
'doctor' => { method: :doctor, needs_server: false },
|
|
38
|
+
'setup' => { method: :setup, needs_server: false }
|
|
27
39
|
}
|
|
40
|
+
# `dependency`, `app` and `secrets` are not in the table: they have
|
|
41
|
+
# subcommands of their own and are handled by the guards below, which return
|
|
42
|
+
# before the table is read. The table used to list them anyway, naming
|
|
43
|
+
# methods — dependency_dispatch, app_dispatch, secrets_dispatch — that the
|
|
44
|
+
# CLI has never had. Unreachable, so harmless, until someone moved a guard.
|
|
28
45
|
|
|
29
46
|
command = ARGV[0]
|
|
30
|
-
command_args = ARGV[1
|
|
31
|
-
|
|
32
|
-
# Handle accessory
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
command_args = ARGV[1..] || []
|
|
48
|
+
|
|
49
|
+
# Handle dependency subcommands. `dep` is shorthand; `accessory` was the name
|
|
50
|
+
# before the rename and still works, with a notice — a silent alias never gets
|
|
51
|
+
# migrated away from.
|
|
52
|
+
if DEPENDENCY_ALIASES.include?(command)
|
|
53
|
+
puts "Note: 'accessory' was renamed to 'dependency'. The old name still works for now." if command == 'accessory'
|
|
54
|
+
handle_dependency_command(cli, command_args)
|
|
35
55
|
return
|
|
36
56
|
end
|
|
37
57
|
|
|
@@ -60,9 +80,10 @@ def main
|
|
|
60
80
|
opts.on('--push', 'Push image to registry after build') { |v| options[:push] = v }
|
|
61
81
|
opts.on('--context PATH', 'Build context path') { |v| options[:context] = v }
|
|
62
82
|
opts.on('--build', 'Build image before pussh') { |v| options[:build] = v }
|
|
63
|
-
opts.on('--role ROLE', '
|
|
83
|
+
opts.on('--role ROLE', 'Server role: web, jobs, worker, ... (default: web)') { |v| options[:role] = v }
|
|
64
84
|
opts.on('--service NAME', 'Service name') { |v| options[:service] = v }
|
|
65
85
|
opts.on('--dry-run', 'Don\'t actually deploy') { |v| options[:'dry-run'] = v }
|
|
86
|
+
opts.on('--list', 'List rollback candidates per host without changing anything') { |v| options[:list] = v }
|
|
66
87
|
opts.on('-v', '--verbose', 'Show commands being executed') { |v| options[:verbose] = v }
|
|
67
88
|
# Cleanup options
|
|
68
89
|
opts.on('--prune-images', 'Also prune dangling images') { |v| options[:all] = v }
|
|
@@ -70,8 +91,17 @@ def main
|
|
|
70
91
|
opts.on('-f', '--follow', 'Follow log output') { |v| options[:follow] = v }
|
|
71
92
|
opts.on('-n', '--lines N', Integer, 'Number of lines to show') { |v| options[:lines] = v }
|
|
72
93
|
opts.on('--since TIME', 'Show logs since timestamp') { |v| options[:since] = v }
|
|
94
|
+
# Setup options
|
|
95
|
+
opts.on('--as USER', 'Bootstrap identity for setup (default: ubuntu)') { |v| options[:as] = v }
|
|
96
|
+
opts.on('--key PATH', 'Public key to install (repeatable; default: ssh.keys siblings)') do |v|
|
|
97
|
+
(options[:key] ||= []) << v
|
|
98
|
+
end
|
|
73
99
|
end.parse!(command_args)
|
|
74
100
|
|
|
101
|
+
# rollback takes an optional positional VERSION; OptionParser#parse! has
|
|
102
|
+
# already removed the flags, so anything left is that argument.
|
|
103
|
+
options[:version] = command_args[0] if command == 'rollback' && command_args[0]
|
|
104
|
+
|
|
75
105
|
cmd_info = commands[command]
|
|
76
106
|
|
|
77
107
|
if cmd_info[:needs_server]
|
|
@@ -87,52 +117,52 @@ def main
|
|
|
87
117
|
end
|
|
88
118
|
end
|
|
89
119
|
|
|
90
|
-
def
|
|
120
|
+
def handle_dependency_command(cli, args)
|
|
91
121
|
subcommand = args[0]
|
|
92
|
-
subcommand_args = args[1
|
|
122
|
+
subcommand_args = args[1..] || []
|
|
93
123
|
|
|
94
124
|
# Commands that don't need a server argument (hosts come from config)
|
|
95
|
-
|
|
96
|
-
'boot' => :
|
|
97
|
-
'boot-all' => :
|
|
98
|
-
'remove' => :
|
|
99
|
-
'restart' => :
|
|
100
|
-
'upgrade' => :
|
|
101
|
-
'status' => :
|
|
125
|
+
dependency_commands_no_server = {
|
|
126
|
+
'boot' => :dependency_boot,
|
|
127
|
+
'boot-all' => :dependency_boot_all,
|
|
128
|
+
'remove' => :dependency_remove,
|
|
129
|
+
'restart' => :dependency_restart,
|
|
130
|
+
'upgrade' => :dependency_upgrade,
|
|
131
|
+
'status' => :dependency_status
|
|
102
132
|
}
|
|
103
133
|
|
|
104
134
|
# Commands that need a server argument (for interactive/logs on specific host)
|
|
105
|
-
|
|
106
|
-
'logs' => :
|
|
107
|
-
'exec' => :
|
|
108
|
-
'shell' => :
|
|
135
|
+
dependency_commands_with_server = {
|
|
136
|
+
'logs' => :dependency_logs,
|
|
137
|
+
'exec' => :dependency_exec,
|
|
138
|
+
'shell' => :dependency_shell
|
|
109
139
|
}
|
|
110
140
|
|
|
111
|
-
all_commands =
|
|
141
|
+
all_commands = dependency_commands_no_server.merge(dependency_commands_with_server)
|
|
112
142
|
|
|
113
143
|
unless subcommand && all_commands[subcommand]
|
|
114
|
-
puts
|
|
115
|
-
puts
|
|
116
|
-
puts
|
|
117
|
-
puts
|
|
118
|
-
puts
|
|
119
|
-
puts
|
|
120
|
-
puts
|
|
121
|
-
puts
|
|
122
|
-
puts
|
|
123
|
-
puts
|
|
124
|
-
puts
|
|
125
|
-
puts
|
|
126
|
-
puts
|
|
127
|
-
puts
|
|
128
|
-
puts
|
|
129
|
-
puts
|
|
130
|
-
puts
|
|
131
|
-
puts
|
|
132
|
-
puts
|
|
133
|
-
puts
|
|
134
|
-
puts
|
|
135
|
-
puts
|
|
144
|
+
puts 'Usage: odysseus dependency <subcommand> [options]'
|
|
145
|
+
puts ''
|
|
146
|
+
puts 'Subcommands (hosts from config):'
|
|
147
|
+
puts ' boot Boot a dependency (requires --name)'
|
|
148
|
+
puts ' boot-all Boot all dependencies'
|
|
149
|
+
puts ' remove Remove a dependency (requires --name)'
|
|
150
|
+
puts ' restart Restart a dependency (requires --name)'
|
|
151
|
+
puts ' upgrade Upgrade a dependency to a new image (requires --name)'
|
|
152
|
+
puts ' status Show dependency status'
|
|
153
|
+
puts ''
|
|
154
|
+
puts 'Subcommands (require server):'
|
|
155
|
+
puts ' logs <server> Show dependency logs (requires --name)'
|
|
156
|
+
puts ' exec <server> Execute a command in a dependency (requires --name)'
|
|
157
|
+
puts ' shell <server> Open an interactive shell in a dependency (requires --name)'
|
|
158
|
+
puts ''
|
|
159
|
+
puts 'Options:'
|
|
160
|
+
puts ' --config FILE Path to deploy.yml (default: deploy.yml)'
|
|
161
|
+
puts ' --name NAME Dependency name'
|
|
162
|
+
puts ' -f, --follow Follow log output (logs only)'
|
|
163
|
+
puts ' -n, --lines N Number of lines (logs only, default: 100)'
|
|
164
|
+
puts ' --since TIME Show logs since timestamp (logs only)'
|
|
165
|
+
puts ' --command CMD Command to execute (exec only)'
|
|
136
166
|
exit 1
|
|
137
167
|
end
|
|
138
168
|
|
|
@@ -140,18 +170,18 @@ def handle_accessory_command(cli, args)
|
|
|
140
170
|
|
|
141
171
|
OptionParser.new do |opts|
|
|
142
172
|
opts.on('--config FILE', 'Path to deploy.yml') { |v| options[:config] = v }
|
|
143
|
-
opts.on('--name NAME', '
|
|
173
|
+
opts.on('--name NAME', 'Dependency name') { |v| options[:name] = v }
|
|
144
174
|
opts.on('-f', '--follow', 'Follow log output') { |v| options[:follow] = v }
|
|
145
175
|
opts.on('-n', '--lines N', Integer, 'Number of lines') { |v| options[:lines] = v }
|
|
146
176
|
opts.on('--since TIME', 'Show logs since timestamp') { |v| options[:since] = v }
|
|
147
177
|
opts.on('--command CMD', 'Command to execute') { |v| options[:command] = v }
|
|
148
178
|
end.parse!(subcommand_args)
|
|
149
179
|
|
|
150
|
-
if
|
|
180
|
+
if dependency_commands_with_server[subcommand]
|
|
151
181
|
# These commands need a server argument
|
|
152
182
|
server = subcommand_args[0]
|
|
153
183
|
unless server
|
|
154
|
-
puts "Error:
|
|
184
|
+
puts "Error: dependency #{subcommand} requires a server argument"
|
|
155
185
|
exit 1
|
|
156
186
|
end
|
|
157
187
|
cli.send(all_commands[subcommand], server, options)
|
|
@@ -163,7 +193,7 @@ end
|
|
|
163
193
|
|
|
164
194
|
def handle_app_command(cli, args)
|
|
165
195
|
subcommand = args[0]
|
|
166
|
-
subcommand_args = args[1
|
|
196
|
+
subcommand_args = args[1..] || []
|
|
167
197
|
|
|
168
198
|
app_commands = {
|
|
169
199
|
'shell' => :app_shell,
|
|
@@ -172,17 +202,18 @@ def handle_app_command(cli, args)
|
|
|
172
202
|
}
|
|
173
203
|
|
|
174
204
|
unless subcommand && app_commands[subcommand]
|
|
175
|
-
puts
|
|
176
|
-
puts
|
|
177
|
-
puts
|
|
178
|
-
puts
|
|
179
|
-
puts
|
|
180
|
-
puts
|
|
181
|
-
puts
|
|
182
|
-
puts
|
|
183
|
-
puts
|
|
184
|
-
puts
|
|
185
|
-
puts
|
|
205
|
+
puts 'Usage: odysseus app <subcommand> <server> [options]'
|
|
206
|
+
puts ''
|
|
207
|
+
puts 'Subcommands:'
|
|
208
|
+
puts ' shell <server> Open an interactive shell in a temporary container'
|
|
209
|
+
puts ' exec <server> Run a command in a new container (requires --command)'
|
|
210
|
+
puts ' console <server> Start a custom console (e.g., rails c, irb)'
|
|
211
|
+
puts ''
|
|
212
|
+
puts 'Options:'
|
|
213
|
+
puts ' --config FILE Path to deploy.yml (default: deploy.yml)'
|
|
214
|
+
puts ' --role ROLE Role whose image to run: web, jobs, ... (default: web)'
|
|
215
|
+
puts ' --command CMD Command to execute (exec only)'
|
|
216
|
+
puts ' --cmd CMD Console command (console only, default: /bin/sh)'
|
|
186
217
|
exit 1
|
|
187
218
|
end
|
|
188
219
|
|
|
@@ -190,6 +221,7 @@ def handle_app_command(cli, args)
|
|
|
190
221
|
|
|
191
222
|
OptionParser.new do |opts|
|
|
192
223
|
opts.on('--config FILE', 'Path to deploy.yml') { |v| options[:config] = v }
|
|
224
|
+
opts.on('--role ROLE', 'Server role: web, jobs, worker, ... (default: web)') { |v| options[:role] = v }
|
|
193
225
|
opts.on('--command CMD', 'Command to execute') { |v| options[:command] = v }
|
|
194
226
|
opts.on('--cmd CMD', 'Console command') { |v| options[:cmd] = v }
|
|
195
227
|
end.parse!(subcommand_args)
|
|
@@ -205,7 +237,7 @@ end
|
|
|
205
237
|
|
|
206
238
|
def handle_secrets_command(cli, args)
|
|
207
239
|
subcommand = args[0]
|
|
208
|
-
subcommand_args = args[1
|
|
240
|
+
subcommand_args = args[1..] || []
|
|
209
241
|
|
|
210
242
|
secrets_commands = {
|
|
211
243
|
'generate-key' => :secrets_generate_key,
|
|
@@ -215,20 +247,20 @@ def handle_secrets_command(cli, args)
|
|
|
215
247
|
}
|
|
216
248
|
|
|
217
249
|
unless subcommand && secrets_commands[subcommand]
|
|
218
|
-
puts
|
|
219
|
-
puts
|
|
220
|
-
puts
|
|
221
|
-
puts
|
|
222
|
-
puts
|
|
223
|
-
puts
|
|
224
|
-
puts
|
|
225
|
-
puts
|
|
226
|
-
puts
|
|
227
|
-
puts
|
|
228
|
-
puts
|
|
229
|
-
puts
|
|
230
|
-
puts
|
|
231
|
-
puts
|
|
250
|
+
puts 'Usage: odysseus secrets <subcommand> [options]'
|
|
251
|
+
puts ''
|
|
252
|
+
puts 'Subcommands:'
|
|
253
|
+
puts ' generate-key Generate a new master key'
|
|
254
|
+
puts ' encrypt Encrypt a secrets file'
|
|
255
|
+
puts ' decrypt Decrypt and display secrets'
|
|
256
|
+
puts ' edit Edit encrypted secrets'
|
|
257
|
+
puts ''
|
|
258
|
+
puts 'Options:'
|
|
259
|
+
puts ' --file FILE Path to secrets file (default: secrets.yml.enc)'
|
|
260
|
+
puts ' --input FILE Input file to encrypt (encrypt only)'
|
|
261
|
+
puts ''
|
|
262
|
+
puts 'Environment:'
|
|
263
|
+
puts ' ODYSSEUS_MASTER_KEY Master key for encryption/decryption'
|
|
232
264
|
exit 1
|
|
233
265
|
end
|
|
234
266
|
|
|
@@ -242,52 +274,78 @@ def handle_secrets_command(cli, args)
|
|
|
242
274
|
cli.send(secrets_commands[subcommand], options)
|
|
243
275
|
end
|
|
244
276
|
|
|
277
|
+
def print_version
|
|
278
|
+
puts "odysseus #{Odysseus::CLI::VERSION} (odysseus-core #{Odysseus::Core::VERSION})"
|
|
279
|
+
puts "ruby #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} [#{RUBY_PLATFORM}]"
|
|
280
|
+
end
|
|
281
|
+
|
|
245
282
|
def print_help
|
|
246
|
-
puts
|
|
247
|
-
puts
|
|
248
|
-
puts
|
|
249
|
-
puts
|
|
250
|
-
puts
|
|
251
|
-
puts
|
|
252
|
-
puts
|
|
253
|
-
puts
|
|
254
|
-
puts
|
|
255
|
-
puts
|
|
256
|
-
puts
|
|
257
|
-
puts
|
|
258
|
-
puts
|
|
259
|
-
puts
|
|
260
|
-
puts
|
|
261
|
-
puts
|
|
262
|
-
puts
|
|
263
|
-
puts
|
|
264
|
-
puts
|
|
265
|
-
puts
|
|
266
|
-
puts
|
|
267
|
-
puts
|
|
268
|
-
puts
|
|
283
|
+
puts 'Usage: odysseus <command> [options]'
|
|
284
|
+
puts ''
|
|
285
|
+
puts 'Global options:'
|
|
286
|
+
puts ' --debug Verbose output (show all commands and details)'
|
|
287
|
+
puts ' Or set ODYSSEUS_DEBUG=1 environment variable'
|
|
288
|
+
puts ' --version Show odysseus, odysseus-core and ruby versions'
|
|
289
|
+
puts ''
|
|
290
|
+
puts 'Commands:'
|
|
291
|
+
puts ' deploy Deploy all roles to servers defined in config'
|
|
292
|
+
puts ' rollback [VERSION] Roll every role back to a previously deployed version'
|
|
293
|
+
puts ' build Build Docker image (locally or on build host)'
|
|
294
|
+
puts ' pussh Push image to hosts via SSH (no registry needed)'
|
|
295
|
+
puts ' status <server> Show service status on server'
|
|
296
|
+
puts ' containers <server> List containers for service on server'
|
|
297
|
+
puts ' logs <server> Show logs for a service'
|
|
298
|
+
puts ' cleanup <server> Remove the service from a server: all its containers,'
|
|
299
|
+
puts ' dependencies included, and its proxy routes'
|
|
300
|
+
puts ' validate Validate deploy.yml'
|
|
301
|
+
puts ' doctor Check that hosts are ready to deploy to (changes nothing)'
|
|
302
|
+
puts ' setup Prepare hosts: deploy user, docker group, key, state dir, Docker'
|
|
303
|
+
puts ' dependency <subcommand> Manage dependencies (db, redis, ...); alias: dep'
|
|
304
|
+
puts ' app <subcommand> Run commands in app containers'
|
|
305
|
+
puts ' secrets <subcommand> Manage encrypted secrets'
|
|
306
|
+
puts ' version Show version information'
|
|
307
|
+
puts ''
|
|
308
|
+
puts 'Options:'
|
|
309
|
+
puts ' --config FILE Path to deploy.yml (default: deploy.yml)'
|
|
310
|
+
puts ' --image TAG Docker image tag (default: the git commit being deployed;'
|
|
311
|
+
puts ' required outside a clean git repository)'
|
|
312
|
+
puts ' --service NAME Service name (for containers command)'
|
|
269
313
|
puts " --dry-run Don't actually deploy"
|
|
270
|
-
puts
|
|
271
|
-
puts
|
|
272
|
-
puts
|
|
273
|
-
puts
|
|
274
|
-
puts
|
|
275
|
-
puts
|
|
276
|
-
puts
|
|
277
|
-
puts
|
|
278
|
-
puts
|
|
279
|
-
puts
|
|
280
|
-
puts
|
|
281
|
-
puts
|
|
282
|
-
puts
|
|
283
|
-
puts
|
|
284
|
-
puts
|
|
285
|
-
puts
|
|
286
|
-
puts
|
|
314
|
+
puts ' -v, --verbose Show commands being executed'
|
|
315
|
+
puts ''
|
|
316
|
+
puts 'Deploy options:'
|
|
317
|
+
puts ' --build Build and distribute image before deploying'
|
|
318
|
+
puts ' (uses registry if configured, otherwise pussh)'
|
|
319
|
+
puts ''
|
|
320
|
+
puts 'Rollback options:'
|
|
321
|
+
puts ' --list Show each host: versions deployed, which images remain,'
|
|
322
|
+
puts ' and what is serving. Changes nothing.'
|
|
323
|
+
puts ''
|
|
324
|
+
puts 'Build options:'
|
|
325
|
+
puts ' --push Push image to registry after build'
|
|
326
|
+
puts ' --context PATH Build context path (default: . relative to deploy.yml)'
|
|
327
|
+
puts ''
|
|
328
|
+
puts 'Pussh options:'
|
|
329
|
+
puts ' --build Build image before pushing via SSH'
|
|
330
|
+
puts ''
|
|
331
|
+
puts 'App options:'
|
|
332
|
+
puts ' --role ROLE Role whose running image to run: web, jobs, etc (default: web)'
|
|
333
|
+
puts ' A service with no web role needs this on every host'
|
|
334
|
+
puts ' --command CMD Command to run (app exec)'
|
|
335
|
+
puts ' --cmd CMD Console command (app console, default: /bin/sh)'
|
|
336
|
+
puts ''
|
|
337
|
+
puts 'Logs options:'
|
|
338
|
+
puts ' --role ROLE Role to read logs from: web, jobs, worker, etc (default: web)'
|
|
339
|
+
puts ' -f, --follow Follow log output'
|
|
340
|
+
puts ' -n, --lines N Number of lines to show (default: 100)'
|
|
287
341
|
puts " --since TIME Show logs since timestamp (e.g., '10m', '2h')"
|
|
288
|
-
puts
|
|
289
|
-
puts
|
|
290
|
-
puts
|
|
342
|
+
puts ''
|
|
343
|
+
puts 'Cleanup options:'
|
|
344
|
+
puts ' --prune-images Also prune dangling images after removing containers'
|
|
345
|
+
puts ''
|
|
346
|
+
puts 'Setup options:'
|
|
347
|
+
puts ' --as USER Bootstrap identity for setup (default: ubuntu)'
|
|
348
|
+
puts ' --key PATH Public key to install (repeatable; default: ssh.keys siblings)'
|
|
291
349
|
end
|
|
292
350
|
|
|
293
351
|
main
|