Class: Convox::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/convox/client.rb

Constant Summary collapse

CONVOX_DIR =
File.expand_path("~/.convox").freeze
AUTH_FILE =
File.join(CONVOX_DIR, "auth")
HOST_FILE =
File.join(CONVOX_DIR, "host")

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Client

Returns a new instance of Client.



19
20
21
22
23
# File 'lib/convox/client.rb', line 19

def initialize(options = {})
  @logger = Logger.new(STDOUT)
  logger.level = options[:log_level] || Logger::INFO
  @config = options[:config] || {}
end

Instance Attribute Details

#configObject

Returns the value of attribute config.



13
14
15
# File 'lib/convox/client.rb', line 13

def config
  @config
end

#loggerObject

Returns the value of attribute logger.



13
14
15
# File 'lib/convox/client.rb', line 13

def logger
  @logger
end

Instance Method Details

#add_docker_registry!Object



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
# File 'lib/convox/client.rb', line 336

def add_docker_registry!
  require_config(%i[docker_registry_url docker_registry_username docker_registry_password])

  registry_url = config.fetch(:docker_registry_url)

  logger.debug "Looking up existing Docker registries..."
  registries_response = `convox api get /registries`
  unless $?.success?
    raise "Something went wrong while fetching the list of registries!"
  end
  registries = JSON.parse(registries_response)

  if registries.any? { |r| r["server"] == registry_url }
    logger.debug "=> Docker Registry already exists: #{registry_url}"
    return true
  end

  logger.info "Adding Docker Registry: #{registry_url}..."
  logger.info "=> Documentation: " \
              "https://docs.convox.com/deployment/private-registries"

  `convox registries add "#{registry_url}" \
    "#{config.fetch(:docker_registry_username)}" \
    "#{config.fetch(:docker_registry_password)}"`
  unless $?.success?
    raise "Something went wrong while adding the #{registry_url} registry!"
  end
end

#authObject



15
16
17
# File 'lib/convox/client.rb', line 15

def auth
  load_auth_from_file
end

#backup_convox_host_and_rackObject



25
26
27
28
29
30
31
32
33
34
# File 'lib/convox/client.rb', line 25

def backup_convox_host_and_rack
  %w[host rack].each do |f|
    path = File.join(CONVOX_DIR, f)
    if File.exist?(path)
      bak_file = "#{path}.bak"
      logger.info "Moving existing #{path} to #{bak_file}..."
      FileUtils.mv(path, bak_file)
    end
  end
end

#convox_app_exists?Boolean

Returns:

  • (Boolean)


193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/convox/client.rb', line 193

def convox_app_exists?
  require_config(%i[convox_app_name])
  app_name = config.fetch(:convox_app_name)

  logger.debug "Looking for existing #{app_name} app..."
  convox_output = `convox api get /apps`
  raise "convox command failed!" unless $?.success?

  apps = JSON.parse(convox_output)
  apps.each do |app|
    if app["name"] == app_name
      logger.debug "=> Found #{app_name} app."
      return true
    end
  end
  logger.debug "=> Did not find #{app_name} app."
  false
end

#convox_rack_dataObject



149
150
151
152
153
154
155
156
# File 'lib/convox/client.rb', line 149

def convox_rack_data
  @convox_rack_data ||= begin
    logger.debug "Fetching convox rack attributes..."
    convox_output = `convox api get /system`
    raise "convox command failed!" unless $?.success?
    JSON.parse(convox_output)
  end
end

#create_convox_app!Object



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
# File 'lib/convox/client.rb', line 158

def create_convox_app!
  require_config(%i[convox_app_name])
  return true if convox_app_exists?

  app_name = config.fetch(:convox_app_name)

  logger.info "Creating app: #{app_name}..."
  logger.info "=> Documentation: " \
              "https://docs.convox.com/deployment/creating-an-application"

  run_convox_command! "apps create #{app_name} --wait"

  retries = 0
  loop do
    break if convox_app_exists?
    if retries > 5
      raise "Something went wrong while creating the #{app_name} app! " \
            "(Please wait a few moments and then restart the installation script.)"
    end
    logger.info "Waiting for #{app_name} to be ready..."
    sleep 3
    retries += 1
  end

  logger.info "=> #{app_name} app created!"
end

#create_s3_bucket!Object

Create the s3 bucket, and also apply a CORS configuration



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
# File 'lib/convox/client.rb', line 213

