AWSTemplateFormatVersion: 2010-09-09
Description: >
  Deploys a solution to demonstrate how to configure multiple retention policies in Amazon Connect.
Parameters:
  ConnectInstanceId:
    Type: String
    AllowedPattern: '\w{8}-\w{4}-\w{4}-\w{4}-\w{12}'
    ConstraintDescription: 'Invalid Amazon Connect Instance ID'
    Description: Enter the ID of the Amazon Connect Instance that you would like to deploy the solution for.
  BasicQueueId:
    Type: String
    AllowedPattern: '\w{8}-\w{4}-\w{4}-\w{4}-\w{12}'
    Description: Enter the ID of the Amazon Connect Basic Queue.
    ConstraintDescription: 'Invalid Basic Queue ID'
  RecordingsBucketName:
    Type: String
    AllowedPattern: ^[a-z0-9][a-z0-9.-]{3,63}[a-z0-9]$
    Description: Enter the S3 bucket name that the recordings are stored at.
    ConstraintDescription: The bucket name must be between 3 and 63 characters long and can contain only lowercase letters, numbers, periods, and hyphens. The bucket name must start and end with a lowercase letter or number.
    MinLength: 3
    MaxLength: 63
  KinesisDataStreamName: 
    Type: String
    AllowedPattern: ^[a-zA-Z0-9_.-]{1,128}$
    Description: Enter a name for the Kinesis data stream that will be created.
    ConstraintDescription: The stream name must be between 1 and 128 characters long and can only contain alphanumeric characters, underscores, periods, and dashes.
    Default: multiple-retention-ctr
    MinLength: 1
    MaxLength: 128
