aptible-cli 0.6.9 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/README.md +1 -1
  4. data/aptible-cli.gemspec +6 -3
  5. data/bin/aptible +3 -1
  6. data/codecov.yml +12 -0
  7. data/lib/aptible/cli/agent.rb +3 -0
  8. data/lib/aptible/cli/helpers/app.rb +98 -33
  9. data/lib/aptible/cli/helpers/database.rb +3 -3
  10. data/lib/aptible/cli/subcommands/apps.rb +4 -3
  11. data/lib/aptible/cli/subcommands/backup.rb +55 -0
  12. data/lib/aptible/cli/subcommands/config.rb +2 -2
  13. data/lib/aptible/cli/subcommands/db.rb +11 -2
  14. data/lib/aptible/cli/subcommands/rebuild.rb +1 -1
  15. data/lib/aptible/cli/subcommands/restart.rb +1 -1
  16. data/lib/aptible/cli/version.rb +1 -1
  17. data/spec/aptible/cli/helpers/git_remote_handle_strategy_spec.rb +54 -0
  18. data/spec/aptible/cli/helpers/{handle_from_git_remote.rb → handle_from_git_remote_spec.rb} +0 -0
  19. data/spec/aptible/cli/helpers/options_handle_strategy_spec.rb +14 -0
  20. data/spec/aptible/cli/helpers/tunnel_spec.rb +0 -1
  21. data/spec/aptible/cli/subcommands/apps_spec.rb +141 -54
  22. data/spec/aptible/cli/subcommands/backup_spec.rb +115 -0
  23. data/spec/aptible/cli/subcommands/db_spec.rb +35 -61
  24. data/spec/aptible/cli/subcommands/domains_spec.rb +21 -38
  25. data/spec/aptible/cli/subcommands/logs_spec.rb +12 -17
  26. data/spec/aptible/cli/subcommands/ps_spec.rb +5 -12
  27. data/spec/fabricators/account_fabricator.rb +10 -0
  28. data/spec/fabricators/app_fabricator.rb +14 -0
  29. data/spec/fabricators/backup_fabricator.rb +10 -0
  30. data/spec/fabricators/database_fabricator.rb +15 -0
  31. data/spec/fabricators/operation_fabricator.rb +6 -0
  32. data/spec/fabricators/service_fabricator.rb +9 -0
  33. data/spec/fabricators/vhost_fabricator.rb +9 -0
  34. data/spec/spec_helper.rb +9 -1
  35. metadata +81 -17
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Helpers::App::OptionsHandleStrategy do
4
+ it 'is usable when app is set' do
5
+ s = described_class.new(app: 'foo')
6
+ expect(s.usable?).to be_truthy
7
+ end
8
+
9
+ it 'passes options through' do
10
+ s = described_class.new(app: 'foo', environment: 'bar')
11
+ expect(s.app_handle).to eq('foo')
12
+ expect(s.env_handle).to eq('bar')
13
+ end
14
+ end
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'climate_control'
3
2
 
4
3
  describe Aptible::CLI::Helpers::Tunnel do
5
4
  around do |example|
@@ -1,16 +1,17 @@
1
- require 'ostruct'
2
- require 'spec_helper'
3
-
4
- class App < OpenStruct
5
- end
6
-
7
- class Service < OpenStruct
8
- end
9
-
10
- class Operation < OpenStruct
11
- end
12
-
13
- class Account < OpenStruct
1
+ def dummy_strategy_factory(app_handle, env_handle, usable,
2
+ options_receiver = [])
3
+ Class.new do
4
+ attr_reader :options
5
+
6
+ define_method(:initialize) { |options| options_receiver << options }
7
+ define_method(:app_handle) { app_handle }
8
+ define_method(:env_handle) { env_handle }
9
+ define_method(:usable?) { usable }
10
+
11
+ def explain
12
+ '(options from dummy)'
13
+ end
14
+ end
14
15
  end
15
16
 
16
17
  describe Aptible::CLI::Agent do
@@ -19,49 +20,40 @@ describe Aptible::CLI::Agent do
19
20
  before { subject.stub(:fetch_token) { double 'token' } }
