Пример #1
0
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
Пример #2
0
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)
Пример #3
0
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
Пример #4
0
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])
Пример #5
0
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),
    )
Пример #6
0
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)
Пример #7
0
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
Пример #8
0
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)
Пример #9
0
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)
        ],
    )
Пример #10
0
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,
    )
Пример #11
0
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)
        ],
    )
Пример #12
0
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)
        ],
    )
Пример #13
0
 def _inputs_stack_logical_name() -> str:
     """Determine the logical name for the inputs stack."""
     return resource_name(cloudformation.Stack, "Inputs")
Пример #14
0
 def _artifacts_bucket_logical_name() -> str:
     """Determine the logical name for the artifacts S3 bucket."""
     return resource_name(s3.Bucket, "Artifacts")