Skip to content

Commit

Permalink
- Added Resource state upload to Paco work bucket
Browse files Browse the repository at this point in the history
- Fixed SNS Topics policy to work cross account + cross region
  • Loading branch information
gitwater committed Apr 3, 2021
1 parent 0df8687 commit c59636a
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 52 deletions.
66 changes: 64 additions & 2 deletions src/paco/application/reseng_deploymentpipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from paco.core.yaml import YAML
from paco.core.exception import InvalidAWSConfiguration
from paco.models.locations import get_parent_by_interface
from paco.models.references import get_model_obj_from_ref
from paco.models.references import get_model_obj_from_ref, Reference, resolve_ref_outputs
from paco.models.resources import SSMDocument
from paco.models import schemas
from paco.stack import StackHooks
Expand Down Expand Up @@ -618,6 +618,7 @@ def init_resource(self):
# Pipeline Execution Notification Rules
# Create Notifications Rules CFTemplate
# Get the SNS topic to attach
rules_arn_ref_list = []
env_name = '.'.join(self.app_engine.app.paco_ref_parts.split('.')[0:2])
notification_rules_stack = self.stack_group.add_new_stack(
self.aws_region,
Expand All @@ -626,9 +627,14 @@ def init_resource(self):
account_ctx=self.pipeline_account_ctx,
stack_tags=self.stack_tags,
support_resource_ref_ext='notification_rules',
extra_context={'env_name': env_name, 'app_name': self.app.name}
extra_context={'env_name': env_name, 'app_name': self.app.name, 'rules_arn_ref_list': rules_arn_ref_list}
)

# Stack hook to upload DeploymentPipeline configuration
# to the Paco S3 work bucket for use by external services
# notification rule arn associated with SNS topics and Slack Channels
self.add_notification_rules_stack_hooks(notification_rules_stack, rules_arn_ref_list)

# Add CodeBuild Role ARN to KMS Key principal now that the role is created
kms_config_dict['crypto_principal']['aws'] = self.kms_crypto_principle_list
kms_stack = self.stack_group.add_new_stack(
Expand Down Expand Up @@ -943,6 +949,41 @@ def create_image_definitions_artifact(self, hook, pipeline):
},
)

def store_notification_rules_config_cache(self, hook, config):
"Create a cache id for the notification rules configuration"
cache = ""
for rule_arn_ref in config['rules_arn_ref_list']:
cache += rule_arn_ref
return cache

def store_notification_rules_config(self, hook, config):
"Create an imageDefinitions file"
pipeline = config['pipeline']
monitoring = pipeline.monitoring

monitor_config = {
'groups': [],
'slack_channels': []
}
if monitoring != None and monitoring.is_enabled() and monitoring.notifications != None:
for notification_name in monitoring.notifications.keys():
notification = monitoring.notifications[notification_name]
monitor_config['groups'].extend(notification.groups)
monitor_config['slack_channels'].extend(notification.slack_channels)

rules_config = {
'rules': [],
'monitor_config': monitor_config
}

for rule_arn_ref in config['rules_arn_ref_list']:
rule_arn = resolve_ref_outputs(Reference(rule_arn_ref), self.paco_ctx.project['home'])
rules_config['rules'].append(rule_arn)


self.paco_ctx.store_resource_state(self.pipeline, rules_config)


def init_stage_action_ecr_source(self, action_config):
"Initialize an ECR Source action"
if not action_config.is_enabled():
Expand Down Expand Up @@ -980,6 +1021,25 @@ def add_ecr_source_hooks(self, action):
hook_arg=self.pipeline,
)

def add_notification_rules_stack_hooks(self, notification_rules_stack, rules_arn_ref_list):
# Hook to create and upload imageDefinitions.json S3 source artifact
notification_rules_stack.hooks.add(
name='StoreNotificationRulesConfig.' + self.resource.name,
stack_action='update',
stack_timing='post',
hook_method=self.store_notification_rules_config,
cache_method=self.store_notification_rules_config_cache,
hook_arg={'pipeline': self.pipeline, 'rules_arn_ref_list': rules_arn_ref_list}
)
notification_rules_stack.hooks.add(
name='StoreNotificationRulesConfig.' + self.resource.name,
stack_action='create',
stack_timing='post',
hook_method=self.store_notification_rules_config,
cache_method=self.store_notification_rules_config_cache,
hook_arg={'pipeline': self.pipeline, 'rules_arn_ref_list': rules_arn_ref_list}
)