20
21
  before { subject.stub(:attach_to_operation_logs) }
21
22
 
22
- let(:service) { Service.new(process_type: 'web') }
23
- let(:op) { Operation.new(status: 'succeeded') }
24
- let(:account) do
25
- Account.new(bastion_host: 'localhost',
26
- dumptruck_port: 1234,
27
- handle: 'aptible')
28
- end
29
- let(:services) { [service] }
30
- let(:apps) do
31
- [App.new(handle: 'hello', services: services, account: account)]
32
- end
23
+ let!(:account) { Fabricate(:account) }
24
+ let!(:app) { Fabricate(:app, handle: 'hello', account: account) }
25
+ let!(:service) { Fabricate(:service, app: app) }
26
+ let(:op) { Fabricate(:operation, status: 'succeeded', resource: app) }
33
27
 
34
28
  describe '#apps:scale' do
29
+ before do
30
+ allow(Aptible::Api::App).to receive(:all) { [app] }
31
+ allow(Aptible::Api::Account).to receive(:all) { [account] }
32
+ end
33
+
35
34
  it 'should pass given correct parameters' do
36
- allow(service).to receive(:create_operation) { op }
37
35
  allow(subject).to receive(:options) do
38
36
  { app: 'hello', environment: 'foobar' }
39
37
  end
40
- allow(op).to receive(:resource) { apps.first }
41
- allow(Aptible::Api::App).to receive(:all) { apps }
42
-
38
+ expect(service).to receive(:create_operation!) { op }
43
39
  expect(subject).to receive(:environment_from_handle)
44
40
  .with('foobar')
45
41
  .and_return(account)
46
- expect(subject).to receive(:apps_from_handle).and_return(apps)
42
+ expect(subject).to receive(:apps_from_handle).and_return([app])
47
43
  subject.send('apps:scale', 'web', 3)
48
44
  end
49
45
 
50
46
  it 'should pass container size param to operation if given' do
51
- expect(service).to receive(:create_operation)
52
- .with(type: 'scale', container_count: 3, container_size: 90210)
53
- .and_return(op)
54
47
  allow(subject).to receive(:options) do
55
48
  { app: 'hello', size: 90210, environment: 'foobar' }
56
49
  end
57
-
58
- allow(op).to receive(:resource) { apps.first }
59
- allow(Aptible::Api::App).to receive(:all) { apps }
60
-
50
+ expect(service).to receive(:create_operation!)
51
+ .with(type: 'scale', container_count: 3, container_size: 90210)
52
+ .and_return(op)
61
53
  expect(subject).to receive(:environment_from_handle)
62
54
  .with('foobar')
63
55
  .and_return(account)
64
- expect(subject).to receive(:apps_from_handle).and_return(apps)
56
+ expect(subject).to receive(:apps_from_handle).and_return([app])
65
57
  subject.send('apps:scale', 'web', 3)
66
58
  end
67
59
 
@@ -69,9 +61,8 @@ describe Aptible::CLI::Agent do
69
61
  allow(subject).to receive(:options) do
70
62
  { environment: 'foo', app: 'web' }
71
63
  end
72
- allow(service).to receive(:create_operation) { op }
73
64
  allow(Aptible::Api::Account).to receive(:all) { [] }
74
- allow(account).to receive(:apps) { [apps] }
65
+ allow(service).to receive(:create_operation!) { op }
75
66
 
76
67
  expect do
77
68
  subject.send('apps:scale', 'web', 3)
@@ -79,19 +70,14 @@ describe Aptible::CLI::Agent do
79
70
  end
80
71
 
81
72
  it 'should fail if app is non-existent' do
82
- allow(service).to receive(:create_operation) { op }
83
- allow(Aptible::Api::Account).to receive(:all) { [account] }
84
- allow(account).to receive(:apps) { [] }
85
-
86
73
  expect do
87
74
  subject.send('apps:scale', 'web', 3)
88
75
  end.to raise_error(Thor::Error)
89
76
  end
90
77
 
91
78
  it 'should fail if number is not a valid number' do
92
- allow(service).to receive(:create_operation) { op }
93
79
  allow(subject).to receive(:options) { { app: 'hello' } }
