# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License.
# A copy of the License is located at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# or in the "license" file accompanying this file. This file is distributed
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied. See the License for the specific language governing
# permissions and limitations under the License.

AWSTemplateFormatVersion: 2010-09-09

Description: (SO0086) - The Multi Region Infrastructure Deployment Solution validates and deploys CloudFormation templates to stage and prod environments across AWS Regions (Version SOLUTION_VERSION)

Parameters:
  NotificationEmailAddress:
    Type: String
    Description: (Optional) Change and drift detection notification E-mail Address
    AllowedPattern: "^$|^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"

  SolutionSecretName:
    Type: String
    Description: AWS Secrets Manager name
  GitHubRepo:
    Type: String
    Description: GitHub repository name
  GitHubBranch:
    Type: String
    Description: e.g., mainline

  SecondaryRegion:
    Type: String
    Description: Specified in the standard format, e.g., us-east-1, us-west-2, etc...

  TemplatePath:
    Type: String
    Description: Specified from the root of the Git repository, e.g., templates/my_template.yaml

  CloudFormationExecutionPolicy:
    Type: String
    Description: The ARN of the IAM Policy used to allow CloudFormation to create/execute change sets. Create an IAM Policy specific to administering the resources that will be provisioned by the CloudFormation Template in the GitHub repository.

  StageParameters:
    Type: String
    Description: CloudFormation Template Parameters Application's Stack. Leave this field blank if the CloudFormation template specified in the TemplatePath parameter has no parameters.
    Default: ''

  SecondaryParameters:
    Type: String
    Description: CloudFormation Template Parameters Application's Stack. Leave this field blank if the CloudFormation template specified in the TemplatePath parameter has no parameters.
    Default: ''

  PrimaryParameters:
    Type: String
    Description: CloudFormation Template Parameters Application's Stack. Leave this field blank if the CloudFormation template specified in the TemplatePath parameter has no parameters.
    Default: ''

  DeleteStageStack:
    Type: String
    Description: Deleting stage stack after approval to deploy production stacks
    Default: "Yes"
    AllowedValues:
      - "Yes"
      - "No"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Notification
        Parameters:
          - NotificationEmailAddress
      - Label:
          default: GitHub Repository Configuration
        Parameters:
          - SolutionSecretName
          - GitHubRepo
          - GitHubBranch
          - TemplatePath
      - Label:
          default: Application Stack Parameters
        Parameters:
          - StageParameters
          - PrimaryParameters
          - SecondaryParameters
      - Label:
          default: Deployment Configuration
        Parameters:
          - DeleteStageStack
          - CloudFormationExecutionPolicy
          - SecondaryRegion
    ParameterLabels:
      NotificationEmailAddress:
        default: E-Mail to notify changes to approve and drift detection
      SolutionSecretName:
        default: Name of the AWS Secrets Manager secret that stores the GitHub Username and Personal Access Token to access the repository
      GitHubRepo:
        default: Name of the GitHub repository that should trigger the pipeline
      GitHubBranch:
        default: Name of the branch in the above repository that should trigger the pipeline
      TemplatePath:
        default: Path to the CloudFormation template in the above repository
      StageParameters:
        default: Stage Stack (Primary Region) Parameters
      PrimaryParameters:
        default: Primary Region Parameters
      SecondaryParameters:
        default: Secondary Region Parameters
      SecondaryRegion:
        default: The Secondary Region the application's CloudFormation template should be deployed to
      CloudFormationExecutionPolicy:
        default: CloudFormation Execution Policy ARN
      DeleteStageStack:
        default: Deleting stage stack after approval to deploy production stacks

Conditions:
  HasNotificationEmailAddress: !Not [ !Equals [ !Ref NotificationEmailAddress, '' ] ]
  HasStageParameters: !Not [ !Equals [ !Ref StageParameters, '' ] ]
  HasSecondaryParameters: !Not [ !Equals [ !Ref SecondaryParameters, '' ] ]
  HasPrimaryParameters: !Not [ !Equals [ !Ref PrimaryParameters, '' ] ]
  DeletingStage: !Equals [ !Ref DeleteStageStack, 'Yes' ]

