
Description: AWS ParallelCluster ActiveDirectory Database

Parameters:
  DomainName:
    Description: AD Domain Name.
    Type: String
    Default: corp.example.com
    AllowedPattern: ^([a-zA-Z0-9]+[\\.-])+([a-zA-Z0-9])+$
  AdminPassword:
    Description: AD Admin Password.
    Type: String
    MinLength: 8
    MaxLength: 64
    AllowedPattern: (?=^.{8,64}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9\s])(?=.*[a-z])|(?=.*[^A-Za-z0-9\s])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9\s]))^.*
    NoEcho: true
  ReadOnlyPassword:
    Description: AD ReadOnlyUser Password.
    Type: String
    MinLength: 8
    MaxLength: 64
    AllowedPattern: (?=^.{8,64}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9\s])(?=.*[a-z])|(?=.*[^A-Za-z0-9\s])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9\s]))^.*
    NoEcho: true
  UserNames:
    Description: Comma separated cluster users to create in the Active Directory.
    Type: String
    Default: user000
    MinLength: 3
  UserPassword:
    Description: Cluster user Password for all the users specified in 'Users'.
    Type: String
    MinLength: 8
    MaxLength: 64
    AllowedPattern: (?=^.{8,64}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9\s])(?=.*[a-z])|(?=.*[^A-Za-z0-9\s])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9\s]))^.*
    NoEcho: true
  DirectoryType:
    Description: Type of directory to use
    Type: String
    Default: MicrosoftAD
    AllowedValues:
      - MicrosoftAD
      - SimpleAD

  Keypair:
    Description: EC2 Keypair to access management instance.
    Type: AWS::EC2::KeyPair::KeyName
  Vpc:
    Description: VPC ID to create the Directory Service in (leave BLANK to create a new one.)
    Type: String
    # Type: AWS::EC2::VPC::Id
    Default: ""
  # Ad:
  #   Description: AD ID
  #   Type: String
  #   Default: ""
  PrivateSubnetOne:
    Description: Subnet ID of the first subnet. If the VPC is left blank, this will be created.
    Type: String
    # Type: AWS::EC2::Subnet::Id
    Default: ""
  PrivateSubnetTwo:
    Description: Subnet ID of the second subnet. This subnet should be in another availability zone. . If the VPC is left blank, this will be created.
    Type: String
    # Type: AWS::EC2::Subnet::Id
    Default: ""
  AdminNodeAmiId:
    Description: AMI for the Admin Node
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64'

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Directory
        Parameters:
          - DirectoryType
          - DomainName
          - Vpc
          - PrivateSubnetOne
          - PrivateSubnetTwo
      - Label:
          default: Users
        Parameters:
          - AdminPassword
          - ReadOnlyPassword
          - UserNames
          - UserPassword
      - Label:
          default: Administrative Node
        Parameters:
          - AdminNodeAmiId
          - Keypair

Transform: AWS::Serverless-2016-10-31

Conditions:
  CreateVPC: !Equals [!Ref Vpc, '']
  # CreateAD: !Equals [!Ref Ad, '']
  CreateAD: !Equals ['', '']
  UseMicrosoftAD:
    !Equals [ !Ref DirectoryType, MicrosoftAD ]
  UseSimpleAD:
    !Equals [ !Ref DirectoryType, SimpleAD ]