94
- allow(Aptible::Api::App).to receive(:all) { apps }
80
+ allow(service).to receive(:create_operation) { op }
95
81
 
96
82
  expect do
97
83
  subject.send('apps:scale', 'web', 'potato')
@@ -105,8 +91,7 @@ describe Aptible::CLI::Agent do
105
91
  expect(subject).to receive(:environment_from_handle)
106
92
  .with('foobar')
107
93
  .and_return(account)
108
- expect(subject).to receive(:apps_from_handle).and_return(apps)
109
- allow(Aptible::Api::App).to receive(:all) { apps }
94
+ expect(subject).to receive(:apps_from_handle).and_return([app])
110
95
 
111
96
  expect do
112
97
  subject.send('apps:scale', 'potato', 1)
@@ -114,18 +99,16 @@ describe Aptible::CLI::Agent do
114
99
  end
115
100
 
116
101
  context 'no service' do
117
- let(:services) { [] }
102
+ before { app.services = [] }
118
103
 
119
104
  it 'should fail if the app has no services' do
120
- expect(subject).to receive(:environment_from_handle)
121
- .with('foobar')
122
- .and_return(account)
123
- expect(subject).to receive(:apps_from_handle).and_return(apps)
124
105
  allow(subject).to receive(:options) do
125
106
  { app: 'hello', environment: 'foobar' }
126
107
  end
127
-
128
- allow(Aptible::Api::App).to receive(:all) { apps }
108
+ expect(subject).to receive(:environment_from_handle)
109
+ .with('foobar')
110
+ .and_return(account)
111
+ expect(subject).to receive(:apps_from_handle).and_return([app])
129
112
 
130
113
  expect do
131
114
  subject.send('apps:scale', 'web', 1)
@@ -133,4 +116,108 @@ describe Aptible::CLI::Agent do
133
116
  end
134
117
  end
135
118
  end
119
+
120
+ describe '#ensure_app' do
121
+ it 'fails if no usable strategy is found' do
122
+ strategies = [dummy_strategy_factory(nil, nil, false)]
123
+ subject.stub(:handle_strategies) { strategies }
124
+
125
+ expect { subject.ensure_app }.to raise_error(/Could not find app/)
126
+ end
127
+
128
+ it 'fails if an environment is specified but not found' do
129
+ strategies = [dummy_strategy_factory('hello', 'aptible', true)]
130
+ subject.stub(:handle_strategies) { strategies }
131
+
132
+ expect(subject).to receive(:environment_from_handle).and_return(nil)
133
+
134
+ expect { subject.ensure_app }.to raise_error(/Could not find environment/)
135
+ end
136
+
137
+ context 'with apps' do
138
+ let(:apps) { [app] }
139
+
140
+ before do
141
+ account.apps = apps
142
+ allow(Aptible::Api::App).to receive(:all).and_return(apps)
143
+ end
144
+
145
+ it 'scopes the app search to an environment if provided' do
146
+ strategies = [dummy_strategy_factory('hello', 'aptible', true)]
147
+ subject.stub(:handle_strategies) { strategies }
148
+
149
+ expect(subject).to receive(:environment_from_handle).with('aptible')
150
+ .and_return(account)
151
+
152
+ expect(subject.ensure_app).to eq(apps.first)
153
+ end
154
+
155
+ it 'does not scope the app search to an environment if not provided' do
156
+ strategies = [dummy_strategy_factory('hello', nil, true)]
157
+ subject.stub(:handle_strategies) { strategies }
158
+
159
+ expect(subject.ensure_app).to eq(apps.first)
160
+ end
161
+
162
+ it 'fails if no app is found' do
163
+ apps.pop
164
+
165
+ strategies = [dummy_strategy_factory('hello', nil, true)]
166
+ subject.stub(:handle_strategies) { strategies }
167
+
168
+ expect { subject.ensure_app }.to raise_error(/not find app hello/)
169
+ end
170
+
171
+ it 'explains the strategy when it fails' do
172
+ apps.pop
173
+
174
+ strategies = [dummy_strategy_factory('hello', nil, true)]
175
+ subject.stub(:handle_strategies) { strategies }
176
+
177
+ expect { subject.ensure_app }.to raise_error(/from dummy/)
178
+ end
179
+
180
+ it 'indicates the environment when the app search was scoped' do
181
+ apps.pop
182
+
183
+ strategies = [dummy_strategy_factory('hello', 'aptible', true)]
184
+ subject.stub(:handle_strategies) { strategies }
185
+
186
+ expect(subject).to receive(:environment_from_handle).with('aptible')
187
+ .and_return(account)
188
+
189
+ expect { subject.ensure_app }.to raise_error(/in environment aptible/)
190
+ end
191
+
192
+ it 'fails if multiple apps are found' do
193
+ apps << Fabricate(:app, handle: 'hello')
194
+
195
+ strategies = [dummy_strategy_factory('hello', nil, true)]
196
+ subject.stub(:handle_strategies) { strategies }
197
+
198
+ expect { subject.ensure_app }.to raise_error(/Multiple apps/)
199
+ end
200
+
201
+ it 'falls back to another strategy when the first one is unusable' do
202
+ strategies = [
203
+ dummy_strategy_factory('hello', nil, false),
204
+ dummy_strategy_factory('hello', nil, true)
205
+ ]
206
+ subject.stub(:handle_strategies) { strategies }
207
+
208
+ expect(subject.ensure_app).to eq(apps.first)
209
+ end
210
+
211
+ it 'passes options to the strategy' do
212
+ receiver = []
213
+ strategies = [dummy_strategy_factory('hello', nil, false, receiver)]
214
+ subject.stub(:handle_strategies) { strategies }
215
+
216
+ options = { app: 'foo', environment: 'bar' }
217
+ expect { subject.ensure_app options }.to raise_error(/not find app/)
218
+
219
+ expect(receiver).to eq([options])
220
+ end
221
+ end
222
+ end
136
223
  end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Agent do
