AWSTemplateFormatVersion: '2010-09-09'
Description: 'This AWS solution automates the generation of Seekable OCI (SOCI) index artifacts and stores them in Amazon ECR. It provides an easy way for customers to try SOCI technology to lazily load container images. CFN AWS SOCI Index Builder asynchronously generates SOCI index artifacts triggered by image pushes to designated Amazon ECR repositories and stores them in the repository by the images they reference. (qs-1tsurulid)'

Parameters:
  SociRepositoryImageTagFilters:
    Description: >
      Comma-separated list of SOCI repository image tag filters. Each filter is a
      repository name followed by a colon, ":" and followed by a tag. Both repository
      names and tags may contain wildcards denoted by an asterisk, "*". For example,
      "prod*:latest" matches all images tagged with "latest" that are pushed to any
      repositories that start with "prod", while "dev:*" matches all images pushed
      to the "dev" repository. Use "*:*" to match all images pushed to all
      repositories in your private registry. This stack builds a SOCI index for
      any images that are pushed to your private registry after this stack is
      created and match at least one filter. Empty values are NOT accepted.
    Type: CommaDelimitedList
    Default: '*:*'
    AllowedPattern: '^(?:[a-z0-9\*]+(?:[._-][a-z0-9\*]+)*\/)*[a-z0-9\*]+(?:[._-][a-z0-9\*]+)*(?::[a-z0-9\*]+(?:[._-][a-z0-9\*]+)*)$'
  QSS3BucketName: 
    AllowedPattern: ^[0-9a-z]+([0-9a-z-\.]*[0-9a-z])*$
    ConstraintDescription: >-
      The S3 bucket name can include numbers, lowercase letters, 
      and hyphens (-), but it cannot start or end with a hyphen.
    Default: aws-quickstart
    Description: >-
      Name of the S3 bucket for your copy of the deployment assets. Keep the default
      name unless you are customizing the template. Changing the name updates code 
      references to point to a new location.
    MinLength: 3
    MaxLength: 63
    Type: String
  QSS3KeyPrefix:
    AllowedPattern: ^([0-9a-zA-Z!-_\.\*'\(\)/]+/)*$
    ConstraintDescription: >-
      The S3 key prefix can include numbers, lowercase letters, uppercase letters, 
      hyphens (-), underscores (_), periods (.), asterisks (*), single quotes ('),
      open parenthesis ((), close parenthesis ()), and forward slashes (/). End the
      prefix with a forward slash.
    Default: cfn-ecr-aws-soci-index-builder/
    Description: >-
      S3 key prefix that is used to simulate a folder for your copy of the 
      deployment assets. Keep the default prefix unless you are customizing 
      the template. Changing the prefix updates code references to point to 
      a new location.
    Type: String
  IamPermissionsBoundaryArn:
    Type: String
    Description: >-
      IAM Roles might require an IAM Permissions boundary in order to be created and
      perform subsequent API calls to services, IAM itself included.
      This parameter expects the ARN of an IAM policy, or to be set to none.
      If set to None, the PermissionsBoundary property is omitted on IAM Role creation.
    Default: none
    AllowedPattern: none|^arn:(?:aws|aws-(?:us-gov|cn)):iam::[\d]{12}:policy/[0-9a-zA-Z!-_\.\*'\(\)/]+$

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: SOCI Index Builder configuration
        Parameters:
          - SociRepositoryImageTagFilters
      - Label:
          default: AWS Partner Solution configuration
        Parameters:
          - QSS3BucketName
          - QSS3KeyPrefix

      - Label:
          default: AWS IAM Settings
        Parameters:
          - IamPermissionsBoundaryArn

    ParameterLabels:
      SociRepositoryImageTagFilters:
        default: SOCI repository image tag filters
      QSS3BucketName:
        default: Partner Solution S3 bucket name
      QSS3KeyPrefix:
        default: Partner Solution S3 key prefix
      IamPermissionsBoundaryArn:
        default: IAM Permissions Boundary (optional, default none)


Conditions:
  UsingDefaultBucket: 
    !Equals [!Ref QSS3BucketName, "aws-quickstart"]
  UsePermissionsBoundary: !Not [!Equals [!Ref IamPermissionsBoundaryArn, "none"]]

Resources:
  ECRImageActionEventFilteringLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: >
        Given an Amazon ECR image action event from EventBridge, matches event detail.repository-name 
        and detail.image-tag against one or more known patterns and invokes Executor Lambda with the same event on a match.
      Handler: ecr_image_action_event_filtering_lambda_function.lambda_handler
      Runtime: python3.9
      Role: !GetAtt ECRImageActionEventFilteringLambdaRole.Arn
      Timeout: 900
      Code:
        S3Bucket:
          !If [
            UsingDefaultBucket,
            !Sub "${QSS3BucketName}-${AWS::Region}",
            !Ref QSS3BucketName,
          ]
        S3Key: !Sub "${QSS3KeyPrefix}functions/packages/ecr-image-action-event-filtering/lambda.zip"
      Environment:
        Variables:
          soci_repository_image_tag_filters:
            !Join [ ",", !Ref SociRepositoryImageTagFilters ]
          soci_index_generator_lambda_arn:
            !GetAtt SociIndexGeneratorLambda.Arn

  ECRImageActionEventFilteringLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary:
        Fn::If:
          - UsePermissionsBoundary
          - !Ref IamPermissionsBoundaryArn
          - !Ref "AWS::NoValue"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: "lambda.amazonaws.com"

  ECRImageActionEventFilteringLambdaCloudwatchPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: ECRImageActionEventFilteringLambdaCloudwatchPolicy
      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/${ImageActionEventFilteringLambdaId}:*"
                - ImageActionEventFilteringLambdaId: !Ref ECRImageActionEventFilteringLambda
      Roles:
        - Ref: "ECRImageActionEventFilteringLambdaRole"

  ECRImageActionEventFilteringLambdaInvokeSociIndexGeneratorLambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: ECRImageActionEventFilteringLambdaInvokeSociIndexGeneratorLambdaPolicy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - "lambda:InvokeFunction"
              - "lambda:InvokeAsync"
            Resource:
              - !GetAtt SociIndexGeneratorLambda.Arn
      Roles:
        - Ref: "ECRImageActionEventFilteringLambdaRole"

  ECRImageActionEventBridgeRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "Invokes Amazon ECR image action event filtering Lambda function when image is successfully pushed to ECR."
      EventPattern:
        source: ["aws.ecr"]
        detail-type: ["ECR Image Action"]
        detail:
          action-type: [ "PUSH" ]
          result: [ "SUCCESS" ]
        region:
          - !Sub ${AWS::Region}
      Name: "ECRImageActionEventBridgeRule"
      State: "ENABLED"
      Targets:
        - Id: "ecr-image-action-lambda-target"
          Arn: !GetAtt ECRImageActionEventFilteringLambda.Arn

  ECRImageActionEventFilteringLambdaInvokePermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !Ref ECRImageActionEventFilteringLambda
      Principal: "events.amazonaws.com"
      SourceArn: !GetAtt ECRImageActionEventBridgeRule.Arn

  SociIndexGeneratorLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: >
        Given an Amazon ECR container repository and image, Lambda generates image SOCI artifacts and pushes to repository.
      Handler: main
      Runtime: provided.al2
      Role: !GetAtt SociIndexGeneratorLambdaRole.Arn
      Timeout: 900
      Code:
        S3Bucket:
          !If [
            UsingDefaultBucket,
            !Sub "${QSS3BucketName}-${AWS::Region}",
            !Ref QSS3BucketName,
          ]
        S3Key: !Sub "${QSS3KeyPrefix}functions/packages/soci-index-generator-lambda/soci_index_generator_lambda.zip"
      EphemeralStorage:
        Size: 10240  # 10GB - default is 512MB
      MemorySize: 1024

  SociIndexGeneratorLambdaCloudwatchPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: SociIndexGeneratorLambdaCloudwatchPolicy
      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/${SociIndexGeneratorLambdaId}:*"
                  - SociIndexGeneratorLambdaId: !Ref SociIndexGeneratorLambda
      Roles:
        - Ref: "SociIndexGeneratorLambdaRole"

  RepositoryNameParsingLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary:
        Fn::If:
          - UsePermissionsBoundary
          - !Ref IamPermissionsBoundaryArn
          - !Ref "AWS::NoValue"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: "lambda.amazonaws.com"

  RepositoryNameParsingLambda:
    Type: "AWS::Lambda::Function"
    Properties:
      FunctionName: RepositoryNameParsingLambda
      Handler: index.handler
      Runtime: python3.9
      Role: !GetAtt RepositoryNameParsingLambdaRole.Arn
      Code:
        ZipFile: !Sub |
          import json
          import cfnresponse

          def handler(event, context):
            filters = event['ResourceProperties']['filters']
            REPO_PREFIX = 'arn:${AWS::Partition}:ecr:${AWS::Region}:${AWS::AccountId}:repository/'
            repository_arns = []
            response = {}

            try:
              repositories = [filter.split(':')[0] for filter in filters]
              for repository in repositories:
                if repository == '*':
                  repository_arns = [REPO_PREFIX + '*']
                  break

                repository_arns.append(REPO_PREFIX + repository)

              response['repository_arns'] = repository_arns
              cfnresponse.send(event, context, cfnresponse.SUCCESS, response)
            except Exception:
              cfnresponse.send(event, context, cfnresponse.FAILED, response)

  InvokeRepositoryNameParsingLambda:
    DependsOn: RepositoryNameParsingLambda
    Type: Custom::MyCustomResource
    Properties:
      ServiceToken: !GetAtt RepositoryNameParsingLambda.Arn
      filters: !Ref SociRepositoryImageTagFilters

  SociIndexGeneratorLambdaECRRepositoryPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: SociIndexGeneratorLambdaECRRepositoryPolicy
      PolicyDocument:
             Version: "2012-10-17"
             Statement:
               - Effect: "Allow"
                 Action:
                   - "ecr:BatchGetImage"
                   - "ecr:GetDownloadUrlForLayer"
                   - "ecr:CompleteLayerUpload"
                   - "ecr:UploadLayerPart"
                   - "ecr:InitiateLayerUpload"
                   - "ecr:BatchCheckLayerAvailability"
                   - "ecr:PutImage"
                 Resource: !GetAtt InvokeRepositoryNameParsingLambda.repository_arns
      Roles:
        - Ref: "SociIndexGeneratorLambdaRole"

  SociIndexGeneratorLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary:
        Fn::If:
          - UsePermissionsBoundary
          - !Ref IamPermissionsBoundaryArn
          - !Ref "AWS::NoValue"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: "lambda.amazonaws.com"
      Policies:
        -  PolicyName: "AllowEcrGetAuthorizationToken"
           PolicyDocument:
             Version: "2012-10-17"
             Statement:
               - Effect: "Allow"
                 Action: [
                   "ecr:GetAuthorizationToken"
                 ]
                 Resource: "*"

Outputs:
  ExportsStackName:
    Value: !Ref 'AWS::StackName'
    Export:
      Name: !Sub 'ExportsStackName-${AWS::StackName}'
