Skip to content
This repository has been archived by the owner on Jan 19, 2022. It is now read-only.

Commit

Permalink
Merge pull request #74 from ministryofjustice/feature/extend-template
Browse files Browse the repository at this point in the history
Cross account IAM deployment
  • Loading branch information
ashb committed Apr 14, 2015
2 parents 28934a0 + 21d6a8e commit 3e1f2eb
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 20 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Unreleased

* Add include functionality for including extra cloudformation json files.
* Add ability to use cross account IAM roles when authenticating to AWS.

## Version 0.2.1

* Fix rsync missing passwords yaml from salt-pillar
Expand Down
17 changes: 17 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ This tool needs AWS credentials to create stacks and the credentials should be p
aws_access_key_id = AKIAI***********
aws_secret_access_key = *******************************************

If you wish to authenticate to a separate AWS account using cross account IAM roles you should create a profile called `cross-account` with the access keys of the user with permission to assume roles from the second account::

[cross-account]
aws_access_key_id = AKIAI***********
aws_secret_access_key = *******************************************

And when you run the tool you must set the ARN ID of the role in the separate account which you wish to assume. For example::

AWS_ROLE_ARN_ID='arn:aws:iam::123456789012:role/S3Access' fab application:courtfinder aws:prod environment:dev config:/path/to/courtfinder-dev.yaml cfn_create

Project specific YAML file
+++++++++++++++++++++++++++
Expand Down Expand Up @@ -120,3 +129,11 @@ You can add a custom s3 policy to override bootstrap-cfn's default settings. For
static-bucket-name: moj-test-dev-static
policy: tests/sample-custom-s3-policy.json

Includes
++++++++++
If you wish to include some static cloudformation json and have it merged with the template generated by bootstrap-cfn. You can do the following in your template yaml file::

includes:
- /path/to/cloudformation.json

The tool will then perform a deep merge of the includes with the generated template dictionary. Any keys or subkeys in the template dictionary that clash will have their values **overwritten** by the included dictionary or recursively merged if the value is itself a dictionary.
26 changes: 7 additions & 19 deletions bootstrap_cfn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import sys
import yaml
import bootstrap_cfn.errors as errors
import bootstrap_cfn.utils as utils

from copy import deepcopy

class ProjectConfig:
Expand All @@ -14,31 +16,13 @@ def __init__(self, config, environment, passwords=None):
self.config = self.load_yaml(config)[environment]
if passwords:
passwords_dict = self.load_yaml(passwords)[environment]
self.config = self.dict_merge(self.config, passwords_dict)
self.config = utils.dict_merge(self.config, passwords_dict)

@staticmethod
def load_yaml(fp):
if os.path.exists(fp):
return yaml.load(open(fp).read())

def dict_merge(self, target, *args):
# Merge multiple dicts
if len(args) > 1:
for obj in args:
self.dict_merge(target, obj)
return target

# Recursively merge dicts and set non-dict values
obj = args[0]
if not isinstance(obj, dict):
return obj
for k, v in obj.iteritems():
if k in target and isinstance(target[k], dict):
self.dict_merge(target[k], v)
else:
target[k] = deepcopy(v)
return target


class ConfigParser:

Expand Down Expand Up @@ -92,6 +76,10 @@ def process(self):
template['Outputs'] = {}
for t in output_templates:
template['Outputs'].update(json.loads(pkgutil.get_data('bootstrap_cfn', t)))
if 'includes' in self.data:
for inc_path in self.data['includes']:
inc = json.load(open(inc_path))
template = utils.dict_merge(template, inc)
return json.dumps(
template, sort_keys=True, indent=4, separators=(',', ': '))

Expand Down
7 changes: 6 additions & 1 deletion bootstrap_cfn/salt_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
#!/usr/bin/env python
import sys
import os
#We adjust the path here so that this script can be run
#by a user without changing their PYTHONPATH, useful if they
#have limited sudo access to run the script.
sys.path.append(os.path.dirname(__file__) + '/..')
from bootstrap_cfn import utils
from bootstrap_cfn import errors
import salt
import salt.runner
import salt.client
import pprint
import time
import sys

def start_highstate(target):
local = salt.client.LocalClient()
Expand Down
37 changes: 37 additions & 0 deletions bootstrap_cfn/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import boto.exception
import boto.provider
import boto.sts
import sys
import time
import os

import bootstrap_cfn.errors as errors
from copy import deepcopy


def timeout(timeout, interval):
Expand All @@ -24,6 +27,22 @@ def wrapper(*args, **kwargs):

def connect_to_aws(module, instance):
try:
if instance.aws_profile_name == 'cross-account':
sts = boto.sts.connect_to_region(
region_name=instance.aws_region_name,
profile_name=instance.aws_profile_name
)
role = sts.assume_role(
role_arn=os.environ['AWS_ROLE_ARN_ID'],
role_session_name="AssumeRoleSession1"
)
conn = module.connect_to_region(
region_name=instance.aws_region_name,
aws_access_key_id=role.credentials.access_key,
aws_secret_access_key=role.credentials.secret_key,
security_token=role.credentials.session_token
)
return conn
conn = module.connect_to_region(
region_name=instance.aws_region_name,
profile_name=instance.aws_profile_name
Expand All @@ -33,3 +52,21 @@ def connect_to_aws(module, instance):
raise errors.NoCredentialsError()
except boto.provider.ProfileNotFoundError as e:
raise errors.ProfileNotFoundError(instance.aws_profile_name)

def dict_merge(target, *args):
# Merge multiple dicts
if len(args) > 1:
for obj in args:
dict_merge(target, obj)
return target

# Recursively merge dicts and set non-dict values
obj = args[0]
if not isinstance(obj, dict):
return obj
for k, v in obj.iteritems():
if k in target and isinstance(target[k], dict):
dict_merge(target[k], v)
else:
target[k] = deepcopy(v)
return target
9 changes: 9 additions & 0 deletions tests/sample-include.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Outputs":
{
"someoutput": {
"Description": "For tests",
"Value": "BLAHBLAH"
}
}
}
25 changes: 25 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from bootstrap_cfn.config import ProjectConfig, ConfigParser
import bootstrap_cfn.errors as errors
from testfixtures import compare
import json


class TestConfig(unittest.TestCase):
Expand Down Expand Up @@ -339,6 +340,30 @@ def test_elb_custom_sg(self):
compare(elb['Properties']['SecurityGroups'],
[{u'Ref': u'SGName'}])

def test_cf_includes(self):
project_config = ProjectConfig('tests/sample-project.yaml',
'dev',
'tests/sample-project-passwords.yaml')
project_config.config['includes'] = ['tests/sample-include.json']
known_outputs = {
"dbhost": {
"Description": "RDS Hostname",
"Value": {"Fn::GetAtt" : [ "RDSInstance" , "Endpoint.Address" ]}
},
"dbport": {
"Description": "RDS Port",
"Value": {"Fn::GetAtt" : [ "RDSInstance" , "Endpoint.Port" ]}
},
"someoutput":{
"Description": "For tests",
"Value": "BLAHBLAH"
}
}
config = ConfigParser(project_config.config, 'my-stack-name')
cfg = json.loads(config.process())
outputs = cfg['Outputs']
compare(known_outputs, outputs)

def test_process_no_elbs_no_rds(self):
project_config = ProjectConfig('tests/sample-project.yaml', 'dev')
# Assuming there's no ELB defined
Expand Down

0 comments on commit 3e1f2eb

Please sign in to comment.