odysseus-cli 0.1.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 (5) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +482 -0
  3. data/bin/odysseus +285 -0
  4. data/lib/odysseus/cli/cli.rb +1042 -0
  5. metadata +100 -0
data/bin/odysseus ADDED
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env ruby
2
+ # odysseus-cli/bin/odysseus
3
+
4
+ require 'odysseus'
5
+ require 'odysseus/cli/cli'
6
+ require 'optparse'
7
+
8
+ def main
9
+ cli = Odysseus::CLI::CLI.new
10
+
11
+ commands = {
12
+ 'deploy' => { method: :deploy, needs_server: false },
13
+ 'build' => { method: :build, needs_server: false },
14
+ 'pussh' => { method: :pussh, needs_server: false },
15
+ 'status' => { method: :status, needs_server: true },
16
+ 'containers' => { method: :containers, needs_server: true },
17
+ 'logs' => { method: :logs, needs_server: true },
18
+ 'cleanup' => { method: :cleanup, needs_server: true },
19
+ 'validate' => { method: :validate, needs_server: false },
20
+ 'accessory' => { method: :accessory_dispatch, needs_server: false },
21
+ 'app' => { method: :app_dispatch, needs_server: false },
22
+ 'secrets' => { method: :secrets_dispatch, needs_server: false }
23
+ }
24
+
25
+ command = ARGV[0]
26
+ command_args = ARGV[1..-1] || []
27
+
28
+ # Handle accessory subcommands
29
+ if command == 'accessory'
30
+ handle_accessory_command(cli, command_args)
31
+ return
32
+ end
33
+
34
+ # Handle app subcommands
35
+ if command == 'app'
36
+ handle_app_command(cli, command_args)
37
+ return
38
+ end
39
+
40
+ # Handle secrets subcommands
41
+ if command == 'secrets'
42
+ handle_secrets_command(cli, command_args)
43
+ return
44
+ end
45
+
46
+ unless command && commands[command]
47
+ print_help
48
+ exit 1
49
+ end
50
+
51
+ options = {}
52
+
53
+ OptionParser.new do |opts|
54
+ opts.on('--config FILE', 'Path to deploy.yml') { |v| options[:config] = v }
55
+ opts.on('--image TAG', 'Docker image tag') { |v| options[:image] = v }
56
+ opts.on('--push', 'Push image to registry after build') { |v| options[:push] = v }
57
+ opts.on('--context PATH', 'Build context path') { |v| options[:context] = v }
58
+ opts.on('--build', 'Build image before pussh') { |v| options[:build] = v }
59
+ opts.on('--role ROLE', 'Role for logs command') { |v| options[:role] = v }
60
+ opts.on('--service NAME', 'Service name') { |v| options[:service] = v }
61
+ opts.on('--dry-run', 'Don\'t actually deploy') { |v| options[:'dry-run'] = v }
62
+ opts.on('-v', '--verbose', 'Show commands being executed') { |v| options[:verbose] = v }
63
+ # Cleanup options
64
+ opts.on('--prune-images', 'Also prune dangling images') { |v| options[:all] = v }
65
+ # Logs options
66
+ opts.on('-f', '--follow', 'Follow log output') { |v| options[:follow] = v }
67
+ opts.on('-n', '--lines N', Integer, 'Number of lines to show') { |v| options[:lines] = v }
68
+ opts.on('--since TIME', 'Show logs since timestamp') { |v| options[:since] = v }
69
+ end.parse!(command_args)
70
+
71
+ cmd_info = commands[command]
72
+
73
+ if cmd_info[:needs_server]
74
+ server = command_args[0]
75
+ unless server
76
+ puts "Error: #{command} requires a server argument"
77
+ puts "Usage: odysseus #{command} <server> [options]"
78
+ exit 1
79
+ end
80
+ cli.send(cmd_info[:method], server, options)
81
+ else
82
+ cli.send(cmd_info[:method], options)
83
+ end
84
+ end
85
+
86
+ def handle_accessory_command(cli, args)
87
+ subcommand = args[0]
88
+ subcommand_args = args[1..-1] || []
89
+
90
+ # Commands that don't need a server argument (hosts come from config)
91
+ accessory_commands_no_server = {
92
+ 'boot' => :accessory_boot,
93
+ 'boot-all' => :accessory_boot_all,
94
+ 'remove' => :accessory_remove,
95
+ 'restart' => :accessory_restart,
96
+ 'upgrade' => :accessory_upgrade,
97
+ 'status' => :accessory_status
98
+ }
99
+
100
+ # Commands that need a server argument (for interactive/logs on specific host)
101
+ accessory_commands_with_server = {
102
+ 'logs' => :accessory_logs,
103
+ 'exec' => :accessory_exec,
104
+ 'shell' => :accessory_shell
105
+ }
106
+
107
+ all_commands = accessory_commands_no_server.merge(accessory_commands_with_server)
108
+
109
+ unless subcommand && all_commands[subcommand]
110
+ puts "Usage: odysseus accessory <subcommand> [options]"
111
+ puts ""
112
+ puts "Subcommands (hosts from config):"
113
+ puts " boot Boot an accessory (requires --name)"
114
+ puts " boot-all Boot all accessories"
115
+ puts " remove Remove an accessory (requires --name)"
116
+ puts " restart Restart an accessory (requires --name)"
117
+ puts " upgrade Upgrade accessory to new image (requires --name)"
118
+ puts " status Show accessory status"
119
+ puts ""
120
+ puts "Subcommands (require server):"
121
+ puts " logs <server> Show accessory logs (requires --name)"
122
+ puts " exec <server> Execute command in accessory (requires --name)"
123
+ puts " shell <server> Open interactive shell in accessory (requires --name)"
124
+ puts ""
125
+ puts "Options:"
126
+ puts " --config FILE Path to deploy.yml (default: deploy.yml)"
127
+ puts " --name NAME Accessory name"
128
+ puts " -f, --follow Follow log output (logs only)"
129
+ puts " -n, --lines N Number of lines (logs only, default: 100)"
130
+ puts " --since TIME Show logs since timestamp (logs only)"
131
+ puts " --command CMD Command to execute (exec only)"
132
+ exit 1
133
+ end
134
+
135
+ options = {}
136
+
137
+ OptionParser.new do |opts|
138
+ opts.on('--config FILE', 'Path to deploy.yml') { |v| options[:config] = v }
139
+ opts.on('--name NAME', 'Accessory name') { |v| options[:name] = v }
140
+ opts.on('-f', '--follow', 'Follow log output') { |v| options[:follow] = v }
141
+ opts.on('-n', '--lines N', Integer, 'Number of lines') { |v| options[:lines] = v }
142
+ opts.on('--since TIME', 'Show logs since timestamp') { |v| options[:since] = v }
143
+ opts.on('--command CMD', 'Command to execute') { |v| options[:command] = v }
144
+ end.parse!(subcommand_args)
145
+
146
+ if accessory_commands_with_server[subcommand]
147
+ # These commands need a server argument
148
+ server = subcommand_args[0]
149
+ unless server
150
+ puts "Error: accessory #{subcommand} requires a server argument"
151
+ exit 1
152
+ end
153
+ cli.send(all_commands[subcommand], server, options)
154
+ else
155
+ # These commands get hosts from config
156
+ cli.send(all_commands[subcommand], options)
157
+ end
158
+ end
159
+
160
+ def handle_app_command(cli, args)
161
+ subcommand = args[0]
162
+ subcommand_args = args[1..-1] || []
163
+
164
+ app_commands = {
165
+ 'shell' => :app_shell,
166
+ 'exec' => :app_exec,
167
+ 'console' => :app_console
168
+ }
169
+
170
+ unless subcommand && app_commands[subcommand]
171
+ puts "Usage: odysseus app <subcommand> <server> [options]"
172
+ puts ""
173
+ puts "Subcommands:"
174
+ puts " shell <server> Open an interactive shell in a temporary container"
175
+ puts " exec <server> Run a command in a new container (requires --command)"
176
+ puts " console <server> Start a custom console (e.g., rails c, irb)"
177
+ puts ""
178
+ puts "Options:"
179
+ puts " --config FILE Path to deploy.yml (default: deploy.yml)"
180
+ puts " --command CMD Command to execute (exec only)"
181
+ puts " --cmd CMD Console command (console only, default: /bin/sh)"
182
+ exit 1
183
+ end
184
+
185
+ options = {}
186
+
187
+ OptionParser.new do |opts|
188
+ opts.on('--config FILE', 'Path to deploy.yml') { |v| options[:config] = v }
189
+ opts.on('--command CMD', 'Command to execute') { |v| options[:command] = v }
190
+ opts.on('--cmd CMD', 'Console command') { |v| options[:cmd] = v }
191
+ end.parse!(subcommand_args)
192
+
193
+ server = subcommand_args[0]
194
+ unless server
195
+ puts "Error: app #{subcommand} requires a server argument"
196
+ exit 1
197
+ end
198
+
199
+ cli.send(app_commands[subcommand], server, options)
200
+ end
201
+
202
+ def handle_secrets_command(cli, args)
203
+ subcommand = args[0]
204
+ subcommand_args = args[1..-1] || []
205
+
206
+ secrets_commands = {
207
+ 'generate-key' => :secrets_generate_key,
208
+ 'encrypt' => :secrets_encrypt,
209
+ 'decrypt' => :secrets_decrypt,
210
+ 'edit' => :secrets_edit
211
+ }
212
+
213
+ unless subcommand && secrets_commands[subcommand]
214
+ puts "Usage: odysseus secrets <subcommand> [options]"
215
+ puts ""
216
+ puts "Subcommands:"
217
+ puts " generate-key Generate a new master key"
218
+ puts " encrypt Encrypt a secrets file"
219
+ puts " decrypt Decrypt and display secrets"
220
+ puts " edit Edit encrypted secrets"
221
+ puts ""
222
+ puts "Options:"
223
+ puts " --file FILE Path to secrets file (default: secrets.yml.enc)"
224
+ puts " --input FILE Input file to encrypt (encrypt only)"
225
+ puts ""
226
+ puts "Environment:"
227
+ puts " ODYSSEUS_MASTER_KEY Master key for encryption/decryption"
228
+ exit 1
229
+ end
230
+
231
+ options = {}
232
+
233
+ OptionParser.new do |opts|
234
+ opts.on('--file FILE', 'Path to secrets file') { |v| options[:file] = v }
235
+ opts.on('--input FILE', 'Input file to encrypt') { |v| options[:input] = v }
236
+ end.parse!(subcommand_args)
237
+
238
+ cli.send(secrets_commands[subcommand], options)
239
+ end
240
+
241
+ def print_help
242
+ puts "Usage: odysseus <command> [options]"
243
+ puts ""
244
+ puts "Commands:"
245
+ puts " deploy Deploy all roles to servers defined in config"
246
+ puts " build Build Docker image (locally or on build host)"
247
+ puts " pussh Push image to hosts via SSH (no registry needed)"
248
+ puts " status <server> Show service status on server"
249
+ puts " containers <server> List containers for service on server"
250
+ puts " logs <server> Show logs for a service"
251
+ puts " cleanup <server> Clean up old containers and images"
252
+ puts " validate Validate deploy.yml"
253
+ puts " accessory <subcommand> Manage accessories"
254
+ puts " app <subcommand> Run commands in app containers"
255
+ puts " secrets <subcommand> Manage encrypted secrets"
256
+ puts ""
257
+ puts "Options:"
258
+ puts " --config FILE Path to deploy.yml (default: deploy.yml)"
259
+ puts " --image TAG Docker image tag (default: latest)"
260
+ puts " --service NAME Service name (for containers command)"
261
+ puts " --dry-run Don't actually deploy"
262
+ puts " -v, --verbose Show commands being executed"
263
+ puts ""
264
+ puts "Deploy options:"
265
+ puts " --build Build and distribute image before deploying"
266
+ puts " (uses registry if configured, otherwise pussh)"
267
+ puts ""
268
+ puts "Build options:"
269
+ puts " --push Push image to registry after build"
270
+ puts " --context PATH Build context path (default: . relative to deploy.yml)"
271
+ puts ""
272
+ puts "Pussh options:"
273
+ puts " --build Build image before pushing via SSH"
274
+ puts ""
275
+ puts "Logs options:"
276
+ puts " --role ROLE Role for logs: web, jobs, worker, etc (default: web)"
277
+ puts " -f, --follow Follow log output"
278
+ puts " -n, --lines N Number of lines to show (default: 100)"
279
+ puts " --since TIME Show logs since timestamp (e.g., '10m', '2h')"
280
+ puts ""
281
+ puts "Cleanup options:"
282
+ puts " --prune-images Also prune dangling images after removing containers"
283
+ end
284
+
285
+ main