---
# TODO: export target bucket as a resource and delete it when the stack is deleted
AWSTemplateFormatVersion: '2010-09-09'
Description: Automation solution for archival of Amazon EBS snapshots.
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label: 
          default: Snapshot eligibility criteria
        Parameters:
          - SnapshotAge
          - Tags
          - ExecTime
      -
        Label: 
          default: Administrative options
        Parameters:
          - LogLevel
          - LogRetention
          - DryRun
    ParameterLabels:
      SnapshotAge:
        default: Use the following parameters to define the snapshots to be archived 
Parameters:
  SnapshotAge:
    Type: Number
    Default: 90
    Description: How old Amazon EBS Snapshots must be before archiving
  Tags:
    Type: String
    Description: 'Optional but recommended tag pattern to select snapshots. Syntax: tag:tag_name,Values=value1[,value2,value3,...] . Example: tag:Environment,Values=Production,Staging'
    AllowedPattern: (?:tag:[\w+-=\.:/@ ]{1,128},Values=[\w+-=\.:/@ ]{1,256})?|(?:^$)?
    ConstraintDescription: Invalid tag format
  ExecTime:
    Type: Number
    Description: UTC time to check for new Amazon EBS Snapshots eligible for archival
    Default: 4
    AllowedValues:
      - 0
      - 1
      - 2
      - 3
      - 4
      - 5
      - 6
      - 7
      - 8
      - 9
      - 10
      - 11
      - 12
      - 13
      - 14
      - 15
      - 16
      - 17
      - 18
      - 19
      - 20
      - 21
      - 22
      - 23
  LogRetention:
    Type: Number
    Default: 90
    Description: How long logs will be retained
    AllowedValues:
      - 1
      - 3
      - 5
      - 7
      - 14
      - 30
      - 60
      - 90
      - 120
      - 150
      - 180
      - 365
      - 400
      - 545
      - 731
      - 1827
      - 3653
  DryRun:
    Type: String
    Description: Dry-run enabled will only test the tool without any modification upon existent Amazon EBS Snapshots.
    Default: true
    AllowedValues: [true, false]
  LogLevel:
    Type: String
    Description: Controls how detailed logs will be
    Default: info
    AllowedValues: [debug, info, warning, error, critical]
Mappings:
  CloudFormationAttributes:
    SnapshotArchiveCustomResource:
      SourceBucket: 000789b4770c17eee592a0688e26cba6966e7ee63dcad8ba3f62d73e98bf725
      SourceFiles: 'SnapshotArchiveHandler.zip,SnapshotArchiveLayer.zip,SnapshotArchiveQueue.zip,SnapshotArchiveReport.zip'
      
