Skip to content
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Notes:

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

```
Expand All @@ -79,9 +79,9 @@ $ cat ~/.restacker/restacker.yml
:role_name: ctrl-ctrl-DeployAdmin
:role_prefix: "/dso/ctrl/ctrl/"
:bucket:
:name: kaos-installers
:prefix: cloudformation
:ami_key: latest_amis
:name: my-bucket
:prefix: "s3/bucket/prefix/"
:ami_key: ami_object_key

:ctrlAcct:
:region: us-west-2
Expand Down
8 changes: 4 additions & 4 deletions docs/02-RESTACKER_YML.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# RESTACKER.YML
This is the configuration file for Restacker CLI.
See the sample [here](../source/restacker-sample.yml).
See the sample [here](../source/restacker-example.yml).

## STRUCTURE
In order for Restacker to work as expected, the following key:value pairs are required:
Expand Down Expand Up @@ -35,9 +35,9 @@ In order for Restacker to work as expected, the following key:value pairs are re
:role_name: ctrl-ctrl-DeployAdmin
:role_prefix: "/dso/ctrl/ctrl/"
:bucket:
:name: kaos-installers
:prefix: cloudformation
:ami_key: latest_amis
:name: my-bucket
:prefix: "s3/bucket/prefix/"
:ami_key: ami_object_key

:ctrlAcct:
:region: us-west-2
Expand Down
14 changes: 7 additions & 7 deletions source/bin/restacker
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,16 @@ end
options, unparsed = Parser.parse(ARGV)

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

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

if action == 'configure'
printf "%-30s : %s\n", Rainbow("CONFIGURING PLANE").white.bright.underline, plane
printf "%-s (%s)\n", Rainbow("CONFIGURING PLANE").white.bright.underline, plane
RestackerConfig.configure(plane)
exit(0)
end
Expand All @@ -129,7 +129,7 @@ begin
restacker = Restacker.new(options.to_h) unless ['dump', 'amis'].include?(action)
case action
when 'list'
printf "%-30s : %s\n", Rainbow("LISTING STACKS").white.bright.underline, plane
printf "%-s (%s)\n", Rainbow("LISTING STACKS").white.bright.underline, plane
restacker.list_stacks
when 'desc', 'describe'
if options[:name]
Expand All @@ -140,7 +140,7 @@ begin
end
when 'restack'
if options[:name]
printf "%-30s : %s\n", Rainbow("RESTACKING").white.bright.underline, options[:name]
printf "%-s (%s)\n", Rainbow("RESTACKING").white.bright.underline, options[:name]
restacker.restack_by_name(options[:name])
puts Rainbow("Now run migrate followed by remove").white.bright
else
Expand All @@ -162,7 +162,7 @@ begin
end
when 'remove'
if options[:name]
printf "%-30s : %s\n", Rainbow("REMOVING STACK").white.bright.underline, options[:name]
printf "%-s (%s)\n", Rainbow("REMOVING STACK").white.bright.underline, options[:name]
restacker.delete_stack(options[:name])
else
usage "Please specify a stack name (-n) to remove"
Expand All @@ -182,7 +182,7 @@ begin
AwsCli.new(options).cmd(options[:params], options[:debug])
end
else
usage "Unknown ACTION: #{action}"
usage Rainbow("Unknown ACTION: #{action}").red
end
rescue => e
puts options[:debug]
Expand Down
132 changes: 87 additions & 45 deletions source/lib/auth.rb
Original file line number Diff line number Diff line change
@@ -1,72 +1,114 @@
require 'yaml'
require_relative 'restacker_config'

CREDS_FILE="#{CONFIG_DIR}/auth"

class Auth

# TODO use keychain to save creds
def self.login(options, config, location)
auth_file = "#{CREDS_FILE}.#{location}"
region = config.fetch(:region)
profile_name = RestackerConfig.find_profile(options)
username = RestackerConfig.find_user(options)

# if no ctrl plane specified, authenticate directly
return target_plane_auth(region, profile_name) if config[:ctrl].nil?

if File.exists?(auth_file)
session = YAML.load_file(auth_file)
if session && valid_session?(region, session)
create_auth_file(auth_file, session)
return cloudformation_client(region, session)
else # if session expired
session = get_auth_session(profile_name, username, config)
create_auth_file(auth_file, session)
return cloudformation_client(region, session)
end
else # if file does not exist
session = get_auth_session(profile_name, username, config)
create_auth_file(auth_file, session)
return cloudformation_client(region, session)
end

end

private

def self.get_mfa_code
print Rainbow("Enter MFA: ").yellow
STDOUT.flush
STDIN.gets(7).chomp
end

def self.get_creds(username, defaults)
region = defaults.fetch(:region)
ctrl = defaults.fetch(:ctrl)
ctrl_account_number = ctrl.fetch(:account_number)
ctrl_role_prefix = ctrl.fetch(:role_prefix)
ctrl_role_name = ctrl.fetch(:role_name)

target = defaults.fetch(:target)
target_account_number = target.fetch(:account_number)
target_role_prefix = target.fetch(:role_prefix)
target_role_name = target.fetch(:role_name)
target_label = target.fetch(:label)
serial_number = "arn:aws:iam::#{ctrl_account_number}:mfa/#{username}"
puts "Logging into #{Rainbow(target_label.upcase).yellow} using MFA: #{serial_number} (#{region})"
role_arn = "arn:aws:iam::#{ctrl_account_number}:role#{ctrl_role_prefix}#{ctrl_role_name}"
def self.get_creds(username, config)
region = config.fetch(:region)
target = RestackerConfig.target_config(config) # target account will always exist in restacker.yml

if config[:ctrl].nil?
target_plane_auth(target)
else
ctrl = RestackerConfig.ctrl_config(config)
control_plane_auth(ctrl, target, username, region)
end
end

def self.control_plane_auth(ctrl, target, username, region)
serial_number = "arn:aws:iam::#{ctrl[:account_number]}:mfa/#{username}"
puts "Logging into #{Rainbow(target[:label].upcase).yellow} using MFA: #{serial_number} (#{region})"
role_arn = "arn:aws:iam::#{ctrl[:account_number]}:role#{ctrl[:role_prefix]}#{ctrl[:role_name]}"
session_name = username[0..31]

sts_client = Aws::STS::Client.new(region: region)
sts_role = sts_client.assume_role(role_arn: role_arn, role_session_name: session_name, serial_number: serial_number, token_code: get_mfa_code)
sts_role = sts_client.assume_role(role_arn: role_arn,
role_session_name: session_name,
serial_number: serial_number,
token_code: get_mfa_code)
creds = sts_role[:credentials]
creds_obj = Aws::Credentials.new(creds.access_key_id, creds.secret_access_key, creds.session_token)
creds_obj = Aws::Credentials.new( creds.access_key_id,
creds.secret_access_key,
creds.session_token )

role_arn = "arn:aws:iam::#{target_account_number}:role#{target_role_prefix}#{target_role_name}"
role_arn = "arn:aws:iam::#{target[:account_number]}:role#{target[:role_prefix]}#{target[:role_name]}"
session_name = username[0..31]
sts_client = Aws::STS::Client.new(region: region, credentials: creds_obj)
sts_role = sts_client.assume_role(role_arn: role_arn, role_session_name: session_name)
sts_role = sts_client.assume_role(role_arn: role_arn,
role_session_name: session_name)
creds = sts_role[:credentials]
Aws::Credentials.new(creds.access_key_id, creds.secret_access_key, creds.session_token)
Aws::Credentials.new( creds.access_key_id,
creds.secret_access_key,
creds.session_token)
end

# TODO use keychain to save creds
def self.login(options, defaults, plane)
auth_file = "#{CREDS_FILE}.#{plane}"
def self.target_plane_auth(region, profile_name)
Aws.config[:credentials] = Aws::SharedCredentials.new(profile_name: profile_name)
return Aws::CloudFormation::Client.new(region: region), Aws.config[:credentials].credentials
end

def self.valid_session?(region, creds)
begin
creds = YAML.load(File.read(auth_file))
cf = Aws::CloudFormation::Client.new(region: defaults[:region], credentials: creds)
cf.list_stacks # testing that creds are still good
rescue => e
begin
profile_name = options[:profile]
Aws.config[:credentials] = Aws::SharedCredentials.new(profile_name: profile_name)
creds = get_creds(options.fetch(:username), defaults)
rescue KeyError => e
error = Rainbow("Error parsing #{CONFIG_FILE}, (#{e.message}), please ensure it is properly formatted").red
raise error
rescue => err
error = Rainbow(err.message).red
raise error
exit
end
# now save to yaml
File.open(auth_file, 'w') do |f|
f.write YAML.dump(creds)
end
Aws::CloudFormation::Client.new(region: region, credentials: creds).list_stacks
return true
rescue Aws::CloudFormation::Errors::ExpiredToken => expired
puts expired.message
return false
end
end

def self.get_auth_session(profile_name, username, config)
Aws.config[:credentials] = Aws::SharedCredentials.new(profile_name: profile_name)

get_creds(username, config)
end

def self.cloudformation_client(region, session)
cf = Aws::CloudFormation::Client.new(region: region, credentials: session)
return cf, session
end

cf = Aws::CloudFormation::Client.new(region: defaults[:region], credentials: creds)
def self.create_auth_file(file_name, session)
File.open(file_name, 'w') do |f|
f.write YAML.dump(session)
end
return cf, creds
end
end
5 changes: 2 additions & 3 deletions source/lib/base_stacker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
VERSION = '1.0.0'
CONFIG_DIR="#{ENV['HOME']}/.restacker"
CONFIG_FILE="#{CONFIG_DIR}/restacker.yml"
SAMPLE_FILE = "#{__dir__}/../restacker-sample.yml"
SAMPLE_FILE = "#{__dir__}/../restacker-example.yml"

# needed here (after config_dir and defaults_file)
require_relative 'auth'
Expand All @@ -27,9 +27,8 @@

class BaseStacker
def initialize(options)
location = RestackerConfig.get_plane(options)
location = RestackerConfig.find_plane(options)
config = RestackerConfig.load_config(location)

# use default region if not passed in from cli
config[:region] = options[:region] if options[:region]
options[:region] = config[:region] unless options[:region]
Expand Down
57 changes: 36 additions & 21 deletions source/lib/restacker_config.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class RestackerConfig
def self.load_config(plane)
plane = get_plane if plane.nil?
plane = find_plane if plane.nil?
config = find_config
if config[plane].nil?
puts "Plane not found (#{plane}). Please see #{CONFIG_FILE}."
Expand All @@ -9,15 +9,6 @@ def self.load_config(plane)
config[plane]
end

def self.get_plane(options)
if options[:location]
plane = options[:location]
else
plane = find_default_plane()
end
plane.to_sym
end

def self.configure(location)
config = find_config()
puts Rainbow("Configuration file location:").white.bright + " #{CONFIG_FILE}"
Expand Down Expand Up @@ -60,21 +51,49 @@ def self.configure(location)

def self.latest_amis(rhel=nil)
latest_amis = YAML.load(get_object(find_config[:ctrl][:bucket][:ami_key]))
if rhel
return latest_amis[rhel]
end
latest_amis
return latest_amis[rhel] || latest_amis
end

def self.target_config(config)
target_config = config.fetch(:target)
target = {}
target[:label] = target_config.fetch(:account_number)
target[:account_number] = target_config.fetch(:account_number)
target[:role_prefix] = target_config.fetch(:role_prefix, nil)
target[:role_name] = target_config.fetch(:role_name, nil)
target
end

def self.ctrl_config(config)
ctrl_config = config.fetch(:ctrl)
ctrl = {}
ctrl[:account_number] = ctrl_config.fetch(:account_number)
ctrl[:role_prefix] = ctrl_config.fetch(:role_prefix)
ctrl[:role_name] = ctrl_config.fetch(:role_name)
ctrl
end

def self.find_profile(options)
plane = find_plane(options)
options[:profile] || find_config[plane][:profile] || find_config[:default][:profile]
end

def self.find_user(options)
plane = find_plane(options)
options[:username] || find_config[plane].fetch(:username, nil) || ENV['USER']
end

def self.find_plane(options)
(options[:location] || find_config[:default][:label]).to_sym || raise(Rainbow("Location was not provided and no default location was found in #{CONFIG_FILE}.").red)
end

private
def self.find_config
Dir.mkdir(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
begin
if File.exist?(CONFIG_FILE)
config = YAML.load_file(CONFIG_FILE)
else
config = YAML.load_file(SAMPLE_FILE)
File.open(CONFIG_FILE, 'w') { |f| f.write config.to_yaml }
File.open(CONFIG_FILE, 'w') { |f| f.write SAMPLE_FILE.to_yaml }
end
rescue Psych::SyntaxError
raise "Improperly formatted YAML file: #{CONFIG_FILE}."
Expand All @@ -84,10 +103,6 @@ def self.find_config
config
end

def self.find_default_plane
find_config[:default][:label] || raise(Rainbow("Location was not provided and no default location was found in #{CONFIG_FILE}.").red)
end

def self.bucket
find_config[:ctrl][:bucket][:name]
end
Expand Down
6 changes: 3 additions & 3 deletions source/restacker-sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
:role_name: ctrl-ctrl-DeployAdmin
:role_prefix: "/dso/ctrl/ctrl/"
:bucket:
:name: kaos-installers
:prefix: cloudformation
:ami_key: latest_amis
:name: my-bucket
:prefix: "s3/bucket/prefix/"
:ami_key: ami_object_key

:ctrlAcct:
:region: us-west-2
Expand Down