aptible-cli 0.26.2 → 0.26.4

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -0
  3. data/Gemfile.lock +1 -1
  4. data/lib/aptible/cli/agent.rb +7 -4
  5. data/lib/aptible/cli/helpers/app.rb +11 -0
  6. data/lib/aptible/cli/helpers/database.rb +29 -13
  7. data/lib/aptible/cli/helpers/log_drain.rb +3 -1
  8. data/lib/aptible/cli/helpers/metric_drain.rb +3 -1
  9. data/lib/aptible/cli/resource_formatter.rb +14 -1
  10. data/lib/aptible/cli/subcommands/config.rb +2 -2
  11. data/lib/aptible/cli/subcommands/db.rb +4 -3
  12. data/lib/aptible/cli/subcommands/log_drain.rb +2 -0
  13. data/lib/aptible/cli/subcommands/logs.rb +5 -5
  14. data/lib/aptible/cli/subcommands/metric_drain.rb +2 -0
  15. data/lib/aptible/cli/version.rb +1 -1
  16. data/lib/aptible/cli.rb +14 -0
  17. data/spec/aptible/cli/subcommands/config_spec.rb +8 -0
  18. data/spec/aptible/cli/subcommands/db_spec.rb +3 -51
  19. data/spec/aptible/cli/subcommands/log_drain_spec.rb +6 -0
  20. data/spec/aptible/cli/subcommands/metric_drain_spec.rb +4 -0
  21. data/spec/fabricators/account_fabricator.rb +9 -1
  22. data/spec/fabricators/app_external_aws_rds_connection_fabricator.rb +1 -1
  23. data/spec/fabricators/app_fabricator.rb +1 -1
  24. data/spec/fabricators/backup_fabricator.rb +1 -1
  25. data/spec/fabricators/backup_retention_policy_fabricator.rb +1 -1
  26. data/spec/fabricators/certificate_fabricator.rb +1 -1
  27. data/spec/fabricators/configuration_fabricator.rb +1 -1
  28. data/spec/fabricators/database_credential_fabricator.rb +1 -1
  29. data/spec/fabricators/database_disk_fabricator.rb +1 -1
  30. data/spec/fabricators/database_fabricator.rb +10 -1
  31. data/spec/fabricators/database_image_fabricator.rb +1 -1
  32. data/spec/fabricators/external_aws_account_fabricator.rb +1 -1
  33. data/spec/fabricators/external_aws_database_credential_fabricator.rb +1 -1
  34. data/spec/fabricators/external_aws_resource_fabricator.rb +1 -1
  35. data/spec/fabricators/log_drain_fabricator.rb +1 -1
  36. data/spec/fabricators/maintenance_app_fabricator.rb +1 -1
  37. data/spec/fabricators/maintenance_database_fabricator.rb +1 -1
  38. data/spec/fabricators/metric_drain_fabricator.rb +15 -1
  39. data/spec/fabricators/operation_fabricator.rb +1 -1
  40. data/spec/fabricators/service_fabricator.rb +1 -1
  41. data/spec/fabricators/service_sizing_policy_fabricator.rb +1 -1
  42. data/spec/fabricators/stack_fabricator.rb +1 -1
  43. data/spec/fabricators/vhost_fabricator.rb +1 -1
  44. data/spec/spec_helper.rb +5 -0
  45. data/spec/support/stub_aptible_resource.rb +25 -0
  46. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 800b50ac57828f37560b0a291a280fbf4e9bbf121b35fd812d662e45ef39bd45
4
- data.tar.gz: e0c70006be582d93f6e5648fae3894a481bde9cc9b43042ee399e745790aac95
3
+ metadata.gz: 1c756f930a26ab5b060f01270b80c9ec2963c8f739be4ffcd2a5d56e9dac2860
4
+ data.tar.gz: 55fdde5cde257bd36c6c16a17e7f639745f85947b1cf40269c0d73ab0093615c
5
5
  SHA512:
6
- metadata.gz: 3653523bd6b9f944e5f88128a63c058aee80722288a44f5886393059f8b1ca2f7c506c45a7df06e0794ba75814574e36372ff02c73ee1efcd1c20ee803b1d0a1
7
- data.tar.gz: a47e8d8f71c9ac07d5001ad65616ac123884ac97ed5ab151d6d8895564bffffa623e1ef055b32fc3d4d0c661a9e71b72859b84480bde108f645f7ce33252e81f
6
+ metadata.gz: 01122dab06813f9b67100c11e6e59ec6942f5263340efc26bbf73440b51c39c7cd0be7258b987fa39056092b29bf181ded4d14423d27ff795cd18bf85b97fe96
7
+ data.tar.gz: d7ed67882c43c958dcc60c7ef5ba518f3eb6ad72ca014f4c0b761478f2748054af64286aca26b8ac97b5ca5ea03964bc34fd422d3cf42f4996b4c6949815d870
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ inherit_gem:
2
+ aptible-tasks: .rubocop.yml
3
+
4
+ Metrics/LineLength:
5
+ Max: 120
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- aptible-cli (0.26.2)
4
+ aptible-cli (0.26.4)
5
5
  activesupport (>= 4.0, < 6.0)
6
6
  aptible-api (~> 1.12)
7
7
  aptible-auth (~> 1.4)
@@ -92,10 +92,13 @@ module Aptible
92
92
  level = Logger::WARN
93
93
  debug_level = ENV['APTIBLE_DEBUG']
94
94
  level = debug_level if debug_level
95
- require 'httplog' if ENV['BUNDLER_VERSION'] && \
96
- ENV['APTIBLE_LOG_HTTP_REQUEST_RESPONSE'] && \
97
- !ENV['APTIBLE_LOG_HTTP_REQUEST_RESPONSE'] \
98
- .downcase.start_with?('f')
95
+ if ENV['BUNDLER_VERSION'] && \
96
+ ENV['APTIBLE_LOG_HTTP_REQUEST_RESPONSE'] && \
97
+ !ENV['APTIBLE_LOG_HTTP_REQUEST_RESPONSE'] \
98
+ .downcase.start_with?('f')
99
+ require 'httplog'
100
+ HttpLog.configure { |c| c.log_headers = true }
101
+ end
99
102
  conf.logger.tap { |l| l.level = level }
100
103
  end
101
104
  warn_sso_enforcement
@@ -196,6 +196,17 @@ module Aptible
196
196
  raise Thor::Error, "Invalid argument: #{k}" if v.nil?
197
197
  end
198
198
 
199
+ def current_configuration(app)
200
+ conf_link = app.links['current_configuration']
201
+ return unless conf_link
202
+
203
+ Aptible::Api::Configuration.find_by_url(
204
+ conf_link.href,
205
+ token: fetch_token,
206
+ headers: { 'Prefer' => 'no_sensitive_extras=false' }
207
+ )
208
+ end
209
+
199
210
  private
200
211
 
201
212
  def handle_strategies
@@ -185,6 +185,14 @@ module Aptible
185
185
  # Creates a local tunnel and yields the helper
186
186
 
187
187
  def with_local_tunnel(credential, port = 0, target_account = nil)
188
+ # Credential has the senstive header set, and for some reason
189
+ # credential.create_operation! _lists all operations_. This would
190
+ # generate a show activity for every previous tunnel operation.
191
+ # So, we strip the sensitive header first to prevent that from happening
192
+ # This will also strip the connection_url, but we don't need it from
193
+ # this point on.
194
+ credential = without_sensitive(credential)
195
+ # Twice by here??
188
196
  op = if target_account.nil?
