Parameters:
  AdminUserEmail:
    Description: Email address of administrative user to setup by default (only with new Cognito instances).
    Type: String
    Default: ''
  PublicEcrImageUri:
    Description: When specified, the URI of the Docker image for the Lambda of the ParallelCluster UI container
    Type: String
    Default: public.ecr.aws/pcm/parallelcluster-ui:2025.04.0
  VpcEndpointId:
    Description: Enter a VPC endpoint with type interface for the execute-api service to enable private PCUI implementation. When enabled, the API will only accept requests from within the given VPC.
    Type: String
    Default: ''
  LambdaSubnetIds:
    Description: Comma separated list of subnet IDs to be associated with the PCUI Lambda function. These subnets should be private and associated with your VPC endpoint.
    Type: CommaDelimitedList
    Default: ''
  LambdaSecurityGroupIds:
    Description: Comma separated list of security groups to be associated with the PCUI Lambda function.
    Type: CommaDelimitedList
    Default: ''
  UserPoolId:
    Description: UserPoolId of a previously deployed PCUI Cognito User Pool. Leave blank to create a new one.
    Type: String
    Default: ''
  UserPoolAuthDomain:
    Description: UserPoolAuthDomain of a previously deployed PCUI Cognito User Pool. Leave blank to create a new one.
    Type: String
    Default: ''
  SNSRole:
    Description: SNSRole ARN of a previously deployed PCUI Cognito Stack. Leave blank to create a new one.
    Type: String
    Default: ''
  Version:
    Description: Version of AWS ParallelCluster to deploy.
    Type: String
    AllowedPattern: "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$"
    ConstraintDescription: Please specify a valid ParallelCluster version.
  ImageBuilderVpcId:
    Description: (Optional) Select the VPC to use for building the container images. If not selected, default VPC will be used.
    Type: String
    Default: ''
  ImageBuilderSubnetId:
    Description: (Optional) Select the subnet to use for building the container images. The subnet must be public and auto-assign public IPs. If not selected, the default Subnet will be used.
    Type: String
    Default: ''
  InfrastructureBucket:
    Description: (Optional) S3 bucket where CloudFormation files are stored. Change this parameter only when testing changes made to the infrastructure itself.
    Type: String
    Default: ''
  PermissionsBoundaryPolicy:
    Type: String
    Description: 'ARN of the IAM policy to use as permissions boundary for every IAM role created by ParallelCluster UI infrastructure.'
    Default: ''
    AllowedPattern: "^(arn:.*:iam::.*:policy\\/([a-zA-Z0-9_-]+))|()$"
  PermissionsBoundaryPolicyPCAPI:
    Type: String
    Description: 'ARN of the IAM policy to use as permissions boundary for every IAM role created by ParallelCluster API infrastructure. [ParallelCluster >= 3.8.0]'
    Default: ''
    AllowedPattern: "^(arn:.*:iam::.*:policy\\/([a-zA-Z0-9_-]+))|()$"
  AdditionalPoliciesPCAPI:
    Type: String
    Description: |
      (OPTIONAL) ARN of the additional IAM policy to be attached to the default execution role for the ParallelCluster Lambda function. 
      Only one policy can be specified.
    Default: ''
    AllowedPattern: "^(arn:.*:iam::.*:policy\\/([a-zA-Z0-9_-]+))|()$"
  IAMRoleAndPolicyPrefix:
    Type: String
    Description: 'Prefix applied to the name of every IAM role and policy (max length: 10). [ParallelCluster >= 3.8.0]'
    Default: ''
    MaxLength: 10
  CustomDomain:
    Type: String
    Description: (Optional) Custom domain name. If omitted, the default domain name will be used.
    Default: ''
  CustomDomainCertificateArn:
    Type: String
    Description: '(Optional) ARN of the ACM Certificate issued for the custom domain. This is required only if `CustomDomain` is specified.'
    Default: ''
  CognitoCustomDomain:
    Type: String
    Description: '(Optional) Custom domain name for Cognito. If omitted, the default Cognito domain name will be used.'
    Default: ''
  CognitoCustomDomainCertificateArn:
    Type: String
    Description: '(Optional) ARN of the ACM Certificate issued for the Cognito custom domain. This is required only if `CognitoCustomDomain` is specified.'
    Default: ''
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Admin User (only with new Cognito instances)
        Parameters: 
          - AdminUserEmail
      - Label:
          default: ParallelCluster UI
        Parameters:
          - PublicEcrImageUri
      - Label:
          default: (Optional) Private PCUI
        Parameters:
          - VpcEndpointId
          - LambdaSubnetIds
          - LambdaSecurityGroupIds
      - Label:
          default: (Optional) External PCUI Cognito
        Parameters:
          - UserPoolId
          - UserPoolAuthDomain
          - SNSRole
      - Label:
          default: ParallelCluster API
        Parameters:
          - Version
      - Label:
          default: (Optional) ImageBuilder Custom VPC
        Parameters:
          - ImageBuilderVpcId
          - ImageBuilderSubnetId
      - Label:
          default: (Optional) Permissions
        Parameters:
          - AdditionalPoliciesPCAPI
          - IAMRoleAndPolicyPrefix
          - PermissionsBoundaryPolicy
          - PermissionsBoundaryPolicyPCAPI
      - Label:
          default: (Optional) Custom Domain
        Parameters:
          - CustomDomain
          - CustomDomainCertificateArn
          - CognitoCustomDomain
          - CognitoCustomDomainCertificateArn
      - Label:
          default: (Debugging only) Infrastructure S3 Bucket
        Parameters:
          - InfrastructureBucket

    ParameterLabels:
      AdminUserEmail:
        default: Admin's Email
      UserPoolId:
        default: UserPoolId from a previously deployed PCUI
      UserPoolAuthDomain:
        default: UserPoolAuthDomain from a previously deployed PCUI
      SNSRole:
        default: SNSRole ARN from a previously deployed PCUI