def init_stage_action_ecs_deploy(self, action_config):
"Initialize an ECS stack for the action"
if not action_config.is_enabled():
Expand Down Expand Up @@ -1173,6 +1233,8 @@ def resolve_ref(self, ref):
return ref.resource._stack.template.get_codepipeline_role_arn()
elif ref.resource_ref == 'arn':
return ref.resource._stack.template.pipeline_arn
elif ref.resource_ref == 'notification_rule.arn':
return ref.resource._stack

elif schemas.IDeploymentPipelineSourceCodeCommit.providedBy(ref.resource):
# CodeCommit
Expand Down
35 changes: 20 additions & 15 deletions src/paco/cftemplates/notification_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class NotificationRules(StackTemplate):
def __init__(self, stack, paco_ctx, app_name, env_name):
def __init__(self, stack, paco_ctx, app_name, env_name, rules_arn_ref_list):
super().__init__(stack, paco_ctx)
self.set_aws_name('NotificationRules', self.resource_group_name, self.resource.name)
self.app_name = app_name
Expand All @@ -18,21 +18,24 @@ def __init__(self, stack, paco_ctx, app_name, env_name):
self.notification_groups = {}
rule_target_list = []
if self.resource.monitoring != None and self.resource.monitoring.notifications != None:
notify_param_cache = []
for notify_group_name in self.resource.monitoring.notifications.keys():
for sns_group_name in self.resource.monitoring.notifications[notify_group_name].groups:
notify_param = self.create_notification_param(sns_group_name)
rule_target_list.append(
{
'TargetAddress': troposphere.Ref(notify_param),
'TargetType': 'SNS'
}
)
# Only append if the are unique
if notify_param not in notify_param_cache:
rule_target_list.append(
{
'TargetAddress': troposphere.Ref(notify_param),
'TargetType': 'SNS'
}
)
notify_param_cache.append(notify_param)

event_id_list = []
for event_id in self.resource.notification_events:
event_id_list.append(f'codepipeline-pipeline-pipeline-execution-{event_id}')