def create_s3_bucket!
  require_config(%i[s3_bucket_name])
  bucket_name = config.fetch(:s3_bucket_name)
  if s3_bucket_exists?
    logger.info "#{bucket_name} S3 bucket already exists!"
  else
    logger.info "Creating S3 bucket resource (#{bucket_name})..."
    run_convox_command! "rack resources create s3 " \
                        "--name \"#{bucket_name}\" " \
                        "--wait"

    retries = 0
    loop do
      break if s3_bucket_exists?

      if retries > 10
        raise "Something went wrong while creating the #{bucket_name} S3 bucket! " \
              "(Please wait a few moments and then restart the installation script.)"
      end
      logger.debug "Waiting for S3 bucket to be ready..."
      sleep 3
      retries += 1
    end

    logger.debug "=> S3 bucket created!"
  end

  set_s3_bucket_cors_policy
end

#default_service_domain_nameObject



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/convox/client.rb', line 365

def default_service_domain_name
  require_config(%i[convox_app_name default_service])

  @default_service_domain_name ||= begin
    convox_domain = convox_rack_data["domain"]
    elb_name_and_region = convox_domain[/([^\.]*\.[^\.]*)\..*/, 1]
    unless elb_name_and_region.present?
      raise "Something went wrong while parsing the ELB name and region! " \
            "(#{elb_name_and_region})"
    end
    app = config.fetch(:convox_app_name)
    service = config.fetch(:default_service)

    # Need to return downcase host so that `config.hosts` works with Rails applications
    "#{app}-#{service}.#{elb_name_and_region}.convox.site".downcase
  end
end

#install_convoxObject



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
# File 'lib/convox/client.rb', line 36

def install_convox
  require_config(%i[ aws_region stack_name ])
  region = config.fetch(:aws_region)
  stack_name = config.fetch(:stack_name)

  if rack_already_installed?
    logger.info "There is already a Convox stack named #{stack_name} " \
                "in the #{region} AWS region. Using this rack. "
    return true
  end

  require_config(%i[
    aws_region
    aws_access_key_id
    aws_secret_access_key
    stack_name
    instance_type
  ])

  logger.info "Installing Convox (#{stack_name})..."

  env = {
    "AWS_REGION" => region,
    "AWS_ACCESS_KEY_ID" => config.fetch(:aws_access_key_id),
    "AWS_SECRET_ACCESS_KEY" => config.fetch(:aws_secret_access_key),
  }
  command = %Q{rack install aws \
--name "#{config.fetch(:stack_name)}" \
"InstanceType=#{config.fetch(:instance_type)}" \
"BuildInstance="}

  run_convox_command!(command, env)
end

#rack_already_installed?Boolean

Returns:

  • (Boolean)


70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/convox/client.rb', line 70

