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.
- checksums.yaml +7 -0
- data/README.md +482 -0
- data/bin/odysseus +285 -0
- data/lib/odysseus/cli/cli.rb +1042 -0
- 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
|