def build(project: Config, stage: InputResolver) -> Template: """Build a stack template for all CodeBuild actions in a CodePipeline stage. :param project: PipeFormer project to build for :param stage: Stage for which to construct CodeBuild projects :return: Constructed template """ resources = Template( Description= f"CodeBuild projects for {stage.name} stage in pipeformer-managed project: {project.name}" ) # set all non-input parameters resources_bucket = resources.add_parameter( Parameter(reference_name(resource_name(s3.Bucket, "ProjectResources"), "Name"), Type="String")) role = resources.add_parameter( Parameter(reference_name(resource_name(iam.Role, "CodeBuild"), "Arn"), Type="String")) default_tags = project_tags(project) required_inputs = set() # add all resources for pos in range(len(stage.actions)): action = stage.actions[pos] if action.provider != "CodeBuild": continue action_resource = resources.add_resource( _build_project(name=project_name(pos), action=action, role=role.ref(), bucket=resources_bucket.ref(), tags=default_tags)) resources.add_output( Output(reference_name(action_resource.title, "Name"), Value=action_resource.ref())) required_inputs.update(action.required_inputs) # use collected parameters to set all input values needed as parameters for name in required_inputs: resources.add_parameter( Parameter(project.inputs[name].reference_name(), Type="String")) return resources
def _wait_condition_stack( base_name: str, parameters: Dict[str, Any], artifacts_bucket: s3.Bucket, tags: Tags, depends_on: Optional[Iterable] = None, ) -> WaitConditionStack: """Construct a wait-condition-managed stack. :param base_name: Name to use for base of logical names :param parameters: Stack parameters :param artifacts_bucket: Artifacts bucket resource :param tags: Tags to set on stack :param depends_on: Resources that stack will depend on :return: Constructed resources """ if depends_on is None: depends_on = [] condition, handle = _wait_condition("Template", base_name) stack = cloudformation.Stack( resource_name(cloudformation.Stack, base_name), DependsOn=[condition.title] + depends_on, TemplateURL=_wait_condition_data_to_s3_url(condition, artifacts_bucket), Parameters=parameters, Tags=tags, ) return WaitConditionStack(condition=condition, handle=handle, stack=stack)
def build(project: Config) -> Template: """Build an Inputs stack template from the provided project. :param project: Source project :return: Generated Inputs stack template """ inputs = Template( Description= f"Input values for pipeformer-managed project: {project.name}") cmk = inputs.add_parameter( Parameter(reference_name(resource_name(kms.Key, "Stack"), "Arn"), Type="String")) default_tags = project_tags(project) for value in project.inputs.values(): if value.secret: resource = _secret_value(resource=value, tags=default_tags, cmk_arn=cmk.ref()) resource_output = "Arn" else: resource = _standard_value(value) resource_output = "Name" inputs.add_resource(resource) inputs.add_output( Output(reference_name(resource.title, resource_output), Value=Ref(resource))) return inputs
def project_name(action_number: int) -> str: """Construct the project logical resource name. :param action_number: Unique count identifier for project in stack :return: Logical resource name """ return resource_name(codebuild.Project, string.ascii_letters[action_number])
def _project_key(project: Config) -> kms.Key: """Construct the AWS CMK that will be used to protect project resources. :param project: Source project :return: Constructed key """ policy = AWS.PolicyDocument( Version="2012-10-17", Statement=[ AWS.Statement( Effect=AWS.Allow, Principal=AWS.Principal("AWS", account_arn("iam", "root")), Action=[ KMS.Encrypt, KMS.Decrypt, KMS.ReEncrypt, KMS.GenerateDataKey, KMS.GenerateDataKeyWithoutPlaintext, KMS.DescribeKey, KMS.GetKeyPolicy, ], Resource=["*"], ), # TODO: Change admin statement to some other principal? AWS.Statement( Effect=AWS.Allow, Principal=AWS.Principal("AWS", account_arn("iam", "root")), Action=[ KMS.GetKeyPolicy, KMS.PutKeyPolicy, KMS.ScheduleKeyDeletion, KMS.CancelKeyDeletion, KMS.CreateAlias, KMS.DeleteAlias, KMS.UpdateAlias, KMS.DescribeKey, KMS.EnableKey, KMS.DisableKey, KMS.GetKeyRotationStatus, KMS.EnableKeyRotation, KMS.DisableKeyRotation, KMS.ListKeyPolicies, KMS.ListResourceTags, KMS.TagResource, KMS.UntagResource, ], Resource=["*"], ), ], ) return kms.Key( resource_name(kms.Key, "Stack"), Enabled=True, EnableKeyRotation=False, KeyPolicy=policy, Tags=project_tags(project), )
def _standard_value(resource: Input) -> ssm.Parameter: """Construct a Parameter Store parameter containing the input value. :param resource: Input to store :return: Constructed resource """ return ssm.Parameter(resource_name(ssm.Parameter, resource.name), Type="String", Value=resource.value)
def build(project: Config) -> Template: """Build an IAM stack template for the provided project. :param project: Source project :return: Generated IAM stack template """ resources = Template( Description= f"IAM resources for pipeformer-managed project: {project.name}") artifacts_bucket_arn = resources.add_parameter( Parameter(reference_name(resource_name(s3.Bucket, "Artifacts"), "Arn"), Type="String")) resources_bucket_arn = resources.add_parameter( Parameter(reference_name(resource_name(s3.Bucket, "ProjectResources"), "Arn"), Type="String")) cmk_arn = resources.add_parameter( Parameter(reference_name(resource_name(kms.Key, "Stack"), "Arn"), Type="String")) cloudformation_role = resources.add_resource(_cloudformation_role()) resources.add_output( Output(reference_name(cloudformation_role.title, "Arn"), Value=cloudformation_role.get_att("Arn"))) codepipeline_role = resources.add_resource( _codepipeline_role(artifacts_bucket=artifacts_bucket_arn, resources_bucket=resources_bucket_arn, cmk=cmk_arn)) resources.add_output( Output(reference_name(codepipeline_role.title, "Arn"), Value=codepipeline_role.get_att("Arn"))) codebuild_role = resources.add_resource( _codebuild_role(artifacts_bucket=artifacts_bucket_arn, resources_bucket=resources_bucket_arn, cmk=cmk_arn)) resources.add_output( Output(reference_name(codebuild_role.title, "Arn"), Value=codebuild_role.get_att("Arn"))) return resources
def _secret_value(resource: Input, tags: Tags, cmk_arn: Ref) -> secretsmanager.Secret: """Construct a Secrets Manager secret to store the input value. :param resource: Input for which to create secret :param tags: Tags to set on secret :param cmk_arn: Key with which to protect secret :return: Constructed resource """ return secretsmanager.Secret(resource_name(secretsmanager.Secret, resource.name), KmsKeyId=cmk_arn, SecretString="REPLACEME", Tags=tags)
def _codebuild_role(artifacts_bucket: Parameter, resources_bucket: Parameter, cmk: Parameter) -> iam.Role: """Construct a role for use by CodeBuild. :param artifacts_bucket: Artifacts bucket parameter :param resources_bucket: Resources bucket parameter :param cmk: KMS CMK parameter :return: Constructed Role """ assume_policy = AWS.PolicyDocument(Statement=[ AWS.Statement( Principal=AWS.Principal( "Service", make_service_domain_name(CODEBUILD.prefix)), Effect=AWS.Allow, Action=[STS.AssumeRole], ) ]) policy = AWS.PolicyDocument(Statement=[ AWS.Statement( Effect=AWS.Allow, Action=[ LOGS.CreateLogGroup, LOGS.CreateLogStream, LOGS.PutLogEvents ], Resource=[account_arn(service_prefix=LOGS.prefix, resource="*")], ), AWS.Statement( Effect=AWS.Allow, Action=[S3.GetObject, S3.GetObjectVersion, S3.PutObject], Resource=[ Sub(f"${{{artifacts_bucket.title}}}/*"), Sub(f"${{{resources_bucket.title}}}/*") ], ), AWS.Statement(Effect=AWS.Allow, Action=[KMS.Encrypt, KMS.Decrypt, KMS.GenerateDataKey], Resource=[cmk.ref()]), ]) return iam.Role( resource_name(iam.Role, "CodeBuild"), AssumeRolePolicyDocument=assume_policy, Policies=[ iam.Policy(PolicyName=_policy_name("CodeBuild"), PolicyDocument=policy) ], )
def _bucket(name: str, cmk_arn: GetAtt, tags: Tags) -> s3.Bucket: """Construct a S3 bucket resource with default SSE-KMS using the specified CMK. :param name: Logical resource name :param cmk_arn: Reference to Arn of CMK resource :param tags: Tags to apply to bucket :return: Constructed S3 bucket resource """ return s3.Bucket( resource_name(s3.Bucket, name), BucketEncryption=s3. BucketEncryption(ServerSideEncryptionConfiguration=[ s3.ServerSideEncryptionRule( ServerSideEncryptionByDefault=s3.ServerSideEncryptionByDefault( SSEAlgorithm="aws:kms", KMSMasterKeyID=cmk_arn)) ]), Tags=tags, )
def _cloudformation_role() -> iam.Role: """Construct a role for use by CloudFormation. :return: Constructed Role """ assume_policy = AWS.PolicyDocument(Statement=[ AWS.Statement( Principal=AWS.Principal( "Service", make_service_domain_name(CLOUDFORMATION.prefix)), Effect=AWS.Allow, Action=[STS.AssumeRole], ) ]) # TODO: Figure out how to scope this down without breaking IAM # IAM policies break if there is a * in certain fields, # so this does not work: # arn:PARTITION:*:REGION:ACCOUNT:* # # _desired_policy = AWS.PolicyDocument( # Statement=[ # AWS.Statement( # Effect=AWS.Allow, # Action=[AWS.Action("*")], # Resource=[ # account_arn(service_prefix="*", resource="*"), # account_arn(service_prefix=S3.prefix, resource="*"), # account_arn(service_prefix=IAM.prefix, resource="*"), # ], # ) # ] # ) policy = AWS.PolicyDocument(Statement=[ AWS.Statement( Effect=AWS.Allow, Action=[AWS.Action("*")], Resource=["*"]) ]) return iam.Role( resource_name(iam.Role, "CloudFormation"), AssumeRolePolicyDocument=assume_policy, Policies=[ iam.Policy(PolicyName=_policy_name("CloudFormation"), PolicyDocument=policy) ], )
def _codepipeline_role(artifacts_bucket: Parameter, resources_bucket: Parameter, cmk: Parameter) -> iam.Role: """Construct a role for use by CodePipeline. :param artifacts_bucket: Artifacts bucket parameter :param resources_bucket: Resources bucket parameter :param cmk: KMS CMK parameter :return: Constructed Role """ assume_policy = AWS.PolicyDocument(Statement=[ AWS.Statement( Principal=AWS.Principal( "Service", make_service_domain_name(CODEPIPELINE.prefix)), Effect=AWS.Allow, Action=[STS.AssumeRole], ) ]) policy = AWS.PolicyDocument(Statement=[ AWS.Statement( Effect=AWS.Allow, Action=[S3.GetBucketVersioning, S3.PutBucketVersioning], Resource=[artifacts_bucket.ref(), resources_bucket.ref()], ), AWS.Statement( Effect=AWS.Allow, Action=[S3.GetObject, S3.PutObject], Resource=[ Sub(f"${{{artifacts_bucket.title}}}/*"), Sub(f"${{{resources_bucket.title}}}/*") ], ), AWS.Statement(Effect=AWS.Allow, Action=[KMS.Encrypt, KMS.Decrypt, KMS.GenerateDataKey], Resource=[cmk.ref()]), AWS.Statement( Effect=AWS.Allow, Action=[CLOUDWATCH.Action("*")], Resource=[ account_arn(service_prefix=CLOUDWATCH.prefix, resource="*") ], ), AWS.Statement( Effect=AWS.Allow, Action=[IAM.PassRole], Resource=[ account_arn(service_prefix=IAM.prefix, resource="role/*") ], ), AWS.Statement( Effect=AWS.Allow, Action=[LAMBDA.InvokeFunction, LAMBDA.ListFunctions], Resource=[account_arn(service_prefix=LAMBDA.prefix, resource="*")], ), AWS.Statement( Effect=AWS.Allow, Action=[ CLOUDFORMATION.CreateStack, CLOUDFORMATION.DeleteStack, CLOUDFORMATION.DescribeStacks, CLOUDFORMATION.UpdateStack, CLOUDFORMATION.CreateChangeSet, CLOUDFORMATION.DeleteChangeSet, CLOUDFORMATION.DescribeChangeSet, CLOUDFORMATION.ExecuteChangeSet, CLOUDFORMATION.SetStackPolicy, CLOUDFORMATION.ValidateTemplate, ], Resource=[ account_arn(service_prefix=CLOUDFORMATION.prefix, resource="*") ], ), AWS.Statement( Effect=AWS.Allow, Action=[CODEBUILD.BatchGetBuilds, CODEBUILD.StartBuild], Resource=[ account_arn(service_prefix=CODEBUILD.prefix, resource="*") ], ), ]) return iam.Role( resource_name(iam.Role, "CodePipeline"), AssumeRolePolicyDocument=assume_policy, Policies=[ iam.Policy(PolicyName=_policy_name("CodePipeline"), PolicyDocument=policy) ], )
def _inputs_stack_logical_name() -> str: """Determine the logical name for the inputs stack.""" return resource_name(cloudformation.Stack, "Inputs")
def _artifacts_bucket_logical_name() -> str: """Determine the logical name for the artifacts S3 bucket.""" return resource_name(s3.Bucket, "Artifacts")