Resources:
  ContactFlow:
    Type: 'AWS::Connect::ContactFlow'
    Properties:
      Name: !Join 
        - ''
        - ['Configure multiple retention policies - ', !Select [2, !Split ['/', !Ref AWS::StackId]]]
      Description: Inbound Contact Flow used to configure multiple retention policies
      InstanceArn:
        !Join
          - ''
          - - 'arn:aws:connect:'
            - !Ref AWS::Region
            - ':'
            - !Ref AWS::AccountId
            - ':'
            - 'instance/'
            - !Ref ConnectInstanceId
      Type: CONTACT_FLOW
      Content: !Sub
        - "{\"Version\":\"2019-10-30\",\"StartAction\":\"e2adde80-e5f8-47d2-af42-c243873ec64d\",\"Metadata\":{\"entryPointPosition\":{\"x\":40,\"y\":40},\"ActionMetadata\":{\"e2adde80-e5f8-47d2-af42-c243873ec64d\":{\"position\":{\"x\":254.4,\"y\":161.6}},\"ea10d493-0443-481a-9ee6-3ba92a4ac499\":{\"position\":{\"x\":540.8,\"y\":272}},\"bb51b05d-de82-4638-afcb-3ca73130d019\":{\"position\":{\"x\":1221.6,\"y\":58.4},\"dynamicParams\":[]},\"0f27836c-27eb-4911-84e4-f41c78a74f38\":{\"position\":{\"x\":1218.4,\"y\":343.2},\"dynamicParams\":[]},\"385efe7f-cbc2-4fa1-9838-e24cc6b88d4e\":{\"position\":{\"x\":1805.6,\"y\":548}},\"89b621e4-fb72-440f-976a-eedade48f1b5\":{\"position\":{\"x\":2064,\"y\":604.8}},\"2c15d3ac-aed9-4ffc-b9df-80d16c5fc312\":{\"position\":{\"x\":846.4,\"y\":264.8},\"conditionMetadata\":[{\"id\":\"594503c4-caee-443e-b688-d663ac3451a8\",\"value\":\"1\"},{\"id\":\"8c2b48ac-85fd-4662-90dc-db47389ee629\",\"value\":\"2\"}]},\"9510bdbe-aa5c-4e66-b834-fdaecc20c548\":{\"position\":{\"x\":1546.4,\"y\":484},\"parameters\":{\"QueueId\":{\"displayName\":\"BasicQueue\"}},\"queue\":{\"text\":\"BasicQueue\"}},\"ca676a12-6337-4f6f-8ef9-b7a5062f2569\":{\"position\":{\"x\":1220.8,\"y\":680.8}}},\"name\":\"Configure multiple retention policies\",\"description\":\"\",\"type\":\"contactFlow\",\"status\":\"published\",\"hash\":{}},\"Actions\":[{\"Parameters\":{\"FlowLoggingBehavior\":\"Enabled\"},\"Identifier\":\"e2adde80-e5f8-47d2-af42-c243873ec64d\",\"Type\":\"UpdateFlowLoggingBehavior\",\"Transitions\":{\"NextAction\":\"ea10d493-0443-481a-9ee6-3ba92a4ac499\"}},{\"Parameters\":{\"RecordingBehavior\":{\"RecordedParticipants\":[\"Agent\",\"Customer\"]}},\"Identifier\":\"ea10d493-0443-481a-9ee6-3ba92a4ac499\",\"Type\":\"UpdateContactRecordingBehavior\",\"Transitions\":{\"NextAction\":\"2c15d3ac-aed9-4ffc-b9df-80d16c5fc312\"}},{\"Parameters\":{\"Attributes\":{\"retention\":\"short\"}},\"Identifier\":\"bb51b05d-de82-4638-afcb-3ca73130d019\",\"Type\":\"UpdateContactAttributes\",\"Transitions\":{\"NextAction\":\"9510bdbe-aa5c-4e66-b834-fdaecc20c548\",\"Errors\":[{\"NextAction\":\"9510bdbe-aa5c-4e66-b834-fdaecc20c548\",\"ErrorType\":\"NoMatchingError\"}]}},{\"Parameters\":{\"Attributes\":{\"retention\":\"long\"}},\"Identifier\":\"0f27836c-27eb-4911-84e4-f41c78a74f38\",\"Type\":\"UpdateContactAttributes\",\"Transitions\":{\"NextAction\":\"9510bdbe-aa5c-4e66-b834-fdaecc20c548\",\"Errors\":[{\"NextAction\":\"9510bdbe-aa5c-4e66-b834-fdaecc20c548\",\"ErrorType\":\"NoMatchingError\"}]}},{\"Parameters\":{},\"Identifier\":\"385efe7f-cbc2-4fa1-9838-e24cc6b88d4e\",\"Type\":\"TransferContactToQueue\",\"Transitions\":{\"NextAction\":\"89b621e4-fb72-440f-976a-eedade48f1b5\",\"Errors\":[{\"NextAction\":\"89b621e4-fb72-440f-976a-eedade48f1b5\",\"ErrorType\":\"QueueAtCapacity\"},{\"NextAction\":\"89b621e4-fb72-440f-976a-eedade48f1b5\",\"ErrorType\":\"NoMatchingError\"}]}},{\"Parameters\":{},\"Identifier\":\"89b621e4-fb72-440f-976a-eedade48f1b5\",\"Type\":\"DisconnectParticipant\",\"Transitions\":{}},{\"Parameters\":{\"StoreInput\":\"False\",\"InputTimeLimitSeconds\":\"5\",\"Text\":\"Welcome. This call will be recorded. Press 1 to configure short retention, press 2 to configure long retention.\"},\"Identifier\":\"2c15d3ac-aed9-4ffc-b9df-80d16c5fc312\",\"Type\":\"GetParticipantInput\",\"Transitions\":{\"NextAction\":\"ca676a12-6337-4f6f-8ef9-b7a5062f2569\",\"Conditions\":[{\"NextAction\":\"bb51b05d-de82-4638-afcb-3ca73130d019\",\"Condition\":{\"Operator\":\"Equals\",\"Operands\":[\"1\"]}},{\"NextAction\":\"0f27836c-27eb-4911-84e4-f41c78a74f38\",\"Condition\":{\"Operator\":\"Equals\",\"Operands\":[\"2\"]}}],\"Errors\":[{\"NextAction\":\"ca676a12-6337-4f6f-8ef9-b7a5062f2569\",\"ErrorType\":\"InputTimeLimitExceeded\"},{\"NextAction\":\"ca676a12-6337-4f6f-8ef9-b7a5062f2569\",\"ErrorType\":\"NoMatchingCondition\"},{\"NextAction\":\"9510bdbe-aa5c-4e66-b834-fdaecc20c548\",\"ErrorType\":\"NoMatchingError\"}]}},{\"Parameters\":{\"QueueId\":\"${BASICQUEUEID}\"},\"Identifier\":\"9510bdbe-aa5c-4e66-b834-fdaecc20c548\",\"Type\":\"UpdateContactTargetQueue\",\"Transitions\":{\"NextAction\":\"385efe7f-cbc2-4fa1-9838-e24cc6b88d4e\",\"Errors\":[{\"NextAction\":\"385efe7f-cbc2-4fa1-9838-e24cc6b88d4e\",\"ErrorType\":\"NoMatchingError\"}]}},{\"Parameters\":{\"Text\":\"Please make a selection.\"},\"Identifier\":\"ca676a12-6337-4f6f-8ef9-b7a5062f2569\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"2c15d3ac-aed9-4ffc-b9df-80d16c5fc312\",\"Errors\":[{\"NextAction\":\"9510bdbe-aa5c-4e66-b834-fdaecc20c548\",\"ErrorType\":\"NoMatchingError\"}]}}]}"
        - BASICQUEUEID:
            !Join
              - ''
              - - 'arn:aws:connect:'
                - !Ref AWS::Region
                - ':'
                - !Ref AWS::AccountId
                - ':'
                - 'instance/'
                - !Ref ConnectInstanceId
                - '/queue/'
                - !Ref BasicQueueId  
      State: ACTIVE
  KinesisStream:
        Type: "AWS::Kinesis::Stream"
        Properties:
            Name: !Ref KinesisDataStreamName
            StreamModeDetails: 
                StreamMode: "ON_DEMAND"
  LambdaFunctionTagS3Objects:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          const AWS = require('aws-sdk');
          var s3 = new AWS.S3();

          exports.handler = (event, context, callback)  => {
              
              console.log(JSON.stringify(event)); // raw event
              
              let data = event.Records[0].kinesis.data;
              let buff = new Buffer.from(data, 'base64');
              let text = buff.toString('utf-8');
              console.log(text); // event parced to utf-8
              
              let ctr = JSON.parse(text);
              
              let retentionValue = ctr.Attributes?.retention;
              
              if(retentionValue && ctr.Recordings.length > 0){

                  ctr.Recordings.forEach(recording => {
                      let recordingBucketName = recording.Location.split('/')[0];
                      let key = recording.Location.replace(`${recordingBucketName}/`,'');
                      
                      var params = {
                          Bucket: recordingBucketName, 
                          Key: key, 
                          Tagging: {
                              TagSet: [
                                  {
                                      Key: 'retention', 
                                      Value: retentionValue
                                  }
                              ]
                          }
                      }
                      
                      s3.putObjectTagging(params, function(err, data) {
                          if (err) console.log(err, err.stack); // an error occurred
                          else     {
                              console.log(`Added retention tag with value ${retentionValue} to recording with key ${key} in bucket ${recordingBucketName}`);
                          }; 
                      });
          
                  });
              }
          };
      Handler: "index.handler"
      Role: !GetAtt IamRoleS3PutTag.Arn
      Runtime: "nodejs16.x"     
  IamRoleS3PutTag:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: lambda.amazonaws.com
        Path: '/'
        Policies:
          -
            PolicyName: tag-s3-for-retention
            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/*"
                -
                  Effect: "Allow"
                  Action:
                    - 'kinesis:Get*'
                    - 'kinesis:List*'
                    - 'kinesis:Describe*'
                  Resource:
                    - !GetAtt KinesisStream.Arn
                -
                  Effect: "Allow"
                  Action:
                    - 's3:PutObjectTagging'
                  Resource:
                    - !Sub 'arn:aws:s3:::${RecordingsBucketName}/*'
  KinesisStreamEventSourceMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      BatchSize: 100
      Enabled: true
      EventSourceArn: !GetAtt KinesisStream.Arn
      FunctionName: !GetAtt LambdaFunctionTagS3Objects.Arn
      StartingPosition: LATEST
  LambdaFunctionCreateLifecycleConfiguration:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: nodejs16.x
      Handler: index.handler
      Timeout: 30
      Code:
        ZipFile: |
          const AWS = require('aws-sdk');
          const response = require('cfn-response');
          const s3 = new AWS.S3();

          exports.handler = async (event, context) => {
              
              console.log(JSON.stringify(event));

              const bucketName = process.env.BucketName;
              const requestType = event.RequestType;
              console.log(`Received event with type ${requestType}`);
              var result = { responseStatus: 'FAILED', responseData: { Data: 'Never updated' } };

              const lifecycleConfiguration = {
                  Rules: [
                      {
                          Status: 'Enabled',
                          ID: 'ShortRetention',
                          Filter: {
                              Tag: {
                                  Key: 'retention',
                                  Value: 'short'
                              }
                          },
                          Expiration: {
                              Days: 30
                          }
                      },
                      {
                          Status: 'Enabled',
                          ID: 'LongRetention',
                          Filter: {
                              Tag: {
                                  Key: 'retention',
                                  Value: 'long'
                              }
                          },
                          Expiration: {
                              Days: 90
                          }
                      }
                  ]
              };

              try {

                  try {
                      var existingConfiguration = await s3.getBucketLifecycleConfiguration({ Bucket: bucketName }).promise();
                      var existingRules = existingConfiguration.Rules;

                      console.log('Existing S3 lifecycle rules:');
                      console.log(existingRules);
                      var configExist = true;
                  }
                  catch (e) {
                      if (e.code === 'NoSuchLifecycleConfiguration') {
                          configExist = false;
                      }
                      else {
                          console.log(JSON.stringify(e));
                          throw e;
                      }
                  }

                  if (requestType === 'Create' || requestType === 'Update') {

                      if (configExist) {
                          const updatedRules = existingRules.concat(lifecycleConfiguration.Rules);
                          await s3.putBucketLifecycleConfiguration({
                              Bucket: bucketName,
                              LifecycleConfiguration: { Rules: updatedRules }
                          }).promise();

                          console.log('Updated S3 lifecycle rules:');
                          console.log(updatedRules);
                      }
                      else {
                          await s3.putBucketLifecycleConfiguration({
                              Bucket: bucketName,
                              LifecycleConfiguration: lifecycleConfiguration
                          }).promise();

                          console.log('Lifecycle rules added successfully.');
                      }

                      result.responseStatus = 'SUCCESS';
                      result.responseData['Data'] = 'Lifecycle rules added successfully.';
                  }
                  else if (requestType === 'Delete') {

                      if (configExist) {

                          const rulesToDelete = ['LongRetention', 'ShortRetention'];
                          const updatedRules = existingRules.filter(rule => !rulesToDelete.includes(rule.ID));

                          if (updatedRules.length > 0) {
                              await s3.putBucketLifecycleConfiguration({
                                  Bucket: bucketName,
                                  LifecycleConfiguration: { Rules: updatedRules }
                              }).promise();

                              console.log('Updated S3 lifecycle rules:');
                              console.log(updatedRules);

                          }
                          else {
                              await s3.deleteBucketLifecycle({
                                  Bucket: bucketName
                              }).promise();

                              console.log('Deleted S3 lifecycle rules.');
                          }
                      }

                      console.log('Lifecycle rules deleted successfully.');

                      result.responseStatus = 'SUCCESS';
                      result.responseData['Data'] = 'Lifecycle rules deleted successfully.';
                  }

              }
              catch (error) {
                  console.error('Error handling lifecycle operation ', error);
                  result.responseStatus = 'FAILED';
                  result.responseData['Data'] = 'Failed to process event';
              }
              finally {
                  return await responsePromise(event, context, result.responseStatus, result.responseData, `mainstack`);
              }
          };

          function responsePromise(event, context, responseStatus, responseData, physicalResourceId) {
              return new Promise(() => response.send(event, context, responseStatus, responseData, physicalResourceId));
          }

      Role: !GetAtt IamRoleLifecycleConfiguration.Arn
      Environment:
        Variables:
          BucketName: !Ref RecordingsBucketName
  IamRoleLifecycleConfiguration:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: LambdaExecutionPolicy
          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/*"
              - Effect: Allow
                Action:
                  - s3:GetLifecycleConfiguration
                  - s3:PutLifecycleConfiguration
                Resource: !Sub 'arn:aws:s3:::${RecordingsBucketName}'
  LambdaCustomResourceLifecycleConfiguration:
    Type: Custom::LambdaCustomResource
    Properties:
      ServiceToken: !GetAtt LambdaFunctionCreateLifecycleConfiguration.Arn
      BucketName: !Ref RecordingsBucketName