189
197
  credential.create_operation!(
190
198
  type: 'tunnel',
@@ -284,7 +292,7 @@ module Aptible
284
292
  raise Thor::Error, 'This command only works for PostgreSQL'
285
293
  end
286
294
 
287
- credential = find_credential(database)
295
+ credential, _credentials = find_credential(database)
288
296
 
289
297
  with_local_tunnel(credential) do |tunnel_helper|
290
298
  yield local_url(credential, tunnel_helper.port)
@@ -304,7 +312,7 @@ module Aptible
304
312
  remote_url = credential.connection_url
305
313
 
306
314
  uri = URI.parse(remote_url)
307
- domain = credential.database.account.stack.internal_domain
315
+ domain = without_sensitive(credential).database.account.stack.internal_domain
308
316
  "#{uri.scheme}://#{uri.user}:#{uri.password}@" \
309
317
  "localhost.#{domain}:#{local_port}#{uri.path}"
310
318
  end
@@ -314,21 +322,25 @@ module Aptible
314
322
  raise Thor::Error, "Database #{database.handle} is not provisioned"
315
323
  end
316
324
 
325
+ # Get the database credentials, without going using `with_senstive(database)`, as that
326
+ # would get the embedded last_operation, and generate an extra show activity
327
+ creds_link = database.links['database_credentials']
328
+ database_credentials = Aptible::Api::DatabaseCredential.all(
329
+ href: creds_link.href,
330
+ token: fetch_token,
331
+ headers: { 'Prefer' => 'no_sensitive_extras=false' }
332
+ )
333
+
317
334
  finder = proc { |c| c.default }
318
335
  finder = proc { |c| c.type == type } if type
319
- credential = database.database_credentials.find(&finder)
336
+ credential = database_credentials.find(&finder)
320
337
 
321
- return credential if credential
338
+ # It may be weird to return the credential and all the credentials, but the db:tunnel
339
+ # command lists all the credential types if you do not provide one, and we want to avoid
340
+ # generating more show activity than needed
341
+ return credential, database_credentials if credential
322
342
 
323
- types = database.database_credentials.map(&:type)
324
-
325
- # On v1, we fallback to the DB. We make sure to make --type work, to
326
- # avoid a confusing experience for customers.
327
- if database.account.stack.version == 'v1'
328
- types << database.type
329
- types.uniq!
330
- return database if type.nil? || type == database.type
331
- end
343
+ types = database_credentials.map(&:type)
332
344
 
333
345
  valid = types.join(', ')
334
346
 
@@ -365,6 +377,10 @@ module Aptible
365
377
  end
366
378
 
367
379
  def render_database(database, account)
380
+ # Maybe reload with senstive data
381
+ # Definately don't load the embedded last_operation
382
+ database.href = database.href + '?no_embed=true'
383
+ database = with_sensitive(database) if database.connection_url.nil?
368
384
  Formatter.render(Renderer.current) do |root|
369
385
  root.keyed_object('connection_url') do |node|
370
386
  ResourceFormatter.inject_database(node, database, account)
@@ -65,7 +65,9 @@ module Aptible
65
65
  end
66
66
 
67
67
  def ensure_log_drain(account, handle)
68
- drains = account.log_drains.select { |d| d.handle == handle }
68
+ link = account.links['log_drains'].base_href
69
+ account_drains = Aptible::Api::LogDrain.all(href: link, token: fetch_token)
70
+ drains = account_drains.select { |d| d.handle == handle }
69
71
 
70
72
  if drains.empty?
71
73
  raise Thor::Error, "No drain found with handle #{handle}"
@@ -19,7 +19,9 @@ module Aptible
19
19
  end
20
20
 
21
21
  def ensure_metric_drain(account, handle)
22
- drains = account.metric_drains.select { |d| d.handle == handle }
22
+ link = account.links['metric_drains'].base_href
23
+ account_drains = Aptible::Api::MetricDrain.all(href: link, token: fetch_token)
24
+ drains = account_drains.select { |d| d.handle == handle }
23
25
 
24
26
  if drains.empty?
25
27
  raise Thor::Error, "No drain found with handle #{handle}"
@@ -128,6 +128,10 @@ module Aptible
128
128
  end
129
129
 
130
130
  def inject_database(node, database, account)
131
+ # Some callers pass a database object with sensitive attributes already, others do not.
132
+ # Avoid creating extra 'show' activity if we already have the needed info
133
+ database = with_sensitive(database) if database.objects[:database_credentials].nil?
134
+
131
135
  node.value('id', database.id)
132
136
  node.value('handle', database.handle)
133
137
  node.value('created_at', database.created_at)
@@ -243,6 +247,9 @@ module Aptible
243
247
  log_drain.drain_ephemeral_sessions)
244
248
  node.value('drain_proxies', log_drain.drain_proxies)
245
249
 
