aptible-cli 0.16.6 → 0.16.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00beffb5298954baab427a850fca6f30be69ab59f6c5ff098a2d65ee64546094
4
- data.tar.gz: 5c56645ed8df1aeb780248bdbf68658410d8aba3589f8daa84b6b89498464539
3
+ metadata.gz: 35f34899ac1d54918f9f5852b665196f65f8a40daa70a405ffd10c64f0a6014a
4
+ data.tar.gz: 8c0703501a28277b3775fe85b80b53ccdfc9a36256073da64119995f84b17534
5
5
  SHA512:
6
- metadata.gz: 01f8a8d9f7370c9ffbdcb83f9c17f83668b5e7c42cd7a35f5a397156f72a279f394777fd92feebf93395520a267027b04c12f5b5a557000b50c3cb67c6f64583
7
- data.tar.gz: b1182433b759c81561d79488d6937f5badc84b3b58f75c2ed1387cb71f4102a2dc15a304810c3db5b25f3c39fd9f1a3a8e2d290a20f97a97e114c8b5efe0dfc6
6
+ metadata.gz: 3da14de30e9885ee58a3d03141a6c854beaf499825faac9dae1deeebb676ceab16ee57b0a68f6148592ccca8b87d385c3052c426cf6fc028d34b86eb84d3b8d2
7
+ data.tar.gz: 634ece7c98758d5164ef56fa5a82bfbbe7974568f5d806d8e93cfd4e50c027ec7834785eaebb05307eb00dfa2a6e9b769b5b20c1415f84c4b026e73c5a14bbe1
data/Gemfile CHANGED
@@ -6,7 +6,7 @@ gem 'rack', '~> 1.0'
6
6
 
7
7
  group :test do
8
8
  gem 'webmock'
9
- gem 'codecov', require: false
9
+ gem 'codecov', '~> 0.1.0', require: false
10
10
  end
11
11
 
12
12
  # Specify your gem's dependencies in aptible-cli.gemspec
data/README.md CHANGED
@@ -34,7 +34,8 @@ Commands:
34
34
  aptible apps:deprovision # Deprovision an app
35
35
  aptible apps:scale SERVICE [--container-count COUNT] [--container-size SIZE_MB] # Scale a service
36
36
  aptible backup:list DB_HANDLE # List backups for a database
37
- aptible backup:purge BACKUP_ID # Permanently delete a backup
37
+ aptible backup:orphaned # List backups associated with deprovisioned databases
38
+ aptible backup:purge BACKUP_ID # Permanently delete a backup and any copies of it
38
39
  aptible backup:restore BACKUP_ID [--environment ENVIRONMENT_HANDLE] [--handle HANDLE] [--container-size SIZE_MB] [--disk-size SIZE_GB] # Restore a backup
39
40
  aptible config # Print an app's current configuration
40
41
  aptible config:add [VAR1=VAL1] [VAR2=VAL2] [...] # Add an ENV variable to an app
@@ -49,7 +50,7 @@ Commands:
49
50
  aptible db:execute HANDLE SQL_FILE [--on-error-stop] # Executes sql against a database
50
51
  aptible db:list # List all databases
51
52
  aptible db:reload HANDLE # Reload a database
52
- aptible db:replicate HANDLE REPLICA_HANDLE [--container-size SIZE_MB] [--disk-size SIZE_GB] # Create a replica/follower of a database
53
+ aptible db:replicate HANDLE REPLICA_HANDLE [--container-size SIZE_MB] [--disk-size SIZE_GB] [--logical --version VERSION] # Create a replica/follower of a database
53
54
  aptible db:restart HANDLE [--container-size SIZE_MB] [--disk-size SIZE_GB] # Restart a database
54
55
  aptible db:tunnel HANDLE # Create a local tunnel to a database
55
56
  aptible db:url HANDLE # Display a database URL
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ['lib']
22
22
 
23
23
  spec.add_dependency 'aptible-resource', '~> 1.1'
24
- spec.add_dependency 'aptible-api', '~> 1.0'
24
+ spec.add_dependency 'aptible-api', '~> 1.2'
25
25
  spec.add_dependency 'aptible-auth', '~> 1.1.0'
26
26
  spec.add_dependency 'aptible-billing', '~> 1.0'
27
27
  spec.add_dependency 'thor', '~> 0.20.0'
@@ -49,11 +49,19 @@ module Aptible
49
49
 
50
50
  def replicate_database(source, dest_handle, options)