codepipeline_arn_param = self.create_cfn_parameter(
param_type='String',
name='CodePipelineArn',
Expand All @@ -56,13 +59,15 @@ def __init__(self, stack, paco_ctx, app_name, env_name):

self.template.add_resource( rule_res )

# # Outputs
# self.create_output(
# title='ExampleResourceId',
# description="Example resource Id.",
# value=troposphere.Ref(example_res),
# ref=self.resource.paco_ref_parts + ".id"
# )
# Outputs
rule_arn_ref = self.resource.paco_ref_parts + ".notification_rule.arn"
self.create_output(
title='NotificationRuleArn',
description="CodeStar Notification Rule Arn",
value=troposphere.Ref(rule_res),
ref=rule_arn_ref
)
rules_arn_ref_list.append('paco.ref '+rule_arn_ref)


def create_notification_param(self, group):
Expand Down
74 changes: 39 additions & 35 deletions src/paco/cftemplates/snstopics.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(
for topic in topics:
if not topic.is_enabled():
continue
statement_list = []
topic_logical_id = self.create_cfn_logical_id(topic.name)

# Do not specify a TopicName, as then updates cannot be performed that require
Expand Down Expand Up @@ -92,11 +93,46 @@ def __init__(
'Topic' + topic_logical_id,
cfn_export_dict
)
if topic.cross_account_access:
topics_ref_cross_list.append(troposphere.Ref(topic_resource))

topic.topic_resource = topic_resource
template.add_resource(topic_resource)

if topic.codestar_notification_access:
statement = Statement(
Effect = Allow,
Sid = 'CodeStarNotificationAccess',
Principal = Principal("Service", 'codestar-notifications.amazonaws.com'),
Action = [ awacs.sns.Publish ],
Resource = [troposphere.Ref(topic_resource)],
)
statement_list.append(statement)

if topic.cross_account_access:
account_id_list = [
account.account_id for account in self.paco_ctx.project.accounts.values()
]
for account_id in account_id_list:
statement = Statement(
Effect = Allow,
Sid = self.create_cfn_logical_id(account_id),
Principal = Principal("AWS", f'arn:aws:iam::{account_id}:root'),
Action = [ awacs.sns.Publish, awacs.sns.Subscribe ],
Resource = [ troposphere.Ref(topic_resource) ],
)
statement_list.append(statement)

if len(statement_list) > 0:
topic_policy_resource = troposphere.sns.TopicPolicy(
f'Paco{topic_logical_id}TopicPolicy',
Topics = [troposphere.Ref(topic_resource)],
PolicyDocument = Policy(
Version = '2012-10-17',
Id = "PacoSNSTopicPolicy",
Statement=statement_list
)
)
template.add_resource(topic_policy_resource)

# Topic Outputs
if grp_id == None:
output_ref = stack.resource.paco_ref_parts
Expand All @@ -111,36 +147,4 @@ def __init__(
title='SNSTopicName' + topic_logical_id,
value=troposphere.GetAtt(topic_resource, "TopicName"),
ref=output_ref + '.name',
)

# Cross-account access policy
if len(topics_ref_cross_list) > 0:
account_id_list = [
account.account_id for account in self.paco_ctx.project.accounts.values()
]
statement_list = []
for account_id in account_id_list:
statement = Statement(
Effect = Allow,
Sid = self.create_cfn_logical_id(account_id),
Principal = Principal("AWS", f'arn:aws:iam::{account_id}:root'),
Action = [ awacs.sns.Publish, awacs.sns.Subscribe ],
Resource = topics_ref_cross_list,
#Condition = Condition(
# StringEquals({
# 'AWS:SourceOwner': account_id_list,
# })
#)
)
statement_list.append(statement)

topic_policy_resource = troposphere.sns.TopicPolicy(
'TopicPolicyCrossAccountPacoProject',
Topics = topics_ref_cross_list,
PolicyDocument = Policy(
Version = '2012-10-17',
Id = "CrossAccountPublish",
Statement=statement_list
)
)
template.add_resource(topic_policy_resource)
)
25 changes: 25 additions & 0 deletions src/paco/config/paco_context.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from paco import utils
from paco.core.exception import StackException
from paco.core.exception import PacoErrorCode, MissingAccountId, InvalidAccountName
from paco.models import vocabulary
Expand All @@ -7,6 +8,7 @@
from paco.models import load_project_from_yaml
from paco.models.references import get_model_obj_from_ref
from paco.core.yaml import read_yaml_file
from paco.core.yaml import YAML
from paco.config.interfaces import IAccountContext
from paco.config.paco_buckets import PacoBuckets
from shutil import copyfile
Expand Down Expand Up @@ -676,3 +678,26 @@ def log_action_col(
if return_it == True:
return message+'\n'
print(message)

def store_resource_state(self, resource, config):
yaml=YAML()
yaml.default_flow_sytle = False

file_contents = yaml.dump(data=config)
work_path = pathlib.Path(self.build_path)
work_path = work_path / 'ResourceState' / resource.type
pathlib.Path(work_path).mkdir(parents=True, exist_ok=True)

file_name = resource.paco_ref_parts + '.state'
utils.write_to_file(work_path, file_name, file_contents)

file_location = work_path / file_name

# Upload to S3
paco_bucket_account_ctx = self.get_account_context(self.project.shared_state.paco_work_bucket.account)
self.paco_buckets.upload_file(
file_location=str(file_location),
s3_key=f'Paco/ResourceState/{resource.type}/{resource.paco_ref_parts}',
account_ctx=paco_bucket_account_ctx,
region=self.project.shared_state.paco_work_bucket.region
)

0 comments on commit c59636a

Please sign in to comment.