Skip to content

Commit ba71d29

Browse files
authored
Merge pull request #10 from pmbenjamin/feat/support-non-ctrl-plane
Support non-control plane accounts (e.g. learning/testing accounts)
2 parents c5a83bb + ca9d1b8 commit ba71d29

File tree

7 files changed

+143
-87
lines changed

7 files changed

+143
-87
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Notes:
6464

6565
## Configure it
6666
- `restacker configure -l <location>`
67-
- Or copy the `restacker-sample.yml` to `~/.restacker/restacker.yml` & update the configurations
67+
- Or copy the `restacker-example.yml` to `~/.restacker/restacker.yml` & update the configurations
6868
The below configuration is an example of MyApp1 and MyApp2 as target accounts and CTRL as master.
6969

7070
```
@@ -79,9 +79,9 @@ $ cat ~/.restacker/restacker.yml
7979
:role_name: ctrl-ctrl-DeployAdmin
8080
:role_prefix: "/dso/ctrl/ctrl/"
8181
:bucket:
82-
:name: kaos-installers
83-
:prefix: cloudformation
84-
:ami_key: latest_amis
82+
:name: my-bucket
83+
:prefix: "s3/bucket/prefix/"
84+
:ami_key: ami_object_key
8585
8686
:ctrlAcct:
8787
:region: us-west-2

docs/02-RESTACKER_YML.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# RESTACKER.YML
22
This is the configuration file for Restacker CLI.
3-
See the sample [here](../source/restacker-sample.yml).
3+
See the sample [here](../source/restacker-example.yml).
44

55
## STRUCTURE
66
In order for Restacker to work as expected, the following key:value pairs are required:
@@ -35,9 +35,9 @@ In order for Restacker to work as expected, the following key:value pairs are re
3535
:role_name: ctrl-ctrl-DeployAdmin
3636
:role_prefix: "/dso/ctrl/ctrl/"
3737
:bucket:
38-
:name: kaos-installers
39-
:prefix: cloudformation
40-
:ami_key: latest_amis
38+
:name: my-bucket
39+
:prefix: "s3/bucket/prefix/"
40+
:ami_key: ami_object_key
4141
4242
:ctrlAcct:
4343
:region: us-west-2

source/bin/restacker

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,16 @@ end
111111
options, unparsed = Parser.parse(ARGV)
112112

113113
# set the username to $USER if not specified
114-
options[:username] = ENV['USER'] if options[:username].nil?
114+
options[:username] = RestackerConfig.find_user(options)
115115

116116
begin
117117
action = unparsed.pop
118118
puts(VERSION) || exit(0) if options[:version]
119119
usage("Please specify an ACTION") && exit(0) if action.nil?
120-
plane = RestackerConfig.get_plane(options)
120+
plane = RestackerConfig.find_plane(options)
121121

122122
if action == 'configure'
123-
printf "%-30s : %s\n", Rainbow("CONFIGURING PLANE").white.bright.underline, plane
123+
printf "%-s (%s)\n", Rainbow("CONFIGURING PLANE").white.bright.underline, plane
124124
RestackerConfig.configure(plane)
125125
exit(0)
126126
end
@@ -129,7 +129,7 @@ begin
129129
restacker = Restacker.new(options.to_h) unless ['dump', 'amis'].include?(action)
130130
case action
131131
when 'list'
132-
printf "%-30s : %s\n", Rainbow("LISTING STACKS").white.bright.underline, plane
132+
printf "%-s (%s)\n", Rainbow("LISTING STACKS").white.bright.underline, plane
133133
restacker.list_stacks
134134
when 'desc', 'describe'
135135
if options[:name]
@@ -140,7 +140,7 @@ begin
140140
end
141141
when 'restack'
142142
if options[:name]
143-
printf "%-30s : %s\n", Rainbow("RESTACKING").white.bright.underline, options[:name]
143+
printf "%-s (%s)\n", Rainbow("RESTACKING").white.bright.underline, options[:name]
144144
restacker.restack_by_name(options[:name])
145145
puts Rainbow("Now run migrate followed by remove").white.bright
146146
else
@@ -162,7 +162,7 @@ begin
162162
end
163163
when 'remove'
164164
if options[:name]
165-
printf "%-30s : %s\n", Rainbow("REMOVING STACK").white.bright.underline, options[:name]
165+
printf "%-s (%s)\n", Rainbow("REMOVING STACK").white.bright.underline, options[:name]
166166
restacker.delete_stack(options[:name])
167167
else
168168
usage "Please specify a stack name (-n) to remove"
@@ -182,7 +182,7 @@ begin
182182
AwsCli.new(options).cmd(options[:params], options[:debug])
183183
end
184184
else
185-
usage "Unknown ACTION: #{action}"
185+
usage Rainbow("Unknown ACTION: #{action}").red
186186
end
187187
rescue => e
188188
puts options[:debug]

source/lib/auth.rb

Lines changed: 87 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,114 @@
11
require 'yaml'
2+
require_relative 'restacker_config'
23

34
CREDS_FILE="#{CONFIG_DIR}/auth"
45

56
class Auth
67

8+
# TODO use keychain to save creds
9+
def self.login(options, config, location)
10+
auth_file = "#{CREDS_FILE}.#{location}"
11+
region = config.fetch(:region)
12+
profile_name = RestackerConfig.find_profile(options)
13+
username = RestackerConfig.find_user(options)
14+
15+
# if no ctrl plane specified, authenticate directly
16+
return target_plane_auth(region, profile_name) if config[:ctrl].nil?
17+
18+
if File.exists?(auth_file)
19+
session = YAML.load_file(auth_file)
20+
if session && valid_session?(region, session)
21+
create_auth_file(auth_file, session)
22+
return cloudformation_client(region, session)
23+
else # if session expired
24+
session = get_auth_session(profile_name, username, config)
25+
create_auth_file(auth_file, session)
26+
return cloudformation_client(region, session)
27+
end
28+
else # if file does not exist
29+
session = get_auth_session(profile_name, username, config)
30+
create_auth_file(auth_file, session)
31+
return cloudformation_client(region, session)
32+
end
33+
34+
end
35+
36+
private
37+
738
def self.get_mfa_code
839
print Rainbow("Enter MFA: ").yellow
940
STDOUT.flush
1041
STDIN.gets(7).chomp
1142
end
1243

13-
def self.get_creds(username, defaults)
14-
region = defaults.fetch(:region)
15-
ctrl = defaults.fetch(:ctrl)
16-
ctrl_account_number = ctrl.fetch(:account_number)
17-
ctrl_role_prefix = ctrl.fetch(:role_prefix)
18-
ctrl_role_name = ctrl.fetch(:role_name)
19-
20-
target = defaults.fetch(:target)
21-
target_account_number = target.fetch(:account_number)
22-
target_role_prefix = target.fetch(:role_prefix)
23-
target_role_name = target.fetch(:role_name)
24-
target_label = target.fetch(:label)
25-
serial_number = "arn:aws:iam::#{ctrl_account_number}:mfa/#{username}"
26-
puts "Logging into #{Rainbow(target_label.upcase).yellow} using MFA: #{serial_number} (#{region})"
27-
role_arn = "arn:aws:iam::#{ctrl_account_number}:role#{ctrl_role_prefix}#{ctrl_role_name}"
44+
def self.get_creds(username, config)
45+
region = config.fetch(:region)
46+
target = RestackerConfig.target_config(config) # target account will always exist in restacker.yml
47+
48+
if config[:ctrl].nil?
49+
target_plane_auth(target)
50+
else
51+
ctrl = RestackerConfig.ctrl_config(config)
52+
control_plane_auth(ctrl, target, username, region)
53+
end
54+
end
55+
56+
def self.control_plane_auth(ctrl, target, username, region)
57+
serial_number = "arn:aws:iam::#{ctrl[:account_number]}:mfa/#{username}"
58+
puts "Logging into #{Rainbow(target[:label].upcase).yellow} using MFA: #{serial_number} (#{region})"
59+
role_arn = "arn:aws:iam::#{ctrl[:account_number]}:role#{ctrl[:role_prefix]}#{ctrl[:role_name]}"
2860
session_name = username[0..31]
2961

3062
sts_client = Aws::STS::Client.new(region: region)
31-
sts_role = sts_client.assume_role(role_arn: role_arn, role_session_name: session_name, serial_number: serial_number, token_code: get_mfa_code)
63+
sts_role = sts_client.assume_role(role_arn: role_arn,
64+
role_session_name: session_name,
65+
serial_number: serial_number,
66+
token_code: get_mfa_code)
3267
creds = sts_role[:credentials]
33-
creds_obj = Aws::Credentials.new(creds.access_key_id, creds.secret_access_key, creds.session_token)
68+
creds_obj = Aws::Credentials.new( creds.access_key_id,
69+
creds.secret_access_key,
70+
creds.session_token )
3471

35-
role_arn = "arn:aws:iam::#{target_account_number}:role#{target_role_prefix}#{target_role_name}"
72+
role_arn = "arn:aws:iam::#{target[:account_number]}:role#{target[:role_prefix]}#{target[:role_name]}"
3673
session_name = username[0..31]
3774
sts_client = Aws::STS::Client.new(region: region, credentials: creds_obj)
38-
sts_role = sts_client.assume_role(role_arn: role_arn, role_session_name: session_name)
75+
sts_role = sts_client.assume_role(role_arn: role_arn,
76+
role_session_name: session_name)
3977
creds = sts_role[:credentials]
40-
Aws::Credentials.new(creds.access_key_id, creds.secret_access_key, creds.session_token)
78+
Aws::Credentials.new( creds.access_key_id,
79+
creds.secret_access_key,
80+
creds.session_token)
4181
end
4282

43-
# TODO use keychain to save creds
44-
def self.login(options, defaults, plane)
45-
auth_file = "#{CREDS_FILE}.#{plane}"
83+
def self.target_plane_auth(region, profile_name)
84+
Aws.config[:credentials] = Aws::SharedCredentials.new(profile_name: profile_name)
85+
return Aws::CloudFormation::Client.new(region: region), Aws.config[:credentials].credentials
86+
end
87+
88+
def self.valid_session?(region, creds)
4689
begin
47-
creds = YAML.load(File.read(auth_file))
48-
cf = Aws::CloudFormation::Client.new(region: defaults[:region], credentials: creds)
49-
cf.list_stacks # testing that creds are still good
50-
rescue => e
51-
begin
52-
profile_name = options[:profile]
53-
Aws.config[:credentials] = Aws::SharedCredentials.new(profile_name: profile_name)
54-
creds = get_creds(options.fetch(:username), defaults)
55-
rescue KeyError => e
56-
error = Rainbow("Error parsing #{CONFIG_FILE}, (#{e.message}), please ensure it is properly formatted").red
57-
raise error
58-
rescue => err
59-
error = Rainbow(err.message).red
60-
raise error
61-
exit
62-
end
63-
# now save to yaml
64-
File.open(auth_file, 'w') do |f|
65-
f.write YAML.dump(creds)
66-
end
90+
Aws::CloudFormation::Client.new(region: region, credentials: creds).list_stacks
91+
return true
92+
rescue Aws::CloudFormation::Errors::ExpiredToken => expired
93+
puts expired.message
94+
return false
95+
end
96+
end
97+
98+
def self.get_auth_session(profile_name, username, config)
99+
Aws.config[:credentials] = Aws::SharedCredentials.new(profile_name: profile_name)
100+
101+
get_creds(username, config)
102+
end
103+
104+
def self.cloudformation_client(region, session)
105+
cf = Aws::CloudFormation::Client.new(region: region, credentials: session)
106+
return cf, session
107+
end
67108

68-
cf = Aws::CloudFormation::Client.new(region: defaults[:region], credentials: creds)
109+
def self.create_auth_file(file_name, session)
110+
File.open(file_name, 'w') do |f|
111+
f.write YAML.dump(session)
69112
end
70-
return cf, creds
71113
end
72114
end

source/lib/base_stacker.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
VERSION = '1.0.0'
77
CONFIG_DIR="#{ENV['HOME']}/.restacker"
88
CONFIG_FILE="#{CONFIG_DIR}/restacker.yml"
9-
SAMPLE_FILE = "#{__dir__}/../restacker-sample.yml"
9+
SAMPLE_FILE = "#{__dir__}/../restacker-example.yml"
1010

1111
# needed here (after config_dir and defaults_file)
1212
require_relative 'auth'
@@ -27,9 +27,8 @@
2727

2828
class BaseStacker
2929
def initialize(options)
30-
location = RestackerConfig.get_plane(options)
30+
location = RestackerConfig.find_plane(options)
3131
config = RestackerConfig.load_config(location)
32-
3332
# use default region if not passed in from cli
3433
config[:region] = options[:region] if options[:region]
3534
options[:region] = config[:region] unless options[:region]

source/lib/restacker_config.rb

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
class RestackerConfig
22
def self.load_config(plane)
3-
plane = get_plane if plane.nil?
3+
plane = find_plane if plane.nil?
44
config = find_config
55
if config[plane].nil?
66
puts "Plane not found (#{plane}). Please see #{CONFIG_FILE}."
@@ -9,15 +9,6 @@ def self.load_config(plane)
99
config[plane]
1010
end
1111

12-
def self.get_plane(options)
13-
if options[:location]
14-
plane = options[:location]
15-
else
16-
plane = find_default_plane()
17-
end
18-
plane.to_sym
19-
end
20-
2112
def self.configure(location)
2213
config = find_config()
2314
puts Rainbow("Configuration file location:").white.bright + " #{CONFIG_FILE}"
@@ -60,21 +51,49 @@ def self.configure(location)
6051

6152
def self.latest_amis(rhel=nil)
6253
latest_amis = YAML.load(get_object(find_config[:ctrl][:bucket][:ami_key]))
63-
if rhel
64-
return latest_amis[rhel]
65-
end
66-
latest_amis
54+
return latest_amis[rhel] || latest_amis
55+
end
56+
57+
def self.target_config(config)
58+
target_config = config.fetch(:target)
59+
target = {}
60+
target[:label] = target_config.fetch(:account_number)
61+
target[:account_number] = target_config.fetch(:account_number)
62+
target[:role_prefix] = target_config.fetch(:role_prefix, nil)
63+
target[:role_name] = target_config.fetch(:role_name, nil)
64+
target
65+
end
66+
67+
def self.ctrl_config(config)
68+
ctrl_config = config.fetch(:ctrl)
69+
ctrl = {}
70+
ctrl[:account_number] = ctrl_config.fetch(:account_number)
71+
ctrl[:role_prefix] = ctrl_config.fetch(:role_prefix)
72+
ctrl[:role_name] = ctrl_config.fetch(:role_name)
73+
ctrl
74+
end
75+
76+
def self.find_profile(options)
77+
plane = find_plane(options)
78+
options[:profile] || find_config[plane][:profile] || find_config[:default][:profile]
79+
end
80+
81+
def self.find_user(options)
82+
plane = find_plane(options)
83+
options[:username] || find_config[plane].fetch(:username, nil) || ENV['USER']
84+
end
85+
86+
def self.find_plane(options)
87+
(options[:location] || find_config[:default][:label]).to_sym || raise(Rainbow("Location was not provided and no default location was found in #{CONFIG_FILE}.").red)
6788
end
6889

69-
private
7090
def self.find_config
7191
Dir.mkdir(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
7292
begin
7393
if File.exist?(CONFIG_FILE)
7494
config = YAML.load_file(CONFIG_FILE)
7595
else
76-
config = YAML.load_file(SAMPLE_FILE)
77-
File.open(CONFIG_FILE, 'w') { |f| f.write config.to_yaml }
96+
File.open(CONFIG_FILE, 'w') { |f| f.write SAMPLE_FILE.to_yaml }
7897
end
7998
rescue Psych::SyntaxError
8099
raise "Improperly formatted YAML file: #{CONFIG_FILE}."
@@ -84,10 +103,6 @@ def self.find_config
84103
config
85104
end
86105

87-
def self.find_default_plane
88-
find_config[:default][:label] || raise(Rainbow("Location was not provided and no default location was found in #{CONFIG_FILE}.").red)
89-
end
90-
91106
def self.bucket
92107
find_config[:ctrl][:bucket][:name]
93108
end

source/restacker-sample.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
:role_name: ctrl-ctrl-DeployAdmin
88
:role_prefix: "/dso/ctrl/ctrl/"
99
:bucket:
10-
:name: kaos-installers
11-
:prefix: cloudformation
12-
:ami_key: latest_amis
10+
:name: my-bucket
11+
:prefix: "s3/bucket/prefix/"
12+
:ami_key: ami_object_key
1313

1414
:ctrlAcct:
1515
:region: us-west-2

0 commit comments

Comments
 (0)