51
51
  replication_params = {
52
- type: 'replicate',
53
52
  handle: dest_handle,
54
53
  container_size: options[:container_size],
55
54
  disk_size: options[:size]
56
55
  }.reject { |_, v| v.nil? }
56
+
57
+ if options[:logical]
58
+ replication_params[:type] = 'replicate_logical'
59
+ replication_params[:docker_ref] =
60
+ options[:database_image].docker_repo
61
+ else
62
+ replication_params[:type] = 'replicate'
63
+ end
64
+
57
65
  op = source.create_operation!(replication_params)
58
66
  attach_to_operation_logs(op)
59
67
 
@@ -4,6 +4,41 @@ module Aptible
4
4
  class << self
5
5
  NO_NESTING = Object.new.freeze
6
6
 
7
+ def inject_backup(node, backup, include_db: false)
8
+ description = "#{backup.id}: #{backup.created_at}, " \
9
+ "#{backup.aws_region}"
10
+
11
+ if include_db
12
+ db = backup.database_with_deleted
13
+ node.keyed_object('database', 'id') do |n|
14
+ inject_deleted_database(n, db, backup.account)
15
+ end
16
+
17
+ description = "#{description}, " \
18
+ "#{db.handle} deleted at #{db.deleted_at}"
19
+ end
20
+
21
+ node.value('id', backup.id)
22
+ node.value('description', description)
23
+ node.value('created_at', backup.created_at)
24
+ node.value('region', backup.aws_region)
25
+ node.value('size', backup.size)
26
+
27
+ if backup.copied_from
28
+ node.keyed_object('copied_from', 'description') do |n|
29
+ inject_backup(n, backup.copied_from)
30
+ end
31
+ end
32
+ end
33
+
34
+ def inject_deleted_database(node, database, account)
35
+ node.value('id', database.id)
36
+ node.value('handle', database.handle)
37
+ node.value('type', database.type)
38
+ node.value('deleted_at', database.deleted_at)
39
+ attach_account(node, account)
40
+ end
41
+
7
42
  def inject_account(node, account)
8
43
  node.value('id', account.id)
9
44
  node.value('handle', account.handle)
@@ -47,6 +82,7 @@ module Aptible
47
82
 
48
83
  node.value('type', database.type)
49
84
  node.value('status', database.status)
85
+
50
86
  node.value('connection_url', database.connection_url)
51
87
 
52
88
  node.list('credentials') do |creds_list|
@@ -54,7 +90,6 @@ module Aptible
54
90
  creds_list.object { |n| inject_credential(n, cred) }
55
91
  end
56
92
  end
57
-
58
93
  attach_account(node, account)
59
94
  end
60
95
 
@@ -23,7 +23,8 @@ module Aptible
23
23
  handle = options[:handle]
24
24
  unless handle
25
25
  ts_suffix = backup.created_at.getgm.strftime '%Y-%m-%d-%H-%M-%S'
26
- handle = "#{backup.database.handle}-at-#{ts_suffix}"
26
+ handle =
27
+ "#{backup.database_with_deleted.handle}-at-#{ts_suffix}"
27
28
  end
28
29
 
29
30
  destination_account = if options[:environment]
@@ -69,24 +70,50 @@ module Aptible
69
70
  database = ensure_database(options.merge(db: handle))
70
71
 
71
72
  Formatter.render(Renderer.current) do |root|
72
- root.keyed_list('description') do |l|
73
+ root.keyed_list('description') do |node|
73
74
  database.each_backup do |backup|
74
- break if backup.created_at < min_created_at
75
- description = "#{backup.id}: #{backup.created_at}, " \
76
- "#{backup.aws_region}"
75
+ if backup.created_at < min_created_at && !backup.copied_from
76
+ break
77
+ end
78
+ node.object do |n|
79
+ ResourceFormatter.inject_backup(n, backup)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
77
85
 
78
- l.object do |o|
79
- o.value('id', backup.id)
80
- o.value('description', description)
81
- o.value('created_at', backup.created_at)
82
- o.value('region', backup.aws_region)
86
+ desc 'backup:orphaned', 'List backups associated with ' \
87
+ 'deprovisioned databases'
88
+ option :environment
89
+ option :max_age, default: '1y',
90
+ desc: 'Limit backups returned '\
91
+ '(example usage: 1w, 1y, etc.)'
92
+ define_method 'backup:orphaned' do
93
+ age = ChronicDuration.parse(options[:max_age])
94
+ raise Thor::Error, "Invalid age: #{options[:max_age]}" if age.nil?
95
+ min_created_at = Time.now - age
96
+
97
+ Formatter.render(Renderer.current) do |root|
98
+ root.keyed_list('description') do |node|
99
+ scoped_environments(options).each do |account|
100
+ account.each_orphaned_backup do |backup|
101
+ created_at = backup.created_at
102
+ copied_from = backup.copied_from
103
+ break if created_at < min_created_at && !copied_from
104
+ node.object do |n|
105
+ ResourceFormatter.inject_backup(
106
+ n, backup, include_db: true
107
+ )
108
+ end
83
109
  end
