aptible-cli 0.25.0 → 0.26.2
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/.github/workflows/release.yml +27 -0
- data/.github/workflows/test.yml +1 -1
- data/Dockerfile +8 -5
- data/Gemfile.lock +28 -17
- data/Makefile +53 -2
- data/README.md +83 -82
- data/aptible-cli.gemspec +5 -2
- data/docker-compose.yml +5 -1
- data/lib/aptible/cli/agent.rb +16 -0
- data/lib/aptible/cli/helpers/aws_account.rb +158 -0
- data/lib/aptible/cli/helpers/database.rb +182 -2
- data/lib/aptible/cli/helpers/token.rb +14 -0
- data/lib/aptible/cli/renderer/text.rb +33 -2
- data/lib/aptible/cli/resource_formatter.rb +2 -2
- data/lib/aptible/cli/subcommands/apps.rb +2 -2
- data/lib/aptible/cli/subcommands/aws_accounts.rb +252 -0
- data/lib/aptible/cli/subcommands/config.rb +6 -6
- data/lib/aptible/cli/subcommands/db.rb +67 -3
- data/lib/aptible/cli/subcommands/deploy.rb +46 -12
- data/lib/aptible/cli/subcommands/organizations.rb +55 -0
- data/lib/aptible/cli/subcommands/rebuild.rb +2 -1
- data/lib/aptible/cli/subcommands/restart.rb +2 -1
- data/lib/aptible/cli/subcommands/services.rb +24 -12
- data/lib/aptible/cli/subcommands/ssh.rb +1 -1
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/agent_spec.rb +70 -0
- data/spec/aptible/cli/helpers/database_spec.rb +118 -0
- data/spec/aptible/cli/helpers/token_spec.rb +70 -0
- data/spec/aptible/cli/subcommands/db_spec.rb +553 -0
- data/spec/aptible/cli/subcommands/deploy_spec.rb +42 -7
- data/spec/aptible/cli/subcommands/external_aws_accounts_spec.rb +737 -0
- data/spec/aptible/cli/subcommands/organizations_spec.rb +90 -0
- data/spec/aptible/cli/subcommands/services_spec.rb +77 -0
- data/spec/fabricators/app_external_aws_rds_connection_fabricator.rb +55 -0
- data/spec/fabricators/external_aws_account_fabricator.rb +49 -0
- data/spec/fabricators/external_aws_database_credential_fabricator.rb +46 -0
- data/spec/fabricators/external_aws_resource_fabricator.rb +72 -0
- metadata +65 -7
|
@@ -10,7 +10,7 @@ module Aptible
|
|
|
10
10
|
include Helpers::App
|
|
11
11
|
include Helpers::Telemetry
|
|
12
12
|
|
|
13
|
-
desc 'ssh [COMMAND]', 'Run a command against an app'
|
|
13
|
+
desc 'ssh [--app APP] [COMMAND]', 'Run a command against an app'
|
|
14
14
|
long_desc <<-LONGDESC
|
|
15
15
|
Runs an interactive command against a remote Aptible app
|
|
16
16
|
|
data/lib/aptible/cli/version.rb
CHANGED
|
@@ -428,6 +428,76 @@ describe Aptible::CLI::Agent do
|
|
|
428
428
|
end
|
|
429
429
|
end
|
|
430
430
|
|
|
431
|
+
context 'failed_login_attempt_id handling' do
|
|
432
|
+
before do
|
|
433
|
+
allow(subject).to receive(:options)
|
|
434
|
+
.and_return(email: email, password: password)
|
|
435
|
+
expect(subject).to receive(:ask).with('2FA Token: ')
|
|
436
|
+
.once
|
|
437
|
+
.and_return(token)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
it 'should extract failed_login_attempt_id when present' do
|
|
441
|
+
e = make_oauth2_error(
|
|
442
|
+
'otp_token_required',
|
|
443
|
+
'failed_login_attempt_id' => 'attempt-123'
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
|
447
|
+
.with(email: email, password: password, expires_in: 1.week.seconds)
|
|
448
|
+
.once
|
|
449
|
+
.and_raise(e)
|
|
450
|
+
|
|
451
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
|
452
|
+
.with(email: email, password: password, otp_token: token,
|
|
453
|
+
previous_login_attempt_id: 'attempt-123',
|
|
454
|
+
expires_in: 12.hours.seconds)
|
|
455
|
+
.once
|
|
456
|
+
.and_return(token)
|
|
457
|
+
|
|
458
|
+
subject.login
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it 'should handle nil exception_context gracefully' do
|
|
462
|
+
parsed = { 'error' => 'otp_token_required' }
|
|
463
|
+
response = double('response',
|
|
464
|
+
parsed: parsed,
|
|
465
|
+
body: 'error otp_token_required')
|
|
466
|
+
allow(response).to receive(:error=)
|
|
467
|
+
e = OAuth2::Error.new(response)
|
|
468
|
+
|
|
469
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
|
470
|
+
.with(email: email, password: password, expires_in: 1.week.seconds)
|
|
471
|
+
.once
|
|
472
|
+
.and_raise(e)
|
|
473
|
+
|
|
474
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
|
475
|
+
.with(email: email, password: password, otp_token: token,
|
|
476
|
+
expires_in: 12.hours.seconds)
|
|
477
|
+
.once
|
|
478
|
+
.and_return(token)
|
|
479
|
+
|
|
480
|
+
subject.login
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
it 'should handle missing failed_login_attempt_id gracefully' do
|
|
484
|
+
e = make_oauth2_error('otp_token_required', {})
|
|
485
|
+
|
|
486
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
|
487
|
+
.with(email: email, password: password, expires_in: 1.week.seconds)
|
|
488
|
+
.once
|
|
489
|
+
.and_raise(e)
|
|
490
|
+
|
|
491
|
+
expect(Aptible::Auth::Token).to receive(:create)
|
|
492
|
+
.with(email: email, password: password, otp_token: token,
|
|
493
|
+
expires_in: 12.hours.seconds)
|
|
494
|
+
.once
|
|
495
|
+
.and_return(token)
|
|
496
|
+
|
|
497
|
+
subject.login
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
431
501
|
context 'SSO logins' do
|
|
432
502
|
let(:token) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpZCI6I' }
|
|
433
503
|
|
|
@@ -33,4 +33,122 @@ describe Aptible::CLI::Helpers::Database do
|
|
|
33
33
|
expect(subject.validate_image_type(pg.type)).to be(true)
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
describe '#derive_account_from_conns' do
|
|
38
|
+
let(:stack) { Fabricate(:stack) }
|
|
39
|
+
let(:account1) { Fabricate(:account, handle: 'account1', stack: stack) }
|
|
40
|
+
let(:account2) { Fabricate(:account, handle: 'account2', stack: stack) }
|
|
41
|
+
let(:app1) { Fabricate(:app, account: account1) }
|
|
42
|
+
let(:app2) { Fabricate(:app, account: account2) }
|
|
43
|
+
|
|
44
|
+
let(:raw_rds_resource) do
|
|
45
|
+
Fabricate(:external_aws_resource, resource_type: 'aws_rds_db_instance')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
let(:rds_db) do
|
|
49
|
+
Aptible::CLI::Helpers::Database::RdsDatabase.new(
|
|
50
|
+
'aws:rds::test-db',
|
|
51
|
+
raw_rds_resource.id,
|
|
52
|
+
raw_rds_resource.created_at,
|
|
53
|
+
raw_rds_resource
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
let(:conn1) do
|
|
58
|
+
double('connection1', present?: true, app: app1)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
let(:conn2) do
|
|
62
|
+
double('connection2', present?: true, app: app2)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
before do
|
|
66
|
+
allow(app1).to receive(:account).and_return(account1)
|
|
67
|
+
allow(app2).to receive(:account).and_return(account2)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
context 'when connections are empty' do
|
|
71
|
+
it 'returns nil' do
|
|
72
|
+
raw_rds_resource.instance_variable_set(
|
|
73
|
+
:@app_external_aws_rds_connections,
|
|
74
|
+
[]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
result = subject.derive_account_from_conns(rds_db)
|
|
78
|
+
expect(result).to be_nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
context 'when no preferred account is specified' do
|
|
83
|
+
it 'returns the account from the first connection' do
|
|
84
|
+
raw_rds_resource.instance_variable_set(
|
|
85
|
+
:@app_external_aws_rds_connections,
|
|
86
|
+
[conn1, conn2]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
result = subject.derive_account_from_conns(rds_db)
|
|
90
|
+
expect(result).to eq(account1)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'handles a single connection' do
|
|
94
|
+
raw_rds_resource.instance_variable_set(
|
|
95
|
+
:@app_external_aws_rds_connections,
|
|
96
|
+
[conn2]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
result = subject.derive_account_from_conns(rds_db)
|
|
100
|
+
expect(result).to eq(account2)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
context 'when a preferred account is specified' do
|
|
105
|
+
it 'returns the matching account when found' do
|
|
106
|
+
raw_rds_resource.instance_variable_set(
|
|
107
|
+
:@app_external_aws_rds_connections,
|
|
108
|
+
[conn1, conn2]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
result = subject.derive_account_from_conns(rds_db, account2)
|
|
112
|
+
expect(result).to eq(account2)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'returns nil when no matching connection is found' do
|
|
116
|
+
account3 = Fabricate(:account, handle: 'account3', stack: stack)
|
|
117
|
+
raw_rds_resource.instance_variable_set(
|
|
118
|
+
:@app_external_aws_rds_connections,
|
|
119
|
+
[conn1, conn2]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
result = subject.derive_account_from_conns(rds_db, account3)
|
|
123
|
+
expect(result).to be_nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'skips connections where conn.present? is false' do
|
|
127
|
+
conn_not_present = double('connection_not_present', present?: false)
|
|
128
|
+
raw_rds_resource.instance_variable_set(
|
|
129
|
+
:@app_external_aws_rds_connections,
|
|
130
|
+
[conn_not_present, conn2]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
result = subject.derive_account_from_conns(rds_db, account2)
|
|
134
|
+
expect(result).to eq(account2)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'returns the first matching account when multiple matches exist' do
|
|
138
|
+
app1_duplicate = Fabricate(:app, account: account1)
|
|
139
|
+
allow(app1_duplicate).to receive(:account).and_return(account1)
|
|
140
|
+
conn1_duplicate = double('connection1_dup',
|
|
141
|
+
present?: true,
|
|
142
|
+
app: app1_duplicate)
|
|
143
|
+
|
|
144
|
+
raw_rds_resource.instance_variable_set(
|
|
145
|
+
:@app_external_aws_rds_connections,
|
|
146
|
+
[conn1, conn1_duplicate]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
result = subject.derive_account_from_conns(rds_db, account1)
|
|
150
|
+
expect(result).to eq(account1)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
36
154
|
end
|
|
@@ -7,6 +7,10 @@ describe Aptible::CLI::Helpers::Token do
|
|
|
7
7
|
|
|
8
8
|
subject { Class.new.send(:include, described_class).new }
|
|
9
9
|
|
|
10
|
+
let(:token) { 'test-token' }
|
|
11
|
+
let(:user) { double('user', id: 'user-id', email: 'test@example.com') }
|
|
12
|
+
let(:auth_token) { double('auth_token', user: user) }
|
|
13
|
+
|
|
10
14
|
describe '#save_token / #fetch_token' do
|
|
11
15
|
it 'reads back a token it saved' do
|
|
12
16
|
subject.save_token('foo')
|
|
@@ -38,4 +42,70 @@ describe Aptible::CLI::Helpers::Token do
|
|
|
38
42
|
end
|
|
39
43
|
end
|
|
40
44
|
end
|
|
45
|
+
|
|
46
|
+
describe '#current_token' do
|
|
47
|
+
before do
|
|
48
|
+
subject.save_token(token)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'returns the current auth token' do
|
|
52
|
+
expect(Aptible::Auth::Token).to receive(:current_token)
|
|
53
|
+
.with(token: token)
|
|
54
|
+
.and_return(auth_token)
|
|
55
|
+
|
|
56
|
+
expect(subject.current_token).to eq(auth_token)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'raises Thor::Error on 401 unauthorized' do
|
|
60
|
+
response = Faraday::Response.new(status: 401)
|
|
61
|
+
error = HyperResource::ClientError.new(
|
|
62
|
+
'401 (invalid_token) Invalid Token', response: response
|
|
63
|
+
)
|
|
64
|
+
expect(Aptible::Auth::Token).to receive(:current_token)
|
|
65
|
+
.with(token: token)
|
|
66
|
+
.and_raise(error)
|
|
67
|
+
|
|
68
|
+
expect { subject.current_token }
|
|
69
|
+
.to raise_error(Thor::Error, /Invalid Token/)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'raises Thor::Error on 403 forbidden' do
|
|
73
|
+
response = Faraday::Response.new(status: 403)
|
|
74
|
+
error = HyperResource::ClientError.new('403 (forbidden) Access denied',
|
|
75
|
+
response: response)
|
|
76
|
+
expect(Aptible::Auth::Token).to receive(:current_token)
|
|
77
|
+
.with(token: token)
|
|
78
|
+
.and_raise(error)
|
|
79
|
+
|
|
80
|
+
expect { subject.current_token }
|
|
81
|
+
.to raise_error(Thor::Error, /Access denied/)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe '#whoami' do
|
|
86
|
+
before do
|
|
87
|
+
subject.save_token(token)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'returns the current user' do
|
|
91
|
+
expect(Aptible::Auth::Token).to receive(:current_token)
|
|
92
|
+
.with(token: token)
|
|
93
|
+
.and_return(auth_token)
|
|
94
|
+
|
|
95
|
+
expect(subject.whoami).to eq(user)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'raises Thor::Error on API error' do
|
|
99
|
+
response = Faraday::Response.new(status: 401)
|
|
100
|
+
error = HyperResource::ClientError.new(
|
|
101
|
+
'401 (invalid_token) Invalid Token', response: response
|
|
102
|
+
)
|
|
103
|
+
expect(Aptible::Auth::Token).to receive(:current_token)
|
|
104
|
+
.with(token: token)
|
|
105
|
+
.and_raise(error)
|
|
106
|
+
|
|
107
|
+
expect { subject.whoami }
|
|
108
|
+
.to raise_error(Thor::Error, /Invalid Token/)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
41
111
|
end
|