# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0

AWSTemplateFormatVersion: "2010-09-09"

Description: Route53 Hosted Zones Sync - Serverless Architecture & Private Hosted Zone

Parameters:
  PublicHostedZoneId:
    Type: String
  ZoneName:
    Type: String
  AliasDontUpdate:
    Type: String
  VpcId:
    Type: String
  VpcRegion:
    Type: String

Resources:
# ---------- PRIVATE HOSTED ZONE ----------
  PrivateHostedZone:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: !Ref ZoneName
      VPCs:
        - 
          VPCId: !Ref VpcId
          VPCRegion: !Ref VpcRegion
  
# ---------- AMAZON EVENTBRIDGE RULE ----------
  # Rule and target
  EventBridgeRule:
    Type: AWS::Events::Rule
    Properties:
      Name: "route53-publichostedzone-changes"
      Description: "Capture Changes in Route53 Public Hosted Zones."
      EventPattern:
        source: 
          - aws.route53
        detail-type: 
          - "AWS API Call via CloudTrail"
        detail: 
          eventSource:
            - route53.amazonaws.com
          eventName:
            - ChangeResourceRecordSets
          requestParameters:
            hostedZoneId:
              - !Ref PublicHostedZoneId
      Targets:
        - Arn: !GetAtt Route53LambdaFunction.Arn
          Id: "LambdaFunction"
  
  # Lambda permission (for the EventBridge rule)
  EventBridgeLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref Route53LambdaFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventBridgeRule.Arn

# ---------- LAMBDA FUNCTION ----------
  Route53LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Description: "Update Private Hosted Zone from Public Hosted Zone updates."
      Runtime: python3.9
      Timeout: 10
      Role: !GetAtt LambdaFunctionRole.Arn
      Handler: index.lambda_handler
      Environment:
        Variables:
          PRIVATE_HOSTED_ZONE_ID: !Ref PrivateHostedZone
          DONT_UPDATE: !Ref AliasDontUpdate

      Code: 
        ZipFile: |-
          import json
          import logging
          import os
          import boto3
            
          log = logging.getLogger("handler")
          log.setLevel(logging.INFO)

          def lambda_handler(event, context):
              try:
                  # We log the event received
                  log.info("Received event: %s", json.dumps(event))
                  
                  # We obtain the environment variables
                  private_hz_id = os.environ["PRIVATE_HOSTED_ZONE_ID"]
                  aliases = os.environ["DONT_UPDATE"]
                  # boto3 configuration
                  r53 = boto3.client('route53')
                  # We obtain the information of the event
                  changes = event["detail"]["requestParameters"]["changeBatch"]["changes"]
                  # We initialize the phz_changes variable
                  phz_changes = []
                  
                  for c in changes:
                      # We add the change if the alias is not part of the "DONT_UPDATE" list
                      alias = (c["resourceRecordSet"]["name"].split("."))[0]
                      if alias in str(aliases):
                          log.info("Alias %s should not be updated", alias)
                      else:
                          # We need to capitalize all the keys in the map (to comply with the boto3 format) and add it to the Changes map
                          c = update_change(c)
                          phz_changes.append(c)

                  # If we have updated the variable phz_changes, we update the Private Hosted Zone
                  if len(phz_changes) > 0:
                      log.info("To update: %s", phz_changes)
                      updateHZ = r53.change_resource_record_sets(
                          HostedZoneId=private_hz_id,
                          ChangeBatch={
                              'Comment': 'Updating Private Hosted Zone from changes in Public Hosted Zone',
                              'Changes': phz_changes
                          }
                      )
                      log.info("Successfully updated Private Hosted Zone %s", private_hz_id)
                  else:
                      log.info("No updates needed in Private Hosted Zone %s", private_hz_id)
                  
              except Exception as e:
                  log.exception("whoops")
                  log.info(e)

          def update_change(c):
              try:
                  # Initialize new map (Change)
                  newc = {}
                  # We iterate over the keys
                  for k, v in c.items():
                      # ResourceRecordSet has a map, not a string
                      if k == "resourceRecordSet":
                          # We initialize the new ResourceRecordSet
                          rrs = {}
                          # We iterate over the keys
                          for i, j in v.items():
                              # Special case 1: TTL
                              if i == "tTL":
                                  rrs['TTL'] = j
                              
                              # Special case 2: GeoLocation, AliasTarget and CidrRoutingConfig have maps, not strings
                              elif i == "geoLocation" or i == "aliasTarget" or i == "cidrRoutingConfig":
                                  keys = {}
                                  for key, value in j.items():
                                      keys[key.title()] = value
                                  # Once we finish the iteration, we update resourceRecordSet
                                  rrs['GeoLocation'] = keys
                              
                              # Special case 3: ResourceRecords has a list of maps
                              elif i == "resourceRecords":
                                  for rs in j:
                                      records = []
                                      for key, value in rs.items():
                                          records.append({key.title() : value})
                                  # Once we finish the iteration over resourceRecords, we update resourceRecordSet
                                  rrs["ResourceRecords"] = records
                              
                              # For other key-value pairs, we capitalize the first letter
                              else:
                                  rrs[i.title()] = j
                          
                          # We create the new ResourceRecordSet map and we add it to the new Change
                          newc['ResourceRecordSet'] = rrs
                      
                      else:
                          # We capitalize the first letter of the Action
                          newc[k.title()] = v
                  
                  # We return the new Change
                  return newc
              
              except Exception as e:
                  log.exception("whoops")
                  log.info(e)

  # Lambda Function IAM Role
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: LambdaPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - arn:aws:logs:*:*:*
              - Effect: Allow
                Action:
                  - route53:ChangeResourceRecordSets
                Resource:
                  - !Join
                    - "/"
                    - - arn:aws:route53:::hostedzone
                      - !Ref PrivateHostedZone
  
  # CloudWatch Log Group
  LambdaFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${Route53LambdaFunction}
      RetentionInDays: 7