250
+ # These can be either optional for the drain type,
251
+ # or sensitive attributes we don't need to worry about
252
+ # in text output
246
253
  optional_attrs = %w(drain_username drain_host drain_port url)
247
254
  optional_attrs.each do |attr|
248
255
  value = log_drain.attributes[attr]
@@ -257,7 +264,13 @@ module Aptible
257
264
  node.value('handle', metric_drain.handle)
258
265
  node.value('drain_type', metric_drain.drain_type)
259
266
  node.value('created_at', metric_drain.created_at)
260
- node.value('drain_configuration', metric_drain.drain_configuration)
267
+
268
+ # Sensitive attributes we don't need to worry about being missing in text output
269
+ optional_attrs = %w(drain_configuration)
270
+ optional_attrs.each do |attr|
271
+ value = metric_drain.attributes[attr]
272
+ node.value(attr, value) unless value.nil?
273
+ end
261
274
 
262
275
  attach_account(node, account)
263
276
  end
@@ -15,7 +15,7 @@ module Aptible
15
15
  telemetry(__method__, options)
16
16
 
17
17
  app = ensure_app(options)
18
- config = app.current_configuration
18
+ config = current_configuration(app)
19
19
  env = config ? config.env : {}
20
20
 
21
21
  Formatter.render(Renderer.current) do |root|
@@ -38,7 +38,7 @@ module Aptible
38
38
  telemetry(__method__, options)
39
39
 
40
40
  app = ensure_app(options)
41
- config = app.current_configuration
41
+ config = current_configuration(app)
42
42
  env = config ? config.env : {}
43
43
 
44
44
  Formatter.render(Renderer.current) do |root|
@@ -335,13 +335,13 @@ module Aptible
335
335
  return use_rds_tunnel(handle, desired_port) if aws_rds_db?(handle)
336
336
 
337
337
  database = ensure_database(options.merge(db: handle))
338
- credential = find_credential(database, options[:type])
338
+ credential, credentials = find_credential(database, options[:type])
339
339
 
340
340
  m = "Creating #{credential.type} tunnel to #{database.handle}..."
341
341
  CLI.logger.info m
342
342
 
343
343
  if options[:type].nil?
344
- types = database.database_credentials.map(&:type)
344
+ types = credentials.map(&:type)
345
345
  unless types.empty?
346
346
  valid = types.join(', ')
347
347
  CLI.logger.info 'Use --type TYPE to specify a tunnel type'
@@ -481,7 +481,8 @@ module Aptible
481
481
  telemetry(__method__, options.merge(handle: handle))
482
482
 
483
483
  database = ensure_database(options.merge(db: handle))
484
- credential = find_credential(database, options[:type])
484
+
485
+ credential, _credentials = find_credential(database, options[:type])
485
486
 
486
487
  Formatter.render(Renderer.current) do |root|
487
488
  root.keyed_object('connection_url') do |node|
@@ -44,6 +44,8 @@ module Aptible
44
44
  account = acc_map[drain.links.account.href]
45
45
  next if account.nil?
46
46
 
47
+ # JSON output format we potentially show sensitive attributes
48
+ drain = with_sensitive(drain) if Renderer.format == 'json'
47
49
  node.object do |n|
48
50
  ResourceFormatter.inject_log_drain(n, drain, account)
49
51
  end
@@ -134,11 +134,11 @@ module Aptible
134
134
  options[:string_matches]
135
135
  )
136
136
  elsif id_options.any?
