Module: Aptible::CLI::Helpers::Database

Includes:
Environment, Ssh, Token
Included in:
AppOrDatabase
Defined in:
lib/aptible/cli/helpers/database.rb

Defined Under Namespace

Classes: MockRdsDatabaseAccountShell, RdsDatabase

Constant Summary collapse

UNATTACHED_RDS_ACCOUNT_ID =

using an ID that cannot be hit for visual segregation of unattached databases

-9999

Constants included from Token

Token::TOKEN_ENV_VAR

Instance Method Summary collapse

Methods included from Ssh

#connect_to_ssh_portal, #exit_with_ssh_portal, #with_ssh_cmd

Methods included from ConfigPath

#aptible_config_path

Methods included from Environment

#ensure_default_environment, #ensure_environment, #environment_from_handle, #environment_href, #environment_map, #scoped_environments

Methods included from Token

#current_token, #current_token_hash, #decode_token, #fetch_token, #save_token, #token_file, #whoami

Instance Method Details

#accounts_external_rds_databases_map(rds_map) ⇒ Object



74
75
76
77
78
# File 'lib/aptible/cli/helpers/database.rb', line 74

def accounts_external_rds_databases_map(rds_map)
  return {} if rds_map.empty?

  map_of_accounts_to_rds(rds_map)
end

#aws_rds_db?(handle) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/aptible/cli/helpers/database.rb', line 60

def aws_rds_db?(handle)
  handle.start_with? 'aws:rds::'
end

#clone_database(source, dest_handle) ⇒ Object



153
154
155
156
157
158
# File 'lib/aptible/cli/helpers/database.rb', line 153

def clone_database(source, dest_handle)
  op = source.create_operation!(type: 'clone', handle: dest_handle)
  attach_to_operation_logs(op)

  database_from_handle(dest_handle, source.)
end

#database_from_handle(handle, environment) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/aptible/cli/helpers/database.rb', line 137

def database_from_handle(handle, environment)
  url = "/find/database?handle=#{handle}"
  url += "&environment=#{environment.handle}" unless environment.nil?

  Aptible::Api::Database.find_by_url(
    url,
    token: fetch_token
  )
rescue HyperResource::ClientError => e
  raise unless e.body.is_a?(Hash) &&
               e.body['error'] == 'multiple_resources_found'
  raise Thor::Error,
        'Multiple databases exist, please specify ' \
        'with --environment'
end

#databases_allObject



53
54
55
56
57
58
# File 'lib/aptible/cli/helpers/database.rb', line 53

def databases_all
  Aptible::Api::Database.all(
    token: fetch_token,
    href: databases_href
  )
end

#databases_hrefObject



45
46
47
48
49
50
51
# File 'lib/aptible/cli/helpers/database.rb', line 45

def databases_href
  href = '/databases'
  if Renderer.format != 'json'
    href = '/databases?per_page=5000&no_embed=true'
  end
  href
end

#derive_account_from_conns(db, preferred_acct = nil) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/aptible/cli/helpers/database.rb', line 116

def (db, preferred_acct = nil)
  conns = db.raw.app_external_aws_rds_connections
  return nil if conns.empty?

  if preferred_acct.present?
    valid_conns = conns.find do |conn|
      conn.present? && conn.app..id == preferred_acct.id
    end
    return nil if valid_conns.nil?
    return valid_conns.app.
  end

  first_present_conn = conns.find(&:present?)
  return nil if first_present_conn.nil?
  first_present_conn.app.
end

#ensure_database(options = {}) ⇒ Object

Raises:

  • (Thor::Error)


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/aptible/cli/helpers/database.rb', line 28

def ensure_database(options = {})
  db_handle = options[:db]
  environment_handle = options[:environment]

  raise Thor::Error, 'Database handle not specified' unless db_handle

  environment = environment_from_handle(environment_handle)
  if environment_handle && !environment
    raise Thor::Error,
          "Could not find environment #{environment_handle}"
  end
  db = database_from_handle(db_handle, environment)
  raise Thor::Error, "Could not find database #{db_handle}" if db.nil?

  db
