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.
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
- 'accessory' => { method: :accessory_dispatch, needs_server: false },
25
- 'app' => { method: :app_dispatch, needs_server: false },
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..-1] || []
31
-
32
- # Handle accessory subcommands
33
- if command == 'accessory'
34
- handle_accessory_command(cli, command_args)
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', 'Role for logs command') { |v| options[:role] = v }
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 handle_accessory_command(cli, args)
120
+ def handle_dependency_command(cli, args)
91
121
  subcommand = args[0]
92
- subcommand_args = args[1..-1] || []
122
+ subcommand_args = args[1..] || []
93
123
 
94
124
  # Commands that don't need a server argument (hosts come from config)
95
- accessory_commands_no_server = {
96
- 'boot' => :accessory_boot,
97
- 'boot-all' => :accessory_boot_all,
98
- 'remove' => :accessory_remove,
99
- 'restart' => :accessory_restart,
100
- 'upgrade' => :accessory_upgrade,
101
- 'status' => :accessory_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
- accessory_commands_with_server = {
106
- 'logs' => :accessory_logs,
107
- 'exec' => :accessory_exec,
108
- 'shell' => :accessory_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 = accessory_commands_no_server.merge(accessory_commands_with_server)
141
+ all_commands = dependency_commands_no_server.merge(dependency_commands_with_server)
112
142
 
113
143
  unless subcommand && all_commands[subcommand]
114
- puts "Usage: odysseus accessory <subcommand> [options]"
115
- puts ""
116
- puts "Subcommands (hosts from config):"
117
- puts " boot Boot an accessory (requires --name)"
118
- puts " boot-all Boot all accessories"
119
- puts " remove Remove an accessory (requires --name)"
120
- puts " restart Restart an accessory (requires --name)"
121
- puts " upgrade Upgrade accessory to new image (requires --name)"
122
- puts " status Show accessory status"
123
- puts ""
124
- puts "Subcommands (require server):"
125
- puts " logs <server> Show accessory logs (requires --name)"
126
- puts " exec <server> Execute command in accessory (requires --name)"
127
- puts " shell <server> Open interactive shell in accessory (requires --name)"
128
- puts ""
129
- puts "Options:"
130
- puts " --config FILE Path to deploy.yml (default: deploy.yml)"
131
- puts " --name NAME Accessory name"
132
- puts " -f, --follow Follow log output (logs only)"
133
- puts " -n, --lines N Number of lines (logs only, default: 100)"
134
- puts " --since TIME Show logs since timestamp (logs only)"
135
- puts " --command CMD Command to execute (exec only)"
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', 'Accessory name') { |v| options[:name] = v }
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 accessory_commands_with_server[subcommand]
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: accessory #{subcommand} requires a server argument"
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..-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 "Usage: odysseus app <subcommand> <server> [options]"
176
- puts ""
177
- puts "Subcommands:"
178
- puts " shell <server> Open an interactive shell in a temporary container"
179
- puts " exec <server> Run a command in a new container (requires --command)"
180
- puts " console <server> Start a custom console (e.g., rails c, irb)"
181
- puts ""
182
- puts "Options:"
183
- puts " --config FILE Path to deploy.yml (default: deploy.yml)"
184
- puts " --command CMD Command to execute (exec only)"
185
- puts " --cmd CMD Console command (console only, default: /bin/sh)"
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..-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 "Usage: odysseus secrets <subcommand> [options]"
219
- puts ""
220
- puts "Subcommands:"
221
- puts " generate-key Generate a new master key"
222
- puts " encrypt Encrypt a secrets file"
223
- puts " decrypt Decrypt and display secrets"
224
- puts " edit Edit encrypted secrets"
225
- puts ""
226
- puts "Options:"
227
- puts " --file FILE Path to secrets file (default: secrets.yml.enc)"
228
- puts " --input FILE Input file to encrypt (encrypt only)"
229
- puts ""
230
- puts "Environment:"
231
- puts " ODYSSEUS_MASTER_KEY Master key for encryption/decryption"
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 "Usage: odysseus <command> [options]"
247
- puts ""
248
- puts "Global options:"
249
- puts " --debug Verbose output (show all commands and details)"
250
- puts " Or set ODYSSEUS_DEBUG=1 environment variable"
251
- puts ""
252
- puts "Commands:"
253
- puts " deploy Deploy all roles to servers defined in config"
254
- puts " build Build Docker image (locally or on build host)"
255
- puts " pussh Push image to hosts via SSH (no registry needed)"
256
- puts " status <server> Show service status on server"
257
- puts " containers <server> List containers for service on server"
258
- puts " logs <server> Show logs for a service"
259
- puts " cleanup <server> Clean up old containers and images"
260
- puts " validate Validate deploy.yml"
261
- puts " accessory <subcommand> Manage accessories"
262
- puts " app <subcommand> Run commands in app containers"
263
- puts " secrets <subcommand> Manage encrypted secrets"
264
- puts ""
265
- puts "Options:"
266
- puts " --config FILE Path to deploy.yml (default: deploy.yml)"
267
- puts " --image TAG Docker image tag (default: latest)"
268
- puts " --service NAME Service name (for containers command)"
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 " -v, --verbose Show commands being executed"
271
- puts ""
272
- puts "Deploy options:"
273
- puts " --build Build and distribute image before deploying"
274
- puts " (uses registry if configured, otherwise pussh)"
275
- puts ""
276
- puts "Build options:"
277
- puts " --push Push image to registry after build"
278
- puts " --context PATH Build context path (default: . relative to deploy.yml)"
279
- puts ""
280
- puts "Pussh options:"
281
- puts " --build Build image before pushing via SSH"
282
- puts ""
283
- puts "Logs options:"
284
- puts " --role ROLE Role for logs: web, jobs, worker, etc (default: web)"
285
- puts " -f, --follow Follow log output"
286
- puts " -n, --lines N Number of lines to show (default: 100)"
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 "Cleanup options:"
290
- puts " --prune-images Also prune dangling images after removing containers"
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