Resources:
  SnapshotArchiveGetBucketName:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.handler
      Role: !GetAtt  SnapshotArchiveCustomResourceLambdaRole.Arn
      Runtime: python3.7
      Timeout: 120
      Code:
        ZipFile: |
          import logging
          import boto3
          from botocore.exceptions import ClientError
          import cfnresponse
          import os
          import string
          import sys

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          try:
              region = os.environ['AWS_REGION']
              s3_client = boto3.client('s3', region_name=region)
              s3_resource = boto3.resource('s3')
          except Exception as e:
              logger.exception(f'Could not create S3 client with region from Lambda runtime variable: {repr(e)}')
              raise
          def handler(event, context):
            Data = {}
            logger.info(f'event: {event}')
            try:
              token = event['ResourceProperties']['ServiceToken']
              token = token.split('-')[-1]
            except Exception as e:
              raise NameError('could not extract random string from ServiceToken')
            try:
                target_bucket_name = f'{event["LogicalResourceId"]}-{region}-{token}'.lower()
                #target_bucket_name = event['ResourceProperties']['TargetBucket']
                logger.info(f'Bucket name: {target_bucket_name}')
            except Exception as e:
                logger.exception(f'Failed to create bucket name based on ResourceProperties: {repr(e)}')
                raise

            try:
              if not 'TargetBucket' in event['ResourceProperties']:
                Data['BucketName'] = target_bucket_name
                cfnresponse.send(event, context, cfnresponse.SUCCESS, Data)
                sys.exit("S3 Bucket name provided")
            except Exception as e:
              cfnresponse.send(event, context, cfnresponse.FAILED, Data)
              sys.exit("Failed to provide S3 Bucket name")
  

  SnapshotArchiveCreateS3Function:
    Type: 'AWS::Lambda::Function'
    DependsOn: SnapshotArchiveCustomResourceDeletePolicy
    Properties:
      Code:
        ZipFile: |
          import logging
          import boto3
          from botocore.exceptions import ClientError
          import cfnresponse
          import os
          import string
          import sys

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          try:
              region = os.environ['AWS_REGION']
              s3_client = boto3.client('s3', region_name=region)
              s3_resource = boto3.resource('s3')
          except Exception as e:
              logger.exception(f'Could not create S3 client with region from Lambda runtime variable: {repr(e)}')
              raise
          def create_bucket(bucket_name, region=None):
              """Create an S3 bucket in a specified region

              :param bucket_name: Bucket to create
              :param region: String region to create bucket in, e.g., 'us-west-2'
              :return: True if bucket created, else False

              """
              # Create bucket
              return_code = True
              try:
                location = {'LocationConstraint': region}
                if region == 'us-east-1':
                  s3_client.create_bucket(
                    Bucket=bucket_name
                  )
                else:
                  s3_client.create_bucket(
                      Bucket=bucket_name, 
                      CreateBucketConfiguration=location
                  )
              except ClientError as e:
                  if e.response['Error']['Code'] in ['BucketAlreadyExists', 'BucketAlreadyOwnedByYou']:
                      logger.info(f'Bucket {bucket_name} already exists')
                      return_code = True
                  else:
                      logger.exception(e)
                      return_code = False
              return return_code

          def delete_bucket(bucket_name, recursive=False):
              """Delete an S3 bucket

              :param bucket_name: Bucket to delete
              :param recursive: Delete objects in the bucket first. Non empty buckets will fail to delete if this option is False.
              :return: True if bucket has been deleted, else False

              """
              
              bucket = s3_resource.Bucket(bucket_name)
              
              if recursive:
                # Delete objects
                try:         
                  bucket.objects.all().delete()
                except ClientError as e:
                  if e.response['Error']['Code'] in ['NoSuchBucket']:
                    #Bucket not found while trying to delete. Assume successful request
                    return True
                  logger.warning(f'Failed to remove objects from S3 bucket: {repr(e)}')
                  return False

              # Delete bucket
              try:
                bucket.delete()
              except ClientError as e:
                logger.warning(f'Failed to delete S3 bucket: {repr(e)}')
                return False

              return True

          def copy_data(source_bucket=None, source_files=None, target_bucket_name=None):
              if None in [source_bucket, source_files, target_bucket_name]:
                  raise NameError('source_bucket, source_files, and target_bucket_name are mandatory parameters')
              
              keys = source_files
              source_params = {
                  'Bucket': source_bucket,
                  'Key': ''
              }
              for key in keys:
                  source_params['Key'] = key
                  copy_response = s3_resource.meta.client.copy(source_params, target_bucket_name, key)
                  logger.info(f'Copy from s3://{source_params["Bucket"]}/{key} to s3://{target_bucket_name}/{key} requested')

          def handler(event, context):
              Data = {}
              logger.info(f'event: {event}')
              # try:
              #   token = event['ResourceProperties']['ServiceToken']
              #   token = token.split('-')[-1]
              # except Exception as e:
              #   raise NameError('could not extract random string from ServiceToken')
              try:
                  #target_bucket_name = f'{event["LogicalResourceId"]}-{region}-{token}'.lower()
                  target_bucket_name = event['ResourceProperties']['TargetBucket']
                  logger.info(f'Bucket name: {target_bucket_name}')
              except Exception as e:
                  logger.exception(f'Failed to create bucket name based on ResourceProperties: {repr(e)}')
                  raise

              try:
                  source_bucket = event['ResourceProperties']['SourceBucket']
                  source_files = event['ResourceProperties']['SourceFiles']
                  logger.info(f'source_bucket: {source_bucket}')
                  logger.info(f'source_files: {source_files}')
              except Exception as e:
                  logger.exception(f'SourceBucket not found in ResourceProperties: {event} ')

              try:
                  source_files = event['ResourceProperties']['SourceFiles']
                  source_files = source_files.split(',')
              except Exception as e:
                  logger.exception(f'SourceBucket not found in ResourceProperties or is invalid. event: {event} , exception: {repr(e)}')


              try:
                  if event['RequestType'] == 'Create':
                      bucket_action=create_bucket(target_bucket_name, region=region)
                      if bucket_action:
                          copy_data(source_bucket=source_bucket, source_files=source_files, target_bucket_name=target_bucket_name)
                          result = 'created'
                          Data['BucketName'] = target_bucket_name
                      else:
                          result = 'not created'
                  elif event['RequestType'] == 'Update':
                      copy_data(source_bucket=source_bucket, source_files=source_files, target_bucket_name=target_bucket_name)
                      Data['BucketName'] = target_bucket_name
                  elif event['RequestType'] == 'Delete':
                      bucket_action=delete_bucket(target_bucket_name,recursive=True)
                      if bucket_action:
                          result = 'deleted'
                      else:
                          result = 'not deleted'
                  
                  if bucket_action:
                      logger.info(f'S3 bucket {target_bucket_name} {result} succesfully')
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, Data)
                  else:
                      logger.exception(f'S3 bucket {target_bucket_name} {result}')
                      cfnresponse.send(event, context, cfnresponse.FAILED, Data)
              except Exception as e:
                  logger.exception(f'Failure to create S3 bucket: {repr(e)}')
                  cfnresponse.send(event, context, cfnresponse.FAILED, Data)

      #FunctionName: SnapshotArchivalTool-CustomResource
      Handler: index.handler
      Role: !GetAtt  SnapshotArchiveCustomResourceLambdaRole.Arn
      Runtime: python3.7
      Timeout: 120

  SnapshotArchiveQueueLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub
        - '/aws/lambda/${SnapshotArchiveCreateS3Function}'
        - SnapshotArchiveCreateS3Function: !Ref SnapshotArchiveCreateS3Function
      RetentionInDays: !Ref 'LogRetention'

  SnapshotArchiveCustomResourceLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          Action: sts:AssumeRole
          Effect: Allow
          Principal:
            Service: lambda.amazonaws.com
        Version: '2012-10-17'
      Description: Role for Amazon EBS Snapshot archival automation tool 
      ManagedPolicyArns:
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
  SnapshotArchiveCustomResourceLambdaPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      #PolicyName: !Sub 'SnapshotArchiveCustomResourceLambdaPolicy-${AWS::Region}'
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - s3:GetObject
              - s3:GetBucketLocation
              - s3:CreateBucket
              - s3:PutObject
            Resource:
              - !Sub 'arn:${AWS::Partition}:s3:::*'
              - !Sub 'arn:${AWS::Partition}:s3:::*/*'
        Version: '2012-10-17'
      Roles: 
        - !Ref SnapshotArchiveCustomResourceLambdaRole
      
  SnapshotArchiveCustomResourceDeletePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      #PolicyName: !Sub 'SnapshotArchiveCustomResourceDeletePolicy-${AWS::Region}'
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - s3:DeleteBucket
              - s3:DeleteObject
              - s3:ListBucket
            Resource:
              - !Sub 
                - 'arn:${AWS::Partition}:s3:::${TargetBucket}'
                - TargetBucket: !GetAtt SnapshotArchiveFirstCustomResource.BucketName
              - !Sub
                - 'arn:${AWS::Partition}:s3:::${TargetBucket}/*'
                - TargetBucket: !GetAtt SnapshotArchiveFirstCustomResource.BucketName
        Version: '2012-10-17'
      Roles: 
        - !Ref SnapshotArchiveCustomResourceLambdaRole

  SnapshotArchiveFirstCustomResource:
    Type: Custom::CreateS3BucketName
    Properties:
      ServiceToken: !GetAtt SnapshotArchiveGetBucketName.Arn
  
  SnapshotArchiveSecondCustomResource:
    Type: Custom::CreateS3Bucket
    Properties:
      ServiceToken: !GetAtt SnapshotArchiveCreateS3Function.Arn
      SourceBucket: !FindInMap [ CloudFormationAttributes, SnapshotArchiveCustomResource, SourceBucket ]
      SourceFiles: !FindInMap [ CloudFormationAttributes, SnapshotArchiveCustomResource, SourceFiles ]
      TargetBucket: !GetAtt SnapshotArchiveFirstCustomResource.BucketName


  ChildStack:
    Type: AWS::CloudFormation::Stack
    DependsOn: SnapshotArchiveSecondCustomResource
    Properties:
      TemplateURL: !Sub 
        - 'https://${SourceBucket}.s3.amazonaws.com/SnapshotArchiveCloudformationTemplate.yaml'
        - SourceBucket: !FindInMap 
          - CloudFormationAttributes
          - SnapshotArchiveCustomResource
          - SourceBucket
      Parameters:
        SnapshotAge: !Ref SnapshotAge
        Tags: !Ref Tags
        ExecTime: !Ref ExecTime
        LogRetention: !Ref LogRetention
        LogLevel: !Ref LogLevel
        S3BucketName: !GetAtt SnapshotArchiveFirstCustomResource.BucketName
        DryRun: !Ref DryRun