end

#external_rds_database_from_handle(handle) ⇒ Object



133
134
135
# File 'lib/aptible/cli/helpers/database.rb', line 133

def external_rds_database_from_handle(handle)
  external_rds_databases_all.find { |a| a.handle == handle }
end

#external_rds_databases_allObject



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/aptible/cli/helpers/database.rb', line 100

def external_rds_databases_all
  Aptible::Api::ExternalAwsResource
    .all(
      token: fetch_token
    )
    .select { |db| db.resource_type == 'aws_rds_db_instance' }
    .map do |db|
      RdsDatabase.new(
        "aws:rds::#{db.resource_name}",
        db.id,
        db.created_at,
        db
      )
    end
end

#external_rds_databases_mapObject



64
65
66
# File 'lib/aptible/cli/helpers/database.rb', line 64

def external_rds_databases_map
  external_rds_databases_all.map { |rds| [rds[:id], rds] }.to_h
end

#fetch_rds_databases_with_accountsObject



68
69
70
71
72
# File 'lib/aptible/cli/helpers/database.rb', line 68

def fetch_rds_databases_with_accounts
  rds_map = external_rds_databases_map
  accts_rds_map = accounts_external_rds_databases_map(rds_map)
  [rds_map, accts_rds_map]
end

#find_credential(database, type = nil) ⇒ Object

Raises:

  • (Thor::Error)


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
# File 'lib/aptible/cli/helpers/database.rb', line 321

def find_credential(database, type = nil)
  unless database.provisioned?
    raise Thor::Error, "Database #{database.handle} is not provisioned"
  end

  # Get the database credentials, without going using `with_senstive(database)`, as that
  # would get the embedded last_operation, and generate an extra show activity
  creds_link = database.links['database_credentials']
  database_credentials = Aptible::Api::DatabaseCredential.all(
    href: creds_link.href,
    token: fetch_token,
    headers: { 'Prefer' => 'no_sensitive_extras=false' }
  )

  finder = proc { |c| c.default }
  finder = proc { |c| c.type == type } if type
  credential = database_credentials.find(&finder)

  # It may be weird to return the credential and all the credentials, but the db:tunnel
  # command lists all the credential types if you do not provide one, and we want to avoid
  # generating more show activity than needed
  return credential, database_credentials if credential

  types = database_credentials.map(&:type)

  valid = types.join(', ')

  err = 'No default credential for database'
  err = "No credential with type #{type} for database" if type
  raise Thor::Error, "#{err}, valid credential types: #{valid}"
end

#find_database_image(type, version) ⇒ Object

Raises:

  • (Thor::Error)


353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/aptible/cli/helpers/database.rb', line 353

def find_database_image(type, version)
  available_versions = []

  Aptible::Api::DatabaseImage.all(token: fetch_token).each do |i|
    next unless i.type == type
    return i if i.version == version
    available_versions << i.version
  end

  err = "No Database Image of type #{type} with version #{version}"
  err = "#{err}, valid versions: #{available_versions.join(' ')}"
  raise Thor::Error, err
end

#local_rds_url(credential, local_port, forced_account) ⇒ Object



303
304
305
306
307
308
309
310
# File 'lib/aptible/cli/helpers/database.rb', line 303

def local_rds_url(credential, local_port, )
  remote_url = credential.connection_url

  uri = URI.parse(remote_url)
  domain = .stack.internal_domain
  "#{uri.scheme}://#{uri.user}:#{uri.password}@" \
  "localhost.#{domain}:#{local_port}#{uri.path}"
end

#local_url(credential, local_port) ⇒ Object



312
313
314
315
316
317
318
319
# File 'lib/aptible/cli/helpers/database.rb', line 312