4
+ let(:token) { 'some-token' }
5
+ let(:account) { Fabricate(:account) }
6
+ let(:database) { Fabricate(:database, account: account, handle: 'some-db') }
7
+ let!(:backup) do
8
+ # created_at: 2016-06-14 13:24:11 +0000
9
+ Fabricate(:backup, database: database, created_at: Time.at(1465910651))
10
+ end
11
+
12
+ let(:messages) { [] }
13
+
14
+ before do
15
+ allow(subject).to receive(:fetch_token).and_return(token)
16
+ allow(subject).to receive(:say) { |m| messages << m }
17
+ end
18
+
19
+ describe '#backup:restore' do
20
+ it 'fails if the backup cannot be found' do
21
+ expect(Aptible::Api::Backup).to receive(:find).with(1, token: token)
22
+ .and_return(nil)
23
+
24
+ expect { subject.send('backup:restore', 1) }
25
+ .to raise_error('Backup #1 not found')
26
+ end
27
+
28
+ context 'successful restore' do
29
+ let(:op) { Fabricate(:operation, resource: backup) }
30
+
31
+ before do
32
+ expect(Aptible::Api::Backup).to receive(:find).with(1, token: token)
33
+ .and_return(backup)
34
+ expect(subject).to receive(:attach_to_operation_logs).with(op)
35
+ end
36
+
37
+ it 'provides a default handle and no disk size' do
38
+ h = 'some-db-at-2016-06-14-13-24-11'
39
+
40
+ expect(backup).to receive(:create_operation!) do |options|
41
+ expect(options[:handle]).to eq(h)
42
+ expect(options[:disk_size]).not_to be_present
43
+ op
44
+ end
45
+
46
+ subject.send('backup:restore', 1)
47
+ expect(messages).to eq(["Restoring backup into #{h}"])
48
+ end
49
+
50
+ it 'accepts a custom handle and disk size' do
51
+ h = 'some-handle'
52
+ s = 40
53
+
54
+ expect(backup).to receive(:create_operation!) do |options|
55
+ expect(options[:handle]).to eq(h)
56
+ expect(options[:disk_size]).to eq(s)
57
+ op
58
+ end
59
+
60
+ subject.options = { handle: h, size: s }
61
+ subject.send('backup:restore', 1)
62
+ expect(messages).to eq(["Restoring backup into #{h}"])
63
+ end
64
+ end
65
+ end
66
+
67
+ describe '#backup:list' do
68
+ before { allow(Aptible::Api::Account).to receive(:all) { [account] } }
69
+ before { allow(Aptible::Api::Database).to receive(:all) { [database] } }
70
+
71
+ before do
72
+ m = allow(database).to receive(:each_backup)
73
+
74
+ [
75
+ 1.day, 2.days, 3.days, 4.days,
76
+ 5.days, 2.weeks, 3.weeks, 1.month,
77
+ 1.year
78
+ ].each do |age|
79
+ b = Fabricate(:backup, database: database, created_at: age.ago)
80
+ m.and_yield(b)
81
+ end
82
+ end
83
+
84
+ # The default value isn't set when we run sepcs
85
+ before { subject.options = { max_age: '1w' } }
86
+
87
+ it 'can show a subset of backups' do
88
+ subject.send('backup:list', database.handle)
89
+ expect(messages.size).to eq(5)
90
+ end
91
+
92
+ it 'allows scoping via environment' do
93
+ subject.options = { max_age: '1w', environment: database.account.handle }
94
+ subject.send('backup:list', database.handle)
95
+ expect(messages.size).to eq(5)
96
+ end
97
+
98
+ it 'shows more backups if requested' do
99
+ subject.options = { max_age: '2y' }
100
+ subject.send('backup:list', database.handle)
101
+ expect(messages.size).to eq(9)
102
+ end
103
+
104
+ it 'errors out if max_age is invalid' do
105
+ subject.options = { max_age: 'foobar' }
106
+ expect { subject.send('backup:list', database.handle) }
107
+ .to raise_error(Thor::Error, 'Invalid age: foobar')
108
+ end
109
+
110
+ it 'fails if the DB is not found' do
111
+ expect { subject.send('backup:list', 'nope') }
112
+ .to raise_error(Thor::Error, 'Could not find database nope')
113
+ end
114
+ end
115
+ end
@@ -1,12 +1,5 @@
1
- require 'ostruct'
2
1
  require 'spec_helper'