def rack_already_installed?
  require_config(%i[ aws_region stack_name ])

  unless File.exist?(AUTH_FILE)
    raise "Could not find auth file at #{AUTH_FILE}!"
  end

  region = config.fetch(:aws_region)
  stack_name = config.fetch(:stack_name)

  auth.each do |host, password|
    if host.match?(/^#{stack_name}-\d+\.#{region}\.elb\.amazonaws\.com$/)
      return true
    end
  end
  false
end

#run_convox_command!(cmd, env = {}) ⇒ Object



383
384
385
386
387
# File 'lib/convox/client.rb', line 383

def run_convox_command!(cmd, env = {})
  command = "convox #{cmd}"
  system env, command
  raise "Error running: #{command}" unless $?.success?
end

#s3_bucket_detailsObject



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
# File 'lib/convox/client.rb', line 251

def s3_bucket_details
  require_config(%i[s3_bucket_name])
  @s3_bucket_details ||= begin
    bucket_name = config.fetch(:s3_bucket_name)
    logger.debug "Fetching S3 bucket resource details for #{bucket_name}..."

    response = `convox api get /resources/#{bucket_name}`
    raise "convox command failed!" unless $?.success?

    bucket_data = JSON.parse(response)
    s3_url = bucket_data["url"]
    matches = s3_url.match(
      /^s3:\/\/(?<access_key_id>[^:]*):(?<secret_access_key>[^@]*)@(?<bucket_name>.*)$/
    )

    match_keys = %i[access_key_id secret_access_key bucket_name]
    unless matches && match_keys.all? { |k| matches[k].present? }
      raise "#{s3_url} is an invalid S3 URL!"
    end

    {
      access_key_id: matches[:access_key_id],
      secret_access_key: matches[:secret_access_key],
      name: matches[:bucket_name],
    }
  end
end

#s3_bucket_exists?Boolean

Returns:

  • (Boolean)


243
244
245
246
247
248
249
# File 'lib/convox/client.rb', line 243

def s3_bucket_exists?
  require_config(%i[s3_bucket_name])
  bucket_name = config.fetch(:s3_bucket_name)
  logger.debug "Looking up S3 bucket resource: #{bucket_name}"
  `convox api get /resources/#{bucket_name} 2>/dev/null`
  $?.success?
end

#set_default_app_for_directory!Object



185
186
187
188
189
190
191
# File 'lib/convox/client.rb', line 185

def set_default_app_for_directory!
  logger.info "Setting default app in ./.convox/app..."
  FileUtils.mkdir_p File.expand_path("./.convox")
  File.open(File.expand_path("./.convox/app"), "w") do |f|
    f.puts config.fetch(:convox_app_name)
  end
end

#set_host(host) ⇒ Object



121
122
123
124
# File 'lib/convox/client.rb', line 121

def set_host(host)
  logger.debug "Setting convox host to #{host} (in #{HOST_FILE})..."
  File.open(HOST_FILE, "w") { |f| f.puts host }
end

#set_s3_bucket_cors_policyObject



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
# File 'lib/convox/client.rb', line 279

def set_s3_bucket_cors_policy
  require_config(%i[aws_access_key_id aws_secret_access_key])
  access_key_id = config.fetch(:aws_access_key_id)
  secret_access_key = config.fetch(:aws_secret_access_key)

  unless config.key? :s3_bucket_cors_policy
    logger.debug "No CORS policy provided in config: s3_bucket_cors_policy"
    return
  end
  cors_policy_string = config.fetch(:s3_bucket_cors_policy)

  bucket_name = s3_bucket_details[:name]

  logger.debug "Looking up existing CORS policy for #{bucket_name}"
  existing_cors_policy_string =
    `AWS_ACCESS_KEY_ID=#{access_key_id} \
    AWS_SECRET_ACCESS_KEY=#{secret_access_key} \
    aws s3api get-bucket-cors --bucket #{bucket_name} 2>/dev/null`
  if $?.success? && existing_cors_policy_string.present?
    # Sort all the nested arrays so that the equality operator works
    existing_cors_policy = JSON.parse(existing_cors_policy_string)
    cors_policy_json = JSON.parse(cors_policy_string)
    [existing_cors_policy, cors_policy_json].each do |policy_json|
      if policy_json.is_a?(Hash) && policy_json["CORSRules"]
        policy_json["CORSRules"].each do |rule|
          rule["AllowedHeaders"].sort! if rule["AllowedHeaders"]
          rule["AllowedMethods"].sort! if rule["AllowedMethods"]
          rule["AllowedOrigins"].sort! if rule["AllowedOrigins"]
        end
      end
    end

    if existing_cors_policy == cors_policy_json
      logger.debug "=> CORS policy is already up to date for #{bucket_name}."
      return
    end
  end

  begin
    logger.info "Setting CORS policy for #{bucket_name}..."

    File.open("cors-policy.json", "w") { |f| f.puts cors_policy_string }

    `AWS_ACCESS_KEY_ID=#{access_key_id} \
    AWS_SECRET_ACCESS_KEY=#{secret_access_key} \
      aws s3api put-bucket-cors \
      --bucket #{bucket_name} \
      --cors-configuration "file://cors-policy.json"`
    unless $?.success?
      raise "Something went wrong while setting the S3 bucket CORS policy!"
    end
    logger.info "=> Successfully set CORS policy for #{bucket_name}."
  ensure
    FileUtils.rm_f "cors-policy.json"
  end
end

#validate_convox_auth_and_set_host!Object



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
# File 'lib/convox/client.rb', line 88

def validate_convox_auth_and_set_host!
  require_config(%i[ aws_region stack_name ])

  unless File.exist?(AUTH_FILE)
    raise "Could not find auth file at #{AUTH_FILE}!"
  end

  region = config.fetch(:aws_region)
  stack = config.fetch(:stack_name)

  match_count = 0
  matching_host = nil
  auth.each do |host, password|
    if host.match?(/^#{stack}-\d+\.#{region}\.elb\.amazonaws\.com$/)
      matching_host = host
      match_count += 1
    end
  end

  if match_count == 1
    set_host(matching_host)
    return matching_host
  end

  if match_count > 1
    error_message = "Found multiple matching hosts for "
  else
    error_message = "Could not find matching authentication for "
  end
  error_message += "region: #{region}, stack: #{stack}"
  raise error_message
end

#validate_convox_rack!Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/convox/client.rb', line 126

def validate_convox_rack!
  require_config(%i[
    aws_region
    stack_name
    instance_type
  ])
  logger.debug "Validating that convox rack has the correct attributes..."
  {
    provider: "aws",
    region: config.fetch(:aws_region),
    type: config.fetch(:instance_type),
    name: config.fetch(:stack_name),
  }.each do |k, v|
    convox_value = convox_rack_data[k.to_s]
    if convox_value != v
      raise "Convox data did not match! Expected #{k} to be '#{v}', " \
            "but was: '#{convox_value}'"
    end
  end
  logger.debug "=> Convox rack has the correct attributes."
  true
end