84
110
  end
85
111
  end
86
112
  end
87
113
  end
88
114
 
89
- desc 'backup:purge BACKUP_ID', 'Permanently delete a backup'
115
+ desc 'backup:purge BACKUP_ID',
116
+ 'Permanently delete a backup and any copies of it'
90
117
  define_method 'backup:purge' do |backup_id|
91
118
  backup = Aptible::Api::Backup.find(backup_id, token: fetch_token)
92
119
  raise Thor::Error, "Backup ##{backup_id} not found" if backup.nil?
@@ -122,20 +122,39 @@ module Aptible
122
122
  end
123
123
 
124
124
  desc 'db:replicate HANDLE REPLICA_HANDLE ' \
125
- '[--container-size SIZE_MB] [--disk-size SIZE_GB]',
125
+ '[--container-size SIZE_MB] [--disk-size SIZE_GB] ' \
126
+ '[--logical --version VERSION]',
126
127
  'Create a replica/follower of a database'
127
128
  option :environment
128
129
  option :container_size, type: :numeric
129
130
  option :size, type: :numeric
130
131
  option :disk_size, type: :numeric
132
+ option :logical, type: :boolean
133
+ option :version, type: :string
131
134
  define_method 'db:replicate' do |source_handle, dest_handle|
132
135
  source = ensure_database(options.merge(db: source_handle))
136
+
137
+ if options[:logical]
138
+ if source.type != 'postgresql'
139
+ raise Thor::Error, 'Logical replication only works for ' \
140
+ 'PostgreSQL'
141
+ end
142
+ if options[:version]
143
+ image = find_database_image(source.type, options[:version])
144
+ else
145
+ raise Thor::Error, '--version is required for logical ' \
146
+ 'replication'
147
+ end
148
+ end
149
+
133
150
  CLI.logger.info "Replicating #{source_handle}..."
134
151
 
135
152
  opts = {
136
153
  environment: options[:environment],
137
154
  container_size: options[:container_size],
138
- size: options[:disk_size] || options[:size]
155
+ size: options[:disk_size] || options[:size],
156
+ logical: options[:logical],
157
+ database_image: image || nil
139
158
  }.delete_if { |_, v| v.nil? }
140
159
 