Mappings:
  SourceCode:
    General:
      S3Bucket: CODE_BUCKET
      KeyPrefix: SOLUTION_NAME/SOLUTION_VERSION
  Solution:
    Metrics:
      SendAnonymousData: Yes
  StageArtifact:
    Name:
      ChangeSet: Multi-Region-Stage-ChangeSet
      Source: StageArtifact-Source
      Parameter: StageArtifact-Parameter
    Parameter:
      Source: source-artifact
      Parameter: parameter-artifact

Resources:
  PrimaryArtifactStore:
    Type: AWS::S3::Bucket
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W51
            reason: Policy not required for this bucket
    DeletionPolicy: Retain
    Properties:
      LoggingConfiguration:
        DestinationBucketName: !Ref PrimaryLogsBucket
        LogFilePrefix: primary-artifact-store-access/
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: aws:kms
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  PrimaryLogsBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W35
            reason: This is the destination logging bucket for S3 buckets in this solution
          - id: W51
            reason: Policy not required for this bucket
    Properties:
      AccessControl: LogDeliveryWrite
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: aws:kms
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  CloudFormationExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          Effect: Allow
          Action:
            - 'sts:AssumeRole'
          Principal:
            Service:
              - cloudformation.amazonaws.com
      Path: /
      ManagedPolicyArns:
        - !Ref CloudFormationExecutionPolicy

  InfraPipelineRole:
    Type: AWS::IAM::Role
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: cloudformation:ValidateTemplate - requires * resource
          - id: W76
            reason: SPCM > 25 - All permissions are required
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
                Resource:
                  - !Sub arn:aws:s3:::${PrimaryArtifactStore}/*
                  - !Sub arn:aws:s3:::${SecondaryBucketCreator.BucketName}/*
              - Effect: Allow
                Action:
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuilds
                Resource:
                  - !Sub arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/*
              - Effect: Allow
                Action:
                  - cloudformation:CreateStack
                  - cloudformation:DescribeStacks
                  - cloudformation:DeleteStack
                  - cloudformation:UpdateStack
                  - cloudformation:CreateChangeSet
                  - cloudformation:ExecuteChangeSet
                  - cloudformation:DeleteChangeSet
                  - cloudformation:DescribeChangeSet
                  - cloudformation:SetStackPolicy
                Resource:
                  - !Sub arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:*
                  - !Sub arn:aws:cloudformation:${SecondaryRegion}:${AWS::AccountId}:*
              - Effect: Allow
                Action:
                  - iam:PassRole
                Resource:
                  - !GetAtt CloudFormationExecutionRole.Arn
              - Effect: Allow
                Action:
                  - cloudformation:ValidateTemplate
                Resource:
                  - '*'
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource:
                  - !GetAtt ChangeSetValidatorLambda.Arn
                  - !GetAtt DriftDetectionLambda.Arn
                  - !GetAtt StageArtifactSsmLambda.Arn
                  - !If
                    - DeletingStage
                    - !GetAtt CreateStageArtifactLambda.Arn
                    - !Ref AWS::NoValue
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource:
                  - !Ref NotificationSnsTopic

  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: cloudformation:ValidateTemplate - requires * resource
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:GetObjectVersion
                Resource:
                  - !Sub arn:aws:s3:::${PrimaryArtifactStore}/*
                  - !Sub arn:aws:s3:::${SecondaryBucketCreator.BucketName}/*
              - Effect: Allow
                Action:
                  - cloudformation:ValidateTemplate
                Resource: '*'

  NotificationSnsTopic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: Multi-region Infrastructure Deployment Notification
      KmsMasterKeyId: alias/aws/sns

  NotificationSnsSubscription:
    Type: AWS::SNS::Subscription
    Condition: HasNotificationEmailAddress
    Properties:
      TopicArn: !Ref NotificationSnsTopic
      Endpoint: !Ref NotificationEmailAddress
      Protocol: email

  InfraPipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStores:
        - Region: !Ref AWS::Region
          ArtifactStore:
            Type: S3
            Location: !Ref PrimaryArtifactStore
        - Region: !Ref SecondaryRegion
          ArtifactStore:
            Type: S3
            Location: !GetAtt SecondaryBucketCreator.BucketName
      RoleArn: !GetAtt InfraPipelineRole.Arn
      Stages:
        - Name: Source
          Actions:
            - Name: GitHub
              ActionTypeId:
                Category: Source
                Owner: ThirdParty
                Version: '1'
                Provider: GitHub
              Configuration:
                Owner: !Sub '{{resolve:secretsmanager:${SolutionSecretName}:SecretString:github-username}}'
                Repo: !Ref GitHubRepo
                Branch: !Ref GitHubBranch
                OAuthToken: !Sub '{{resolve:secretsmanager:${SolutionSecretName}:SecretString:github-access-token}}'
                PollForSourceChanges: false
              OutputArtifacts:
                - Name: Source
        - !If
          - DeletingStage
          - Name: !Sub InitializeStage-${AWS::Region}
            Actions:
              - Name: CreateStageArtifact
                ActionTypeId:
                  Category: Invoke
                  Owner: AWS
                  Provider: Lambda
                  Version: '1'
                Configuration:
                  FunctionName: !Ref CreateStageArtifactLambda
                InputArtifacts:
                  - Name: Source
                OutputArtifacts:
                  - Name: StageArtifact-Source
                  - Name: StageArtifact-Parameter
                RunOrder: 1
                Region: !Ref AWS::Region

              - Name: Deploy
                ActionTypeId:
                  Category: Deploy
                  Owner: AWS
                  Provider: CloudFormation
                  Version: '1'
                Configuration:
                  ActionMode: CREATE_UPDATE
                  RoleArn: !GetAtt CloudFormationExecutionRole.Arn
                  StackName: !Sub ${AWS::StackName}-Stack-Stage-${AWS::Region}
                  TemplatePath: !Sub StageArtifact-Source::${TemplatePath}
                  TemplateConfiguration: StageArtifact-Parameter::parameters.json
                  Capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
                InputArtifacts:
                  - Name: StageArtifact-Source
                  - Name: StageArtifact-Parameter
                RunOrder: 2
                Region: !Ref AWS::Region
          - !Ref AWS::NoValue

        - Name: CheckChanges
          Actions:
            - Name: CreateChangeSet
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: CloudFormation
                Version: '1'
              Configuration:
                ActionMode: CHANGE_SET_REPLACE
                RoleArn: !GetAtt CloudFormationExecutionRole.Arn
                StackName: !Sub ${AWS::StackName}-Stack-Stage-${AWS::Region}
                ChangeSetName: !FindInMap ["StageArtifact", "Name", "ChangeSet"]
                TemplatePath: !Sub Source::${TemplatePath}
                Capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
                ParameterOverrides: !If [ HasStageParameters, !Ref StageParameters, !Ref "AWS::NoValue" ]
              InputArtifacts:
                - Name: Source
              OutputArtifacts:
                - Name: !FindInMap ["StageArtifact", "Name", "ChangeSet"]
              RunOrder: 1
              Region: !Ref AWS::Region

            - Name: CheckChangeSetForChanges
              ActionTypeId:
                Category: Invoke
                Owner: AWS
                Provider: Lambda
                Version: '1'
              Configuration:
                FunctionName: !Ref ChangeSetValidatorLambda
              InputArtifacts:
                - Name: !FindInMap ["StageArtifact", "Name", "ChangeSet"]
              RunOrder: 2
              Region: !Ref AWS::Region

        - Name: ValidateTemplates
          Actions:
            - Name: CfnNag
              InputArtifacts:
                - Name: Source
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: '1'
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CfnNagBuildProject
              OutputArtifacts:
                - Name: CfnNagOutput
              RunOrder: 1

            - Name: CfnLint
              InputArtifacts:
                - Name: Source
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: '1'
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CfnLintBuildProject
              OutputArtifacts:
                - Name: CfnLintOutput
              RunOrder: 2

        - Name: !Sub Stage-${AWS::Region}
          Actions:
            - !If
              - DeletingStage
              - Name: CreateUpdate
                ActionTypeId:
                  Category: Deploy
                  Owner: AWS
                  Provider: CloudFormation
                  Version: '1'
                Configuration:
                  ActionMode: CREATE_UPDATE
                  RoleArn: !GetAtt CloudFormationExecutionRole.Arn
                  StackName: !Sub ${AWS::StackName}-Stack-Stage-${AWS::Region}
                  TemplatePath: !Sub Source::${TemplatePath}
                  Capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
                  ParameterOverrides: !If [ HasStageParameters, !Ref StageParameters, !Ref "AWS::NoValue" ]
                InputArtifacts:
                  - Name: Source
                RunOrder: 1
                Region: !Ref AWS::Region
              - Name: ExecuteChangeSet
                ActionTypeId:
                  Category: Deploy
                  Owner: AWS
                  Provider: CloudFormation
                  Version: '1'
                Configuration:
                  ActionMode: CHANGE_SET_EXECUTE
                  RoleArn: !GetAtt CloudFormationExecutionRole.Arn
                  StackName: !Sub ${AWS::StackName}-Stack-Stage-${AWS::Region}
                  ChangeSetName: !FindInMap ["StageArtifact", "Name", "ChangeSet"]
                OutputArtifacts:
                  - Name: !Sub ${AWS::StackName}-ExecutedChangeSet-Stage-${AWS::Region}
                RunOrder: 1
                Region: !Ref AWS::Region

            - Name: DriftDetection
              ActionTypeId:
                Category: Invoke
                Owner: AWS
                Provider: Lambda
                Version: '1'
              Configuration:
                FunctionName: !Ref DriftDetectionLambda
              InputArtifacts:
                - Name: Source
              RunOrder: 2
              Region: !Ref AWS::Region

            - Name: ApproveChangeSet
              ActionTypeId:
                Category: Approval
                Owner: AWS
                Provider: Manual
                Version: '1'
              Configuration:
                NotificationArn: !Ref NotificationSnsTopic
                CustomData: "Do you want to approve to deploy the changes to production stacks?"
              RunOrder: 3
              Region: !Ref AWS::Region

            - !If
              - DeletingStage
              - Name: DeleteStageStack
                ActionTypeId:
                  Category: Deploy
                  Owner: AWS
                  Provider: CloudFormation
                  Version: '1'
                Configuration:
                  ActionMode: DELETE_ONLY
                  RoleArn: !GetAtt CloudFormationExecutionRole.Arn
                  StackName: !Sub ${AWS::StackName}-Stack-Stage-${AWS::Region}
                RunOrder: 4
                Region: !Ref AWS::Region
              - !Ref AWS::NoValue

            - Name: PutStageArtifact
              ActionTypeId:
                Category: Invoke
                Owner: AWS
                Provider: Lambda
                Version: '1'
              Configuration:
                FunctionName: !Ref StageArtifactSsmLambda
              InputArtifacts:
                - Name: Source
              RunOrder: !If [ DeletingStage, 5, 4 ]
              Region: !Ref AWS::Region

        - Name: !Sub Secondary-${SecondaryRegion}
          Actions:
            - Name: CreateUpdate
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: CloudFormation
                Version: '1'
              Configuration:
                ActionMode: CREATE_UPDATE
                RoleArn: !GetAtt CloudFormationExecutionRole.Arn
                StackName: !Sub ${AWS::StackName}-Stack-Secondary-${SecondaryRegion}
                TemplatePath: !Sub Source::${TemplatePath}
                Capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
                ParameterOverrides: !If [ HasSecondaryParameters, !Ref SecondaryParameters, !Ref "AWS::NoValue" ]
              InputArtifacts:
                - Name: Source
              RunOrder: 1
              Region: !Ref SecondaryRegion

        - Name: !Sub PrimaryProd-${AWS::Region}
          Actions:
            - Name: CreateUpdate
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: CloudFormation
                Version: '1'
              Configuration:
                ActionMode: CREATE_UPDATE
                RoleArn: !GetAtt CloudFormationExecutionRole.Arn
                StackName: !Sub ${AWS::StackName}-Stack-PrimaryProd-${AWS::Region}
                TemplatePath: !Sub Source::${TemplatePath}
                Capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
                ParameterOverrides: !If [ HasPrimaryParameters, !Ref PrimaryParameters, !Ref "AWS::NoValue" ]
              InputArtifacts:
                - Name: Source
              RunOrder: 1
              Region: !Ref AWS::Region

  InfraPipelineWebhook:
    Type: AWS::CodePipeline::Webhook
    Properties:
      Authentication: GITHUB_HMAC
      AuthenticationConfiguration:
        SecretToken: !Sub '{{resolve:secretsmanager:${SolutionSecretName}:SecretString:github-access-token}}'
      Filters:
        - JsonPath: "$.ref"
          MatchEquals: refs/heads/{Branch}
      TargetPipeline: !Ref InfraPipeline
      TargetAction: GitHub
      Name: !Sub ${AWS::StackName}-AppPipelineWebhook
      TargetPipelineVersion: !GetAtt InfraPipeline.Version
      RegisterWithThirdParty: true

  CfnNagBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          phases:
            install:
              runtime-versions:
                ruby: latest
              commands:
                - |
                  if [ $RUNNING_CFN_NAG = 'Yes' ]; then
                    echo Installing cfn_nag - `pwd`
                    gem install -V cfn-nag
                    cfn_nag -v
                    echo cfn_nag installation complete `date`
                  fi
            build:
              commands:
                - |
                  if [ $RUNNING_CFN_NAG = 'Yes' ]; then
                    echo Starting cfn_nag scanning `date` in `pwd`
                    cfn_nag --fail-on-warnings $TEMPLATE_PATH || ec=$?
                    if [ "$ec" -ne "0" ]; then ec=1; else ec=0; fi
                    echo Completed cfn_nag scanning `date`
                    exit $ec
                  fi
          artifacts:
            files: '*'
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:4.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: TEMPLATE_PATH
            Value: !Ref TemplatePath
          - Name: RUNNING_CFN_NAG
            Value: "Yes"
      ServiceRole: !Ref CodeBuildServiceRole
      EncryptionKey: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3

  CfnLintBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          phases:
            install:
              runtime-versions:
                python: latest
              commands:
                - |
                  if [ $RUNNING_CFN_LINT = 'Yes' ]; then
                    echo Installing cfn-lint - `pwd`
                    pip install cfn-lint
                    cfn-lint -v
                    echo cfn-lint installation complete `date`
                  fi
            build:
              commands:
                - |
                  if [ $RUNNING_CFN_LINT = 'Yes' ]; then
                    echo Starting cfn-lint validation of template $TEMPLATE_PATH for regions $PRIMARY_REGION and $SECONDARY_REGION at `date` in `pwd`
                    cfn-lint --template $TEMPLATE_PATH --regions $PRIMARY_REGION $SECONDARY_REGION || ec=$?
                    echo Completed cfn-lint validation `date`
                    if [ "$ec" -ne "0" ]; then ec=1; else ec=0; fi
                    exit $ec
                  fi
          artifacts:
            files: '*'
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:4.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: PRIMARY_REGION
            Value: !Ref AWS::Region
          - Name: SECONDARY_REGION
            Value: !Ref SecondaryRegion
          - Name: TEMPLATE_PATH
            Value: !Ref TemplatePath
          - Name: RUNNING_CFN_LINT
            Value: "Yes"
      ServiceRole: !Ref CodeBuildServiceRole
      EncryptionKey: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3

  UuidGeneratorRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub ${AWS::StackName}-StackNameFormatterRole
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/*

  UuidGeneratorLambda:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt UuidGeneratorRole.Arn
      Code:
        S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]]
        S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],  "uuid-generator.zip"]]
      Runtime:  nodejs12.x
      Timeout: 30

  UuidGenerator:
    Type: Custom::UuidGenerator
    Properties:
      ServiceToken: !GetAtt UuidGeneratorLambda.Arn

  SecondaryBucketCreatorLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: Lambda Custom Resource used to create a CodePipeline Artifact Store S3 bucket in a secondary region
      Handler: index.handler
      Role: !GetAtt SecondaryBucketCreatorRole.Arn
      Code:
        S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]]
        S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],  "secondary-bucket-creator.zip"]]
      Runtime:  nodejs12.x
      Timeout: 120

  SecondaryBucketCreator:
    Type: Custom::ArtifactStoreCreatorCustomResource
    Properties:
      ServiceToken: !GetAtt SecondaryBucketCreatorLambda.Arn
      PrimaryBucketName: !Ref PrimaryArtifactStore
      SecondaryRegion: !Ref SecondaryRegion

  SecondaryBucketCreatorRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: SecondaryBucketCreatorPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*
              - Effect: Allow
                Action:
                  - s3:CreateBucket
                  - s3:PutEncryptionConfiguration
                  - s3:PutBucketAcl
                  - s3:PutBucketPublicAccessBlock
                  - s3:PutBucketLogging
                Resource:
                  - arn:aws:s3:::* # We don't know the name or prefix of the bucket to be created at this point

  ChangeSetValidatorLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: Validates that a CloudFormation ChangeSet contains changes before pushing it through the rest of the CodePipeline
      Handler: index.handler
      Role: !GetAtt ChangeSetValidatorLambdaRole.Arn
      Code:
        S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]]
        S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],  "changeset-validator.zip"]]
      Runtime: nodejs12.x
      Timeout: 180
      Environment:
        Variables:
          STACK_NAME: !Sub ${AWS::StackName}-Stack-Stage-${AWS::Region}
          REGION: !Ref AWS::Region
          SOLUTION_ID: SO0086
          UUID: !GetAtt UuidGenerator.Value
          VERSION: SOLUTION_VERSION
          ARTIFACT_SOURCE: !Join ["", ["/", !Ref "AWS::StackName", "/", !FindInMap ["StageArtifact", "Parameter", "Source"]]]
          DELETE_STAGE_STACK: !Ref DeleteStageStack
          SEND_METRICS: !FindInMap ["Solution" ,"Metrics" ,"SendAnonymousData"]

  ChangeSetValidatorLambdaRole:
    Type: AWS::IAM::Role
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: >-
              codepipeline:PutJobFailureResult/PutJobSuccessResult - requires * resource
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub ${AWS::StackName}-ChangeSetValidatorLambdaRole-CustomPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*
              - Effect: Allow
                Action:
                  - cloudformation:DescribeChangeSet
                Resource:
                  - arn:aws:cloudformation:*:*:stack/*/*
              - Effect: Allow
                Action:
                  - codepipeline:PutJobFailureResult
                  - codepipeline:PutJobSuccessResult
                Resource:
                  - "*"
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                Resource:
                  - !Join ["/", [!Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}", !FindInMap ["StageArtifact", "Parameter", "Source"]]]

  CreateStageArtifactLambda:
    Type: AWS::Lambda::Function
    Condition: DeletingStage
    Properties:
      Description: Creates the stage artifact to prepare the previous version of the prod stack
      Handler: index.handler
      Role: !GetAtt CreateStageArtifactLambdaRole.Arn
      Code:
        S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]]
        S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],  "stage-artifact-creator.zip"]]
      Runtime: nodejs12.x
      Timeout: 120
      Environment:
        Variables:
          STACK_NAME: !Ref AWS::StackName
          ARTIFACT_SOURCE: !Join ["", ["/", !Ref "AWS::StackName", "/", !FindInMap ["StageArtifact", "Parameter", "Source"]]]
          ARTIFACT_PARAMETER: !Join ["", ["/", !Ref "AWS::StackName", "/", !FindInMap ["StageArtifact", "Parameter", "Parameter"]]]
          ARTIFACT_SOURCE_NAME: !FindInMap ["StageArtifact", "Name", "Source"]
          ARTIFACT_PARAMETER_NAME: !FindInMap ["StageArtifact", "Name", "Parameter"]

  CreateStageArtifactLambdaRole:
    Type: AWS::IAM::Role
    Condition: DeletingStage
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: codepipeline:PutJobFailureResult/PutJobSuccessResult - requires * resource
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub ${AWS::StackName}-CreateStageArtifactLambdaRole-CustomPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                Resource:
                  - !Sub arn:aws:s3:::${PrimaryArtifactStore}/*
              - Effect: Allow
                Action:
                  - s3:ListBucket
                Resource:
                  - !Sub arn:aws:s3:::${PrimaryArtifactStore}
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                Resource:
                  - !Join ["/", [!Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}", !FindInMap ["StageArtifact", "Parameter", "Source"]]]
                  - !Join ["/", [!Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}", !FindInMap ["StageArtifact", "Parameter", "Parameter"]]]
              - Effect: Allow
                Action:
                  - cloudformation:DescribeStacks
                Resource:
                  - !Sub arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*
              - Effect: Allow
                Action:
                  - codepipeline:PutJobFailureResult
                  - codepipeline:PutJobSuccessResult
                Resource:
                  - "*"

  StageArtifactSsmLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: Puts the stage artifact to SSM parameter store
      Handler: index.handler
      Role: !GetAtt StageArtifactSsmLambdaRole.Arn
      Code:
        S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]]
        S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],  "stage-artifact-putter.zip"]]
      Runtime: nodejs12.x
      Timeout: 60
      Environment:
        Variables:
          ARTIFACT_SOURCE: !Join ["", ["/", !Ref "AWS::StackName", "/", !FindInMap ["StageArtifact", "Parameter", "Source"]]]
          ARTIFACT_PARAMETER: !Join ["", ["/", !Ref "AWS::StackName", "/", !FindInMap ["StageArtifact", "Parameter", "Parameter"]]]
          CLOUDFORMATION_PARAMETERS: !If [ HasStageParameters, !Ref StageParameters, !Ref "AWS::NoValue" ]

  StageArtifactSsmLambdaRole:
    Type: AWS::IAM::Role
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: codepipeline:PutJobFailureResult/PutJobSuccessResult - requires * resource
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub ${AWS::StackName}-StageArtifactSsmLambdaRole-CustomPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*
              - Effect: Allow
                Action:
                  - ssm:PutParameter
                Resource:
                  - !Join ["/", [!Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}", !FindInMap ["StageArtifact", "Parameter", "Source"]]]
                  - !Join ["/", [!Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}", !FindInMap ["StageArtifact", "Parameter", "Parameter"]]]
              - Effect: Allow
                Action:
                  - codepipeline:PutJobFailureResult
                  - codepipeline:PutJobSuccessResult
                Resource:
                  - "*"

  DriftDetectionLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: Detects drift on primary and secondary stacks
      Handler: index.handler
      Role: !GetAtt DriftDetectionLambdaRole.Arn
      Code:
        S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]]
        S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],  "drift-detection.zip"]]
      Runtime: nodejs12.x
      Timeout: 900
      Environment:
        Variables:
          PRIMARY_STACK: !Sub ${AWS::StackName}-Stack-PrimaryProd-${AWS::Region}
          REGION: !Ref AWS::Region
          SECONDARY_STACK: !Sub ${AWS::StackName}-Stack-Secondary-${SecondaryRegion}
          SECONDARY_REGION: !Ref SecondaryRegion
          NOTIFICATION_SNS_ARN: !Ref NotificationSnsTopic

  DriftDetectionLambdaRole:
    Type: AWS::IAM::Role
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: >-
              cloudformation:DescribeStackDriftDetectionStatus - requires * resource
              codepipeline:PutJobFailureResult/PutJobSuccessResult - requires * resource
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub ${AWS::StackName}-StageArtifactSsmLambdaRole-CustomPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource:
                  - !Ref NotificationSnsTopic
              - Effect: Allow
                Action:
                  - cloudformation:DetectStackDrift
                  - cloudformation:DetectStackResourceDrift
                  - cloudformation:ListStackResources
                Resource:
                  - !Sub arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}-Stack-PrimaryProd-${AWS::Region}/*
                  - !Sub arn:aws:cloudformation:${SecondaryRegion}:${AWS::AccountId}:stack/${AWS::StackName}-Stack-Secondary-${SecondaryRegion}/*
              - Effect: Allow
                Action:
                  - cloudformation:DescribeStackDriftDetectionStatus
                Resource:
                  - "*"
              - Effect: Allow
                Action:
                  - codepipeline:PutJobFailureResult
                  - codepipeline:PutJobSuccessResult
                Resource:
                  - "*"

  RollbackChangeLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: Rolls back the changes of stage stack
      Handler: index.handler
      Role: !GetAtt RollbackChangeLambdaRole.Arn
      Code:
        S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]]
        S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],  "rollback-change.zip"]]
      Runtime:  nodejs12.x
      Timeout: 60
      Environment:
        Variables:
          STAGE_STACK_NAME: !Sub ${AWS::StackName}-Stack-Stage-${AWS::Region}
          PIPELINE_NAME: !Ref InfraPipeline
          STAGE_NAME: !Sub Stage-${AWS::Region}
          ACTION_NAME: ApproveChangeSet
          ARTIFACT_SOURCE: !Join ["", ["/", !Ref "AWS::StackName", "/", !FindInMap ["StageArtifact", "Parameter", "Source"]]]
          ARTIFACT_PARAMETER: !Join ["", ["/", !Ref "AWS::StackName", "/", !FindInMap ["StageArtifact", "Parameter", "Parameter"]]]
          DELETE_STAGE_STACK: !Ref DeleteStageStack
          TEMPLATE_PATH: !Ref TemplatePath

  RollbackChangeLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub ${AWS::StackName}-StageArtifactSsmLambdaRole-CustomPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                Resource:
                  - !Join ["/", [!Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}", !FindInMap ["StageArtifact", "Parameter", "Source"]]]
                  - !Join ["/", [!Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}", !FindInMap ["StageArtifact", "Parameter", "Parameter"]]]
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                Resource:
                  - !Sub arn:aws:s3:::${PrimaryArtifactStore}/*
              - Effect: Allow
                Action:
                  - cloudformation:UpdateStack
                  - cloudformation:DeleteStack
                Resource:
                  - !Sub arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}-Stack-Stage-${AWS::Region}/*

  RejectCloudWatchRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${AWS::StackName}-Reject-Rule-${AWS::Region}
      Description: The rule to trigger rollback when the change is not approved
      EventPattern: |
        {
          "source": [
            "aws.codepipeline"
          ],
          "detail-type": [
            "CodePipeline Action Execution State Change"
          ],
          "detail": {
            "state": [
              "FAILED"
            ]
          }
        }
      State: ENABLED
      Targets:
        - Arn: !GetAtt RollbackChangeLambda.Arn
          Id: !Sub ${AWS::StackName}-Reject-Lambda-${AWS::Region}

  RejectCloudWatchRuleLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt RollbackChangeLambda.Arn
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt RejectCloudWatchRule.Arn

  CustomResourceLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: Custom Resource Lambda Function
      Code:
        S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], !Ref "AWS::Region"]]
        S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"], "custom-resource.zip"]]
      Handler: index.handler
      Role: !GetAtt CustomResourceRole.Arn
      Runtime: nodejs12.x
      Timeout: 60
      MemorySize: 128

  CustomResourceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub ${AWS::StackName}-CustomResourceRole-CustomPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*

  SendAnonymousUsage:
    Type: Custom::SendAnonymousUsage
    Properties:
      ServiceToken: !GetAtt CustomResourceLambda.Arn
      SOLUTION_ID: SO0086
      UUID: !GetAtt UuidGenerator.Value
      VERSION: SOLUTION_VERSION
      SEND_METRICS: !FindInMap ["Solution" ,"Metrics" ,"SendAnonymousData"]