Conditions:
  NonDefaultVpc:
    Fn::And:
      - !Not [!Equals [!Ref ImageBuilderVpcId, ""]]
      - !Not [!Equals [!Ref ImageBuilderSubnetId, ""]]
  IsPrivate: !Not [!Equals [!Ref VpcEndpointId, ""]]
  HasDefaultInfrastructure: !Equals [!Ref InfrastructureBucket, '']
  UseExistingCognito:
    !And
      - !Not [!Equals [!Ref UserPoolId, ""]]
      - !Not [!Equals [!Ref UserPoolAuthDomain, ""]]
      - !Not [!Equals [!Ref SNSRole, ""]]
  UseNewCognito:
    !Not [ Condition: UseExistingCognito]
  UseNonDockerizedPCAPI:
    !Not [ Condition: UseDockerizedPCAPI]
  UseDockerizedPCAPI: !And
    - !Equals ['3', !Select [ 0, !Split ['.', !Ref Version] ] ] # Check PC version major is 3 and PC version minor is 0-5
    - !Or
      - !Equals ['0', !Select [ 1, !Split ['.', !Ref Version] ] ]
      - !Equals ['1', !Select [ 1, !Split ['.', !Ref Version] ] ]
      - !Equals ['2', !Select [ 1, !Split ['.', !Ref Version] ] ]
      - !Equals ['3', !Select [ 1, !Split ['.', !Ref Version] ] ]
      - !Equals ['4', !Select [ 1, !Split ['.', !Ref Version] ] ]
      - !Equals ['5', !Select [ 1, !Split ['.', !Ref Version] ] ]
  InGovCloud: !Equals ['us-gov-west-1', !Ref "AWS::Region"]
  UsePermissionBoundary: !Not [!Equals [!Ref PermissionsBoundaryPolicy, '']]
  UsePermissionBoundaryPCAPI: !Not [!Equals [!Ref PermissionsBoundaryPolicyPCAPI, '']]
  UseIAMRoleAndPolicyPrefix: !Not [!Equals [!Ref IAMRoleAndPolicyPrefix, '']]
  UseCustomDomain: !Not [!Equals [!Ref CustomDomain, '']]
  UseCognitoCustomDomain: !Not [!Equals [!Ref CognitoCustomDomain, '']]
  UseAdditionalPoliciesPCAPI: !Not [!Equals [!Ref AdditionalPoliciesPCAPI, '']]

Mappings:
  ParallelClusterUI:
    Constants:
      Version: 2025.04.0 # format YYYY.MM.REVISION
      CustomDomainBasePath: pcui