def local_url(credential, local_port)
  remote_url = credential.connection_url

  uri = URI.parse(remote_url)
  domain = without_sensitive(credential).database..stack.internal_domain
  "#{uri.scheme}://#{uri.user}:#{uri.password}@" \
  "localhost.#{domain}:#{local_port}#{uri.path}"
end

#map_of_accounts_to_rds(rds_map) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/aptible/cli/helpers/database.rb', line 80

def map_of_accounts_to_rds(rds_map)
  # one rds db can be on multiple accounts
  accts_rds_map = {}
  rds_map.each_value do |db|
     = (db)
    next if .nil?

    accts_rds_map[.id] = [] if accts_rds_map[.id].nil?
    accts_rds_map[.id] << db
  end
  accts_rds_map
end

#rds_shell_accountObject



93
94
95
96
97
98
# File 'lib/aptible/cli/helpers/database.rb', line 93

def 
  MockRdsDatabaseAccountShell.new(
    'unattached rds databases',
    UNATTACHED_RDS_ACCOUNT_ID
  )
end

#render_database(database, account) ⇒ Object



380
381
382
383
384
385
386
387
388
389
390
# File 'lib/aptible/cli/helpers/database.rb', line 380

def render_database(database, )
  # Maybe reload with senstive data
  # Definately don't load the embedded last_operation
  database.href = database.href + '?no_embed=true'
  database = with_sensitive(database) if database.connection_url.nil?
  Formatter.render(Renderer.current) do |root|
    root.keyed_object('connection_url') do |node|
      ResourceFormatter.inject_database(node, database, )
    end
  end
end

#replicate_database(source, dest_handle, options) ⇒ Object



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
# File 'lib/aptible/cli/helpers/database.rb', line 160

def replicate_database(source, dest_handle, options)
  replication_params = {
    handle: dest_handle,
    container_size: options[:container_size],
    disk_size: options[:size],
    key_arn: options[:key_arn],
    instance_profile: options[:instance_profile],
    provisioned_iops: options[:provisioned_iops]
  }.reject { |_, v| v.nil? }

  if options[:logical]
    replication_params[:type] = 'replicate_logical'
    replication_params[:docker_ref] =
      options[:database_image].docker_repo
  else
    replication_params[:type] = 'replicate'
  end

  op = source.create_operation!(replication_params)
  attach_to_operation_logs(op)

  replica = database_from_handle(dest_handle, source.)
  attach_to_operation_logs(replica.operations.last)
  replica
end

#use_rds_dump(handle, filename, dump_options) ⇒ Object



272
273
274
275
276
277
278
# File 'lib/aptible/cli/helpers/database.rb', line 272

def use_rds_dump(handle, filename, dump_options)
  with_rds_tunnel(handle) do |url|
    CLI.logger.info "Dumping to #{filename}"
    `pg_dump #{url} #{dump_options.shelljoin} > #{filename}`
    exit $CHILD_STATUS.exitstatus unless $CHILD_STATUS.success?
  end
end

#use_rds_execute(handle, sql_path, options) ⇒ Object



280
281
282
283
284
285
286
287
# File 'lib/aptible/cli/helpers/database.rb', line 280

def use_rds_execute(handle, sql_path, options)
  with_rds_tunnel(handle) do |url|
    CLI.logger.info "Executing #{sql_path} against #{handle}"
    args = options[:on_error_stop] ? '-v ON_ERROR_STOP=true ' : ''
    `psql #{args}#{url} < #{sql_path}`
    exit $CHILD_STATUS.exitstatus unless $CHILD_STATUS.success?
  end
end

#use_rds_tunnel(handle, port) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/aptible/cli/helpers/database.rb', line 249