141
160
  CLI.logger.warn([
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.16.6'.freeze
3
+ VERSION = '0.16.7'.freeze
4
4
  end
5
5
  end
@@ -2,14 +2,17 @@ require 'spec_helper'
2
2
 
3
3
  describe Aptible::CLI::Agent do
4
4
  let(:token) { 'some-token' }
5
- let(:account) { Fabricate(:account, handle: 'test') }
5
+ let(:account) { Fabricate(:account, handle: 'test', id: 1) }
6
6
  let(:alt_account) { Fabricate(:account, handle: 'alt') }
7
7
  let(:database) { Fabricate(:database, account: account, handle: 'some-db') }
8
8
  let!(:backup) do
9
9
  # created_at: 2016-06-14 13:24:11 +0000
10
10
  Fabricate(
11
11
  :backup,
12
- database: database, created_at: Time.at(1465910651), account: account
12
+ database_with_deleted: database,
13
+ created_at: Time.at(1465910651),
14
+ account: account,
15
+ id: 1
13
16
  )
14
17
  end
15
18
 
@@ -179,6 +182,50 @@ describe Aptible::CLI::Agent do
179
182
  end
180
183
  end
181
184
 
185
+ describe '#backup:orphaned' do
186
+ before { allow(Aptible::Api::Account).to receive(:all) { [account] } }
187
+ before do
188
+ m = allow(account).to receive(:each_orphaned_backup)
189
+ ages = [
190
+ 1.day, 2.days, 3.days, 4.days,
191
+ 5.days, 2.weeks, 3.weeks, 1.month,
192
+ 1.year
193
+ ]
194
+ ages.each do |age|
195
+ b = Fabricate(:backup, database: database, created_at: age.ago,
196
+ account: account)
197
+ allow(b).to receive(:database_with_deleted).and_return(database)
198
+ m.and_yield(b)
199
+ b
200
+ end
201
+ end
202
+ before { subject.options = { max_age: '1w' } }
203
+
204
+ it 'can show a subset of backups' do
205
+ subject.send('backup:orphaned')
206
+ puts captured_output_text
207
+ expect(captured_output_text.split("\n").size).to eq(5)
208
+ end
209
+
210
+ it 'allows scoping via environment' do
211
+ subject.options = { max_age: '1w', environment: database.account.handle }
212
+ subject.send('backup:orphaned')
213
+ expect(captured_output_text.split("\n").size).to eq(5)
214
+ end
215
+
216
+ it 'shows more backups if requested' do
217
+ subject.options = { max_age: '2y' }
218
+ subject.send('backup:orphaned')
219
+ expect(captured_output_text.split("\n").size).to eq(9)
220
+ end
221
+
222
+ it 'errors out if max_age is invalid' do
223
+ subject.options = { max_age: 'foobar' }
224
+ expect { subject.send('backup:orphaned') }
225
+ .to raise_error(Thor::Error, 'Invalid age: foobar')
226
+ end
227
+ end
228
+
182
229
  describe '#backup:purge' do
183
230
  it 'fails if the backup cannot be found' do
184
231
  expect(Aptible::Api::Backup).to receive(:find)
@@ -519,6 +519,69 @@ describe Aptible::CLI::Agent do
519
519
  expect { subject.send('db:replicate', 'nope', 'replica') }
520
520
  .to raise_error(Thor::Error, 'Could not find database nope')
521
521
  end
522
+
523
+ it 'allows logical replication of a database with --version set' do
524
+ master = Fabricate(:database, handle: 'master')
525
+ databases << master
526
+ replica = Fabricate(:database,
527
+ account: master.account,
528
+ handle: 'replica')
529
+
530
+ dbimg = Fabricate(:database_image,
531
+ type: 'postgresql',
532
+ version: 10,
533
+ docker_repo: 'aptible/postgresql:10')
534
+
535
+ expect(subject).to receive(:find_database_image).with('postgresql', 10)
536
+ .and_return(dbimg)
537
+
538
+ op = Fabricate(:operation)
539
+
540
+ params = { type: 'replicate_logical', handle: 'replica',
541
+ docker_ref: dbimg.docker_repo }
542
+ expect(master).to receive(:create_operation!)
543
+ .with(**params).and_return(op)
544
+
545
+ expect(subject).to receive(:attach_to_operation_logs).with(op) do
546
+ databases << replica
547
+ replica
548
+ end
549
+
550
+ provision = Fabricate(:operation)
551
+
552
+ expect(replica).to receive_message_chain(:operations, :last)
553
+ .and_return(provision)
554
+
555
+ expect(subject).to receive(:attach_to_operation_logs).with(provision)
556
+
557
+ expect(replica).to receive(:reload).and_return(replica)
558
+
559
+ subject.options = { logical: true, version: 10 }
560
+ subject.send('db:replicate', 'master', 'replica')
561
+
562
+ expect(captured_logs).to match(/replicating master/i)
563
+ end
564
+
565
+ it 'fails if logical replication requested without --version' do
566
+ master = Fabricate(:database, handle: 'master', type: 'postgresql')
567
+ databases << master
568
+
569
+ subject.options = { type: 'replicate', handle: 'replica', logical: true }
570
+ expect { subject.send('db:replicate', 'master', 'replica') }
571
+ .to raise_error(Thor::Error, '--version is required for logical ' \
572
+ 'replication')
573
+ end
574
+
575
+ it 'fails if logical replication requested for non-postgres db' do
576
+ master = Fabricate(:database, handle: 'master', type: 'mysql')
577
+ databases << master
578
+
579
+ subject.options = { type: 'replicate', handle: 'replica',
580
+ logical: true, version: 10 }
581
+ expect { subject.send('db:replicate', 'master', 'replica') }
582
+ .to raise_error(Thor::Error, 'Logical replication only works for ' \
583
+ 'PostgreSQL')
584
+ end
522
585
  end
523
586
 
524
587
  describe '#db:dump' do
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.16.6
4
+ version: 0.16.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Macreery
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-14 00:00:00.000000000 Z
11
+ date: 2020-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aptible-resource
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.0'
33
+ version: '1.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.0'
40
+ version: '1.2'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: aptible-auth
43
43
  requirement: !ruby/object:Gem::Requirement