3
2
 
4
- class Database < OpenStruct
5
- end
6
-
7
- class Account < OpenStruct
8
- end
9
-
10
3
  class SocatHelperMock < OpenStruct
11
4
  end
12
5
 
@@ -15,33 +8,16 @@ describe Aptible::CLI::Agent do
15
8
  before { subject.stub(:save_token) }
16
9
  before { subject.stub(:fetch_token) { double 'token' } }
17
10
 
18
- let(:account) do
19
- Account.new(bastion_host: 'localhost',
20
- dumptruck_port: 1234,
21
- handle: 'aptible')
22
- end
23
- let(:database) do
24
- Database.new(
25
- type: 'postgresql',
26
- handle: 'foobar',
27
- passphrase: 'password',
28
- connection_url: 'postgresql://aptible:password@10.252.1.125:49158/db',
29
- account: account
30
- )
31
- end
32
-
33
- let(:socat_helper) do
34
- SocatHelperMock.new(
35
- port: 4242
36
- )
37
- end
11
+ let(:handle) { 'foobar' }
12
+ let(:database) { Fabricate(:database, handle: handle) }
13
+ let(:socat_helper) { SocatHelperMock.new(port: 4242) }
38
14
 
39
15
  describe '#db:tunnel' do
40
16
  it 'should fail if database is non-existent' do
41
17
  allow(Aptible::Api::Database).to receive(:all) { [] }
42
18
  expect do
43
- subject.send('db:tunnel', 'foobar')
44
- end.to raise_error('Could not find database foobar')
19
+ subject.send('db:tunnel', handle)
20
+ end.to raise_error("Could not find database #{handle}")
45
21
  end
46
22
 
47
23
  it 'should print a message about how to connect' do
@@ -55,14 +31,28 @@ describe Aptible::CLI::Agent do
55
31
 
56
32
  # db:tunnel should also explain each component of the URL:
57
33
  expect(subject).to receive(:say).exactly(7).times
