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 +4 -4
- data/Gemfile +1 -1
- data/README.md +3 -2
- data/aptible-cli.gemspec +1 -1
- data/lib/aptible/cli/helpers/database.rb +9 -1
- data/lib/aptible/cli/resource_formatter.rb +36 -1
- data/lib/aptible/cli/subcommands/backup.rb +38 -11
- data/lib/aptible/cli/subcommands/db.rb +21 -2
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/subcommands/backup_spec.rb +49 -2
- data/spec/aptible/cli/subcommands/db_spec.rb +63 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35f34899ac1d54918f9f5852b665196f65f8a40daa70a405ffd10c64f0a6014a
|
4
|
+
data.tar.gz: 8c0703501a28277b3775fe85b80b53ccdfc9a36256073da64119995f84b17534
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3da14de30e9885ee58a3d03141a6c854beaf499825faac9dae1deeebb676ceab16ee57b0a68f6148592ccca8b87d385c3052c426cf6fc028d34b86eb84d3b8d2
|
7
|
+
data.tar.gz: 634ece7c98758d5164ef56fa5a82bfbbe7974568f5d806d8e93cfd4e50c027ec7834785eaebb05307eb00dfa2a6e9b769b5b20c1415f84c4b026e73c5a14bbe1
|
data/Gemfile
CHANGED
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:
|
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]
|
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
|
data/aptible-cli.gemspec
CHANGED
@@ -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.
|
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 =
|
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 |
|
73
|
+
root.keyed_list('description') do |node|
|
73
74
|
database.each_backup do |backup|
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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',
|
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([
|
data/lib/aptible/cli/version.rb
CHANGED
@@ -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
|
-
|
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.
|
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-
|
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.
|
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.
|
40
|
+
version: '1.2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: aptible-auth
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|