-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathdeploio
More file actions
executable file
·373 lines (311 loc) · 11.4 KB
/
deploio
File metadata and controls
executable file
·373 lines (311 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'json'
require 'open3'
require 'did_you_mean'
$stdout.sync = true
def check_requirements
check_nctl_version
check_nctl_json_support
end
def check_nctl_version
stdout, _stderr, status = Open3.capture3('nctl --version')
unless status.success?
puts 'Error: nctl not found or not working properly'
puts 'Please install nctl first: https://github.com/ninech/nctl'
exit 1
end
version_output = stdout.strip
version_match = version_output.match(/(\d+\.\d+\.\d+)/)
unless version_match
puts "Warning: Could not parse nctl version from: #{version_output}"
return
end
version = version_match[1]
required_version = '1.10.0'
return unless Gem::Version.new(version) < Gem::Version.new(required_version)
puts "Error: nctl version #{version} does not meet requirements (need at least #{required_version})"
puts 'Please update nctl via homebrew:'
puts ' brew upgrade nctl'
exit 1
end
def check_nctl_json_support
stdout, _stderr, _status = Open3.capture3('nctl get apps --help')
return if stdout.include?('-o, --output') || stdout.include?('--output') || stdout.include?('json')
puts 'Warning: nctl may not support JSON output format'
puts "This could cause issues with the 'list' command"
nil
end
module SuggestionHelper
def self.suggest(input, dictionary)
return [] if dictionary.empty?
spell_checker = DidYouMean::SpellChecker.new(dictionary: dictionary)
spell_checker.correct(input)
end
def self.format_suggestions(input, dictionary, context_message = nil)
suggestions = suggest(input, dictionary)
return nil if suggestions.empty?
message = context_message || 'Did you mean?'
formatted = "\n#{message}"
suggestions.each { |suggestion| formatted += "\n #{suggestion}" }
formatted
end
end
def extract_global_flags(argv, default_org_prefix: 'renuo')
args = argv.dup
org_prefix = default_org_prefix
if (idx = args.index('--org-prefix')) && args[idx + 1]
org_prefix = args[idx + 1]
args.slice!(idx, 2)
end
dry_run = args.delete('--dry-run') ? true : false
[args, org_prefix, dry_run]
end
def usage(exit_code = 1)
message = <<~USAGE
deploio (deplo.io app CLI)
Usage:
deploio [--org-prefix <prefix>] [--dry-run] [--help]
deploio <command> [args]
Commands:
login Authenticate with nctl
new <project-env> Create project and app (git url inferred)
list List apps as <project>-<env>
logs <project-env> [-- ...args] Stream logs for app
exec <project-env> [-- ...args] Exec into app (args passed to nctl)
stats <project-env> Show app stats
config <project-env> Show app config (yaml)
config:edit <project-env> Edit app config
hosts <project-env> Print app hosts
Global flags (must appear before the command):
--org-prefix <prefix> Default: renuo
--dry-run Print commands without executing
--help Show nctl help for a command. If a
command is provided (e.g. "deploio logs --help"),
shows help for the underlying nctl
command and bypasses other flags. If no
command is provided, shows top-level
nctl help.
Examples:
deploio login
deploio new fizzbuzz-main
depl logs fizzbuzz-main
deploio exec fizzbuzz-main -- -c 'echo hi'
USAGE
puts message
exit exit_code
end
class AppRef
attr_reader :project_short, :environment, :org_prefix
def initialize(project_env, org_prefix, available_project_envs = [])
parts = project_env.split('-')
if parts.size < 2
error_msg = "Invalid <project-env>: #{project_env}\n" \
"Expected format: <project>-<environment> (e.g., 'myapp-staging', 'api-prod')"
suggestions = SuggestionHelper.format_suggestions(project_env, available_project_envs)
error_msg += suggestions if suggestions
raise error_msg
end
@environment = parts.pop
@project_short = parts.join('-')
@org_prefix = org_prefix
end
def project_full
[org_prefix, project_short].join('-')
end
def app_name
environment
end
def git_url
"git@github.com:#{org_prefix}/#{project_short}.git"
end
end
class Runner
def initialize(dry_run: false)
@dry_run = dry_run
end
def run(cmd)
puts "> #{cmd}"
return true if @dry_run
system(cmd)
end
def capture(cmd)
puts "> #{cmd}"
return ['', '', 0] if @dry_run
stdout, stderr, status = Open3.capture3(cmd)
[stdout, stderr, status.exitstatus]
end
end
def nctl_app_cmd(action, ref)
"nctl #{action} app #{ref.app_name} --project #{ref.project_full}"
end
def fetch_available_project_envs(org_prefix, runner)
return @cached_project_envs if defined?(@cached_project_envs)
stdout, _stderr, code = runner.capture(%(nctl get apps -A -o json | jq -r '.[] | (.metadata.namespace + "-" + .metadata.name | gsub("#{org_prefix}-"; ""))'))
if code.zero? && !stdout.strip.empty?
@cached_project_envs = stdout.strip.split("\n").reject(&:empty?)
else
# For testing purposes, provide some mock data in dry-run mode
@cached_project_envs = if runner.instance_variable_get(:@dry_run)
%w[myapp-staging myapp-production api-staging fizzbuzz-main shapehub-develop]
else
[]
end
end
@cached_project_envs
rescue StandardError
@cached_project_envs = []
end
def validate_project_env_with_suggestions(project_env, org_prefix, runner, command_name, extra_args = [])
available_project_envs = fetch_available_project_envs(org_prefix, runner)
return true if available_project_envs.include?(project_env)
return true if available_project_envs.empty?
puts "Project-environment '#{project_env}' not found."
suggestions = SuggestionHelper.suggest(project_env, available_project_envs)
unless suggestions.empty?
puts "\nDid you mean?"
suggestions.each do |suggestion|
full_command = "#{command_name} #{suggestion}"
full_command += " #{extra_args.join(' ')}" unless extra_args.empty?
puts " #{full_command}"
end
end
puts "\nRun 'deploio list' to see all available project-environments."
false
end
COMMANDS = {
'login' => {
help: 'nctl auth login --help',
run: lambda { |args, _org_prefix, runner|
extra = args.empty? ? '' : " #{args.join(' ')}"
runner.run("nctl auth login#{extra}")
}
},
'new' => {
help: 'nctl create app --help',
run: lambda { |args, org_prefix, runner|
raise 'missing <project-env>' if args.empty?
project_env = args.shift
available_project_envs = fetch_available_project_envs(org_prefix, runner)
ref = AppRef.new(project_env, org_prefix, available_project_envs)
stdout, _stderr, _code = runner.capture('nctl get projects -o json')
existing = []
begin
json = JSON.parse(stdout)
if json.is_a?(Array)
existing = json.map { |p| p.dig('metadata', 'name') }.compact
elsif json.is_a?(Hash) && json['items'].is_a?(Array)
existing = json['items'].map { |p| p.dig('metadata', 'name') }.compact
end
rescue JSON::ParserError
existing = []
end
unless existing.include?(ref.project_full)
ok = runner.run("nctl create project #{ref.project_full}")
exit 1 unless ok
end
ok = runner.run("nctl create app #{ref.app_name} --project #{ref.project_full} --git-url #{ref.git_url} --git-revision \"#{ref.environment}\" --size=mini")
exit 1 unless ok
}
},
'list' => {
help: 'nctl get apps --help',
run: lambda { |_args, org_prefix, runner|
runner.run(%(nctl get apps -A -o json | jq -r '.[] | (.metadata.namespace + "-" + .metadata.name | gsub("#{org_prefix}-"; ""))'))
}
},
'logs' => {
help: 'nctl logs app --help',
run: lambda { |args, org_prefix, runner|
raise 'missing <project-env>' if args.empty?
project_env = args.shift
exit 1 unless validate_project_env_with_suggestions(project_env, org_prefix, runner, 'logs', args)
ref = AppRef.new(project_env, org_prefix)
extra = args.empty? ? '' : " #{args.join(' ')}"
runner.run(nctl_app_cmd('logs', ref) + extra)
}
},
'exec' => {
help: 'nctl exec app --help',
run: lambda { |args, org_prefix, runner|
raise 'missing <project-env>' if args.empty?
project_env = args.shift
exit 1 unless validate_project_env_with_suggestions(project_env, org_prefix, runner, 'exec', args)
ref = AppRef.new(project_env, org_prefix)
extra = args.empty? ? '' : " #{args.join(' ')}"
runner.run(nctl_app_cmd('exec', ref) + extra)
}
},
'stats' => {
help: 'nctl get app --help',
run: lambda { |args, org_prefix, runner|
raise 'missing <project-env>' if args.empty?
project_env = args.shift
exit 1 unless validate_project_env_with_suggestions(project_env, org_prefix, runner, 'stats', args)
ref = AppRef.new(project_env, org_prefix)
runner.run("#{nctl_app_cmd('get', ref)} -o stats")
}
},
'config' => {
help: 'nctl get app --help',
run: lambda { |args, org_prefix, runner|
raise 'missing <project-env>' if args.empty?
project_env = args.shift
exit 1 unless validate_project_env_with_suggestions(project_env, org_prefix, runner, 'config', args)
ref = AppRef.new(project_env, org_prefix)
runner.run("#{nctl_app_cmd('get', ref)} -o yaml")
}
},
'config:edit' => {
help: 'nctl edit app --help',
run: lambda { |args, org_prefix, runner|
raise 'missing <project-env>' if args.empty?
project_env = args.shift
exit 1 unless validate_project_env_with_suggestions(project_env, org_prefix, runner, 'config:edit', args)
ref = AppRef.new(project_env, org_prefix)
runner.run(nctl_app_cmd('edit', ref))
}
},
'hosts' => {
help: 'nctl get app --help',
run: lambda { |args, org_prefix, runner|
raise 'missing <project-env>' if args.empty?
project_env = args.shift
exit 1 unless validate_project_env_with_suggestions(project_env, org_prefix, runner, 'hosts', args)
ref = AppRef.new(project_env, org_prefix)
runner.run("#{nctl_app_cmd('get', ref)} -o json | jq -r '.status.atProvider.hosts | map(.name) | .[]'")
}
}
}.freeze
if ARGV.include?('--help')
args_ng, _org, dry_for_help = extract_global_flags(ARGV)
args_ng.delete('--help')
dapp_cmd = args_ng[0]
if dapp_cmd.nil?
usage(0)
elsif COMMANDS[dapp_cmd]
help_cmd = COMMANDS[dapp_cmd][:help]
if dry_for_help
puts "> #{help_cmd}"
exit 0
else
exec(help_cmd)
end
else
usage
end
end
args, org_prefix, dry_run = extract_global_flags(ARGV)
check_requirements unless dry_run
usage if args.empty?
cmd = args.shift
runner = Runner.new(dry_run: dry_run)
action = COMMANDS[cmd]
unless action
puts "Unknown command: #{cmd}"
suggestions = SuggestionHelper.format_suggestions(cmd, COMMANDS.keys)
puts suggestions if suggestions
puts "\nRun 'deploio --help' to see available commands."
exit 1
end
action[:run].call(args, org_prefix, runner)