convox_installer 1.0.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/.bundle/config +2 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rubocop.yml +41 -0
- data/.vscode/settings.json +4 -0
- data/Gemfile +15 -0
- data/LICENSE +7 -0
- data/README.md +23 -0
- data/convox_installer.gemspec +28 -0
- data/examples/full_installation.rb +182 -0
- data/lib/convox/client.rb +402 -0
- data/lib/convox.rb +3 -0
- data/lib/convox_installer/config.rb +186 -0
- data/lib/convox_installer/requirements.rb +71 -0
- data/lib/convox_installer/version.rb +5 -0
- data/lib/convox_installer.rb +50 -0
- data/log/.gitkeep +0 -0
- data/spec/lib/convox/client_spec.rb +154 -0
- data/spec/lib/convox_installer/config_spec.rb +208 -0
- data/spec/lib/convox_installer/requirements_spec.rb +87 -0
- data/spec/spec_helper.rb +118 -0
- metadata +139 -0
@@ -0,0 +1,402 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "json"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module Convox
|
8
|
+
class Client
|
9
|
+
CONVOX_DIR = File.expand_path("~/.convox").freeze
|
10
|
+
AUTH_FILE = File.join(CONVOX_DIR, "auth")
|
11
|
+
HOST_FILE = File.join(CONVOX_DIR, "host")
|
12
|
+
|
13
|
+
attr_accessor :logger, :config
|
14
|
+
|
15
|
+
def auth
|
16
|
+
load_auth_from_file
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
@logger = Logger.new(STDOUT)
|
21
|
+
logger.level = options[:log_level] || Logger::INFO
|
22
|
+
@config = options[:config] || {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def backup_convox_host_and_rack
|
26
|
+
%w[host rack].each do |f|
|
27
|
+
path = File.join(CONVOX_DIR, f)
|
28
|
+
if File.exist?(path)
|
29
|
+
bak_file = "#{path}.bak"
|
30
|
+
logger.info "Moving existing #{path} to #{bak_file}..."
|
31
|
+
FileUtils.mv(path, bak_file)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def install_convox
|
37
|
+
require_config(%i[ aws_region stack_name ])
|
38
|
+
region = config.fetch(:aws_region)
|
39
|
+
stack_name = config.fetch(:stack_name)
|
40
|
+
|
41
|
+
if rack_already_installed?
|
42
|
+
logger.info "There is already a Convox stack named #{stack_name} " \
|
43
|
+
"in the #{region} AWS region. Using this rack. "
|
44
|
+
return true
|
45
|
+
end
|
46
|
+
|
47
|
+
require_config(%i[
|
48
|
+
aws_region
|
49
|
+
aws_access_key_id
|
50
|
+
aws_secret_access_key
|
51
|
+
stack_name
|
52
|
+
instance_type
|
53
|
+
])
|
54
|
+
|
55
|
+
logger.info "Installing Convox (#{stack_name})..."
|
56
|
+
|
57
|
+
env = {
|
58
|
+
"AWS_REGION" => region,
|
59
|
+
"AWS_ACCESS_KEY_ID" => config.fetch(:aws_access_key_id),
|
60
|
+
"AWS_SECRET_ACCESS_KEY" => config.fetch(:aws_secret_access_key),
|
61
|
+
}
|
62
|
+
command = %Q{rack install aws \
|
63
|
+
--name "#{config.fetch(:stack_name)}" \
|
64
|
+
"InstanceType=#{config.fetch(:instance_type)}" \
|
65
|
+
"BuildInstance="}
|
66
|
+
|
67
|
+
run_convox_command!(command, env)
|
68
|
+
end
|
69
|
+
|
70
|
+
def rack_already_installed?
|
71
|
+
require_config(%i[ aws_region stack_name ])
|
72
|
+
|
73
|
+
unless File.exist?(AUTH_FILE)
|
74
|
+
raise "Could not find auth file at #{AUTH_FILE}!"
|
75
|
+
end
|
76
|
+
|
77
|
+
region = config.fetch(:aws_region)
|
78
|
+
stack_name = config.fetch(:stack_name)
|
79
|
+
|
80
|
+
auth.each do |host, password|
|
81
|
+
if host.match?(/^#{stack_name}-\d+\.#{region}\.elb\.amazonaws\.com$/)
|
82
|
+
return true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate_convox_auth_and_set_host!
|
89
|
+
require_config(%i[ aws_region stack_name ])
|
90
|
+
|
91
|
+
unless File.exist?(AUTH_FILE)
|
92
|
+
raise "Could not find auth file at #{AUTH_FILE}!"
|
93
|
+
end
|
94
|
+
|
95
|
+
region = config.fetch(:aws_region)
|
96
|
+
stack = config.fetch(:stack_name)
|
97
|
+
|
98
|
+
match_count = 0
|
99
|
+
matching_host = nil
|
100
|
+
auth.each do |host, password|
|
101
|
+
if host.match?(/^#{stack}-\d+\.#{region}\.elb\.amazonaws\.com$/)
|
102
|
+
matching_host = host
|
103
|
+
match_count += 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
if match_count == 1
|
108
|
+
set_host(matching_host)
|
109
|
+
return matching_host
|
110
|
+
end
|
111
|
+
|
112
|
+
if match_count > 1
|
113
|
+
error_message = "Found multiple matching hosts for "
|
114
|
+
else
|
115
|
+
error_message = "Could not find matching authentication for "
|
116
|
+
end
|
117
|
+
error_message += "region: #{region}, stack: #{stack}"
|
118
|
+
raise error_message
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_host(host)
|
122
|
+
logger.debug "Setting convox host to #{host} (in #{HOST_FILE})..."
|
123
|
+
File.open(HOST_FILE, "w") { |f| f.puts host }
|
124
|
+
end
|
125
|
+
|
126
|
+
def validate_convox_rack!
|
127
|
+
require_config(%i[
|
128
|
+
aws_region
|
129
|
+
stack_name
|
130
|
+
instance_type
|
131
|
+
])
|
132
|
+
logger.debug "Validating that convox rack has the correct attributes..."
|
133
|
+
{
|
134
|
+
provider: "aws",
|
135
|
+
region: config.fetch(:aws_region),
|
136
|
+
type: config.fetch(:instance_type),
|
137
|
+
name: config.fetch(:stack_name),
|
138
|
+
}.each do |k, v|
|
139
|
+
convox_value = convox_rack_data[k.to_s]
|
140
|
+
if convox_value != v
|
141
|
+
raise "Convox data did not match! Expected #{k} to be '#{v}', " \
|
142
|
+
"but was: '#{convox_value}'"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
logger.debug "=> Convox rack has the correct attributes."
|
146
|
+
true
|
147
|
+
end
|
148
|
+
|
149
|
+
def convox_rack_data
|
150
|
+
@convox_rack_data ||= begin
|
151
|
+
logger.debug "Fetching convox rack attributes..."
|
152
|
+
convox_output = `convox api get /system`
|
153
|
+
raise "convox command failed!" unless $?.success?
|
154
|
+
JSON.parse(convox_output)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def create_convox_app!
|
159
|
+
require_config(%i[convox_app_name])
|
160
|
+
return true if convox_app_exists?
|
161
|
+
|
162
|
+
app_name = config.fetch(:convox_app_name)
|
163
|
+
|
164
|
+
logger.info "Creating app: #{app_name}..."
|
165
|
+
logger.info "=> Documentation: " \
|
166
|
+
"https://docs.convox.com/deployment/creating-an-application"
|
167
|
+
|
168
|
+
run_convox_command! "apps create #{app_name} --wait"
|
169
|
+
|
170
|
+
retries = 0
|
171
|
+
loop do
|
172
|
+
break if convox_app_exists?
|
173
|
+
if retries > 5
|
174
|
+
raise "Something went wrong while creating the #{app_name} app! " \
|
175
|
+
"(Please wait a few moments and then restart the installation script.)"
|
176
|
+
end
|
177
|
+
logger.info "Waiting for #{app_name} to be ready..."
|
178
|
+
sleep 3
|
179
|
+
retries += 1
|
180
|
+
end
|
181
|
+
|
182
|
+
logger.info "=> #{app_name} app created!"
|
183
|
+
end
|
184
|
+
|
185
|
+
def set_default_app_for_directory!
|
186
|
+
logger.info "Setting default app in ./.convox/app..."
|
187
|
+
FileUtils.mkdir_p File.expand_path("./.convox")
|
188
|
+
File.open(File.expand_path("./.convox/app"), "w") do |f|
|
189
|
+
f.puts config.fetch(:convox_app_name)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def convox_app_exists?
|
194
|
+
require_config(%i[convox_app_name])
|
195
|
+
app_name = config.fetch(:convox_app_name)
|
196
|
+
|
197
|
+
logger.debug "Looking for existing #{app_name} app..."
|
198
|
+
convox_output = `convox api get /apps`
|
199
|
+
raise "convox command failed!" unless $?.success?
|
200
|
+
|
201
|
+
apps = JSON.parse(convox_output)
|
202
|
+
apps.each do |app|
|
203
|
+
if app["name"] == app_name
|
204
|
+
logger.debug "=> Found #{app_name} app."
|
205
|
+
return true
|
206
|
+
end
|
207
|
+
end
|
208
|
+
logger.debug "=> Did not find #{app_name} app."
|
209
|
+
false
|
210
|
+
end
|
211
|
+
|
212
|
+
# Create the s3 bucket, and also apply a CORS configuration
|
213
|
+
def create_s3_bucket!
|
214
|
+
require_config(%i[s3_bucket_name])
|
215
|
+
bucket_name = config.fetch(:s3_bucket_name)
|
216
|
+
if s3_bucket_exists?
|
217
|
+
logger.info "#{bucket_name} S3 bucket already exists!"
|
218
|
+
else
|
219
|
+
logger.info "Creating S3 bucket resource (#{bucket_name})..."
|
220
|
+
run_convox_command! "rack resources create s3 " \
|
221
|
+
"--name \"#{bucket_name}\" " \
|
222
|
+
"--wait"
|
223
|
+
|
224
|
+
retries = 0
|
225
|
+
loop do
|
226
|
+
return if s3_bucket_exists?
|
227
|
+
|
228
|
+
if retries > 10
|
229
|
+
raise "Something went wrong while creating the #{bucket_name} S3 bucket! " \
|
230
|
+
"(Please wait a few moments and then restart the installation script.)"
|
231
|
+
end
|
232
|
+
logger.debug "Waiting for S3 bucket to be ready..."
|
233
|
+
sleep 3
|
234
|
+
retries += 1
|
235
|
+
end
|
236
|
+
|
237
|
+
logger.debug "=> S3 bucket created!"
|
238
|
+
end
|
239
|
+
|
240
|
+
set_s3_bucket_cors_policy
|
241
|
+
end
|
242
|
+
|
243
|
+
def s3_bucket_exists?
|
244
|
+
require_config(%i[s3_bucket_name])
|
245
|
+
bucket_name = config.fetch(:s3_bucket_name)
|
246
|
+
logger.debug "Looking up S3 bucket resource: #{bucket_name}"
|
247
|
+
`convox api get /resources/#{bucket_name} 2>/dev/null`
|
248
|
+
$?.success?
|
249
|
+
end
|
250
|
+
|
251
|
+
def s3_bucket_details
|
252
|
+
require_config(%i[s3_bucket_name])
|
253
|
+
@s3_bucket_details ||= begin
|
254
|
+
bucket_name = config.fetch(:s3_bucket_name)
|
255
|
+
logger.debug "Fetching S3 bucket resource details for #{bucket_name}..."
|
256
|
+
|
257
|
+
response = `convox api get /resources/#{bucket_name}`
|
258
|
+
raise "convox command failed!" unless $?.success?
|
259
|
+
|
260
|
+
bucket_data = JSON.parse(response)
|
261
|
+
s3_url = bucket_data["url"]
|
262
|
+
matches = s3_url.match(
|
263
|
+
/^s3:\/\/(?<access_key_id>[^:]*):(?<secret_access_key>[^@]*)@(?<bucket_name>.*)$/
|
264
|
+
)
|
265
|
+
|
266
|
+
match_keys = %i[access_key_id secret_access_key bucket_name]
|
267
|
+
unless matches && match_keys.all? { |k| matches[k].present? }
|
268
|
+
raise "#{s3_url} is an invalid S3 URL!"
|
269
|
+
end
|
270
|
+
|
271
|
+
{
|
272
|
+
access_key_id: matches[:access_key_id],
|
273
|
+
secret_access_key: matches[:secret_access_key],
|
274
|
+
name: matches[:bucket_name],
|
275
|
+
}
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def set_s3_bucket_cors_policy
|
280
|
+
require_config(%i[aws_access_key_id aws_secret_access_key])
|
281
|
+
access_key_id = config.fetch(:aws_access_key_id)
|
282
|
+
secret_access_key = config.fetch(:aws_secret_access_key)
|
283
|
+
|
284
|
+
unless config.key? :s3_bucket_cors_policy
|
285
|
+
logger.debug "No CORS policy provided in config: s3_bucket_cors_policy"
|
286
|
+
return
|
287
|
+
end
|
288
|
+
cors_policy_string = config.fetch(:s3_bucket_cors_policy)
|
289
|
+
|
290
|
+
bucket_name = s3_bucket_details[:name]
|
291
|
+
|
292
|
+
logger.debug "Looking up existing CORS policy for #{bucket_name}"
|
293
|
+
existing_cors_policy_string =
|
294
|
+
`AWS_ACCESS_KEY_ID=#{access_key_id} \
|
295
|
+
AWS_SECRET_ACCESS_KEY=#{secret_access_key} \
|
296
|
+
aws s3api get-bucket-cors --bucket #{bucket_name} 2>/dev/null`
|
297
|
+
if $?.success? && existing_cors_policy_string.present?
|
298
|
+
# Sort all the nested arrays so that the equality operator works
|
299
|
+
existing_cors_policy = JSON.parse(existing_cors_policy_string)
|
300
|
+
cors_policy_json = JSON.parse(cors_policy_string)
|
301
|
+
[existing_cors_policy, cors_policy_json].each do |policy_json|
|
302
|
+
if policy_json.is_a?(Hash) && policy_json["CORSRules"]
|
303
|
+
policy_json["CORSRules"].each do |rule|
|
304
|
+
rule["AllowedHeaders"].sort! if rule["AllowedHeaders"]
|
305
|
+
rule["AllowedMethods"].sort! if rule["AllowedMethods"]
|
306
|
+
rule["AllowedOrigins"].sort! if rule["AllowedOrigins"]
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
if existing_cors_policy == cors_policy_json
|
312
|
+
logger.debug "=> CORS policy is already up to date for #{bucket_name}."
|
313
|
+
return
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
begin
|
318
|
+
logger.info "Setting CORS policy for #{bucket_name}..."
|
319
|
+
|
320
|
+
File.open("cors-policy.json", "w") { |f| f.puts cors_policy_string }
|
321
|
+
|
322
|
+
`AWS_ACCESS_KEY_ID=#{access_key_id} \
|
323
|
+
AWS_SECRET_ACCESS_KEY=#{secret_access_key} \
|
324
|
+
aws s3api put-bucket-cors \
|
325
|
+
--bucket #{bucket_name} \
|
326
|
+
--cors-configuration "file://cors-policy.json"`
|
327
|
+
unless $?.success?
|
328
|
+
raise "Something went wrong while setting the S3 bucket CORS policy!"
|
329
|
+
end
|
330
|
+
logger.info "=> Successfully set CORS policy for #{bucket_name}."
|
331
|
+
ensure
|
332
|
+
FileUtils.rm_f "cors-policy.json"
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def add_docker_registry!
|
337
|
+
require_config(%i[docker_registry_url docker_registry_username docker_registry_password])
|
338
|
+
|
339
|
+
registry_url = config.fetch(:docker_registry_url)
|
340
|
+
|
341
|
+
logger.debug "Looking up existing Docker registries..."
|
342
|
+
registries_response = `convox api get /registries`
|
343
|
+
unless $?.success?
|
344
|
+
raise "Something went wrong while fetching the list of registries!"
|
345
|
+
end
|
346
|
+
registries = JSON.parse(registries_response)
|
347
|
+
|
348
|
+
if registries.any? { |r| r["server"] == registry_url }
|
349
|
+
logger.debug "=> Docker Registry already exists: #{registry_url}"
|
350
|
+
return true
|
351
|
+
end
|
352
|
+
|
353
|
+
logger.info "Adding Docker Registry: #{registry_url}..."
|
354
|
+
logger.info "=> Documentation: " \
|
355
|
+
"https://docs.convox.com/deployment/private-registries"
|
356
|
+
|
357
|
+
`convox registries add "#{registry_url}" \
|
358
|
+
"#{config.fetch(:docker_registry_username)}" \
|
359
|
+
"#{config.fetch(:docker_registry_password)}"`
|
360
|
+
unless $?.success?
|
361
|
+
raise "Something went wrong while adding the #{registry_url} registry!"
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def default_service_domain_name
|
366
|
+
require_config(%i[convox_app_name default_service])
|
367
|
+
|
368
|
+
@default_service_domain_name ||= begin
|
369
|
+
convox_domain = convox_rack_data["domain"]
|
370
|
+
elb_name_and_region = convox_domain[/([^\.]*\.[^\.]*)\..*/, 1]
|
371
|
+
unless elb_name_and_region.present?
|
372
|
+
raise "Something went wrong while parsing the ELB name and region! " \
|
373
|
+
"(#{elb_name_and_region})"
|
374
|
+
end
|
375
|
+
app = config.fetch(:convox_app_name)
|
376
|
+
service = config.fetch(:default_service)
|
377
|
+
|
378
|
+
"#{app}-#{service}.#{elb_name_and_region}.convox.site"
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def run_convox_command!(cmd, env = {})
|
383
|
+
command = "convox #{cmd}"
|
384
|
+
system env, command
|
385
|
+
raise "Error running: #{command}" unless $?.success?
|
386
|
+
end
|
387
|
+
|
388
|
+
private
|
389
|
+
|
390
|
+
def load_auth_from_file
|
391
|
+
return {} unless File.exist?(AUTH_FILE)
|
392
|
+
|
393
|
+
JSON.parse(File.read(AUTH_FILE))
|
394
|
+
end
|
395
|
+
|
396
|
+
def require_config(required_keys)
|
397
|
+
required_keys.each do |k|
|
398
|
+
raise "#{k} is missing from the config!" unless config[k]
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
data/lib/convox.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "highline"
|
4
|
+
require "fileutils"
|
5
|
+
require "json"
|
6
|
+
require "securerandom"
|
7
|
+
|
8
|
+
module ConvoxInstaller
|
9
|
+
class Config
|
10
|
+
attr_accessor :logger, :config, :prompts, :highline
|
11
|
+
|
12
|
+
CONFIG_FILE = File.expand_path("~/.convox/installer_config").freeze
|
13
|
+
|
14
|
+
DEFAULT_PROMPTS = [
|
15
|
+
{
|
16
|
+
key: :stack_name,
|
17
|
+
title: "Convox Stack Name",
|
18
|
+
prompt: "Please enter a name for your Convox installation",
|
19
|
+
default: "convox",
|
20
|
+
},
|
21
|
+
{
|
22
|
+
key: :aws_region,
|
23
|
+
title: "AWS Region",
|
24
|
+
default: "us-east-1",
|
25
|
+
},
|
26
|
+
{
|
27
|
+
key: :instance_type,
|
28
|
+
title: "EC2 Instance Type",
|
29
|
+
default: "t3.medium",
|
30
|
+
},
|
31
|
+
{
|
32
|
+
section: "Admin AWS Credentials",
|
33
|
+
},
|
34
|
+
{
|
35
|
+
key: :aws_access_key_id,
|
36
|
+
title: "AWS Access Key ID",
|
37
|
+
},
|
38
|
+
{
|
39
|
+
key: :aws_secret_access_key,
|
40
|
+
title: "AWS Secret Access Key",
|
41
|
+
},
|
42
|
+
].freeze
|
43
|
+
|
44
|
+
def initialize(options = {})
|
45
|
+
@logger = Logger.new(STDOUT)
|
46
|
+
logger.level = options[:log_level] || Logger::INFO
|
47
|
+
|
48
|
+
self.prompts = options[:prompts] || DEFAULT_PROMPTS
|
49
|
+
self.config = {}
|
50
|
+
load_config_from_file
|
51
|
+
load_config_from_env
|
52
|
+
self.config = config.merge((options[:config] || {}).symbolize_keys)
|
53
|
+
|
54
|
+
self.highline = options[:highline] || HighLine.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def config_keys
|
58
|
+
prompts.map { |prompt| prompt[:key] }.compact.map(&:to_sym)
|
59
|
+
end
|
60
|
+
|
61
|
+
def prompt_for_config
|
62
|
+
loop do
|
63
|
+
prompts.each do |prompt|
|
64
|
+
if prompt[:section]
|
65
|
+
highline.say "\n#{prompt[:section]}"
|
66
|
+
highline.say "============================================\n\n"
|
67
|
+
end
|
68
|
+
next unless prompt[:key]
|
69
|
+
|
70
|
+
ask_prompt(prompt)
|
71
|
+
end
|
72
|
+
|
73
|
+
show_config_summary
|
74
|
+
|
75
|
+
@completed_prompt = true
|
76
|
+
|
77
|
+
highline.say "Please double check all of these configuration details."
|
78
|
+
|
79
|
+
agree = highline.agree(
|
80
|
+
"Would you like to start the Convox installation?" \
|
81
|
+
" (press 'n' to correct any settings)"
|
82
|
+
)
|
83
|
+
break if agree
|
84
|
+
highline.say "\n"
|
85
|
+
end
|
86
|
+
|
87
|
+
config
|
88
|
+
end
|
89
|
+
|
90
|
+
def show_config_summary
|
91
|
+
highline.say "\n============================================"
|
92
|
+
highline.say " SUMMARY"
|
93
|
+
highline.say "============================================\n\n"
|
94
|
+
|
95
|
+
config_titles = prompts.map do |prompt|
|
96
|
+
prompt[:title] || prompt[:key]
|
97
|
+
end.compact
|
98
|
+
max = config_titles.map(&:length).max
|
99
|
+
|
100
|
+
prompts.each do |prompt|
|
101
|
+
next if !prompt[:key] || prompt[:hidden]
|
102
|
+
|
103
|
+
value = config[prompt[:key]]
|
104
|
+
title = prompt[:title] || prompt[:key]
|
105
|
+
padded_key = "#{title}:".ljust(max + 3)
|
106
|
+
highline.say " #{padded_key} #{value}"
|
107
|
+
end
|
108
|
+
highline.say "\nWe've saved your configuration to: #{CONFIG_FILE}"
|
109
|
+
highline.say "If anything goes wrong during the installation, " \
|
110
|
+
"you can restart the script to reload the config and continue.\n\n"
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def ask_prompt(prompt)
|
116
|
+
key = prompt[:key]
|
117
|
+
title = prompt[:title] || key
|
118
|
+
|
119
|
+
# If looping through the config again, ask for all
|
120
|
+
# the config with defaults.
|
121
|
+
if config[key] && !@completed_prompt
|
122
|
+
logger.debug "Found existing config for #{key} => #{config[key]}"
|
123
|
+
return
|
124
|
+
end
|
125
|
+
|
126
|
+
# Used when we want to force a default value and not prompt the user.
|
127
|
+
# (e.g. securely generated passwords)
|
128
|
+
if prompt[:value]
|
129
|
+
return if config[key]
|
130
|
+
|
131
|
+
default = prompt[:value]
|
132
|
+
config[key] = default.is_a?(Proc) ? default.call : default
|
133
|
+
save_config_to_file
|
134
|
+
return
|
135
|
+
end
|
136
|
+
|
137
|
+
prompt_string = prompt[:prompt] || "Please enter your #{title}: "
|
138
|
+
|
139
|
+
config[key] = highline.ask(prompt_string) do |q|
|
140
|
+
if @completed_prompt
|
141
|
+
q.default = config[key]
|
142
|
+
elsif prompt[:default]
|
143
|
+
q.default = prompt[:default]
|
144
|
+
end
|
145
|
+
q.validate = /.+/
|
146
|
+
end
|
147
|
+
|
148
|
+
save_config_to_file
|
149
|
+
end
|
150
|
+
|
151
|
+
def load_config_from_file
|
152
|
+
return unless Config.config_file_exists?
|
153
|
+
|
154
|
+
logger.debug "Loading saved config from #{CONFIG_FILE}..."
|
155
|
+
|
156
|
+
loaded_config = JSON.parse(Config.read_config_file)["config"].symbolize_keys
|
157
|
+
self.config = config.merge(loaded_config).slice(*config_keys)
|
158
|
+
end
|
159
|
+
|
160
|
+
def load_config_from_env
|
161
|
+
config_keys.each do |key|
|
162
|
+
env_key = key.to_s.upcase
|
163
|
+
value = ENV[env_key]
|
164
|
+
next unless value.present?
|
165
|
+
|
166
|
+
logger.debug "Found value for #{key} in env var: #{env_key} => #{value}"
|
167
|
+
config[key] = value
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def save_config_to_file
|
172
|
+
FileUtils.mkdir_p File.expand_path("~/.convox")
|
173
|
+
File.open(CONFIG_FILE, "w") do |f|
|
174
|
+
f.puts({config: config}.to_json)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.config_file_exists?
|
179
|
+
File.exist?(CONFIG_FILE)
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.read_config_file
|
183
|
+
File.read(CONFIG_FILE)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "highline"
|
4
|
+
require "os"
|
5
|
+
require "logger"
|
6
|
+
|
7
|
+
module ConvoxInstaller
|
8
|
+
class Requirements
|
9
|
+
attr_accessor :ecr_label, :logger
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@ecr_label = options[:ecr_label]
|
13
|
+
@logger = Logger.new(STDOUT)
|
14
|
+
logger.level = options[:log_level] || Logger::INFO
|
15
|
+
end
|
16
|
+
|
17
|
+
def ensure_requirements!
|
18
|
+
logger.debug "Checking for required commands..."
|
19
|
+
|
20
|
+
@missing_packages = []
|
21
|
+
unless has_command? "convox"
|
22
|
+
@missing_packages << {
|
23
|
+
name: "convox",
|
24
|
+
brew: "convox",
|
25
|
+
docs: "https://docs.convox.com/introduction/installation",
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
unless has_command? "aws"
|
30
|
+
@missing_packages << {
|
31
|
+
name: "aws",
|
32
|
+
brew: "awscli",
|
33
|
+
docs: "https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html",
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
if @missing_packages.any?
|
38
|
+
logger.error "This script requires the convox and AWS CLI tools."
|
39
|
+
if OS.mac?
|
40
|
+
logger.error "Please run: brew install " \
|
41
|
+
"#{@missing_packages.map { |p| p[:brew] }.join(" ")}"
|
42
|
+
else
|
43
|
+
logger.error "Installation Instructions:"
|
44
|
+
@missing_packages.each do |package|
|
45
|
+
logger.error "* #{package[:name]}: #{package[:docs]}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
quit!
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def has_command?(command)
|
53
|
+
path = find_command command
|
54
|
+
if path.present?
|
55
|
+
logger.debug "=> Found #{command}: #{path}"
|
56
|
+
return true
|
57
|
+
end
|
58
|
+
logger.debug "=> Could not find #{command}!"
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
# Stubbed in tests
|
63
|
+
def find_command(command)
|
64
|
+
`which #{command} 2>/dev/null`.chomp
|
65
|
+
end
|
66
|
+
|
67
|
+
def quit!
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|