137
- if options[:container_id]
138
- search_attrs = { container_id: options[:container_id] }
139
- else
140
- search_attrs = { type: r_type, id: id_options.compact.first }
141
- end
137
+ search_attrs = if options[:container_id]
138
+ { container_id: options[:container_id] }
139
+ else
140
+ { type: r_type, id: id_options.compact.first }
141
+ end
142
142
  files = find_s3_files_by_attrs(
143
143
  options[:region],
144
144
  options[:bucket],
@@ -38,6 +38,8 @@ module Aptible
38
38
  account = acc_map[drain.links.account.href]
39
39
  next if account.nil?
40
40
 
41
+ # JSON output format we potentially show sensitive attributes
42
+ drain = with_sensitive(drain) if Renderer.format == 'json'
41
43
  node.object do |n|
42
44
  ResourceFormatter.inject_metric_drain(n, drain, account)
43
45
  end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.26.2'.freeze
3
+ VERSION = '0.26.4'.freeze
4
4
  end
5
5
  end
data/lib/aptible/cli.rb CHANGED
@@ -7,6 +7,20 @@ require 'aptible/cli/formatter'
7
7
  require 'aptible/cli/renderer'
8
8
  require 'aptible/cli/resource_formatter'
9
9
 
10
+ # Set no_sensitive_extras=true as the default for all API resources.
11
+ # This avoids returning sensitive embedded data unless explicitly requested.
12
+ Aptible::Api::Resource.headers = { 'Prefer' => 'no_sensitive_extras=true' }
13
+
14
+ def with_sensitive(resource)
15
+ resource.headers['Prefer'] = 'no_sensitive_extras=false'
16
+ resource.find_by_url(resource.href)
17
+ end
18
+
19
+ def without_sensitive(resource)
20
+ resource.headers['Prefer'] = 'no_sensitive_extras=true'
21
+ resource.find_by_url(resource.href)
22
+ end
23
+
10
24
  module Aptible
11
25
  module CLI
12
26
  class TtyLogFormatter
@@ -17,6 +17,9 @@ describe Aptible::CLI::Agent do
17
17
  end
18
18
 
19
19
  before { allow(subject).to receive(:options) { { app: app.handle } } }
20
+ before do
21
+ allow(subject).to receive(:current_configuration, &:current_configuration)
22
+ end
20
23
  let(:operation) { Fabricate(:operation, resource: app) }
21
24
 
22
25
  describe '#config' do
@@ -60,6 +63,11 @@ describe Aptible::CLI::Agent do
60
63
  end
61
64
 
62
65
  describe '#config:get' do
66
+ it 'shows nothing for an unconfigured app' do
67
+ subject.send('config:get', 'FOO')
68
+ expect(captured_output_text).to eq('')
69
+ end
70
+
63
71
  it 'should show single environment variable specified' do
64
72
  app.current_configuration = Fabricate(
65
73
  :configuration, app: app, env: { 'FOO' => 'BAR', 'QUX' => 'two words' }
@@ -10,6 +10,9 @@ describe Aptible::CLI::Agent do
10
10
  allow(subject).to receive(:ask)
11
11
  allow(subject).to receive(:save_token)
12
12
  allow(subject).to receive(:fetch_token) { token }
13
+ allow(Aptible::Api::DatabaseCredential).to receive(:all) do
14
+ database.database_credentials
15
+ end
13
16
  end
14
17
 
15
18
  let(:handle) { 'foobar' }
@@ -226,41 +229,6 @@ describe Aptible::CLI::Agent do
226
229
  expect { subject.send('db:tunnel', handle) }
227
230
  .to raise_error(/foobar is not provisioned/im)
228
231
  end
229
-
230
- context 'v1 stack' do
231
- before do
232
- allow(database.account.stack).to receive(:version) { 'v1' }
233
- end
234
-
235
- it 'falls back to the database itself if no type is given' do
236
- expect(subject).to receive(:with_local_tunnel).with(database, 0)
237
- subject.send('db:tunnel', handle)
238
- end
239
-
240
- it 'falls back to the database itself if type matches' do
241
- subject.options = { type: 'bar' }
242
- allow(database).to receive(:type) { 'bar' }
243
-
244
- expect(subject).to receive(:with_local_tunnel).with(database, 0)
245
- subject.send('db:tunnel', handle)
246
- end
247
-
248
- it 'does not fall back to the database itself if type mismatches' do
249
- subject.options = { type: 'bar' }
250
- allow(database).to receive(:type) { 'foo' }
251
-
252
- expect { subject.send('db:tunnel', handle) }
253
- .to raise_error(/no credential with type bar/im)
254
- end
255
-
256
- it 'does not suggest other types that do not exist' do
257
- expect(subject).to receive(:with_local_tunnel).with(database, 0)
258
-
259
- subject.send('db:tunnel', handle)
260
-
261
- expect(captured_logs).not_to match(/use --type type/i)
262
- end
263
- end
264
232
  end
265
233
  end
266
234
 
@@ -845,22 +813,6 @@ describe Aptible::CLI::Agent do
845
813
  expect { subject.send('db:url', handle) }
846
814
  .to raise_error(/Multiple databases/)
847
815
  end
848
-
849
- context 'v1 stack' do
850
- before do
851
- allow(database.account.stack).to receive(:version) { 'v1' }
852
- end
853
-
854
- it 'returns the URL of a specified DB' do
855
- connection_url = 'postgresql://aptible-v1:password@lega.cy:4242/db'
856
- expect(database).to receive(:connection_url)
857
- .and_return(connection_url)
858
-
859
- subject.send('db:url', handle)
860
-
861
- expect(captured_output_text.chomp).to eq(connection_url)
862
- end
863
- end
864
816
  end
865
817
  end
866
818
 
@@ -14,9 +14,15 @@ describe Aptible::CLI::Agent do
14
14
  .with(token: token, href: '/log_drains?per_page=5000')
15
15
  .and_return([log_drain])
16
16
 
17
+ allow(Aptible::Api::LogDrain).to receive(:all)
18
+ .with(token: token, href: "/accounts/#{account.id}/log_drains")
19
+ .and_return([log_drain])
20
+
17
21
  allow(Aptible::Api::Account).to receive(:all)
18
22
  .with(token: token, href: '/accounts?per_page=5000&no_embed=true')
19
23
  .and_return([account])
24
+
25
+ allow(account).to receive(:reload).and_return(account)
20
26
  end
21
27
 
22
28
  describe '#log_drain:list' do
@@ -14,6 +14,10 @@ describe Aptible::CLI::Agent do
14
14
  .with(token: token, href: '/metric_drains?per_page=5000')
15
15
  .and_return([metric_drain])
16
16
 
17
+ allow(Aptible::Api::MetricDrain).to receive(:all)
18
+ .with(token: token, href: "/accounts/#{account.id}/metric_drains")
19
+ .and_return([metric_drain])
20
+
17
21
  allow(Aptible::Api::Account).to receive(:all)
18
22
  .with(token: token, href: '/accounts?per_page=5000&no_embed=true')
19
23
  .and_return([account])
@@ -1,4 +1,4 @@
1
- class StubAccount < OpenStruct
1
+ class StubAccount < StubAptibleResource
2
2
  def each_app(&block)
3
3
  return enum_for(:each_app) if block.nil?
4
4
  apps.each(&block)
@@ -34,6 +34,14 @@ Fabricator(:account, from: :stub_account) do
34
34
  hash = {
35
35
  self: OpenStruct.new(
36
36
  href: "/accounts/#{attrs[:id]}"
37
+ ),
38
+ metric_drains: OpenStruct.new(
39
+ base_href: "/accounts/#{attrs[:id]}/metric_drains",
40
+ href: "/accounts/#{attrs[:id]}/metric_drains"
41
+ ),
42
+ log_drains: OpenStruct.new(
43
+ base_href: "/accounts/#{attrs[:id]}/log_drains",
44
+ href: "/accounts/#{attrs[:id]}/log_drains"
37
45
  )
38
46
  }
39
47
  OpenStruct.new(hash)
@@ -1,4 +1,4 @@
1
- class StubAppExternalAwsRdsConnection < OpenStruct
1
+ class StubAppExternalAwsRdsConnection < StubAptibleResource
2
2
  def attributes
3
3
  {
4
4
  'id' => id,
@@ -1,4 +1,4 @@
1
- class StubApp < OpenStruct
1
+ class StubApp < StubAptibleResource
2
2
  def vhosts
3
3
  services.map(&:vhosts).flatten
4
4
  end
@@ -1,4 +1,4 @@
1
- class StubBackup < OpenStruct; end
1
+ class StubBackup < StubAptibleResource; end
2
2
 
3
3
  Fabricator(:backup, from: :stub_backup) do
4
4
  id { sequence(:backup_id) }
@@ -1,4 +1,4 @@
1
- class StubBackupRetentionPolicy < OpenStruct
1
+ class StubBackupRetentionPolicy < StubAptibleResource
2
2
  def reload
3
3
  self
4
4
  end
@@ -1,4 +1,4 @@
1
- class StubCertificate < OpenStruct; end
1
+ class StubCertificate < StubAptibleResource; end
2
2
 
3
3
  Fabricator(:certificate, from: :stub_certificate) do
4
4
  account
@@ -1,4 +1,4 @@
1
- class StubConfiguration < OpenStruct
1
+ class StubConfiguration < StubAptibleResource
2
2
  end
3
3
 
4
4
  Fabricator(:configuration, from: :stub_configuration) do
@@ -1,4 +1,4 @@
1
- class StubDatabaseCredential < OpenStruct; end
1
+ class StubDatabaseCredential < StubAptibleResource; end
2
2
 
3
3
  Fabricator(:database_credential, from: :stub_database_credential) do
4
4
  database
@@ -1,4 +1,4 @@
1
- class StubDatabaseDisk < OpenStruct
1
+ class StubDatabaseDisk < StubAptibleResource
2
2
  end
3
3
 
4
4
  Fabricator(:database_disk, from: :stub_database_disk) do
@@ -1,7 +1,13 @@
1
- class StubDatabase < OpenStruct
1
+ class StubDatabase < StubAptibleResource
2
2
  def provisioned?
3
3
  status == 'provisioned'
4
4
  end
5
+
6
+ def objects
7
+ {
8
+ 'database_credentials' => database_credentials
9
+ }
10
+ end
5
11
  end
6
12
 
7
13
  Fabricator(:database, from: :stub_database) do
@@ -23,6 +29,9 @@ Fabricator(:database, from: :stub_database) do
23
29
  hash = {
24
30
  account: OpenStruct.new(
25
31
  href: "/accounts/#{attrs[:account].id}"
32
+ ),
33
+ database_credentials: OpenStruct.new(
34
+ href: "/databases/#{attrs[:handle]}/database_credentials"
26
35
  )
27
36
  }
28
37
  OpenStruct.new(hash)
@@ -1,4 +1,4 @@
1
- class StubDatabaseImage < OpenStruct
1
+ class StubDatabaseImage < StubAptibleResource
2
2
  end
3
3
 
4
4
  Fabricator(:database_image, from: :stub_database_image) do
@@ -1,4 +1,4 @@
1
- class StubExternalAwsAccount < OpenStruct
1
+ class StubExternalAwsAccount < StubAptibleResource
2
2
  def attributes
3
3
  {
4
4
  'aws_account_id' => aws_account_id,
@@ -1,4 +1,4 @@
1
- class StubExternalAwsDatabaseCredential < OpenStruct
1
+ class StubExternalAwsDatabaseCredential < StubAptibleResource
2
2
  def attributes
3
3
  {
4
4
  'id' => id,
@@ -1,4 +1,4 @@
1
- class StubExternalAwsResource < OpenStruct
1
+ class StubExternalAwsResource < StubAptibleResource
2
2
  def attributes
3
3
  {
4
4
  'id' => id,
@@ -1,4 +1,4 @@
1
- class StubLogDrain < OpenStruct
1
+ class StubLogDrain < StubAptibleResource
2
2
  def attributes
3
3
  # I foresee hard-coding values like this
4
4
  # being hard to debug in the future,
@@ -1,4 +1,4 @@
1
- class StubMaintenanceApp < OpenStruct; end
1
+ class StubMaintenanceApp < StubAptibleResource; end
2
2
 
3
3
  Fabricator(:maintenance_app, from: :stub_maintenance_app) do
4
4
  id { Fabricate.sequence(:app_id) { |i| i } }
@@ -1,4 +1,4 @@
1
- class StubMaintenanceDatabase < OpenStruct; end
1
+ class StubMaintenanceDatabase < StubAptibleResource; end
2
2
 
3
3
  Fabricator(:maintenance_database, from: :stub_maintenance_database) do
4
4
  id { Fabricate.sequence(:database_id) { |i| i } }
@@ -1,7 +1,21 @@
1
- class StubMetricDrain < OpenStruct; end
1
+ class StubMetricDrain < StubAptibleResource
2
+ def attributes
3
+ # Don't blame me, I'm just following the example in StubLogDrain,
4
+ # see the comment there.
5
+ {
6
+ 'drain_configuration' => drain_configuration
7
+ }
8
+ end
9
+ end
2
10
 
3
11
  Fabricator(:metric_drain, from: :stub_metric_drain) do
4
12
  id { sequence(:metric_drain_id) }
13
+ drain_configuration do
14
+ {
15
+ 'api_key' => 'asdf',
16
+ 'series_url' => 'https://localhost.aptible.in/api/v1/series'
17
+ }
18
+ end
5
19
  account
6
20
  links do |attrs|
7
21
  hash = {
@@ -1,4 +1,4 @@
1
- class StubOperation < OpenStruct; end
1
+ class StubOperation < StubAptibleResource; end
2
2
 
3
3
  def mock_logs_url(id)
4
4
  "https://api.aptible.com/operations/#{id}/logs"
@@ -1,4 +1,4 @@
1
- class StubService < OpenStruct
1
+ class StubService < StubAptibleResource
2
2
  def each_vhost(&block)
3
3
  return enum_for(:each_vhost) if block.nil?
4
4
  vhosts.each(&block)
@@ -1,4 +1,4 @@
1
- class StubServiceSizingPolicy < OpenStruct; end
1
+ class StubServiceSizingPolicy < StubAptibleResource; end
2
2
 
3
3
  Fabricator(:service_sizing_policy, from: :stub_service_sizing_policy) do
4
4
  autoscaling 'vertical'
@@ -1,4 +1,4 @@
1
- class StubStack < OpenStruct; end
1
+ class StubStack < StubAptibleResource; end
2
2
 
3
3
  Fabricator(:stack, from: :stub_stack) do
4
4
  name 'foo'
@@ -1,4 +1,4 @@
1
- class StubVhost < OpenStruct; end
1
+ class StubVhost < StubAptibleResource; end
2
2
 
3
3
  Fabricator(:vhost, from: :stub_vhost) do
4
4
  service
data/spec/spec_helper.rb CHANGED
@@ -8,6 +8,11 @@ Dir["#{File.dirname(__FILE__)}/shared/**/*.rb"].each do |file|
8
8
  require file
9
9
  end
10
10
 
11
+ # Load support files (shared classes used by fabricators, etc.)
12
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each do |file|
13
+ require file
14
+ end
15
+
11
16
  # Require library up front
12
17
  require 'aptible/cli'
13
18
 
@@ -0,0 +1,25 @@
1
+ class StubAptibleResource < OpenStruct
2
+ def headers
3
+ @headers ||= {}
4
+ end
5
+
6
+ def find_by_url(_url)
7
+ self
8
+ end
9
+
10
+ def href
11
+ self[:href] || "/#{self.class.resource_path}/#{id}"
12
+ end
13
+
14
+ def self.resource_path
15
+ name = to_s.sub(/^Stub/, '')
16
+ snake = name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
17
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
18
+ .downcase
19
+ if snake.end_with?('y')
20
+ snake.sub(/y$/, 'ies')
21
+ else
22
+ "#{snake}s"
23
+ end
24
+ end
25
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aptible-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.2
4
+ version: 0.26.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-13 00:00:00.000000000 Z
11
+ date: 2026-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -478,6 +478,7 @@ files:
478
478
  - ".github/workflows/test.yml"
479
479
  - ".gitignore"
480
480
  - ".rspec"
481
+ - ".rubocop.yml"
481
482
  - Dockerfile
482
483
  - Gemfile
483
484
  - Gemfile.lock
@@ -619,6 +620,7 @@ files:
619
620
  - spec/script/ssh-spawn
620
621
  - spec/shared/mock_ssh_context.rb
621
622
  - spec/spec_helper.rb
623
+ - spec/support/stub_aptible_resource.rb
622
624
  homepage: https://github.com/aptible/aptible-cli
623
625
  licenses:
624
626
  - MIT
@@ -712,3 +714,4 @@ test_files:
712
714
  - spec/script/ssh-spawn
713
715
  - spec/shared/mock_ssh_context.rb
714
716
  - spec/spec_helper.rb
717
+ - spec/support/stub_aptible_resource.rb