58
- subject.send('db:tunnel', 'foobar')
34
+ subject.send('db:tunnel', handle)
59
35
  end
60
36
  end
61
37
 
62
38
  describe '#db:list' do
39
+ before do
40
+ staging = Fabricate(:account, handle: 'staging')
41
+ prod = Fabricate(:account, handle: 'production')
42
+
43
+ [[staging, 'staging-redis-db'], [staging, 'staging-postgres-db'],
44
+ [prod, 'prod-elsearch-db'], [prod, 'prod-postgres-db']].each do |a, h|
45
+ Fabricate(:database, account: a, handle: h)
46
+ end
47
+
48
+ token = 'the-token'
49
+ allow(subject).to receive(:fetch_token).and_return(token)
50
+ allow(Aptible::Api::Account).to receive(:all).with(token: token)
51
+ .and_return([staging, prod])
52
+ end
53
+
63
54
  context 'when no account is specified' do
64
55
  it 'prints out the grouped database handles for all accounts' do
65
- setup_prod_and_staging_accounts
66
56
  allow(subject).to receive(:say)
67
57
 
68
58
  subject.send('db:list')
@@ -79,7 +69,6 @@ describe Aptible::CLI::Agent do
79
69
 
80
70
  context 'when a valid account is specified' do
81
71
  it 'prints out the database handles for the account' do
82
- setup_prod_and_staging_accounts
83
72
  allow(subject).to receive(:say)
84
73
 
85
74
  subject.options = { environment: 'staging' }
@@ -97,7 +86,6 @@ describe Aptible::CLI::Agent do
97
86
 
98
87
  context 'when an invalid account is specified' do
99
88
  it 'prints out an error' do
100
- setup_prod_and_staging_accounts
101
89
  allow(subject).to receive(:say)
102
90
 
103
91
  subject.options = { environment: 'foo' }
@@ -108,37 +96,23 @@ describe Aptible::CLI::Agent do
108
96
  end
109
97
  end
110
98
 
111
- def setup_prod_and_staging_accounts
112
- staging_redis = Database.new(handle: 'staging-redis-db')
113
- staging_postgres = Database.new(handle: 'staging-postgres-db')
114
- prod_elsearch = Database.new(handle: 'prod-elsearch-db')
115
- prod_postgres = Database.new(handle: 'prod-postgres-db')
116
-
117
- stub_local_token_with('the-token')
118
- setup_new_accounts_with_dbs(
119
- token: 'the-token',
120
- account_db_mapping: {
121
- 'staging' => [staging_redis, staging_postgres],
122
- 'production' => [prod_elsearch, prod_postgres]
123
- }
124
- )
125
- end
99
+ describe '#db:backup' do
100
+ before { allow(Aptible::Api::Account).to receive(:all) { [account] } }
101
+ before { allow(Aptible::Api::Database).to receive(:all) { [database] } }
126
102
 
127
- def setup_new_accounts_with_dbs(options)
128
- token = options.fetch(:token)
129
- account_db_mapping = options.fetch(:account_db_mapping)
103
+ let(:op) { Fabricate(:operation) }
130
104
 
131
- accounts_with_dbs = []
132
- account_db_mapping.each do |account_handle, dbs|
133
- account = Account.new(handle: account_handle, databases: dbs)
134
- accounts_with_dbs << account
135
- end
105
+ it 'allows creating a new backup' do
106
+ expect(database).to receive(:create_operation!).and_return(op)
107
+ expect(subject).to receive(:say).with('Backing up foobar...')
108
+ expect(subject).to receive(:attach_to_operation_logs).with(op)
136
109
 
137
- allow(Aptible::Api::Account).to receive(:all).with(token: token)
138
- .and_return(accounts_with_dbs)
139
- end
110
+ subject.send('db:backup', handle)
111
+ end
140
112
 
141
- def stub_local_token_with(token)
142
- allow(subject).to receive(:fetch_token).and_return(token)
113
+ it 'fails if the DB is not found' do
114
+ expect { subject.send('db:backup', 'nope') }
115
+ .to raise_error(Thor::Error, 'Could not find database nope')
116
+ end
143
117
  end
144
118
  end