Skip to content

Commit a4a7bfe

Browse files
authored
feat: aptible organizations subcommand [SC-35706] (#403)
* feat: aptible organizations subcommand [SC-35706] * lint roller * bundle exec script/sync-readme-usage * use in progress version of aptible-auth * simplify `aptible organizations` with user.roles_with_organizations call 😻 * indent output for list of list * lint roller * more tests yay * whoami helper method, plus more testing! * lint roller * bundle update aptible-auth * aptible-auth 1.5.0 * oops rm empty file * tighten up some dep requirements
1 parent 40e5ec1 commit a4a7bfe

File tree

11 files changed

+290
-32
lines changed

11 files changed

+290
-32
lines changed

Gemfile.lock

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ GEM
4040
aptible-resource
4141
gem_config
4242
multipart-post (< 2.2.0)
43-
aptible-auth (1.4.0)
43+
aptible-auth (1.5.0)
4444
aptible-resource (~> 1.0)
4545
gem_config
4646
multipart-post (= 2.1.1)
@@ -49,7 +49,7 @@ GEM
4949
activesupport (>= 4.0, < 6.0)
5050
aptible-resource (~> 1.0)
5151
stripe (>= 1.13.0)
52-
aptible-resource (1.1.3)
52+
aptible-resource (1.1.4)
5353
activesupport
5454
fridge
5555
gem_config (~> 0.3.1)
@@ -86,7 +86,7 @@ GEM
8686
fabrication (2.15.2)
8787
faraday (0.17.6)
8888
multipart-post (>= 1.2, < 3)
89-
fridge (1.0.0)
89+
fridge (1.0.1)
9090
gem_config
9191
jwt (~> 2.3.0)
9292
gem_config (0.3.2)
@@ -104,7 +104,7 @@ GEM
104104
json (2.5.1)
105105
jwt (2.3.0)
106106
method_source (1.1.0)
107-
minitest (5.12.0)
107+
minitest (5.15.0)
108108
multi_xml (0.6.0)
109109
multipart-post (2.1.1)
110110
net-http-persistent (3.1.0)
@@ -184,7 +184,9 @@ DEPENDENCIES
184184
bundler (~> 1.3)
185185
climate_control (= 0.0.3)
186186
fabrication (~> 2.15.2)
187+
hashie (< 5.1)
187188
httplog (< 1.6)
189+
minitest (< 5.16)
188190
pry
189191
rack (~> 1.0)
190192
rake

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ Commands:
100100
aptible operation:cancel OPERATION_ID # Cancel a running operation
101101
aptible operation:follow OPERATION_ID # Follow logs of a running operation
102102
aptible operation:logs OPERATION_ID # View logs for given operation
103+
aptible organizations # List all organizations
103104
aptible rebuild # Rebuild an app, and restart its services
104105
aptible restart # Restart all services associated with an app
105106
aptible services # List Services for an App

aptible-cli.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,6 @@ Gem::Specification.new do |spec|
5555
spec.add_development_dependency 'climate_control', '= 0.0.3'
5656
spec.add_development_dependency 'fabrication', '~> 2.15.2'
5757
spec.add_development_dependency 'httplog', '< 1.6'
58+
spec.add_development_dependency 'minitest', '< 5.16'
59+
spec.add_development_dependency 'hashie', '< 5.1'
5860
end

lib/aptible/cli/agent.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
require_relative 'subcommands/maintenance'
4848
require_relative 'subcommands/backup_retention_policy'
4949
require_relative 'subcommands/aws_accounts'
50+
require_relative 'subcommands/organizations'
5051

5152
module Aptible
5253
module CLI
@@ -77,6 +78,7 @@ class Agent < Thor
7778
include Subcommands::Maintenance
7879
include Subcommands::BackupRetentionPolicy
7980
include Subcommands::AwsAccounts
81+
include Subcommands::Organizations
8082

8183
# Forward return codes on failures.
8284
def self.exit_on_failure?

lib/aptible/cli/helpers/token.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ def decode_token
5252
tok = fetch_token
5353
JWT.decode(tok, nil, false)
5454
end
55+
56+
# Instance of Aptible::Auth::Token from current token
57+
def current_token
58+
Aptible::Auth::Token.current_token(token: fetch_token)
59+
rescue HyperResource::ClientError => e
60+
raise Thor::Error, e.message
61+
end
62+
63+
# Instance of Aptible::Auth::User associated with current token
64+
def whoami
65+
current_token.user
66+
rescue HyperResource::ClientError => e
67+
raise Thor::Error, e.message
68+
end
5569
end
5670
end
5771
end

lib/aptible/cli/renderer/text.rb

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ def visit(node, io)
3232
# children are KeyedObject instances so they can render properly,
3333
# but we need to warn in tests that this is required.
3434
node.children.each_pair do |k, c|
35-
io.print "#{format_key(k)}: "
36-
visit(c, io)
35+
io.print "#{format_key(k)}:"
36+
if c.is_a?(Formatter::List)
37+
io.puts
38+
visit_indented(c, io, ' ')
39+
else
40+
io.print ' '
41+
visit(c, io)
42+
end
3743
end
3844
when Formatter::GroupedKeyedList
3945
enum = spacer_enumerator
@@ -65,6 +71,31 @@ def render(node)
6571

6672
private
6773

74+
def visit_indented(node, io, indent)
75+
return unless node.is_a?(Formatter::List)
76+
77+
node.children.each do |child|
78+
case child
79+
when Formatter::Object
80+
child.children.each_pair do |k, c|
81+
io.print "#{indent}#{format_key(k)}:"
82+
if c.is_a?(Formatter::List)
83+
io.puts
84+
visit_indented(c, io, indent + ' ')
85+
else
86+
io.print ' '
87+
visit(c, io)
88+
end
89+
end
90+
io.puts unless child == node.children.last
91+
when Formatter::Value
92+
io.puts "#{indent}#{child.value}"
93+
else
94+
visit(child, io)
95+
end
96+
end
97+
end
98+
6899
def output_list(nodes, io)
69100
if nodes.all? { |v| v.is_a?(Formatter::Value) }
70101
# All nodes are single values, so we render one per line.

lib/aptible/cli/subcommands/aws_accounts.rb

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -220,32 +220,24 @@ def aws_accounts
220220

221221
response = check_external_aws_account!(id)
222222

223-
if Renderer.format == 'json'
224-
Formatter.render(Renderer.current) do |root|
225-
root.object do |node|
226-
node.value('state', response.state)
227-
node.list('checks') do |check_list|
228-
response.checks.each do |check|
229-
check_list.object do |check_node|
230-
check_node.value('name', check.check_name)
231-
check_node.value('state', check.state)
232-
check_node.value('details', check.details) \
233-
unless check.details.nil?
234-
end
223+
fmt_state = lambda do |state|
224+
Renderer.format == 'json' ? state : format_check_state(state)
225+
end
226+
227+
Formatter.render(Renderer.current) do |root|
228+
root.object do |node|
229+
node.value('state', fmt_state.call(response.state))
230+
node.list('checks') do |check_list|
231+
response.checks.each do |check|
232+
check_list.object do |check_node|
233+
check_node.value('name', check.check_name)
234+
check_node.value('state', fmt_state.call(check.state))
235+
check_node.value('details', check.details) \
236+
unless check.details.nil?
235237
end
236238
end
237239
end
238240
end
239-
else
240-
puts "State: #{format_check_state(response.state)}"
241-
puts ''
242-
puts 'Checks:'
243-
response.checks.each do |check|
244-
puts " Name: #{check.check_name}"
245-
puts " State: #{format_check_state(check.state)}"
246-
puts " Details: #{check.details}" unless check.details.nil?
247-
puts ''
248-
end
249241
end
250242

251243
unless response.state == 'success'
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
module Aptible
4+
module CLI
5+
module Subcommands
6+
module Organizations
7+
def self.included(thor)
8+
thor.class_eval do
9+
include Helpers::Token
10+
include Helpers::Telemetry
11+
12+
desc 'organizations', 'List all organizations'
13+
def organizations
14+
telemetry(__method__, options)
15+
16+
user_orgs_and_roles = {}
17+
begin
18+
roles = whoami.roles_with_organizations
19+
rescue HyperResource::ClientError => e
20+
raise Thor::Error, e.message
21+
end
22+
roles.each do |role|
23+
user_orgs_and_roles[role.organization.id] ||= {
24+
'org' => role.organization,
25+
'roles' => []
26+
}
27+
user_orgs_and_roles[role.organization.id]['roles'] << role
28+
end
29+
Formatter.render(Renderer.current) do |root|
30+
root.list do |list|
31+
user_orgs_and_roles.each do |org_id, org_and_role|
32+
org = org_and_role['org']
33+
roles = org_and_role['roles']
34+
list.object do |node|
35+
node.value('id', org_id)
36+
node.value('name', org.name)
37+
node.list('roles') do |roles_list|
38+
roles.each do |role|
39+
roles_list.object do |role_node|
40+
role_node.value('id', role.id)
41+
role_node.value('name', role.name)
42+
end
43+
end
44+
end
45+
end
46+
end
47+
end
48+
end
49+
end
50+
end
51+
end
52+
end
53+
end
54+
end
55+
end

spec/aptible/cli/helpers/token_spec.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
subject { Class.new.send(:include, described_class).new }
99

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+
1014
describe '#save_token / #fetch_token' do
1115
it 'reads back a token it saved' do
1216
subject.save_token('foo')
@@ -38,4 +42,70 @@
3842
end
3943
end
4044
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
41111
end

spec/aptible/cli/subcommands/external_aws_accounts_spec.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -692,10 +692,9 @@
692692
.with('42', token: token).and_return(ext)
693693
expect(ext).to receive(:check!).and_return(check_result)
694694

695-
# check command uses puts directly (not Formatter) for non-JSON output
696-
expect { subject.send('aws_accounts:check', '42') }.to output(
697-
/State:.*success/m
698-
).to_stdout
695+
subject.send('aws_accounts:check', '42')
696+
697+
expect(captured_output_text).to match(/State:.*success/m)
699698
end
700699

701700
it 'raises error on check failure' do

0 commit comments

Comments
 (0)