def use_rds_tunnel(handle, port)
  with_rds_tunnel(handle, port) do |url, tunnel_helper|
    CLI.logger.info "Connect at #{url}"

    uri = URI(url)
    db = uri.path.gsub(%r{^/}, '')
    CLI.logger.info 'Or, use the following arguments:'
    CLI.logger.info "* Host: #{uri.host}"
    CLI.logger.info "* Port: #{uri.port}"
    CLI.logger.info "* Username: #{uri.user}" unless uri.user.empty?
    CLI.logger.info "* Password: #{uri.password}"
    CLI.logger.info "* Database: #{db}" unless db.empty?

    CLI.logger.info 'Connected. Ctrl-C to close connection.'

    begin
      tunnel_helper.wait
    rescue Interrupt
      CLI.logger.warn 'Closing tunnel'
    end
  end
end

#validate_image_type(type) ⇒ Object

Raises:

  • (Thor::Error)


367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/aptible/cli/helpers/database.rb', line 367

def validate_image_type(type)
  available_types = []

  Aptible::Api::DatabaseImage.all(token: fetch_token).each do |i|
    return true if i.type == type
    available_types << i.type
  end

  err = "No Database Image of type \"#{type}\""
  err = "#{err}, valid types: #{available_types.uniq.join(', ')}"
  raise Thor::Error, err
end

#with_local_tunnel(credential, port = 0, target_account = nil) ⇒ Object

Creates a local tunnel and yields the helper



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
# File 'lib/aptible/cli/helpers/database.rb', line 188

def with_local_tunnel(credential, port = 0,  = nil)
  # Credential has the senstive header set, and for some reason
  # credential.create_operation! _lists all operations_. This would
  # generate a show activity for every previous tunnel operation.
  # So, we strip the sensitive header first to prevent that from happening
  # This will also strip the connection_url, but we don't need it from
  # this point on.
  credential = without_sensitive(credential)
  # Twice by here??
  op = if .nil?
         credential.create_operation!(
           type: 'tunnel',
           status: 'succeeded'
         )
       else
         credential.create_operation!(
           type: 'tunnel',
           status: 'succeeded',
           destination_account: .id
         )
       end

  with_ssh_cmd(op) do |base_ssh_cmd, ssh_credential|
    ssh_cmd = base_ssh_cmd + ['-o', 'SendEnv=ACCESS_TOKEN']
    ssh_env = { 'ACCESS_TOKEN' => fetch_token }

    socket_path = ssh_credential.ssh_port_forward_socket
    tunnel_helper = Helpers::Tunnel.new(ssh_env, ssh_cmd, socket_path)

    tunnel_helper.start(port)
    yield tunnel_helper if block_given?
    tunnel_helper.stop
  end
end

#with_postgres_tunnel(database) ⇒ Object

Creates a local PG tunnel and yields the url to it



291
292
293
294
295
296
297
298
299
300
301
# File 'lib/aptible/cli/helpers/database.rb', line 291

def with_postgres_tunnel(database)
  if database.type != 'postgresql'
    raise Thor::Error, 'This command only works for PostgreSQL'
  end

  credential, _credentials = find_credential(database)

  with_local_tunnel(credential) do |tunnel_helper|
    yield local_url(credential, tunnel_helper.port)
  end
end

#with_rds_tunnel(handle, port = 0) ⇒ Object



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
# File 'lib/aptible/cli/helpers/database.rb', line 223

def with_rds_tunnel(handle, port = 0)
  external_rds = external_rds_database_from_handle(handle)
  if external_rds.nil?
    raise Thor::Error, "No rds db found with handle #{handle}"
  end

  credential = external_rds.raw.external_aws_database_credentials.first
  if credential.nil?
    raise Thor::Error, 'No rds credential found with handle ' \
                       "#{handle}. Check to see if you have run " \
                       'db:attach or a scan has properly completed.'
  end

   = (external_rds)
  if .nil?
    raise Thor::Error,
          "No env for rds found with handle #{handle}. Check to see " \
          'if you have run db:attach or a scan has properly completed.'
  end

  with_local_tunnel(credential, port, ) do |tunnel_helper|
    url = local_rds_url(credential, tunnel_helper.port, )
    yield url, tunnel_helper
  end
end