Skip to content

Commit

Permalink
v2.7.0
Browse files Browse the repository at this point in the history
  • Loading branch information
AWS authored and AWS committed Nov 10, 2023
1 parent b1ba765 commit 2fa6e61
Show file tree
Hide file tree
Showing 66 changed files with 368 additions and 2,253 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
__pycache__
.pytest_cache
.mypy_cache
.coverage

# Ignore virtual environments
venv
Expand All @@ -21,6 +22,7 @@ testing-venv
# Ignore installed dependencies
dist
source/src/build
build

/deployment/open-source
/deployment/state_machines/sample_events/
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.6.0
v2.7.0
93 changes: 56 additions & 37 deletions customizations-for-aws-control-tower.template

Large diffs are not rendered by default.

Empty file modified deployment/build-s3-dist.sh
100755 → 100644
Empty file.
61 changes: 40 additions & 21 deletions deployment/custom-control-tower-initiation.template
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ Resources:
RepositoryDescription: Configuration for Customizations for AWS Control Tower solution
RepositoryName: !Ref CodeCommitRepositoryName
Code:
BranchName: !Ref CodeCommitBranchName
S3:
Bucket: !Sub %TEMPLATE_BUCKET_NAME%
Key: !Sub %SOLUTION_NAME%/%VERSION%/custom-control-tower-configuration-${AWS::Region}.zip
Expand Down Expand Up @@ -1204,14 +1205,14 @@ Resources:
- cloudformation:UpdateStackInstances
- cloudformation:TagResource
- cloudformation:ListStackInstances
- cloudformation:GetTemplateSummary
- cloudformation:DescribeStacks
Resource:
- !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/*
- !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stackset/*
- Effect: Allow
Action:
- cloudformation:ValidateTemplate
- cloudformation:GetTemplateSummary
Resource: '*'
- PolicyName: State-Machine-Lambda-Policy-SSM
PolicyDocument:
Expand Down Expand Up @@ -2188,23 +2189,7 @@ Resources:
"TimeoutSeconds": 300,
"HeartbeatSeconds": 60,
"InputPath": "$",
"Next": "Check List StackInstances Accounts Complete?"
},
"Check List StackInstances Accounts Complete?": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.NextToken",
"StringEquals": "Complete",
"Next": "Skip Update StackSet?"
}
],
"Default": "Check List StackInstances Accounts Wait"
},
"Check List StackInstances Accounts Wait": {
"Type": "Wait",
"Seconds": 5,
"Next": "List StackInstances Accounts"
"Next": "Skip Update StackSet?"
},
"Skip Update StackSet?": {
"Type": "Choice",
Expand Down Expand Up @@ -3118,10 +3103,10 @@ Resources:
RoleArn: !GetAtt CustomControlTowerPipelineTriggerRole.Arn

# Cloudwatch Event Rule for Lifecycle Event (LE): triggered by LE events and send events to SQS
CustomControlTowerLECWEventRule:
CustomControlTowerCreateManagedAccountCWEventRule:
Type: AWS::Events::Rule
Properties:
Description: Custom Control Tower - Rule for lifecycle events from Control Tower Service
Description: Trigger CFCT on CreateManagedAccount events from Control Tower Service
EventPattern:
{
"detail-type": [
Expand Down Expand Up @@ -3150,6 +3135,38 @@ Resources:
SqsParameters:
MessageGroupId: CustomControlTower_Lifecycle_Event

CustomControlTowerUpdateManagedAccountCWEventRule:
Type: AWS::Events::Rule
Properties:
Description: Trigger CFCT on UpdateManagedAccount events from Control Tower Service
EventPattern:
{
"detail-type": [
"AWS Service Event via CloudTrail"
],
"source": [
"aws.controltower"
],
"detail": {
"eventName": [
"UpdateManagedAccount"
],
"serviceEventDetails": {
"updateManagedAccountStatus": {
"state": [
"SUCCEEDED"
]
}
}
}
}
State: ENABLED
Targets:
- Arn: !GetAtt CustomControlTowerLEFIFOQueue.Arn
Id: "CustomControlTower_Lifecycle_Event_FIFO_Queue"
SqsParameters:
MessageGroupId: CustomControlTower_Lifecycle_Event

# Lifecycle event SQS Policy
CustomControlTowerLEQueuePolicy:
Type: AWS::SQS::QueuePolicy
Expand All @@ -3166,7 +3183,9 @@ Resources:
Resource: !GetAtt CustomControlTowerLEFIFOQueue.Arn
Condition:
ArnEquals:
aws:SourceArn: !GetAtt CustomControlTowerLECWEventRule.Arn
aws:SourceArn:
- !GetAtt CustomControlTowerCreateManagedAccountCWEventRule.Arn
- !GetAtt CustomControlTowerUpdateManagedAccountCWEventRule.Arn

Outputs:
CustomControlTowerCodePipeline:
Expand Down
Empty file modified deployment/run-unit-tests.sh
100755 → 100644
Empty file.
3 changes: 1 addition & 2 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
[pytest]
addopts = -v -ra -q -p source.tests.plugins.env_vars
addopts = --verbose -ra -m unit
log_cli = true
log_level=WARN
markers =
unit
integration
e2e

Empty file modified source/codebuild_scripts/execute_stage_scripts.sh
100755 → 100644
Empty file.
Empty file modified source/codebuild_scripts/install_stage_dependencies.sh
100755 → 100644
Empty file.
1 change: 1 addition & 0 deletions source/codebuild_scripts/run-validation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ exit_shell_script() {

validate_template_file() {
echo "Running aws cloudformation validate-template on $template_url"
# TODO: Verify if this works if resource file is homed in opt-in region, and CT mgmt is homed in commercial region
aws cloudformation validate-template --template-url "$template_url" --region "$AWS_REGION"
if [ $? -ne 0 ]
then
Expand Down
2 changes: 1 addition & 1 deletion source/codebuild_scripts/state_machine_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,5 @@ def launch_state_machine_execution(

if __name__ == "__main__":
os.environ["LOG_LEVEL"] = sys.argv[1]
logger = Logger(loglevel=os.environ["LOG_LEVEL"])
logger = Logger(loglevel=os.getenv("LOG_LEVEL", "info"))
main()
89 changes: 20 additions & 69 deletions source/src/cfct/aws/services/cloudformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@
from typing import Any, Dict, List

from botocore.exceptions import ClientError

from cfct.aws.utils.boto3_session import Boto3Session
from cfct.types import (
ResourcePropertiesTypeDef,
StackSetInstanceTypeDef,
StackSetRequestTypeDef,
)
from cfct.types import ResourcePropertiesTypeDef, StackSetInstanceTypeDef, StackSetRequestTypeDef
from cfct.utils.retry_decorator import try_except_retry


Expand All @@ -41,13 +38,9 @@ def __init__(self, logger, **kwargs):
self.logger = logger
__service_name = "cloudformation"
self.max_concurrent_percent = int(os.environ.get("MAX_CONCURRENT_PERCENT", 100))
self.failed_tolerance_percent = int(
os.environ.get("FAILED_TOLERANCE_PERCENT", 10)
)
self.region_concurrency_type = os.environ.get(
"REGION_CONCURRENCY_TYPE", "PARALLEL"
).upper()
self.max_results_per_page = 20
self.failed_tolerance_percent = int(os.environ.get("FAILED_TOLERANCE_PERCENT", 10))
self.region_concurrency_type = os.environ.get("REGION_CONCURRENCY_TYPE", "PARALLEL").upper()
self.max_results_per_page = 100
super().__init__(logger, __service_name, **kwargs)
self.cfn_client = super().get_client()

Expand Down Expand Up @@ -75,9 +68,7 @@ def describe_stack_set_operation(self, stack_set_name, operation_id):
return response
except ClientError as e:
self.logger.error(
"'{}' StackSet Operation ID: {} not found.".format(
stack_set_name, operation_id
)
"'{}' StackSet Operation ID: {} not found.".format(stack_set_name, operation_id)
)
self.logger.log_unhandled_exception(e)
raise
Expand Down Expand Up @@ -108,17 +99,10 @@ def get_accounts_and_regions_per_stack_set(self, stack_name):
# build the account and region list for the stack set
# using list(set(LIST)) to remove the duplicate values from the list
account_list = list(
set(
[
stack_instance["Account"]
for stack_instance in stack_instance_list
]
)
set([stack_instance["Account"] for stack_instance in stack_instance_list])
)
region_list = list(
set(
[stack_instance["Region"] for stack_instance in stack_instance_list]
)
set([stack_instance["Region"] for stack_instance in stack_instance_list])
)
next_token = response.get("NextToken", None)

Expand All @@ -134,20 +118,10 @@ def get_accounts_and_regions_per_stack_set(self, stack_name):

# update account and region lists
additional_account_list = list(
set(
[
stack_instance["Account"]
for stack_instance in stack_instance_list
]
)
set([stack_instance["Account"] for stack_instance in stack_instance_list])
)
additional_region_list = list(
set(
[
stack_instance["Region"]
for stack_instance in stack_instance_list
]
)
set([stack_instance["Region"] for stack_instance in stack_instance_list])
)
account_list = account_list + additional_account_list
region_list = region_list + additional_region_list
Expand Down Expand Up @@ -255,9 +229,7 @@ def create_stack_instances_with_override_params(
self.logger.log_unhandled_exception(e)
raise

def update_stack_instances(
self, stack_set_name, account_list, region_list, override_params
):
def update_stack_instances(self, stack_set_name, account_list, region_list, override_params):
try:
parameters = []
param_dict = {}
Expand Down Expand Up @@ -389,9 +361,7 @@ def list_stack_set_operations(self, **kwargs):
self.logger.log_unhandled_exception(e)
raise

def _filter_managed_stack_set_names(
self, list_stackset_response: Dict[str, Any]
) -> List[str]:
def _filter_managed_stack_set_names(self, list_stackset_response: Dict[str, Any]) -> List[str]:
"""
Reduces a list of given stackset summaries to only those considered managed by CfCT
"""
Expand Down Expand Up @@ -431,19 +401,14 @@ def is_managed_by_cfct(self, describe_stackset_response: Dict[str, Any]) -> bool
A StackSet is considered managed if it has both the prefix we expect, and the proper tag
"""

has_tag = (
StackSet.DEPLOYED_BY_CFCT_TAG
in describe_stackset_response["StackSet"]["Tags"]
)
has_tag = StackSet.DEPLOYED_BY_CFCT_TAG in describe_stackset_response["StackSet"]["Tags"]
has_prefix = describe_stackset_response["StackSet"]["StackSetName"].startswith(
StackSet.CFCT_STACK_SET_PREFIX
)
is_active = describe_stackset_response["StackSet"]["Status"] == "ACTIVE"
return all((has_prefix, has_tag, is_active))

def get_stack_sets_not_present_in_manifest(
self, manifest_stack_sets: List[str]
) -> List[str]:
def get_stack_sets_not_present_in_manifest(self, manifest_stack_sets: List[str]) -> List[str]:
"""
Compares list of stacksets defined in the manifest versus the stacksets in the account
and returns a list of all stackset names to be deleted
Expand All @@ -455,46 +420,32 @@ def get_stack_sets_not_present_in_manifest(
f"{StackSet.CFCT_STACK_SET_PREFIX}{name}" for name in manifest_stack_sets
]
cfct_deployed_stack_sets = self.get_managed_stack_set_names()
return list(
set(cfct_deployed_stack_sets).difference(
set(manifest_stack_sets_with_prefix)
)
)
return list(set(cfct_deployed_stack_sets).difference(set(manifest_stack_sets_with_prefix)))

def generate_delete_request(
self, stacksets_to_delete: List[str]
) -> List[StackSetRequestTypeDef]:
requests: List[StackSetRequestTypeDef] = []
for stackset_name in stacksets_to_delete:
deployed_instances = self._get_stackset_instances(
stackset_name=stackset_name
)
deployed_instances = self._get_stackset_instances(stackset_name=stackset_name)
requests.append(
StackSetRequestTypeDef(
RequestType="Delete",
ResourceProperties=ResourcePropertiesTypeDef(
StackSetName=stackset_name,
TemplateURL="DeleteStackSetNoopURL",
Capabilities=json.dumps(
["CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
),
Capabilities=json.dumps(["CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]),
Parameters={},
AccountList=list(
{instance["account"] for instance in deployed_instances}
),
RegionList=list(
{instance["region"] for instance in deployed_instances}
),
AccountList=list({instance["account"] for instance in deployed_instances}),
RegionList=list({instance["region"] for instance in deployed_instances}),
SSMParameters={},
),
SkipUpdateStackSet="yes",
)
)
return requests

def _get_stackset_instances(
self, stackset_name: str
) -> List[StackSetInstanceTypeDef]:
def _get_stackset_instances(self, stackset_name: str) -> List[StackSetInstanceTypeDef]:
instance_regions_and_accounts: List[StackSetInstanceTypeDef] = []
paginator = self.cfn_client.get_paginator("list_stack_instances")
for page in paginator.paginate(StackSetName=stackset_name):
Expand Down
5 changes: 2 additions & 3 deletions source/src/cfct/aws/services/code_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import inspect

from botocore.exceptions import ClientError

from cfct.aws.utils.boto3_session import Boto3Session


Expand All @@ -33,9 +34,7 @@ def __init__(self, logger, **kwargs):

def start_pipeline_execution(self, code_pipeline_name):
try:
response = self.code_pipeline.start_pipeline_execution(
name=code_pipeline_name
)
response = self.code_pipeline.start_pipeline_execution(name=code_pipeline_name)
return response
except ClientError as e:
self.logger.log_unhandled_exception(e)
Expand Down
1 change: 1 addition & 0 deletions source/src/cfct/aws/services/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# !/bin/python

from botocore.exceptions import ClientError

from cfct.aws.utils.boto3_session import Boto3Session


Expand Down
5 changes: 2 additions & 3 deletions source/src/cfct/aws/services/kms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# !/bin/python

from botocore.exceptions import ClientError

from cfct.aws.utils.boto3_session import Boto3Session


Expand Down Expand Up @@ -56,9 +57,7 @@ def create_key(self, policy, description, tag_key, tag_value):

def create_alias(self, alias_name, key_name):
try:
response = self.kms_client.create_alias(
AliasName=alias_name, TargetKeyId=key_name
)
response = self.kms_client.create_alias(AliasName=alias_name, TargetKeyId=key_name)
return response
except ClientError as e:
self.logger.log_unhandled_exception(e)
Expand Down
Loading

0 comments on commit 2fa6e61

Please sign in to comment.