aptible-cli 0.16.6 → 0.16.7

Sign up to get free protection for your applications and to get access to all the features.
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