Resources:
  DisableImdsv1LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        MetadataOptions:
          HttpEndpoint: enabled
          HttpPutResponseHopLimit: 4
          HttpTokens: required
  PrepRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
      Policies:
        - PolicyName: LogOutput
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
                Effect: Allow
                # Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-Prep:*
                Resource: '*'
        - PolicyName: DescribeDirectory
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ds:DescribeDirectories
                # Resource: !Sub arn:${AWS::Partition}:ds:*:${AWS::AccountId}:directory/*
                Resource: '*'

  PclusterVpc:
    Type: AWS::EC2::VPC
    Condition: CreateVPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
  PclusterVpcIGW:
    Type: AWS::EC2::InternetGateway
    Condition: CreateVPC
  PclusterVpcSubnet1Route:
    Type: AWS::EC2::Route
    Condition: CreateVPC
    DependsOn:
    - PclusterVpcGWA
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: PclusterVpcIGW
      RouteTableId:
        Ref: PclusterVpcSubnet1RouteTable
  PclusterVpcSubnet1RouteTable:
    Type: AWS::EC2::RouteTable
    Condition: CreateVPC
    Properties:
      VpcId:
        Ref: PclusterVpc
  PclusterVpcSubnet1RTA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: CreateVPC
    Properties:
      RouteTableId:
        Ref: PclusterVpcSubnet1RouteTable
      SubnetId:
        Ref: PclusterVpcSubnet1
  PclusterVpcSubnet1:
    Type: AWS::EC2::Subnet
    Condition: CreateVPC
    Properties:
      AvailabilityZone: !Sub ${AWS::Region}a
      CidrBlock: 10.0.0.0/17
      MapPublicIpOnLaunch: true
      VpcId:
        Ref: PclusterVpc
  PclusterVpcSunet1Route:
    Type: AWS::EC2::Route
    Condition: CreateVPC
    DependsOn:
    - PclusterVpcGWA
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: PclusterVpcIGW
      RouteTableId:
        Ref: PclusterVpcSubnet2RouteTable
  PclusterVpcSubnet2RouteTable:
    Type: AWS::EC2::RouteTable
    Condition: CreateVPC
    Properties:
      VpcId:
        Ref: PclusterVpc
  PclusterVpcSubnet2RTA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: CreateVPC
    Properties:
      RouteTableId:
        Ref: PclusterVpcSubnet2RouteTable
      SubnetId:
        Ref: PclusterVpcSubnet2
  PclusterVpcSubnet2:
    Type: AWS::EC2::Subnet
    Condition: CreateVPC
    Properties:
      AvailabilityZone: !Sub ${AWS::Region}b
      CidrBlock: 10.0.128.0/17
      MapPublicIpOnLaunch: true
      VpcId:
        Ref: PclusterVpc
  PclusterVpcGWA:
    Type: AWS::EC2::VPCGatewayAttachment
    Condition: CreateVPC
    Properties:
      InternetGatewayId:
        Ref: PclusterVpcIGW
      VpcId:
        Ref: PclusterVpc

  PrepLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: !Sub "${AWS::StackName}: custom resource handler to prepare the stack."
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt PrepRole.Arn
      Runtime: python3.9
      Timeout: 300
      TracingConfig:
        Mode: Active
      Code:
        ZipFile: |
          import time
          import cfnresponse
          import boto3
          import logging
          import random
          import string
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          ec2 = boto3.client("ec2")
          ds = boto3.client("ds")

          def create_physical_resource_id():
              alnum = string.ascii_uppercase + string.ascii_lowercase + string.digits
              return ''.join(random.choice(alnum) for _ in range(16))

          def handler(event, context):
              print(event)
              print( 'boto version {}'.format(boto3.__version__))
              domain = event['ResourceProperties']['DomainName']
              vpc_id = event['ResourceProperties']['Vpc']
              directory_id = event['ResourceProperties']['DirectoryId']
              subnet1_id = event['ResourceProperties']['PrivateSubnetOne']
              subnet2_id = event['ResourceProperties']['PrivateSubnetTwo']

              directory = ds.describe_directories(DirectoryIds=[directory_id])['DirectoryDescriptions'][0]
              dns_ip_addrs = directory['DnsIpAddrs']

              response_data = {}
              reason = None
              response_status = cfnresponse.SUCCESS
              stack_id_suffix = event['StackId'].split("/")[1]

              if event['RequestType'] == 'Create':
                  response_data['Message'] = 'Resource creation successful!'
                  physical_resource_id = create_physical_resource_id()

                  # provide outputs
                  response_data['DirectoryId'] = directory_id
                  response_data['DomainName'] = domain
                  response_data['DomainShortName'] = domain.split(".")[0].upper()
                  response_data['VpcId'] = vpc_id
                  response_data['Subnet1Id'] = subnet1_id
                  response_data['Subnet2Id'] = subnet2_id
                  response_data['DnsIpAddresses'] = dns_ip_addrs
                  for i, addr in enumerate(dns_ip_addrs):
                    addr_index = i + 1
                    response_data[f'DnsIpAddress{addr_index}'] = addr
              else:
                  physical_resource_id = event['PhysicalResourceId']
              cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason)

  MicrosoftADDirectory:
    Type: AWS::DirectoryService::MicrosoftAD
    Condition: UseMicrosoftAD
    Properties:
      Name: !Ref DomainName
      Password: !Ref AdminPassword
      Edition: Standard
      VpcSettings:
        SubnetIds:
          - !If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
          - !If [CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]
        VpcId: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]

  SimpleADDirectory:
    Type: AWS::DirectoryService::SimpleAD
    Condition: UseSimpleAD
    Properties:
      Name: !Ref DomainName
      Password: !Ref AdminPassword
      Size: Small
      VpcSettings:
        SubnetIds:
          - !If [ CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
          - !If [ CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]
        VpcId: !If [ CreateVPC, !Ref PclusterVpc, !Ref Vpc ]

  Prep:
    Type: Custom::PrepLambda
    Properties:
      ServiceToken: !GetAtt PrepLambda.Arn
      DomainName:  !Ref DomainName
      Vpc: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
      PrivateSubnetOne: !If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
      PrivateSubnetTwo: !If [CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]
      DirectoryId: !If [UseMicrosoftAD, !Ref MicrosoftADDirectory, !Ref SimpleADDirectory ]

  AdDomainAdminNodeSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow SSH access
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: "-1"
          ToPort: -1
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          FromPort: 22
          IpProtocol: tcp
          ToPort: 22
      VpcId: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]


  JoinRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: !Sub ec2.${AWS::URLSuffix}
        Version: "2012-10-17"
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMDirectoryServiceAccess
      Policies:
        - PolicyDocument:
            Statement:
              - Action:
                  - ds:ResetUserPassword
                  - ds:DescribeDirectories
                  - ds:DescribeDomainControllers
                Effect: Allow
                Resource: !Sub
                  - arn:${AWS::Partition}:ds:${AWS::Region}:${AWS::AccountId}:directory/${DirectoryId}
                  # - { DirectoryId: !If [CreateAD, !Ref Directory, !Ref Ad ] }
                  - { DirectoryId: !If [UseMicrosoftAD, !Ref MicrosoftADDirectory, !Ref SimpleADDirectory ] }
          PolicyName: ResetUserPassword
        - PolicyDocument:
            Statement:
              - Action:
                  - secretsmanager:PutSecretValue
                Effect: Allow
                Resource:
                  - !Ref DomainCertificateSecret
                  - !Ref DomainPrivateKeySecret
          PolicyName: PutDomainCertificateSecrets
  JoinProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - Ref: JoinRole

  AdDomainAdminNodeWaitConditionHandle:
    Type: AWS::CloudFormation::WaitConditionHandle

  AdDomainAdminNodeWaitCondition:
    Type: AWS::CloudFormation::WaitCondition
    DependsOn:
      - Prep
    Properties:
      Count: 1
      Handle: !Ref AdDomainAdminNodeWaitConditionHandle
      Timeout: 900

  AdDomainAdminNode:
    Type: AWS::EC2::Instance
    Metadata:
      "AWS::CloudFormation::Init":
        configSets:
          setup:
            - install_dependencies
        install_dependencies:
          packages:
            yum:
              sssd: []
              realmd: []
              oddjob: []
              oddjob-mkhomedir: []
              adcli: []
              samba-common: []
              samba-common-tools: []
              krb5-workstation: []
              openldap-clients: []
              openssl: []
    Properties:
      IamInstanceProfile:
        Ref: JoinProfile
      ImageId: !Ref AdminNodeAmiId
      InstanceType: t3.micro
      KeyName: !Ref Keypair
      LaunchTemplate:
        LaunchTemplateId: !Ref 'DisableImdsv1LaunchTemplate'
        Version: !GetAtt 'DisableImdsv1LaunchTemplate.LatestVersionNumber'
      SecurityGroupIds:
        - Ref: AdDomainAdminNodeSecurityGroup
      SubnetId: !If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
      Tags:
        - Key: "Name"
          Value: !Sub [ "AdDomainAdminNode-${StackIdSuffix}", {StackIdSuffix: !Select [1, !Split ['/', !Ref 'AWS::StackId']]}]
      UserData:
        Fn::Base64:
          !Sub
            - |
              #!/bin/bash -e
              set -o pipefail
              exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
              yum update -y aws-cfn-bootstrap
              /opt/aws/bin/cfn-init -v --stack "${AWS::StackName}" --resource AdDomainAdminNode --configsets setup --region "${AWS::Region}"
              echo "Directory Id: ${DirectoryId}"
              echo "Domain Name: ${DirectoryDomain}"
              echo "Domain DNS IP 1: ${DnsIp1}"
              echo "Domain DNS IP 2: ${DnsIp2}"
              echo "Domain Certificate Secret: ${DomainCertificateSecretArn}"
              echo "Domain Private Key Secret: ${DomainPrivateKeySecretArn}"
              
              echo "Describing directory..."
              aws ds describe-directories --directory-id "${DirectoryId}" --region "${AWS::Region}"
              echo "Describing domain controllers..."
              aws ds describe-domain-controllers --directory-id "${DirectoryId}" --region "${AWS::Region}"
              
              ADMIN_PW="${AdminPassword}"
              
              USERNAMES="ReadOnlyUser,${UserNames}"
              echo "Registering Users: $USERNAMES ..."
              for username in $(echo $USERNAMES | sed "s/,/ /g")
              do
                attempt=0
                max_attempts=5
                until [ $attempt -ge $max_attempts ]; do
                  attempt=$((attempt+1))
                  echo "Registering user $username (attempt $attempt/$max_attempts) ..."
                  echo "$ADMIN_PW" | adcli create-user -v -x -U "${Admin}" --domain ${DirectoryDomain} --domain-controller="${DnsIp1}" --display-name="$username" "$username" && echo "User registered: $username" && break
                  echo "$ADMIN_PW" | adcli create-user -v -x -U "${Admin}" --domain ${DirectoryDomain} --domain-controller="${DnsIp2}" --display-name="$username" "$username" && echo "User registered: $username" && break
                  echo "User creation failed, describing directory and controllers for troubleshooting..."
                  aws ds describe-directories --directory-id "${DirectoryId}" --region "${AWS::Region}"
                  aws ds describe-domain-controllers --directory-id "${DirectoryId}" --region "${AWS::Region}"
                  sleep 10
                done
              done

              echo "Creating domain certificate..."
              PRIVATE_KEY="${DirectoryDomain}.key"
              CERTIFICATE="${DirectoryDomain}.crt"
              printf '.\n.\n.\n.\n.\n%s\n.\n' "${DirectoryDomain}" | openssl req -x509 -sha256 -nodes -newkey rsa:2048 -keyout "$PRIVATE_KEY" -days 365 -out "$CERTIFICATE"

              echo "Storing domain private key to Secrets Manager..."
              aws secretsmanager put-secret-value --secret-id "${DomainPrivateKeySecretArn}" --secret-string "file://$PRIVATE_KEY" --region "${AWS::Region}"

              echo "Storing domain certificate to Secrets Manager..."
              aws secretsmanager put-secret-value --secret-id "${DomainCertificateSecretArn}" --secret-string "file://$CERTIFICATE" --region "${AWS::Region}"

              echo "Deleting private key and certificate from local file system..."
              rm -rf "$PRIVATE_KEY" "$CERTIFICATE"

              /opt/aws/bin/cfn-signal -e "$?" --stack "${AWS::StackName}" --region "${AWS::Region}" "${AdDomainAdminNodeWaitConditionHandle}"

            - { DirectoryId: !GetAtt Prep.DirectoryId,
                DirectoryDomain: !GetAtt Prep.DomainName,
                AdminPassword: !Ref AdminPassword,
                UserNames: !Ref UserNames,
                DnsIp1: !GetAtt Prep.DnsIpAddress1,
                DnsIp2: !GetAtt Prep.DnsIpAddress2,
                DomainCertificateSecretArn: !Ref DomainCertificateSecret,
                DomainPrivateKeySecretArn: !Ref DomainPrivateKeySecret,
                Admin: !If [UseMicrosoftAD, "Admin", "Administrator" ],
                AdDomainAdminNodeWaitConditionHandle: !Ref AdDomainAdminNodeWaitConditionHandle
            }

  PostRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
      Policies:
        - PolicyName: LogOutput
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
                Effect: Allow
                Resource: '*'
        - PolicyName: ResetPassword
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ds:ResetUserPassword
                Resource: !Sub
                  - arn:${AWS::Partition}:ds:${AWS::Region}:${AWS::AccountId}:directory/${DirectoryId}
                  # - { DirectoryId: !If [CreateAD, !Ref Directory, !Ref Ad ] }
                  - { DirectoryId: !If [UseMicrosoftAD, !Ref MicrosoftADDirectory, !Ref SimpleADDirectory ] }
        - PolicyName: StopInstances
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:StopInstances
                Resource: !Sub
                  - arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/${InstanceId}
                  - { InstanceId: !Ref AdDomainAdminNode }

  PostLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: !Sub "${AWS::StackName}: custom resource handler to finish setting up stack after other resources have been created."
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt PostRole.Arn
      Runtime: python3.9
      Timeout: 300
      TracingConfig:
        Mode: Active
      Code:
        ZipFile: |
          import time
          import cfnresponse
          import boto3
          import logging
          import random
          import string
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          ds = boto3.client("ds")
          ec2 = boto3.client("ec2")

          def create_physical_resource_id():
              alnum = string.ascii_uppercase + string.ascii_lowercase + string.digits
              return ''.join(random.choice(alnum) for _ in range(16))

          def redact_keys(event: dict, redactions: set):
              ret = {}
              for k in event.keys():
                  if k in redactions:
                      ret[k] = "[REDACTED]"
                  else:
                      ret[k] = redact_keys(event[k], redactions) if type(event[k]) is dict else event[k] # handle nesting
              return ret

          def handler(event, context):
              print(redact_keys(event, {"ReadOnlyPassword", "UserPassword", "AdminPassword"}))
              print( 'boto version {}'.format(boto3.__version__))
              directory_id = event['ResourceProperties']['DirectoryId']
              instance_id = event['ResourceProperties']['AdminNodeInstanceId']

              read_only_password = event['ResourceProperties']['ReadOnlyPassword']
              user_names = event['ResourceProperties']['UserNames']
              user_password = event['ResourceProperties']['UserPassword']
              admin_password = event['ResourceProperties']['AdminPassword']
              admin = event['ResourceProperties']['Admin']

              response_data = {}
              reason = None
              response_status = cfnresponse.SUCCESS

              if event['RequestType'] == 'Create':
                  response_data['Message'] = 'Resource creation successful!'
                  physical_resource_id = create_physical_resource_id()
                  ds.reset_user_password(DirectoryId=directory_id, UserName='ReadOnlyUser', NewPassword=read_only_password)
                  for name in user_names.split(","):
                    name = name.strip()
                    ds.reset_user_password(DirectoryId=directory_id, UserName=name, NewPassword=user_password)
                  ds.reset_user_password(DirectoryId=directory_id, UserName=admin, NewPassword=admin_password)
                  ec2.stop_instances(InstanceIds=[instance_id])

              else:
                  physical_resource_id = event['PhysicalResourceId']
              cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason)

  Post:
    Type: Custom::PostLambda
    Properties:
      ServiceToken: !GetAtt PostLambda.Arn
      AdminNodeInstanceId: !Ref AdDomainAdminNode
      # DirectoryId: !If [CreateAD, !Ref Directory, !Ref Ad ]
      DirectoryId: !If [UseMicrosoftAD, !Ref MicrosoftADDirectory, !Ref SimpleADDirectory ]
      UserNames: !Ref UserNames
      UserPassword: !Ref UserPassword
      AdminPassword: !Ref AdminPassword
      ReadOnlyPassword: !Ref ReadOnlyPassword
      Admin: !If [UseMicrosoftAD, "Admin", "Administrator" ]

  PasswordSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: ReadOnly password for AD
      Name: !Sub [ "PasswordSecret-${StackIdSuffix}", {StackIdSuffix: !Select [1, !Split ['/', !Ref 'AWS::StackId']]}]
      SecretString: !Ref ReadOnlyPassword

  UserPasswordSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: User password for AD
      Name: !Sub [ "UserPasswordSecret-${StackIdSuffix}", { StackIdSuffix: !Select [ 1, !Split [ '/', !Ref 'AWS::StackId' ] ] } ]
      SecretString: !Ref UserPassword

  DomainCertificateSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: Domain certificate
      Name: !Sub [ "DomainCertificateSecret-${StackIdSuffix}", { StackIdSuffix: !Select [ 1, !Split [ '/', !Ref 'AWS::StackId' ] ] } ]

  DomainPrivateKeySecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: Domain private key
      Name: !Sub [ "DomainPrivateKeySecret-${StackIdSuffix}", { StackIdSuffix: !Select [ 1, !Split [ '/', !Ref 'AWS::StackId' ] ] } ]

  NetworkLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internal
      Subnets:
        - !If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
        - !If [CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]
      Type: network

  NetworkLoadBalancerTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: 389
      Protocol: TCP
      VpcId: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
      HealthCheckEnabled: True
      HealthCheckIntervalSeconds: 10
      HealthCheckPort: 389
      HealthCheckProtocol: TCP
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 3
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 60
      Targets:
        - Id: !Select [0, !GetAtt Prep.DnsIpAddresses]
          Port: 389
        - Id: !Select [1, !GetAtt Prep.DnsIpAddresses]
          Port: 389
      TargetType: ip

  DNS:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: !Ref DomainName
      VPCs:
        - VPCId: !If [ CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
          VPCRegion: !Ref AWS::Region

  DNSRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref DNS
      Name: !Ref DomainName
      AliasTarget:
        DNSName: !GetAtt NetworkLoadBalancer.DNSName
        HostedZoneId: !GetAtt NetworkLoadBalancer.CanonicalHostedZoneID
      Type: A

  NetworkLoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref NetworkLoadBalancerTargetGroup
      LoadBalancerArn: !Ref NetworkLoadBalancer
      Port: '636'
      Protocol: TLS
      SslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01
      Certificates:
        - CertificateArn: !GetAtt DomainCertificateSetup.DomainCertificateArn

  DomainCertificateSetupLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
      Policies:
        - PolicyName: LogOutput
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Effect: Allow
                Resource: '*'
        - PolicyName: ManageDomainCertificate
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - iam:UploadServerCertificate
                  - iam:TagServerCertificate
                Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:server-certificate/*
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource:
                  - !Ref DomainCertificateSecret
                  - !Ref DomainPrivateKeySecret

  DomainCertificateSetupLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: !Sub "${AWS::StackName}: custom resource handler to import the domain certificate into IAM."
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt DomainCertificateSetupLambdaRole.Arn
      Runtime: python3.9
      Timeout: 300
      TracingConfig:
        Mode: Active
      Code:
        ZipFile: |
          import time
          import cfnresponse
          import boto3
          import logging
          import random
          import string
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          iam = boto3.client("iam")
          sm = boto3.client("secretsmanager")

          def create_physical_resource_id():
              alnum = string.ascii_uppercase + string.ascii_lowercase + string.digits
              return ''.join(random.choice(alnum) for _ in range(16))

          def import_certificate(certificate_secret_arn, private_key_secret_arn, domain_name, tags):
            logger.info('Reading secrets from Secrets Manager...')
            domain_certificate = sm.get_secret_value(SecretId=certificate_secret_arn)["SecretString"]
            domain_private_key = sm.get_secret_value(SecretId=private_key_secret_arn)["SecretString"]
            logger.info('Importing certificate into IAM...')

            certificate_arn = iam.upload_server_certificate(
              ServerCertificateName=domain_name,
              CertificateBody=domain_certificate,
              PrivateKey=domain_private_key, Tags=tags
            )["ServerCertificateMetadata"]["Arn"]
            return certificate_arn

          def handler(event, context):
              logger.info(f"Context: {context}")
              logger.info(f"Event: {event}")
              logger.info(f"Boto version: {boto3.__version__}")

              domain_name = event['ResourceProperties']['DomainName']
              certificate_secret_arn = event['ResourceProperties']['DomainCertificateSecretArn']
              private_key_secret_arn = event['ResourceProperties']['DomainPrivateKeySecretArn']
              tags = [{ 'Key': 'StackId', 'Value': event['StackId']}]

              response_data = {}
              reason = None
              response_status = cfnresponse.SUCCESS

              physical_resource_id = event.get("PhysicalResourceId", create_physical_resource_id())

              try:
                if event['RequestType'] == 'Create':
                  certificate_arn = import_certificate(certificate_secret_arn, private_key_secret_arn, domain_name, tags)
                  time.sleep(60)
                  response_data['DomainCertificateArn'] = certificate_arn
                  response_data['Message'] = f"Resource creation successful! IAM certificate imported: {certificate_arn}"
              except Exception as e:
                response_status = cfnresponse.FAILED
                reason = str(e)
              cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason)

  DomainCertificateSetup:
    Type: Custom::DomainCertificateSetupLambda
    DependsOn:
      - Post
    Properties:
      ServiceToken: !GetAtt DomainCertificateSetupLambda.Arn
      DomainName: !Ref DomainName
      DomainCertificateSecretArn: !Ref DomainCertificateSecret
      DomainPrivateKeySecretArn: !Ref DomainPrivateKeySecret

  CleanupLambdaRole:
    Type: AWS::IAM::Role
    DependsOn:
      - DomainCertificateSetup
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
      Policies:
        - PolicyName: LogOutput
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Effect: Allow
                Resource: '*'
        - PolicyName: DeleteDomainCertificate
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - iam:DeleteServerCertificate
                Resource: !GetAtt DomainCertificateSetup.DomainCertificateArn

  CleanupLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: !Sub "${AWS::StackName}: custom resource handler to cleanup resources."
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt CleanupLambdaRole.Arn
      Runtime: python3.9
      Timeout: 900
      TracingConfig:
        Mode: Active
      Code:
        ZipFile: |
          import time
          import cfnresponse
          import boto3
          import logging
          import random
          import string
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          iam = boto3.client("iam")

          def create_physical_resource_id():
            alnum = string.ascii_uppercase + string.ascii_lowercase + string.digits
            return ''.join(random.choice(alnum) for _ in range(16))

          def delete_certificate(domain_name, certificate_arn):
            logger.info(f"Deleting iam certificate {certificate_arn}...")
            max_attempts = 10
            sleep_time = 60
            for attempt in range(1, max_attempts+1):
              try:
                iam.delete_server_certificate(ServerCertificateName=domain_name)
                break
              except iam.exceptions.DeleteConflictException as e:
                logger.info(f"(Attempt {attempt}/{max_attempts}) Cannot delete IAM certificate because it is in use. Retrying in {sleep_time} seconds...")
                if attempt == max_attempts:
                  raise Exception(f"Cannot delete certificate {certificate_arn}: {e}")
                else:
                  time.sleep(sleep_time)

          def handler(event, context):
            logger.info(f"Context: {context}")
            logger.info(f"Event: {event}")
            logger.info(f"Boto version: {boto3.__version__}")

            response_data = {}
            reason = None
            response_status = cfnresponse.SUCCESS

            physical_resource_id = event.get("PhysicalResourceId", create_physical_resource_id())

            try:
              if event['RequestType'] == 'Delete':
                certificate_arn = event['ResourceProperties']['DomainCertificateArn']
                domain_name = event['ResourceProperties']['DomainName']
                delete_certificate(domain_name, certificate_arn)
            except Exception as e:
              response_status = cfnresponse.FAILED
              reason = str(e)
            cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason)

  Cleanup:
    Type: Custom::CleanupLambda
    DependsOn:
      - DomainCertificateSetup
    Properties:
      ServiceToken: !GetAtt CleanupLambda.Arn
      DomainName: !Ref DomainName
      DomainCertificateArn: !GetAtt DomainCertificateSetup.DomainCertificateArn

  DomainCertificateSecretReadPolicy:
    Type: AWS::IAM::ManagedPolicy
    DependsOn:
      - DomainCertificateSecret
    Properties:
      ManagedPolicyName: !Sub [ "DomainCertificateSecretReadPolicy-${StackIdSuffix}", { StackIdSuffix: !Select [ 1, !Split [ '/', !Ref 'AWS::StackId' ] ] } ]
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - secretsmanager:GetSecretValue
            Resource:
              - !Ref DomainCertificateSecret

Outputs:
  VpcId:
    Value: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
  PrivateSubnetIds:
    Value: !Join [",", [!If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ], !If [CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]]]
  DomainName:
    Value: !Ref DomainName
  PasswordSecretArn:
    Value: !Ref PasswordSecret
  UserPasswordSecretArn:
    Value: !Ref UserPasswordSecret
  DomainCertificateArn:
    Value: !GetAtt DomainCertificateSetup.DomainCertificateArn
  DomainCertificateSecretArn:
    Value: !Ref DomainCertificateSecret
  DomainReadOnlyUser:
    Value: !Sub
      - cn=ReadOnlyUser,${type}=Users${ou}${dsn},dc=${dc}
      - { type: !If [UseMicrosoftAD, "ou", "cn" ],
          dc: !Join [",dc=", !Split [".", !Ref DomainName ]],
          ou: !If [UseMicrosoftAD, ",ou=", "" ],
          dsn: !If [UseMicrosoftAD, !GetAtt Prep.DomainShortName, "" ]
      }
  DomainAddrLdap:
    Value: !Sub
      - ldap://${address}
      - address: !Join [",ldap://", !GetAtt Prep.DnsIpAddresses]
  DomainAddrLdaps:
    Value: !Sub ldaps://${DomainName}
  DomainCertificateSecretReadPolicy:
    Value: !Ref DomainCertificateSecretReadPolicy