Resources:

  Cognito:
    Condition: UseNewCognito
    Type: AWS::CloudFormation::Stack
    DeletionPolicy: Retain
    Properties:
      Parameters:
        AdminUserEmail: !Ref AdminUserEmail
        PermissionsBoundaryPolicy: !Ref PermissionsBoundaryPolicy
        IAMRoleAndPolicyPrefix: !Ref IAMRoleAndPolicyPrefix
        CustomDomain: !Ref CognitoCustomDomain
        CustomDomainCertificateArn: !Ref CognitoCustomDomainCertificateArn
      TemplateURL: !Sub
        - '${Bucket}/parallelcluster-ui-cognito.yaml'
        - Bucket: !If 
          - HasDefaultInfrastructure
          - PLACEHOLDER
          - !Sub ${InfrastructureBucket}

      TimeoutInMinutes: 10

  ParallelClusterApi:
    Type: AWS::CloudFormation::Stack
    Properties:
      Parameters:
        PermissionsBoundaryPolicy: !If [ UsePermissionBoundaryPCAPI, !Ref PermissionsBoundaryPolicyPCAPI, !Ref AWS::NoValue ]
        IAMRoleAndPolicyPrefix: !If [ UseIAMRoleAndPolicyPrefix, !Ref IAMRoleAndPolicyPrefix, !Ref AWS::NoValue ]
        ParallelClusterFunctionAdditionalPolicies: !If [ UseAdditionalPoliciesPCAPI, !Ref AdditionalPoliciesPCAPI, !Ref AWS::NoValue ]
        ApiDefinitionS3Uri: !Sub s3://${AWS::Region}-aws-parallelcluster/parallelcluster/${Version}/api/ParallelCluster.openapi.yaml
        CreateApiUserRole: False
        EnableIamAdminAccess: True
        VpcEndpointId: !If [ IsPrivate, !Ref VpcEndpointId, !Ref AWS::NoValue ]
        ImageBuilderSubnetId: !If
          - UseNonDockerizedPCAPI
          - !Ref AWS::NoValue
          - Fn::If:
              - NonDefaultVpc
              - !Ref ImageBuilderSubnetId
              - !Ref AWS::NoValue
        ImageBuilderVpcId: !If
          - UseNonDockerizedPCAPI
          - !Ref AWS::NoValue
          - Fn::If:
            - NonDefaultVpc
            - !Ref ImageBuilderVpcId
            - !Ref AWS::NoValue
      TemplateURL: !Sub https://${AWS::Region}-aws-parallelcluster.s3.${AWS::Region}.amazonaws.com/parallelcluster/${Version}/api/parallelcluster-api.yaml
      TimeoutInMinutes: 30

  ParallelClusterUIFun:
    Type: AWS::Lambda::Function
    Properties:
      Role: !GetAtt ParallelClusterUIUserRole.Arn
      PackageType: Image
      MemorySize: 512
      Timeout: 30
      Tags:
        - Key: 'parallelcluster:ui:version'
          Value: !FindInMap [ ParallelClusterUI, Constants, Version ]
      TracingConfig:
        Mode: Active
      VpcConfig:
        Fn::If:
          - IsPrivate
          - SubnetIds: !Ref LambdaSubnetIds
            SecurityGroupIds: !Ref LambdaSecurityGroupIds
          - !Ref AWS::NoValue
      Environment:
        Variables:
          API_BASE_URL: !GetAtt [ ParallelClusterApi, Outputs.ParallelClusterApiInvokeUrl ]
          API_VERSION: !Ref Version
          SITE_URL: !If
            - UseCustomDomain
            - !Sub
              - https://${CustomDomain}/${CustomDomainBasePath}
              - { CustomDomainBasePath: !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ] }
            - !Sub
              - https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}
              - Api: !If
                  - IsPrivate
                  - !Sub
                    - '${ApiId}-${VpceId}'
                    - { ApiId: !Ref ApiGatewayRestApi, VpceId: !Ref VpcEndpointId }
                  - !Ref ApiGatewayRestApi
                Stage: !Ref ApiGatewayRestStage
          AUTH_PATH: !If [ UseExistingCognito, !Ref UserPoolAuthDomain, !GetAtt [ Cognito, Outputs.UserPoolAuthDomain ]]
          SECRET_ID: !GetAtt UserPoolClientSecret.SecretName
          AUDIENCE: !Ref CognitoAppClient
          OIDC_PROVIDER: 'Cognito'
          API_GATEWAY_BASE_PATH: !If
            - UseCustomDomain
            - !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ]
            - !Ref AWS::NoValue
          SSM_LOG_GROUP_NAME: !Ref SsmLogGroup
      FunctionName: !Sub
        - ParallelClusterUIFun-${StackIdSuffix}
        - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }
      Code:
        ImageUri: !Sub
          - ${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/${Repository}:${Version}
          - Repository: !Ref PrivateEcrRepository
            Version: !Join
              - '-'
              - [!Select [2, !Split ['/', !Ref EcrImage]], !Select [3, !Split ['/', !Ref EcrImage]]]

  ApiGatewayRestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: ParallelClusterUI
      Description: ParallelClusterUI Lambda Proxy
      Policy:
        Fn::If:
          - IsPrivate
          - Version: "2012-10-17"
            Statement:
              - Effect: "Deny"
                Principal: "*"
                Action: "execute-api:Invoke"
                Resource: "execute-api:/*"
                Condition:
                  StringNotEquals:
                    aws:sourceVpce: !Ref VpcEndpointId
              - Effect: "Allow"
                Principal: "*"
                Action: "execute-api:Invoke"
                Resource: "execute-api:/*"
          - Version: "2012-10-17"
            Statement:
            - Effect: "Allow"
              Principal: "*"
              Action: "execute-api:Invoke"
              Resource: "execute-api:/*"
      EndpointConfiguration:
        Types:
          - !If [ IsPrivate, PRIVATE, REGIONAL ]
        VpcEndpointIds:
          - !If [ IsPrivate, !Ref VpcEndpointId, !Ref AWS::NoValue ]
      Tags:
        - Key: 'parallelcluster:ui:version'
          Value: !FindInMap [ParallelClusterUI, Constants, Version]

  ApiGatewayProxyResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      ParentId: !GetAtt ApiGatewayRestApi.RootResourceId
      PathPart: '{proxy+}'

  ApiGatewayMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      ResourceId: !Ref ApiGatewayProxyResource
      HttpMethod: ANY
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub
          - arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:ParallelClusterUIFun-${StackIdSuffix}/invocations
          - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }

  ApiGatewayRootMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      ResourceId: !GetAtt ApiGatewayRestApi.RootResourceId
      HttpMethod: ANY
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub
          - arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:ParallelClusterUIFun-${StackIdSuffix}/invocations
          - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }

  ApiGatewayLogRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub
        - ${IAMRoleAndPolicyPrefix}ApiGatewayLogRole-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: apigateway.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs'
      PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue']

  ApiGatewayAccessLog:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 90

  ApiGatewayAccount:
    Type: AWS::ApiGateway::Account
    Properties:
      CloudWatchRoleArn: !GetAtt ApiGatewayLogRole.Arn

  ApiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn: ApiGatewayMethod
    Properties:
      RestApiId: !Ref ApiGatewayRestApi

  ApiGatewayRestStage:
    Type: AWS::ApiGateway::Stage
    DependsOn: ApiGatewayAccount
    Properties: 
      AccessLogSetting:
        DestinationArn: !GetAtt ApiGatewayAccessLog.Arn
        Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","path":"$context.path", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }'
      RestApiId: !Ref ApiGatewayRestApi
      DeploymentId: !Ref ApiGatewayDeployment
      StageName: !If [ UseCustomDomain, prod, pcui ]
      MethodSettings:
        - ResourcePath: '/*'
          HttpMethod: '*'
          ThrottlingBurstLimit: 50
          ThrottlingRateLimit: 100

  CognitoAppClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      GenerateSecret: true
      AllowedOAuthFlows:
        - code
      AllowedOAuthFlowsUserPoolClient: true
      AllowedOAuthScopes:
        - email
        - openid
      ExplicitAuthFlows:
        - ALLOW_REFRESH_TOKEN_AUTH
      CallbackURLs:
        - !If
          - UseCustomDomain
          - !Sub
            - https://${CustomDomain}/${CustomDomainBasePath}/login
            - { CustomDomainBasePath: !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ] }
          - !Sub
            - https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/login
            - Api: !If
                - IsPrivate
                - !Sub
                  - '${ApiId}-${VpceId}'
                  - { ApiId: !Ref ApiGatewayRestApi, VpceId: !Ref VpcEndpointId }
                - !Ref ApiGatewayRestApi
              Stage: !Ref ApiGatewayRestStage
      SupportedIdentityProviders:
        - COGNITO
      UserPoolId: !If [ UseExistingCognito, !Ref UserPoolId, !GetAtt [ Cognito, Outputs.UserPoolId ]]
      PreventUserExistenceErrors: ENABLED
      RefreshTokenValidity: 7
      AccessTokenValidity: 5
      IdTokenValidity: 5
      TokenValidityUnits:
        AccessToken: "minutes"
        IdToken: "minutes"

  UserPoolClientSecret:
    Type: Custom::UserPoolClientSecret
    Properties:
      ServiceToken: !GetAtt UserPoolClientSecretFunction.Arn
      UserPoolId: !If [ UseExistingCognito, !Ref UserPoolId, !GetAtt [ Cognito, Outputs.UserPoolId ]]
      AppClientId: !Ref CognitoAppClient

  UserPoolClientSecretFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Runtime: python3.12
      MemorySize: 128
      Timeout: 20
      TracingConfig:
        Mode: Active
      Role: !GetAtt UserPoolClientSecretRole.Arn
      Code:
        ZipFile: |
          import cfnresponse
          import boto3
          import random
          import string
          import json

          cognito = boto3.client("cognito-idp")
          secretsmanager = boto3.client("secretsmanager")

          def generate_secret(stack_name, resource_id):
              alnum = string.ascii_uppercase + string.digits
              return f"{stack_name}-{resource_id}-" + "".join(random.choice(alnum) for _ in range(12))

          def handler(event, context):
              print(event)
              print("boto version {}".format(boto3.__version__))

              stack_name = event["StackId"].split("/")[1]
              user_pool_id = event["ResourceProperties"]["UserPoolId"]
              app_client_id = event["ResourceProperties"]["AppClientId"]
              logical_resource_id = event["LogicalResourceId"]

              response_data = {}
              reason = None
              response_status = cfnresponse.SUCCESS
              try:
                  if event["RequestType"] == "Create":
                      response_data["Message"] = "Resource creation successful!"

                      user_pool_client = cognito.describe_user_pool_client(UserPoolId=user_pool_id, ClientId=app_client_id)
                      client_secret = user_pool_client["UserPoolClient"]["ClientSecret"]
                      secret_name = generate_secret(stack_name, logical_resource_id)

                      secret = json.dumps({"userPoolId": user_pool_id, "clientId": app_client_id, "clientSecret": client_secret})

                      resp = secretsmanager.create_secret(
                          Name=secret_name,
                          Description=f"Client Secret for {app_client_id} / user pool {user_pool_id}",
                          SecretString=secret,
                          Tags=[
                              {"Key": "custom:cloudformation:stack-name", "Value": stack_name},
                              {"Key": "custom:cloudformation:logical-id", "Value": logical_resource_id},
                          ],
                      )
                      response_data = {"SecretArn": resp["ARN"], "SecretName": resp["Name"], "SecretVersionId": resp["VersionId"]}

                  elif event["RequestType"] == "Update":
                      user_pool_client = cognito.describe_user_pool_client(UserPoolId=user_pool_id, ClientId=app_client_id)
                      client_secret = user_pool_client["UserPoolClient"]["ClientSecret"]
                      secret_name = event["PhysicalResourceId"]
                      secret = json.dumps({"userPoolId": user_pool_id, "clientId": app_client_id, "clientSecret": client_secret})
                      resp = secretsmanager.update_secret(
                          SecretId=secret_name,
                          Description=f"Client Secret for {app_client_id} / user pool {user_pool_id}",
                          SecretString=secret,
                      )
                      response_data = {"SecretArn": resp["ARN"], "SecretName": resp["Name"], "SecretVersionId": resp["VersionId"]}

                  else:
                      secret_name = event["PhysicalResourceId"]
                      resp = secretsmanager.delete_secret(SecretId=secret_name, ForceDeleteWithoutRecovery=True)
                      response_data = {"SecretArn": resp["ARN"], "SecretName": resp["Name"]}

              except Exception as exception:
                  response_status = cfnresponse.FAILED
                  reason = "Failed {}: {}".format(event["RequestType"], exception)

              cfnresponse.send(event, context, response_status, response_data, secret_name, reason)

  UserPoolClientSecretRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub
        - ${IAMRoleAndPolicyPrefix}UserPoolClientSecretRole-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: !Sub ${IAMRoleAndPolicyPrefix}UserPoolPermissions
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - cognito-idp:DescribeUserPoolClient
                Resource:
                  - !Sub
                    - arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPoolId}
                    - { UserPoolId: !If [UseExistingCognito, !Ref UserPoolId, !GetAtt [ Cognito, Outputs.UserPoolId ]]}
        - PolicyName: !Sub ${IAMRoleAndPolicyPrefix}SecretsManagerPermissions
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:CreateSecret
                  - secretsmanager:TagResource
                  - secretsmanager:UpdateSecret
                  - secretsmanager:DeleteSecret
                Resource:
                  - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${AWS::StackName}*
      PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue']

  PrivateEcrRepository:
    DependsOn: ParallelClusterApi
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Sub
        - 'parallelcluster-ui-${StackIdSuffix}'
        - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }
      RepositoryPolicyText:
        Version: 2012-10-17
        Statement:
          - Sid: ReadEcrImages
            Effect: Allow
            Principal:
              Service: !Sub lambda.${AWS::URLSuffix}
            Action:
              - ecr:BatchGetImage
              - ecr:GetDownloadUrlForLayer
            Condition:
              StringLike:
                aws:SourceArn: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*

  ImageBuilderInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub
        - ${IAMRoleAndPolicyPrefix}ImageBuilderInstanceRole-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore
        - !Sub arn:${AWS::Partition}:iam::aws:policy/EC2InstanceProfileForImageBuilderECRContainerBuilds
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - !Sub ec2.${AWS::URLSuffix}
        Version: '2012-10-17'
      Path: /executionServiceEC2Role/
      PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue']

  ImageBuilderInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub
        - ${IAMRoleAndPolicyPrefix}ImageBuilderInstanceProfile-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      Path: /executionServiceEC2Role/
      Roles:
        - !Ref ImageBuilderInstanceRole

  InfrastructureConfigurationSecurityGroup:
    Condition: NonDefaultVpc
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref ImageBuilderVpcId
      GroupDescription: Parallel cluster image builder security group

  InfrastructureConfiguration:
    Type: AWS::ImageBuilder::InfrastructureConfiguration
    Properties:
      Name: !Sub
        - ParallelClusterUIImageBuilderInfrastructureConfiguration-${Version}-${StackIdSuffix}
        - { Version: !Join ['_', !Split ['.', !FindInMap [ParallelClusterUI, Constants, Version]]], StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }
      InstanceProfileName: !Ref ImageBuilderInstanceProfile
      TerminateInstanceOnFailure: true
      SubnetId:
        Fn::If:
          - NonDefaultVpc
          - !Ref ImageBuilderSubnetId
          - !Ref AWS::NoValue
      SecurityGroupIds:
        Fn::If:
          - NonDefaultVpc
          - [!Ref InfrastructureConfigurationSecurityGroup]
          - !Ref AWS::NoValue
      InstanceMetadataOptions:
        HttpTokens: required

  EcrImageRecipe:
    Type: AWS::ImageBuilder::ContainerRecipe
    Properties:
      Components:
        - ComponentArn: !Sub arn:${AWS::Partition}:imagebuilder:${AWS::Region}:aws:component/update-linux/x.x.x
      ContainerType: DOCKER
      Name: !Sub
        - 'parallelcluster-ui-${Version}-${StackIdSuffix}'
        - { Version: !Join ['_', !Split ['.', !FindInMap [ParallelClusterUI, Constants, Version]]], StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }
      Version: !FindInMap [ParallelClusterUI, Constants, Version]
      ParentImage: !Ref PublicEcrImageUri
      PlatformOverride: Linux
      TargetRepository:
        Service: ECR
        RepositoryName: !Ref PrivateEcrRepository
      DockerfileTemplateData: 'FROM {{{ imagebuilder:parentImage }}}'
      WorkingDirectory: '/tmp'

  EcrImage:
    Type: AWS::ImageBuilder::Image
    Properties:
      ContainerRecipeArn: !Ref EcrImageRecipe
      EnhancedImageMetadataEnabled: true
      InfrastructureConfigurationArn: !Ref InfrastructureConfiguration
      ImageTestsConfiguration:
        ImageTestsEnabled: false

  EcrImagePipeline:
    Type: AWS::ImageBuilder::ImagePipeline
    Properties:
      Name: !Sub
        - 'EcrImagePipeline-${Version}-${StackIdSuffix}'
        - { Version: !Join ['_', !Split ['.', !FindInMap [ParallelClusterUI, Constants, Version]]], StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }
      Status: ENABLED
      ContainerRecipeArn: !Ref EcrImageRecipe
      InfrastructureConfigurationArn: !Ref InfrastructureConfiguration
      ImageTestsConfiguration:
        ImageTestsEnabled: false

  EcrImageDeletionLambda:
    Type: AWS::Lambda::Function
    Properties:
      MemorySize: 128
      Code:
        ZipFile: |
          import cfnresponse
          import boto3
          import random
          import string

          ecr = boto3.client('ecr')
          imagebuilder = boto3.client('imagebuilder')

          def get_image_ids(repository_name, version):
              image_digests = set()
              paginator = ecr.get_paginator('list_images')
              response_iterator = paginator.paginate(repositoryName=repository_name, filter={'tagStatus': 'TAGGED'})
              for response in response_iterator:
                  image_digests.update([image_id['imageDigest'] for image_id in response['imageIds']])
              return list({'imageDigest': image_digest} for image_digest in image_digests)

          def get_imagebuilder_images(ecr_image_pipeline_arn):
              response = imagebuilder.list_image_pipeline_images(imagePipelineArn=ecr_image_pipeline_arn)
              images = [image['arn'] for image in response['imageSummaryList']]
              while 'nextToken' in response:
                  response = imagebuilder.list_image_pipeline_images(imagePipelineArn=ecr_image_pipeline_arn, nextToken=response['nextToken'])
                  images.extend([image['arn'] for image in response['imageSummaryList']])
              return images

          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__))

              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()
              else:
                  physical_resource_id = event['PhysicalResourceId']

              if event['RequestType'] == 'Update' or event['RequestType'] == 'Delete':
                  try:
                      resource_key = 'OldResourceProperties' if 'OldResourceProperties' in event else 'ResourceProperties'
                      ecr_repository_name = event[resource_key]['EcrRepositoryName']
                      ecr_image_pipeline_arn = event[resource_key]['EcrImagePipelineArn']
                      version = event[resource_key]['Version']

                      image_ids = get_image_ids(ecr_repository_name, version)
                      if image_ids:
                          ecr.batch_delete_image(repositoryName=ecr_repository_name, imageIds=image_ids)
                          reason = 'Image deletion successful!'
                      else:
                          reason = 'No image found, considering image deletion successful'

                      for imagebuilder_image in get_imagebuilder_images(ecr_image_pipeline_arn):
                          imagebuilder.delete_image(imageBuildVersionArn=imagebuilder_image)

                  except ecr.exceptions.RepositoryNotFoundException:
                      reason = 'Repository was not found, considering image deletion successfull'
                  except Exception as exception:
                      response_status = cfnresponse.FAILED
                      reason = 'Failed image deletion with error: {}'.format(exception)

              cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason)

      Handler: index.handler
      Runtime: python3.12
      Role: !GetAtt EcrImageDeletionLambdaRole.Arn

  EcrImageDeletionLambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${EcrImageDeletionLambda}

  EcrImageDeletionLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub
        - ${IAMRoleAndPolicyPrefix}EcrImageDeletionLambdaRole-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: !Sub ${IAMRoleAndPolicyPrefix}LogsPermissions
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub arn:${AWS::Partition}:logs:*:*:*
        - PolicyName: !Sub ${IAMRoleAndPolicyPrefix}EcrPermissions
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ecr:BatchDeleteImage
                  - ecr:ListImages
                Resource: !GetAtt PrivateEcrRepository.Arn
              - Effect: Allow
                Action:
                  - imagebuilder:ListImagePipelineImages
                Resource: !Sub
                  - arn:${AWS::Partition}:imagebuilder:${AWS::Region}:${AWS::AccountId}:image-pipeline/ecrimagepipeline-*${StackIdSuffix}*
                  - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }
              - Effect: Allow
                Action:
                  - imagebuilder:DeleteImage
                Resource: !Sub
                  - arn:${AWS::Partition}:imagebuilder:${AWS::Region}:${AWS::AccountId}:image/*${StackIdSuffix}*
                  - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] }
      PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue']

  EcrImagesRemover:
    Type: Custom::EcrImagesRemover
    Properties:
      ServiceToken: !GetAtt EcrImageDeletionLambda.Arn
      EcrRepositoryName: !Ref PrivateEcrRepository
      Version: !FindInMap [ParallelClusterUI, Constants, Version]
      EcrImagePipelineArn: !GetAtt EcrImagePipeline.Arn

  ParallelClusterUILambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${ParallelClusterUIFun}
      RetentionInDays: 90

  SsmLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/ssm/ParallelClusterUI-${AWS::StackName}
      RetentionInDays: 1
      LogGroupClass: STANDARD

  ParallelClusterUIUserRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub
        - ${IAMRoleAndPolicyPrefix}ParallelClusterUIUserRole-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: lambda.amazonaws.com
      ManagedPolicyArns:
        # Required for Lambda logging and XRay
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AWSXRayDaemonWriteAccess
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        # Access to the ParallelCluster API
        - !Ref ParallelClusterApiGatewayInvoke
        # Required to run ParallelClusterUI functionalities
        - !Ref CognitoPolicy
        - !Ref EC2Policy
        - !Ref StoragePolicy
        - !Ref LogsPolicy
        - !Ref CostMonitoringAndPricingPolicy
        - !Ref SsmPolicy
      PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue']

  ParallelClusterUIApiGatewayInvoke:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt ParallelClusterUIFun.Arn
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub
        - arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*
        - { ApiGateway: !Ref ApiGatewayRestApi }

  ParallelClusterApiGatewayInvoke:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub
        - ${IAMRoleAndPolicyPrefix}ParallelClusterApiGatewayInvoke-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
              - execute-api:Invoke
            Effect: Allow
            Resource: !Sub
              - arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PCApiGateway}/*/*
              - { PCApiGateway: !Select [2, !Split ['/', !Select [0, !Split ['.', !GetAtt [ ParallelClusterApi, Outputs.ParallelClusterApiInvokeUrl ]]]]] }

  CognitoPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub
        - ${IAMRoleAndPolicyPrefix}CognitoPolicy-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId']]]] }
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
            - cognito-idp:AdminRemoveUserFromGroup
            - cognito-idp:AdminAddUserToGroup
            - cognito-idp:AdminListGroupsForUser
            - cognito-idp:ListUsers
            - cognito-idp:AdminCreateUser
            - cognito-idp:AdminDeleteUser
            Resource: !Sub
              - arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPoolId}
              - { UserPoolId: !If [UseExistingCognito, !Ref UserPoolId, !GetAtt [ Cognito, Outputs.UserPoolId ]]}
            Effect: Allow
            Sid: CognitoPolicy
          - Action:
            - secretsmanager:GetSecretValue
            Resource:
              - !GetAtt UserPoolClientSecret.SecretArn
            Effect: Allow
            Sid: SecretsRole

  EC2Policy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub
        - ${IAMRoleAndPolicyPrefix}EC2Policy-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
            - ec2:DescribeSecurityGroups
            - ec2:DescribeVpcs
            - ec2:DescribeInstanceTypes
            - ec2:DescribeSubnets
            - ec2:DescribeKeyPairs
            Resource:
              - '*'
            Effect: Allow
            Sid: EC2Policy
          - Action:
            - ec2:StartInstances
            - ec2:StopInstances
            Resource:
              - !Sub arn:${AWS::Partition}:ec2:*:${AWS::AccountId}:instance/*
            Condition:
              StringLike:
                ec2:ResourceTag/parallelcluster:version: "*"
            Effect: Allow
            Sid: EC2ManagePolicy
          - Fn::If:
            - IsPrivate
            - Action:
                - ec2:CreateNetworkInterface
                - ec2:DeleteNetworkInterface
                - ec2:AttachNetworkInterface
              Resource:
                - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:*
              Effect: Allow
              Sid: PrivateDeploymentWritePolicy
            - !Ref AWS::NoValue
          - Fn::If:
            - IsPrivate
            - Action:
                - ec2:DescribeNetworkInterfaces
                - ec2:DescribeInstances
              Resource:
                - '*'
              Effect: Allow
              Sid: PrivateDeploymentReadPolicy
            - !Ref AWS::NoValue

  StoragePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub
        - ${IAMRoleAndPolicyPrefix}StoragePolicy-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
              - fsx:DescribeFileSystems
              - fsx:DescribeVolumes
              - fsx:DescribeFileCaches
            Resource:
              - !Sub arn:${AWS::Partition}:fsx:*:${AWS::AccountId}:volume/*
              - !Sub arn:${AWS::Partition}:fsx:*:${AWS::AccountId}:file-system/*
              - !Sub arn:${AWS::Partition}:fsx:*:${AWS::AccountId}:file-cache/*
            Effect: Allow
            Sid: FsxRead
          - Action:
              - elasticfilesystem:DescribeFileSystems
            Resource:
              - !Sub arn:${AWS::Partition}:elasticfilesystem:*:${AWS::AccountId}:file-system/*
            Effect: Allow
            Sid: EfsRead

  CostMonitoringAndPricingPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub
        - ${IAMRoleAndPolicyPrefix}CostMonitoringAndPricingPolicy-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - !If
            - InGovCloud
            - !Ref AWS::NoValue
            - Action:
              - ce:ListCostAllocationTags
              - ce:UpdateCostAllocationTagsStatus
              - ce:GetCostAndUsage
              Resource:
                - !Sub 'arn:aws:ce:us-east-1:${AWS::AccountId}:/*' # CE only available in us-east-1, aws partition, see https://docs.aws.amazon.com/general/latest/gr/billing.html
              Effect: Allow
              Sid: CostMonitoringPolicy
          - Action:
              - pricing:GetProducts
            Resource:
              - '*'
            Effect: Allow
            Sid: PricingPolicy

  SsmPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub
        - ${IAMRoleAndPolicyPrefix}SsmPolicy-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
              - ssm:SendCommand
            Resource:
              - !Sub arn:${AWS::Partition}:ec2:*:${AWS::AccountId}:instance/*
            Effect: Allow
            Sid: SsmSendPolicyInstance
            Condition:
              StringLike:
                ssm:resourceTag/parallelcluster:version: "*"
          - Action:
            - ssm:SendCommand
            Resource:
              - !Sub arn:${AWS::Partition}:ssm:*::document/AWS-RunShellScript
            Effect: Allow
            Sid: SsmSendPolicyCommand
          - Action:
            - ssm:GetCommandInvocation
            Resource:
              - '*'
            Effect: Allow
            Sid: SsmGetCommandInvocationPolicy

  LogsPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub
        - ${IAMRoleAndPolicyPrefix}LogsPolicy-${StackIdSuffix}
        - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] }
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
              - logs:GetLogEvents
            Resource:
              - !Sub "arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:${SsmLogGroup}:*"
              - !Sub "arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:${SsmLogGroup}:log-stream:*"
            Effect: Allow
            Sid: CloudWatchLogsRead
          - Action:
              - logs:DeleteLogStream
            Resource:
              - !Sub "arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:${SsmLogGroup}:log-stream:*/*/aws-runShellScript/stdout"
            Effect: Allow
            Sid: CloudWatchLogsDelete

  ApiGatewayCustomDomain:
    Condition: UseCustomDomain
    Type: AWS::ApiGateway::DomainName
    Properties:
#      CertificateArn: !Ref CustomDomainCertificateArn
      DomainName: !Ref CustomDomain
      EndpointConfiguration:
        Types:
          - REGIONAL
      RegionalCertificateArn: !Ref CustomDomainCertificateArn
      SecurityPolicy: TLS_1_2

  ApiGatewayCustomDomainMapping:
    Condition: UseCustomDomain
    Type: AWS::ApiGateway::BasePathMapping
    Properties:
      BasePath: !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ]
      DomainName: !Ref ApiGatewayCustomDomain
      RestApiId: !Ref ApiGatewayRestApi
      Stage: !Ref ApiGatewayRestStage

Outputs:
  ParallelClusterUILambdaArn:
    Description: 'ARN of the ParallelCluster UI Lambda function'
    Value: !GetAtt ParallelClusterUIFun.Arn
  ParallelClusterUIUrl:
    Description: 'Url to reach the ParallelCluster UI Site.'
    Export:
      Name: !Sub ${AWS::StackName}-ParallelClusterUISite
    Value: !If
      - UseCustomDomain
      - !Sub
        - https://${CustomDomain}/${CustomDomainBasePath}
        - { CustomDomainBasePath: !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ] }
      - !Sub
        - https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}
        - Api: !If
            - IsPrivate
            - !Sub
              - '${ApiId}-${VpceId}'
              - { ApiId: !Ref ApiGatewayRestApi, VpceId: !Ref VpcEndpointId }
            - !Ref ApiGatewayRestApi
          Stage: !Ref ApiGatewayRestStage
  CustomDomainEndpoint:
    Condition: UseCustomDomain
    Description: |
      The endpoint associated with the custom domain name. 
      Add an A record in your DNS for the PCUI custom domain name pointing to this endpoint.
    Value: !GetAtt ApiGatewayCustomDomain.RegionalDomainName
  AppClientId:
    Description: The id of the Cognito app client
    Value: !Ref CognitoAppClient
  UserPoolClientSecretArn:
    Description: The app client secret ARN for ParallelCluster UI.
    Value: !GetAtt UserPoolClientSecret.SecretArn
  UserPoolClientSecretName:
    Description: The app client secret name for ParallelCluster UI.
    Value: !GetAtt UserPoolClientSecret.SecretName
  CognitoCustomDomainEndpoint:
    Condition: UseCognitoCustomDomain
    Description: |
      The endpoint associated with the Cognito custom domain name. 
      Add an A record in your DNS for the Cognito custom domain name pointing to this endpoint.
    Value: !GetAtt Cognito.Outputs.CustomDomainEndpoint
