Description: Create a VS code-server instance with an Amazon CloudFront distribution for use in Workshop Studio. Version 4.0.0

Parameters:
  VSCodeUser:
    Type: String
    Description: UserName for VS code-server
    Default: participant
  InstanceName:
    Type: String
    Description: VS code-server EC2 instance name
    Default: VSCodeServer
  InstanceVolumeSize:
    Type: Number
    Description: VS code-server EC2 instance volume size in GB
    Default: 40
  InstanceType:
    Description: VS code-server EC2 instance type
    Type: String
    Default: t3.large
    AllowedPattern: '^(t3|t4|c6|c7|m6|m7|m8)[g|i|a]?(d|n|dn|-flex)?\.(nano|micro|small|medium|large|[2|4|9|12|16|18|24|32|48]?xlarge)$'
    ConstraintDescription: Must be a valid t, c or m series EC2 instance type
  InstanceOperatingSystem:
    Description: VS code-server EC2 operating system
    Type: String
    Default: AmazonLinux-2023
    AllowedValues: ['AmazonLinux-2023', 'Ubuntu-22', 'Ubuntu-24']
  HomeFolder:
    Type: String
    Description: Folder to open in VS Code server
    Default: /workspace
  DevServerBasePath:
    Type: String
    Description: Base path for the application to be added to Nginx sites-available list
    Default: app
  DevServerPort:
    Type: Number
    Description: Port for the DevServer
    Default: 8081
  RepoUrl:
    Description: Remote repo URL to clone. To not clone a remote repo, leave blank
    Type: String
    Default: ''
  AssetZipS3Path:
    Description: S3 path holding the asset zip file to be copied into the home folder. To not include any assets, leave blank
    Type: String
    Default: ''
  BranchZipS3Path:
    Description: S3 path holding the branches zip file to be checked into the git repo, with each folder being a branch. The content of each folder will added as under a branch, with the folder name being used as the branch name. To leave the empty, leave blank
    Type: String
    Default: ''
  FolderZipS3Path:
    Description: S3 path holding the folder zip file, with each folder being a subfolder of the home directory. Each folder will have its own local git repo. To not include any folders, leave blank
    Type: String
    Default: ''
  WorkshopName:
    Type: String
    Default: "genai-platform-workshop"
    Description: "Name of the workshop"
  VSCodePassword:
    Type: String
    NoEcho: true
    Default: "workshop2026"
    MinLength: 8
    Description: "Password for VSCode Server (minimum 8 characters)"
Conditions:
  IsAL2023: !Equals [!Ref InstanceOperatingSystem, 'AmazonLinux-2023']
  IsGraviton: !Or
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 't4g']
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'c6g']
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'c7g']
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'c7gd']
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'c8g']
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm6g']
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm6gd']
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm7g']
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm7gd']
    - !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm8g']

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Instance Configuration
        Parameters:
          - InstanceName
          - InstanceVolumeSize
          - InstanceType
          - InstanceOperatingSystem
      - Label:
          default: Code Server Configuration
        Parameters:
          - VSCodeUser
          - HomeFolder
          - DevServerBasePath
          - DevServerPort
          - RepoUrl
          - AssetZipS3Path
          - BranchZipS3Path
          - FolderZipS3Path
    ParameterLabels:
      VSCodeUser:
        default: VS code-server user name
      InstanceName:
        default: Instance name
      InstanceVolumeSize:
        default: Instance volume size
      InstanceType:
        default: Instance type
      InstanceOperatingSystem:
        default: Instance operating system
      HomeFolder:
        default: VS code-server home folder
      DevServerBasePath:
        default: Application base path
      DevServerPort:
        default: Application port
      RepoUrl:
        default: Git repo URL
      AssetZipS3Path:
        default: Asset file S3 path
      BranchZipS3Path:
        default: Branch file S3 path
      FolderZipS3Path:
        default: Folder file S3 path

Mappings:
  ArmImage:
  # aws ssm get-parameters-by-path --path "/aws/service/canonical/ubuntu/" --recursive --query "Parameters[*].Name"  > canonical-ami.txt
  # aws ssm get-parameters-by-path --path "/aws/service/ami-amazon-linux-latest/" --recursive --query "Parameters[*].Name"  > amazon-ami.txt
    Ubuntu-22:
      ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/arm64/hvm/ebs-gp2/ami-id}}'
    Ubuntu-24:
      ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/arm64/hvm/ebs-gp3/ami-id}}'
    AmazonLinux-2023:
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64}}'
  AmdImage:
    Ubuntu-22:
      ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id}}'
    Ubuntu-24:
      ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/amd64/hvm/ebs-gp3/ami-id}}'
    AmazonLinux-2023:
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
  AWSRegionsPrefixListID:
  # aws ec2 describe-managed-prefix-lists  --region <REGION> | jq -r '.PrefixLists[] | select (.PrefixListName == "com.amazonaws.global.cloudfront.origin-facing") | .PrefixListId'
    ap-northeast-1:
      PrefixList: pl-58a04531
    ap-northeast-2:
      PrefixList: pl-22a6434b
    ap-south-1:
      PrefixList: pl-9aa247f3
    ap-southeast-1:
      PrefixList: pl-31a34658
    ap-southeast-2:
      PrefixList: pl-b8a742d1
    ca-central-1:
      PrefixList: pl-38a64351
    eu-central-1:
      PrefixList: pl-a3a144ca
    eu-north-1:
      PrefixList: pl-fab65393
    eu-west-1:
      PrefixList: pl-4fa04526
    eu-west-2:
      PrefixList: pl-93a247fa
    eu-west-3:
      PrefixList: pl-75b1541c
    sa-east-1:
      PrefixList: pl-5da64334
    us-east-1:
      PrefixList: pl-3b927c52
    us-east-2:
      PrefixList: pl-b6a144df
    us-west-1:
      PrefixList: pl-4ea04527
    us-west-2:
      PrefixList: pl-82a045eb

Resources:
  VSCodeSecret:
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W77
            reason: The default KMS Key used by Secrets Manager is appropriate for this password which will be used to log into VSCodeServer, which has very limited permissions. In addition this secret will not be required to be shared across accounts
    Type: AWS::SecretsManager::Secret
    DeletionPolicy: Delete
    UpdateReplacePolicy: Delete
    Properties:
      Name: !Ref InstanceName
      Description: VS code-server user details
      GenerateSecretString:
        PasswordLength: 16
        SecretStringTemplate: !Sub '{"username":"${VSCodeUser}"}'
        GenerateStringKey: 'password'
        ExcludePunctuation: true

  SecretPlaintextLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub lambda.${AWS::URLSuffix}
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: AwsSecretsManager
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Ref VSCodeSecret

  SecretPlaintextLambda:
    Type: AWS::Lambda::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W58
            reason: Warning incorrectly reported. The role associated with the Lambda function has the AWSLambdaBasicExecutionRole managed policy attached, which includes permission to write CloudWatch Logs. See https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicExecutionRole.html
          - id: W89
            reason: CloudFormation custom function does not need the scaffolding of a VPC, to do so would add unnecessary complexity
          - id: W92
            reason: CloudFormation custom function does not need reserved concurrent executions, to do so would add unnecessary complexity
    Properties:
      Description: Return the value of the secret
      Handler: index.lambda_handler
      Runtime: python3.12
      MemorySize: 128
      Timeout: 10
      Architectures:
        - arm64
      Role: !GetAtt SecretPlaintextLambdaRole.Arn
      Code:
        ZipFile: |
          import boto3
          import json
          import cfnresponse
          import logging

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def is_valid_json(json_string):
              logger.debug(f'Calling is_valid_jason:{json_string}')
              try:
                  json.loads(json_string)
                  logger.info('Secret is in json format')
                  return True
              except json.JSONDecodeError:
                  logger.info('Secret is in string format')
                  return False

          def lambda_handler(event, context):
              logger.debug(f'event: {event}')
              logger.debug(f'context: {context}')
              try:
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='No action to take')
                  else:
                      resource_properties = event['ResourceProperties']
                      secret_name = resource_properties['SecretArn']
                      secrets_mgr = boto3.client('secretsmanager')

                      logger.info('Getting secret from %s', secret_name)

                      secret = secrets_mgr.get_secret_value(SecretId = secret_name)
                      logger.debug(f'secret: {secret}')
                      secret_value = secret['SecretString']

                      responseData = {}
                      if is_valid_json(secret_value):
                          responseData = secret_value
                      else:
                          responseData = {'secret': secret_value}
                      logger.debug(f'responseData: {responseData}')
                      logger.debug(f'type(responseData): {type(responseData)}')
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData=json.loads(responseData), reason='OK', noEcho=True)
              except Exception as e:
                  logger.error(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=str(e))

  SecretPlaintext:
    Type: Custom::SecretPlaintextLambda
    Properties:
      ServiceToken: !GetAtt SecretPlaintextLambda.Arn
      ServiceTimeout: 15
      SecretArn: !Ref VSCodeSecret

  VSCodeSSMDoc:
    Type: AWS::SSM::Document
    Properties:
      DocumentType: Command
      Content:
        schemaVersion: '2.2'
        description: Bootstrap VS code-server instance
        parameters:
          LinuxFlavor:
            type: String
            default: 'al2023'
          VSCodePassword:
            type: String
            default: ''
        # all mainSteps scripts are in in /var/lib/amazon/ssm/<instanceid>/document/orchestration/<uuid>/<StepName>/_script.sh
        mainSteps:
          - name: InstallCloudWatchAgent
            action: aws:configurePackage
            inputs:
              name: AmazonCloudWatchAgent
              action: Install
          - name: ConfigureCloudWatchAgent
            action: aws:runDocument
            inputs:
              documentType: SSMDocument
              documentPath: AmazonCloudWatch-ManageAgent
              documentParameters:
                action: configure
                mode: ec2
                optionalConfigurationSource: default
                optionalRestart: 'yes'
          - name: InstallAptPackagesApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dpkg --configure -a
                - apt-get -q update
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q apt-utils
                - apt-get -q update
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q needrestart unattended-upgrades
                - sed -i 's/#$nrconf{kernelhints} = -1;/$nrconf{kernelhints} = 0;/' /etc/needrestart/needrestart.conf
                - sed -i 's/#$nrconf{verbosity} = 2;/$nrconf{verbosity} = 0;/' /etc/needrestart/needrestart.conf
                - sed -i "s/#\$nrconf{restart} = 'i';/\$nrconf{restart} = 'a';/" /etc/needrestart/needrestart.conf
                - echo "Apt helper packages added. Checking configuration"
                - cat /etc/needrestart/needrestart.conf
          - name: InstallBasePackagesDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dnf install -y --allowerasing curl gnupg whois argon2 unzip nginx openssl
          - name: InstallBasePackagesApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dpkg --configure -a
                - apt-get -q update
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl gnupg whois argon2 unzip nginx openssl
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q locales locales-all apt-transport-https ca-certificates software-properties-common
          - name: InstallWorkshopToolsDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - echo "Installing Gen AI Platform Workshop Tools..."
                - echo "Installing kubectl..."
                - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
                - chmod +x kubectl
                - mv kubectl /usr/local/bin/
                - echo "Installing Helm..."
                - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
                - echo "Installing eksctl..."
                - curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
                - mv /tmp/eksctl /usr/local/bin
                - echo "Installing yq for YAML processing..."
                - wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
                - chmod +x /usr/local/bin/yq
                - echo "Installing jq and other packages..."
                - dnf install -y jq
                - echo "Installing Docker..."
                - dnf install -y docker
                - systemctl enable docker
                - systemctl start docker
                - !Sub usermod -aG docker ${VSCodeUser}
                - echo "Installing additional development tools..."
                - dnf install -y make gcc gcc-c++ nodejs npm
                - echo "Workshop tools installation completed"
                - echo "Verifying installations..."
                - kubectl version --client
                - helm version
                - eksctl version
                - yq --version
                - jq --version
                - docker --version
          - name: InstallWorkshopToolsApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - echo "Installing Gen AI Platform Workshop Tools..."
                - dpkg --configure -a
                - echo "Installing kubectl..."
                - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
                - chmod +x kubectl
                - mv kubectl /usr/local/bin/
                - echo "Installing Helm..."
                - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
                - echo "Installing eksctl..."
                - curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
                - mv /tmp/eksctl /usr/local/bin
                - echo "Installing yq for YAML processing..."
                - wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
                - chmod +x /usr/local/bin/yq
                - echo "Installing jq and other tools..."
                - apt-get -q update
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q jq make gcc g++ nodejs npm
                - echo "Installing Docker..."
                - apt-get -q update
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q docker.io
                - systemctl enable docker
                - systemctl start docker
                - !Sub usermod -aG docker ${VSCodeUser}
                - echo "Workshop tools installation completed"
                - echo "Verifying installations..."
                - kubectl version --client
                - helm version
                - eksctl version
                - yq --version
                - jq --version
                - docker --version
          - name: AddUserDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  echo 'Adding user: ${VSCodeUser}'
                  adduser -c '' ${VSCodeUser}
                  passwd -l ${VSCodeUser}
                  echo "${VSCodeUser}:{{ VSCodePassword }}" | chpasswd
                  usermod -aG wheel ${VSCodeUser}
                - echo "User added. Checking configuration"
                - !Sub getent passwd ${VSCodeUser}
          - name: AddUserApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dpkg --configure -a
                - !Sub |
                  if [[ "${VSCodeUser}" == "ubuntu" ]]
                  then
                    echo 'Using existing user: ${VSCodeUser}'
                  else
                    echo 'Adding user: ${VSCodeUser}'
                    adduser --disabled-password --gecos '' ${VSCodeUser}
                    echo "${VSCodeUser}:{{ VSCodePassword }}" | chpasswd
                    usermod -aG sudo ${VSCodeUser}
                  fi
                - !Sub |
                  tee /etc/sudoers.d/91-vscode-user <<EOF
                  ${VSCodeUser} ALL=(ALL) NOPASSWD:ALL
                  EOF
                - !Sub mkdir -p /home/${VSCodeUser} && chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
                - !Sub mkdir -p /home/${VSCodeUser}/.local/bin && chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
                - echo "User added. Checking configuration"
                - !Sub getent passwd ${VSCodeUser}
          - name: UpdateProfile
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - echo LANG=en_US.utf-8 >> /etc/environment
                - echo LC_ALL=en_US.UTF-8 >> /etc/environment
                - !Sub echo 'PATH=$PATH:/home/${VSCodeUser}/.local/bin:/usr/local/bin' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo 'export PATH' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo 'export AWS_REGION=${AWS::Region}' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo 'export AWS_ACCOUNTID=${AWS::AccountId}' >> /home/${VSCodeUser}/.bashrc
                - !Sub chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
          - name: InstallAWSCLI
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - mkdir -p /tmp
                - curl -fsSL https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip -o /tmp/aws-cli.zip
                - !Sub chown -R ${VSCodeUser}:${VSCodeUser} /tmp/aws-cli.zip
                - unzip -q -d /tmp /tmp/aws-cli.zip
                - sudo /tmp/aws/install
                - rm -rf /tmp/aws
                - echo "AWS CLI installed. Checking configuration"
                - aws --version
          - name: InstallPythonDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 300
              runCommand:
                # AL2023 currently ships with Python 3.9 preinstalled, but 3.11 is available in the repository
                - '#!/bin/bash'
                - dnf install -y python3.11 python3.11-pip python3-virtualenv python3-pytest python3-boto3
                - !Sub echo 'alias pytest=pytest-3' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo 'alias python3=python3.11' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo 'alias pip3=pip3.11' >> /home/${VSCodeUser}/.bashrc
                - echo 'alias=python3=python3.11' >> ~/.bashrc
                - echo 'alias pip3=pip3.11' >> ~/.bashrc
                - python3.11 -m pip install --upgrade pip 2>&1
                - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension ms-python.python --force
                - !Sub |
                    if [ -f /home/${VSCodeUser}/.local/share/code-server/User/settings.json ]; then
                      sed -i "2i\\  \"python.testing.pytestEnabled\": true," /home/${VSCodeUser}/.local/share/code-server/User/settings.json
                    else
                      echo '{
                        "python.testing.pytestEnabled": true
                      }' > /home/${VSCodeUser}/.local/share/code-server/User/settings.json
                    fi
                - echo "Python and Pip installed. Checking configuration"
                - python3.11 --version
                - python3.11 -m pip --version 2>&1
          - name: InstallPythonApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                # Ubuntu 22 default is Python 3.10
                # Ubuntu 24 default is Python 3.12
                # Installing Python 3.11 explicitly
                - '#!/bin/bash'
                - dpkg --configure -a
                - apt-get -q update
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q software-properties-common
                - add-apt-repository -y ppa:deadsnakes/ppa
                - apt-get -q update
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q python3.11 python3.11-venv python3.11-dev python3-pip python3-boto3 python3-pytest
                - update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
                - !Sub echo 'alias pytest=pytest-3' >> /home/${VSCodeUser}/.bashrc
                - systemctl start multipathd.service packagekit.service
                - systemctl restart unattended-upgrades.service
                - systemctl restart networkd-dispatcher.service
                - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension ms-python.python --force
                - !Sub |
                    if [ -f /home/${VSCodeUser}/.local/share/code-server/User/settings.json ]; then
                      sed -i "2i\\  \"python.testing.pytestEnabled\": true," /home/${VSCodeUser}/.local/share/code-server/User/settings.json
                    else
                      echo '{
                        "python.testing.pytestEnabled": true
                      }' > /home/${VSCodeUser}/.local/share/code-server/User/settings.json
                    fi
                - echo "Python and Pip installed. Checking configuration"
                - python3 --version
                - pip3 --version
          - name: InstallGitDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dnf install -y git
                - !Sub sudo -u ${VSCodeUser} git config --global user.email "participant@example.com"
                - !Sub sudo -u ${VSCodeUser} git config --global user.name "Workshop Participant"
                - !Sub sudo -u ${VSCodeUser} git config --global init.defaultBranch "main"
                - echo "Git installed. Checking configuration"
                - git --version
                - !Sub |
                  if [[ "${RepoUrl}" =~ ^codecommit ]];
                  then
                    echo "Installing git-remote-codecommit with pip3"
                    PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install git-remote-codecommit
                  fi
          - name: InstallGitApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dpkg --configure -a
                - add-apt-repository ppa:git-core/ppa
                - apt-get -q update
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
                - !Sub sudo -u ${VSCodeUser} git config --global user.email "participant@example.com"
                - !Sub sudo -u ${VSCodeUser} git config --global user.name "Workshop Participant"
                - !Sub sudo -u ${VSCodeUser} git config --global init.defaultBranch "main"
                - echo "Git installed. Checking configuration"
                - git --version
                - !Sub |
                  if [[ "${RepoUrl}" =~ ^codecommit ]];
                  then
                    apt-get -q update
                    DEBIAN_FRONTEND=noninteractive apt-get install -y -q python3-pip
                    echo "Installing git-remote-codecommit with pip3"
                    PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install git-remote-codecommit
                  fi
          - name: CloneRepo
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  if [[ -z "${RepoUrl}" ]]
                  then
                    echo "No Repo"
                  else
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    RepoUrlRegion=$(echo "${RepoUrl}" | sed "s/{.Region}/${AWS::Region}/g")
                    sudo -u ${VSCodeUser} git clone $RepoUrlRegion ${HomeFolder}
                    echo "Repo $(RepoUrlRegion) cloned. Checking configuration"
                    ls -la ${HomeFolder}
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} remote -v
                  fi
          - name: DownloadAssets
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  if [[ -z "${AssetZipS3Path}" ]]
                  then
                    echo "No assets"
                  else
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    mkdir -p /tmp
                    aws s3 cp s3://${AssetZipS3Path} /tmp/asset.zip
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/asset.zip
                    unzip -o /tmp/asset.zip -d ${HomeFolder}
                    chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    if  [[ -d ${HomeFolder}/.git ]] # Indicates that a repo has been cloned
                    then
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} add .
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} commit -m 'Workshop commit'
                    else
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} init
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} add .
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} commit -m 'Initial commit'
                    fi
                    echo "Assets downloaded. Checking configuration: ${HomeFolder}"
                    ls -la ${HomeFolder}
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} branch
                  fi
          - name: DownloadFolders
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  if [[ -z "${FolderZipS3Path}" ]]
                  then
                    echo "No folders"
                  else
                    rm -rf /tmp/folder
                    mkdir -p /tmp/folder && chown -R ${VSCodeUser}:${VSCodeUser} /tmp/folder
                    aws s3 cp s3://${FolderZipS3Path} /tmp/asset-folder.zip
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/asset-folder.zip
                    unzip -o /tmp/asset-folder.zip -d /tmp/folder
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/folder
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    cd "${HomeFolder}" && cd ..
                    if [[ $(pwd) ==  "/" ]]
                    then
                      targetRootFolder=""
                    else
                      targetRootFolder=$(pwd)
                      chown -R ${VSCodeUser}:${VSCodeUser} .
                    fi
                    find "/tmp/folder" -maxdepth 1 -mindepth 1 -type d | while read sourceFolder; do
                      folder="$(basename $sourceFolder)"
                      echo "Processing folder: $folder"
                      targetFolder=$targetRootFolder/$folder
                      if [[ $targetRootFolder == "" ]]
                      then
                        mv $sourceFolder /
                      else
                        mv $sourceFolder $targetRootFolder
                      fi
                      chown -R ${VSCodeUser}:${VSCodeUser} $targetFolder
                      sudo -u ${VSCodeUser} git -C $targetFolder init
                      sudo -u ${VSCodeUser} git -C $targetFolder add .
                      sudo -u ${VSCodeUser} git -C $targetFolder commit -m "Initial commit"
                      echo "Folder downloaded. Checking configuration: $targetFolder"
                      ls -la $targetFolder
                    done
                    rm -rf /tmp/folder
                  fi
          - name: DownloadBranches
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  if [[ -z "${BranchZipS3Path}" ]]
                  then
                    echo "No branches"
                  else
                    rm -rf /tmp/branch
                    rm -rf /tmp/git
                    mkdir -p /tmp/branch && chown -R ${VSCodeUser}:${VSCodeUser} /tmp/branch
                    mkdir -p /tmp/git && chown -R ${VSCodeUser}:${VSCodeUser} /tmp/git
                    aws s3 cp s3://${BranchZipS3Path} /tmp/asset-branch.zip
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/asset-branch.zip
                    unzip -o /tmp/asset-branch.zip -d /tmp/branch
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/branch
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} init
                    mv ${HomeFolder}/.git /tmp/git
                    rm -rf ${HomeFolder}
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    mv /tmp/git/.git ${HomeFolder}
                    find /tmp/branch -maxdepth 1 -mindepth 1 -type d | while read sourceFolder; do
                      branch="$(basename $sourceFolder)"
                      echo "Processing branch: $branch"
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} checkout -b $branch 2>&1
                      cp -a $sourceFolder/. ${HomeFolder}
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} add .
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} commit -m "Initial commit $branch"
                      mv ${HomeFolder}/.git /tmp/git
                      rm -rf ${HomeFolder}
                      mkdir ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                      mv /tmp/git/.git ${HomeFolder}
                    done
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} checkout main 2>&1
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} restore .
                    rm -rf /tmp/branch
                    rm -rf /tmp/git
                    echo "Branches downloaded. Checking configuration: $HomeFolder"
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} branch
                    ls -la ${HomeFolder}
                  fi
          - name: InstallNodeDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dnf install -y nodejs npm
                - echo "Node and npm installed. Checking configuration"
                - node -v
                - npm -v
          - name: InstallNodeApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg
                - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list
                - apt-get -q update
                - DEBIAN_FRONTEND=noninteractive apt-get install -y -q nodejs
                - npm install -g npm@latest
                - echo "Node and npm installed. Checking configuration"
                - node -v
                - npm -v
          - name: InstallDocker
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  dnf install -y docker
                  systemctl enable docker
                  systemctl start docker
                  usermod -aG docker ${VSCodeUser}
                  echo "Docker installed. Checking configuration"
                  docker --version
            precondition:
              StringEquals:
                - "{{ LinuxFlavor }}"
                - "al2023"
          - name: ConfigureCodeServer
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub export HOME=/home/${VSCodeUser}
                - curl -fsSL https://code-server.dev/install.sh | bash -s -- 2>&1
                - !Sub systemctl enable --now code-server@${VSCodeUser} 2>&1
                - !Sub |
                  tee /etc/nginx/conf.d/code-server.conf <<EOF
                  server {
                      listen 80;
                      listen [::]:80;
                      # server_name \$\{CloudFrontDistribution.DomainName\};
                      server_name *.cloudfront.net;
                      location / {
                        proxy_pass http://localhost:8080/;
                        proxy_set_header Host \$host;
                        proxy_set_header Upgrade \$http_upgrade;
                        proxy_set_header Connection upgrade;
                        proxy_set_header Accept-Encoding gzip;
                      }
                      location /${DevServerBasePath} {
                        proxy_pass http://localhost:${DevServerPort}/${DevServerBasePath};
                        proxy_set_header Host \$host;
                        proxy_set_header Upgrade \$http_upgrade;
                        proxy_set_header Connection upgrade;
                        proxy_set_header Accept-Encoding gzip;
                      }
                  }
                  EOF
                - !Sub mkdir -p /home/${VSCodeUser}/.config/code-server
                - !Sub |
                  tee /home/${VSCodeUser}/.config/code-server/config.yaml <<EOF
                  cert: false
                  auth: password
                  hashed-password: "$(echo -n {{ VSCodePassword }} | argon2 $(openssl rand -base64 12) -e)"
                  EOF
                - !Sub mkdir -p /home/${VSCodeUser}/.local/share/code-server/User/
                - !Sub touch /home/${VSCodeUser}/.hushlogin
                - !Sub mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                - !Sub |
                  tee /home/${VSCodeUser}/.local/share/code-server/User/settings.json <<EOF
                  {
                    "extensions.autoUpdate": false,
                    "extensions.autoCheckUpdates": false,
                    "telemetry.telemetryLevel": "off",
                    "security.workspace.trust.startupPrompt": "never",
                    "security.workspace.trust.enabled": false,
                    "security.workspace.trust.banner": "never",
                    "security.workspace.trust.emptyWindow": false,
                    "auto-run-command.rules": [
                      {
                        "command": "workbench.action.terminal.new"
                      }
                    ]
                  }
                  EOF
                - !Sub chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
                - !Sub systemctl restart code-server@${VSCodeUser}
                - systemctl restart nginx
                - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension AmazonWebServices.aws-toolkit-vscode --force
                - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension AmazonWebServices.amazon-q-vscode --force
                - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension ms-vscode.live-server --force
                - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension synedra.auto-run-command --force
                - !Sub chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
                - echo "Nginx installed. Checking configuration"
                - nginx -t 2>&1
                - systemctl status nginx
                - echo "CodeServer installed. Checking configuration"
                - code-server -v
                - !Sub systemctl status code-server@${VSCodeUser}
          - name: InstallKiroCLI
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - set -e
                - cd /tmp
                - |
                  ARCH=$(uname -m)
                  if [[ "$ARCH" == "aarch64" ]]; then
                    KIRO_URL="https://desktop-release.q.us-east-1.amazonaws.com/latest/kirocli-aarch64-linux.zip"
                  else
                    KIRO_URL="https://desktop-release.q.us-east-1.amazonaws.com/latest/kirocli-x86_64-linux.zip"
                  fi
                  echo "Downloading Kiro CLI from: $KIRO_URL"
                  curl --proto '=https' --tlsv1.2 -sSf "$KIRO_URL" -o "kirocli.zip"
                - echo "Downloaded kirocli.zip, checking file:"
                - ls -la kirocli.zip
                - echo "Extracting kirocli.zip:"
                - unzip -q kirocli.zip
                - echo "Contents after extraction:"
                - ls -la
                - |
                  if [ -d "kirocli" ]; then
                    echo "Contents of kirocli directory:"
                    ls -la kirocli/
                    if [ -d "kirocli/bin" ]; then
                      echo "Installing kiro-cli to /usr/local/bin/"
                      cp kirocli/bin/* /usr/local/bin/
                      chmod +x /usr/local/bin/kiro-cli
                      echo "kiro-cli installed successfully"
                      kiro-cli --version || echo "kiro-cli version check failed"
                    else
                      echo "WARNING: kirocli/bin directory not found"
                    fi
                  else
                    echo "WARNING: kirocli directory not found after extraction"
                  fi
                - rm -rf kirocli kirocli.zip
# Install optional packages here - any of these blocks can be deleted if that software is not required for the workshop
# Install Workshop specific packages here

  SSMDocLambdaRole:
    Type: AWS::IAM::Role
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: The Amazon EC2 ssm:*CommandInvocation API actions do not support resource-level permissions, so you cannot control which individual resources users can view in the console. Therefore, the * wildcard is necessary in the Resource element. See https://docs.aws.amazon.com/service-authorization/latest/reference/list_awssystemsmanager.html
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub lambda.${AWS::URLSuffix}
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: SSMDocOnEC2
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:SendCommand
                Resource:
                  - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${VSCodeSSMDoc}
                  - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/AmazonCloudWatch-ManageAgent
                  - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/${VSCodeInstance}
              - Effect: Allow
                Action:
                  - ssm:ListCommandInvocations
                  - ssm:GetCommandInvocation
                Resource: '*'

  RunSSMDocLambda:
    Type: AWS::Lambda::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W58
            reason: Warning incorrectly reported. The role associated with the Lambda function has the AWSLambdaBasicExecutionRole managed policy attached, which includes permission to write CloudWatch Logs. See https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicExecutionRole.html
          - id: W89
            reason: CloudFormation custom function does not need the scaffolding of a VPC, to do so would add unnecessary complexity
          - id: W92
            reason: CloudFormation custom function does not need reserved concurrent executions, to do so would add unnecessary complexity
    Properties:
      Description: Run SSM document on EC2 instance
      Handler: index.lambda_handler
      Runtime: python3.12
      MemorySize: 128
      Timeout: 600
      Environment:
        Variables:
          RetrySleep: 2900
          AbortTimeRemaining: 3200
      Architectures:
        - arm64
      Role: !GetAtt SSMDocLambdaRole.Arn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import logging
          import time
          import os

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def lambda_handler(event, context):
              logger.debug(f'event: {event}')
              logger.debug(f'context: {context}')

              if event['RequestType'] != 'Create':
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='No action to take')
              else:
                  sleep_ms = int(os.environ.get('RetrySleep'))
                  abort_time_remaining_ms = int(os.environ.get('AbortTimeRemaining'))
                  resource_properties = event['ResourceProperties']
                  instance_id = resource_properties['InstanceId']
                  document_name = resource_properties['DocumentName']
                  cloudwatch_log_group_name = resource_properties['CloudWatchLogGroupName']

                  logger.info(f'Running SSM Document {document_name} on EC2 instance {instance_id}. Logging to {cloudwatch_log_group_name}')

                  del resource_properties['ServiceToken']
                  if 'ServiceTimeout' in resource_properties:
                      del resource_properties['ServiceTimeout']
                  del resource_properties['InstanceId']
                  del resource_properties['DocumentName']
                  del resource_properties['CloudWatchLogGroupName']
                  if 'PhysicalResourceId' in resource_properties:
                      del resource_properties['PhysicalResourceId']

                  logger.debug(f'resource_properties filtered: {resource_properties}')

                  parameters = {}
                  for key, value in resource_properties.items():
                      parameters[key] = [value]

                  logger.debug(f'parameters: {parameters}')

                  retry = True
                  attempt_no = 0
                  time_remaining_ms = context.get_remaining_time_in_millis()

                  ssm = boto3.client('ssm')

                  while (retry == True):
                      attempt_no += 1
                      logger.info(f'Attempt: {attempt_no}. Time Remaining: {time_remaining_ms/1000}s')
                      try:
                          response = ssm.send_command(
                              InstanceIds = [instance_id],
                              DocumentName = document_name,
                              CloudWatchOutputConfig = {'CloudWatchLogGroupName': cloudwatch_log_group_name, 'CloudWatchOutputEnabled': True},
                              Parameters = parameters
                          )
                          logger.debug(f'response: {response}')
                          command_id = response['Command']['CommandId']
                          responseData = {'CommandId': command_id}
                          cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, reason='OK')
                          retry = False

                      except ssm.exceptions.InvalidInstanceId as e:
                          time_remaining_ms = context.get_remaining_time_in_millis()
                          if (time_remaining_ms > abort_time_remaining_ms):
                              logger.info(f'Instance {instance_id} not ready. Sleeping: {sleep_ms/1000}s')
                              time.sleep(sleep_ms/1000)
                              retry = True
                          else:
                              logger.info(f'Instance {instance_id} not ready, timed out. Time remaining {time_remaining_ms/1000}s < Abort time remaining {abort_time_remaining_ms/1000}s')
                              logger.error(e, exc_info=True)
                              cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason='Timed out. Time remaining: ' + str(time_remaining_ms/1000) + 's < Abort time remaining: ' + str(abort_time_remaining_ms/1000) + 's')
                              retry = False

                      except Exception as e:
                          logger.error(e, exc_info=True)
                          cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=str(e))
                          retry = False

  RunVSCodeSSMDoc:
    Type: Custom::RunSSMDocLambda
    Properties:
      ServiceToken: !GetAtt RunSSMDocLambda.Arn
      ServiceTimeout: 305
      InstanceId: !Ref VSCodeInstance
      DocumentName: !Ref VSCodeSSMDoc
      CloudWatchLogGroupName: !Sub /aws/ssm/${VSCodeSSMDoc}
      LinuxFlavor: !If [IsAL2023, 'al2023', 'ubuntu']
      VSCodePassword: !Ref VSCodePassword

  VSCodeInstanceBootstrapRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "VSCodeInstanceBootstrapRole"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - !Sub ec2.${AWS::URLSuffix}
                - !Sub ssm.${AWS::URLSuffix}
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore
        - !Sub arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonQDeveloperAccess
        - !Sub arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AWSCodeCommitPowerUser
        - !Sub arn:${AWS::Partition}:iam::aws:policy/IAMFullAccess
        - !Sub arn:${AWS::Partition}:iam::aws:policy/PowerUserAccess
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess
      Policies:
        - PolicyName: ProjectSpecificAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:PutParameter
                  - ssm:GetParameter
                  - ssm:GetParameters
                  - ssm:GetParametersByPath
                  - ssm:DeleteParameter
                  - ssm:DeleteParameters
                  - ssm:DescribeParameters
                Resource:
                  - !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/sample-application/session-tokens/*
                  - !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/*agentcore*
                  - !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/*cognito*
                  - !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/*troubleshooting*
              - Effect: Allow
                Action:
                  - bedrock:*
                  - bedrock-agentcore:*
                Resource: "*"
              - Effect: Allow
                Action:
                  - rds:DescribeDBInstances
                  - rds:DescribeDBClusters
                  - rds:DescribeDBSubnetGroups
                  - rds:DescribeDBParameterGroups
                  - rds:DescribeDBClusterParameterGroups
                  - rds:ListTagsForResource
                Resource: "*"
              - Effect: Allow
                Action:
                  - cloudwatch:ListMetrics
                  - cloudwatch:GetMetricStatistics
                  - cloudwatch:GetMetricData
                  - cloudwatch:DescribeAlarms
                  - cloudwatch:PutMetricData
                Resource: "*"
              - Effect: Allow
                Action:
                  - lambda:ListFunctions
                  - lambda:GetFunction
                  - lambda:GetFunctionConfiguration
                  - lambda:InvokeFunction
                Resource: "*"
        - PolicyName: CDKBootstrapPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Sid: CloudFormationPermissions
                Effect: Allow
                Action:
                  - cloudformation:CreateChangeSet
                  - cloudformation:DeleteStack
                  - cloudformation:DescribeChangeSet
                  - cloudformation:DescribeStackEvents
                  - cloudformation:DescribeStacks
                  - cloudformation:ExecuteChangeSet
                  - cloudformation:GetTemplate
                Resource: !Sub arn:aws:cloudformation:*:${AWS::AccountId}:stack/CDKToolkit/*
              - Sid: IAMRolePermissions
                Effect: Allow
                Action:
                  - iam:CreateRole
                  - iam:DeleteRole
                  - iam:GetRole
                  - iam:AttachRolePolicy
                  - iam:DetachRolePolicy
                  - iam:DeleteRolePolicy
                  - iam:PutRolePolicy
                  - iam:TagRole
                  - iam:GetPolicy
                  - iam:CreatePolicy
                  - iam:DeletePolicy
                  - iam:ListPolicyVersions
                  - iam:CreatePolicyVersion
                  - iam:DeletePolicyVersion
                  - iam:SetDefaultPolicyVersion
                Resource:
                  - !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-*
                  - !Sub arn:aws:iam::${AWS::AccountId}:policy/*
              - Sid: S3Permissions
                Effect: Allow
                Action:
                  - s3:CreateBucket
                  - s3:DeleteBucket
                  - s3:PutBucketPolicy
                  - s3:DeleteBucketPolicy
                  - s3:PutBucketPublicAccessBlock
                  - s3:PutBucketVersioning
                  - s3:PutEncryptionConfiguration
                  - s3:GetBucketPolicy
                  - s3:GetEncryptionConfiguration
                  - s3:PutLifecycleConfiguration
                Resource: "*"
              - Sid: ECRPermissions
                Effect: Allow
                Action:
                  - ecr:CreateRepository
                  - ecr:DescribeRepositories
                  - ecr:PutLifecyclePolicy
                  - ecr:SetRepositoryPolicy
                  - ecr:DeleteRepository
                Resource: "*"
              - Sid: STSPermissions
                Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-*
              - Sid: SSMPermissions
                Effect: Allow
                Action:
                  - ssm:GetParameter
                  - ssm:PutParameter
                  - ssm:DeleteParameter
                Resource: !Sub arn:aws:ssm:*:${AWS::AccountId}:parameter/cdk-bootstrap/*
              - Sid: BedrockModelInvoke
                Effect: Allow
                Action:
                  - bedrock:InvokeModel
                Resource: "*"
              - Sid: EKSClusterAccess
                Effect: Allow
                Action:
                  - eks:*
                Resource: 
                  - !Sub "arn:aws:eks:${AWS::Region}:${AWS::AccountId}:cluster/genai-platform-workshop"
                  - !Sub "arn:aws:eks:${AWS::Region}:${AWS::AccountId}:addon/genai-platform-workshop/*"


  VSCodeInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref VSCodeInstanceBootstrapRole

  VSCodeInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !If
        - IsGraviton
        - !FindInMap [ArmImage, !Ref InstanceOperatingSystem, ImageId]
        - !FindInMap [AmdImage, !Ref InstanceOperatingSystem, ImageId]
      InstanceType: !Ref InstanceType
      BlockDeviceMappings:
        - DeviceName: !If [IsAL2023, /dev/xvda, /dev/sda1]
          Ebs:
            VolumeSize: !Ref InstanceVolumeSize
            VolumeType: gp3
            DeleteOnTermination: true
            Encrypted: true
      Monitoring: true
      SecurityGroupIds:
        - !Ref SecurityGroup
      IamInstanceProfile: !Ref VSCodeInstanceProfile
      UserData:
        Fn::Base64: !Sub |
          #cloud-config
          hostname: ${InstanceName}
          runcmd:
            - mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
      Tags:
      - Key: Name
        Value: !Ref InstanceName

  VSCodeInstanceCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        DefaultTTL: 86400
        MaxTTL: 31536000
        MinTTL: 1
        Name: !Sub
        - ${InstanceName}-${RandomGUID}
        - RandomGUID: !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId ]]]]
        ParametersInCacheKeyAndForwardedToOrigin:
          CookiesConfig:
            CookieBehavior: all
          EnableAcceptEncodingGzip: False
          HeadersConfig:
            HeaderBehavior: whitelist
            Headers:
              - Accept-Charset
              - Authorization
              - Origin
              - Accept
              - Referer
              - Host
              - Accept-Language
              - Accept-Encoding
              - Accept-Datetime
          QueryStringsConfig:
            QueryStringBehavior: all

  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W10
            reason: CloudFront Distribution access logging would require setup of an S3 bucket and changes in IAM, which add unnecessary complexity to the template
          - id: W70
            reason: Workshop Studio does not include a domain that can be used to provision a certificate, so it is not possible to setup TLS. See PFR EE-6016
    Properties:
      DistributionConfig:
        Enabled: True
        HttpVersion: http2and3
        CacheBehaviors:
          - AllowedMethods:
              - GET
              - HEAD
              - OPTIONS
              - PUT
              - PATCH
              - POST
              - DELETE
            CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-policy-caching-disabled
            Compress: False
            OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # Managed-AllViewer - see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#:~:text=When%20using%20AWS,47e4%2Db989%2D5492eafa07d3
            TargetOriginId: !Sub CloudFront-${AWS::StackName}
            ViewerProtocolPolicy: allow-all
            PathPattern: '/proxy/*'
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
            - PUT
            - PATCH
            - POST
            - DELETE
          CachePolicyId: !Ref VSCodeInstanceCachePolicy
          OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # Managed-AllViewer - see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#:~:text=When%20using%20AWS,47e4%2Db989%2D5492eafa07d3
          TargetOriginId: !Sub CloudFront-${AWS::StackName}
          ViewerProtocolPolicy: allow-all
        Origins:
          - DomainName: !GetAtt VSCodeInstance.PublicDnsName
            Id: !Sub CloudFront-${AWS::StackName}
            CustomOriginConfig:
              OriginProtocolPolicy: http-only

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: F1000
            reason: All outbound traffic should be allowed from this instance. The EC2 instance is provisioned in the default VPC, which already has this egress rule, and it is not possible to duplicate this egress rule in the default VPC
    Properties:
      GroupDescription: SG for VSCodeServer - only allow CloudFront ingress
      SecurityGroupIngress:
        - Description: Allow HTTP from com.amazonaws.global.cloudfront.origin-facing
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourcePrefixListId: !FindInMap [AWSRegionsPrefixListID, !Ref 'AWS::Region', PrefixList]

  VSCodeHealthCheckLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub lambda.${AWS::URLSuffix}
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  VSCodeHealthCheckLambda:
    Type: AWS::Lambda::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W58
            reason: Warning incorrectly reported. The role associated with the Lambda function has the AWSLambdaBasicExecutionRole managed policy attached, which includes permission to write CloudWatch Logs. See https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicExecutionRole.html
          - id: W89
            reason: CloudFormation custom function does not need the scaffolding of a VPC, to do so would add unnecessary complexity
          - id: W92
            reason: CloudFormation custom function does not need reserved concurrent executions, to do so would add unnecessary complexity
    Properties:
      Description: Run health check on VS code-server instance
      Handler: index.lambda_handler
      Runtime: python3.12
      MemorySize: 128
      Timeout: 600
      Environment:
        Variables:
          RetrySleep: 2900
          AbortTimeRemaining: 5000
      Architectures:
        - arm64
      Role: !GetAtt VSCodeHealthCheckLambdaRole.Arn
      Code:
        ZipFile: |
          import json
          import cfnresponse
          import logging
          import time
          import os
          import http.client
          from urllib.parse import urlparse

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def healthURLOk(url):
              # Using try block to catch connection errors and JSON conversion errors
              try:
                  logger.debug(f'url: {url}')
                  parsed_url = urlparse(url)
                  if parsed_url.scheme == 'https':
                      logger.debug(f'Trying https: {parsed_url.netloc}. Parsed_url: {parsed_url}')
                      conn = http.client.HTTPSConnection(parsed_url.netloc)
                  else:
                      logger.debug(f'Trying http: {parsed_url.netloc}. Parsed_url: {parsed_url}')
                      conn = http.client.HTTPConnection(parsed_url.netloc)
                  conn.request("GET", parsed_url.path or "/")
                  response = conn.getresponse()
                  logger.debug(f'response: {response}')
                  logger.debug(f'response.status: {response.status}')
                  content = response.read()
                  logger.debug(f'content: {content}')
                  # This will be true for any return code below 4xx (so 3xx and 2xx)
                  if 200 <= response.status < 400:
                      response_dict = json.loads(content.decode('utf-8'))
                      logger.debug(f'response_dict: {response_dict}')
                      # Checking for expected keys and if the key has the expected value
                      if 'status' in response_dict and (response_dict['status'].lower() == 'alive' or response_dict['status'].lower() == 'expired'):
                          # Response code 200 and correct JSON returned
                          logger.info(f'Health check OK. Status: {response_dict['status'].lower()}')
                          return True
                      else:
                          # Response code 200 but the 'status' key is either not present or does not have the value 'alive' or 'expired'
                          logger.info(f'Health check failed. Status: {response_dict['status'].lower()}')
                          return False
                  else:
                      # Response was not ok (error 4xx or 5xx)
                      logger.info(f'Healthcheck failed. Return code: {response.status}')
                      return False

              except http.client.HTTPException as e:
                  # URL malformed or endpoint not ready yet, this should only happen if we can not DNS resolve the URL
                  logger.error(e, exc_info=True)
                  logger.error(f'Healthcheck failed: HTTP Exception. URL invalid and/or endpoint not ready yet')
                  return False

              except json.decoder.JSONDecodeError as e:
                  # The response we got was not a properly formatted JSON
                  logger.error(e, exc_info=True)
                  logger.info(f'Healthcheck failed: Did not get JSON object from URL as expected')
                  return False

              except Exception as e:
                  logger.error(e, exc_info=True)
                  logger.info(f'Healthcheck failed: General error')
                  return False

              finally:
                  if 'conn' in locals():
                      conn.close()

          def is_valid_json(json_string):
              try:
                  json.loads(json_string)
                  return True
              except ValueError:
                  return False

          def lambda_handler(event, context):
              logger.debug(f'event: {event}')
              logger.debug(f'context: {context}')
              try:
                  if event['RequestType'] != 'Create':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='No action to take')
                  else:
                      sleep_ms = int(os.environ.get('RetrySleep'))
                      abort_time_remaining_ms = int(os.environ.get('AbortTimeRemaining'))
                      resource_properties = event['ResourceProperties']
                      url = resource_properties['Url']

                      logger.info(f'Testing url: {url}')

                      time_remaining_ms = context.get_remaining_time_in_millis()
                      attempt_no = 0
                      health_check = False
                      while (attempt_no == 0 or (time_remaining_ms > abort_time_remaining_ms and not health_check)):
                          attempt_no += 1
                          logger.info(f'Attempt: {attempt_no}. Time Remaining: {time_remaining_ms/1000}s')
                          health_check = healthURLOk(url)
                          if not health_check:
                              logger.debug(f'Healthcheck failed. Sleeping: {sleep_ms/1000}s')
                              time.sleep(sleep_ms/1000)
                          time_remaining_ms = context.get_remaining_time_in_millis()
                      if health_check:
                          logger.info(f'Health check successful. Attempts: {attempt_no}. Time Remaining: {time_remaining_ms/1000}s')
                          cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='VS code-server healthcheck successful')
                      else:
                          logger.info(f'Health check failed. Timed out. Attempts: {attempt_no}. Time remaining {time_remaining_ms/1000}s < Abort time remaining {abort_time_remaining_ms/1000}s')
                          cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason='VS code-server healthcheck failed. Timed out after ' + str(attempt_no) + ' attempts')
                          logger.info(f'Response sent')

              except Exception as e:
                  logger.error(e, exc_info=True)
                  logger.info(f'Health check failed. General exception')
                  cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=str(e))

  Healthcheck:
    Type: Custom::VSCodeHealthCheckLambda
    Properties:
      ServiceToken: !GetAtt VSCodeHealthCheckLambda.Arn
      ServiceTimeout: 610
      Url: !Sub https://${CloudFrontDistribution.DomainName}/healthz

  CheckSSMDocLambda:
    Type: AWS::Lambda::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W58
            reason: Warning incorrectly reported. The role associated with the Lambda function has the AWSLambdaBasicExecutionRole managed policy attached, which includes permission to write CloudWatch Logs. See https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicExecutionRole.html
          - id: W89
            reason: CloudFormation custom function does not need the scaffolding of a VPC, to do so would add unnecessary complexity
          - id: W92
            reason: CloudFormation custom function does not need reserved concurrent executions, to do so would add unnecessary complexity
    Properties:
      Description: Check SSM document on EC2 instance
      Handler: index.lambda_handler
      Runtime: python3.12
      MemorySize: 128
      Timeout: 600
      Environment:
        Variables:
          RetrySleep: 2900
          AbortTimeRemaining: 5000
      Architectures:
        - arm64
      Role: !GetAtt SSMDocLambdaRole.Arn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import logging
          import time
          import os

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def lambda_handler(event, context):
              logger.debug(f'event: {event}')
              logger.debug(f'context: {context}')

              if event['RequestType'] != 'Create':
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='No action to take')
              else:
                  sleep_ms = int(os.environ.get('RetrySleep'))
                  abort_time_remaining_ms = int(os.environ.get('AbortTimeRemaining'))
                  resource_properties = event['ResourceProperties']
                  instance_id = resource_properties['InstanceId']
                  document_name = resource_properties['DocumentName']

                  logger.info(f'Checking SSM Document {document_name} on EC2 instance {instance_id}')

                  retry = True
                  attempt_no = 0
                  time_remaining_ms = context.get_remaining_time_in_millis()

                  ssm = boto3.client('ssm')

                  while (retry == True):
                      attempt_no += 1
                      logger.info(f'Attempt: {attempt_no}. Time Remaining: {time_remaining_ms/1000}s')
                      try:
                          # check to see if document has completed running on instance
                          response = ssm.list_command_invocations(
                              InstanceId=instance_id,
                              Details=True
                          )
                          logger.debug(f'Response: {response}')
                          for invocation in response['CommandInvocations']:
                              if invocation['DocumentName'] == document_name:
                                  invocation_status = invocation['Status']
                                  if invocation_status == 'Success':
                                      logger.info(f'SSM Document {document_name} on EC2 instance {instance_id} complete. Status: {invocation_status}')                                  
                                      cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='OK')
                                      retry = False
                                  elif invocation_status == 'Failed' or invocation_status == 'Cancelled' or invocation_status == 'TimedOut':
                                      logger.info(f'SSM Document {document_name} on EC2 instance {instance_id} failed. Status: {invocation_status}')
                                      reason = ''
                                      # Get information on step that failed, otherwise it's cancelled or timeout
                                      for step in invocation['CommandPlugins']:
                                          step_name = step['Name']
                                          step_status = step['Status']
                                          step_output = step['Output']
                                          logger.debug(f'Step {step_name} {step_status}: {step_output}')
                                          if step_status != 'Success':
                                              try:
                                                  response_step = ssm.get_command_invocation(
                                                      CommandId=invocation['CommandId'],
                                                      InstanceId=instance_id,
                                                      PluginName=step_name
                                                  )
                                                  logger.debug(f'Step details: {response_step}')
                                                  step_output = response_step['StandardErrorContent']
                                              except Exception as e:
                                                  logger.error(e, exc_info=True)
                                              logger.info(f'Step {step_name} {step_status}: {step_output}')
                                              if reason == '':
                                                  reason = f'Step {step_name} {step_status}: {step_output}'
                                              else:
                                                  reason += f'\nStep {step_name} {step_status}: {step_output}'
                                      if reason == '':
                                          reason = f'SSM Document {document_name} on EC2 instance {instance_id} failed. Status: {invocation_status}'
                                      logger.info(f'{reason}')
                                      cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=reason)
                                      retry = False
                                  else:
                                      logger.info(f'SSM Document {document_name} on EC2 instance {instance_id} not yet complete. Status: {invocation_status}')
                                      retry = True
                          if retry == True:
                              if (time_remaining_ms > abort_time_remaining_ms):
                                  logger.info(f'Sleeping: {sleep_ms/1000}s')
                                  time.sleep(sleep_ms/1000)
                                  time_remaining_ms = context.get_remaining_time_in_millis()
                              else:
                                  logger.info(f'Time remaining {time_remaining_ms/1000}s < Abort time remaining {abort_time_remaining_ms/1000}s')
                                  logger.info(f'Aborting check as time remaining {time_remaining_ms/1000}s < Abort time remaining {abort_time_remaining_ms/1000}s')
                                  cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason='Timed out. Time remaining: ' + str(time_remaining_ms/1000) + 's < Abort time remaining: ' + str(abort_time_remaining_ms/1000) + 's')
                                  retry = False
                      except Exception as e:
                          logger.error(e, exc_info=True)
                          cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=str(e))
                          retry = False

  CheckVSCodeSSMDoc:
    Type: Custom::CheckSSMDocLambda
    DependsOn: Healthcheck   
    Properties:   
      ServiceToken: !GetAtt CheckSSMDocLambda.Arn
      ServiceTimeout: 610
      InstanceId: !Ref VSCodeInstance
      DocumentName: !Ref VSCodeSSMDoc

Outputs:
  URL:
    Description: VSCode-Server URL
    Value: !Sub https://${CloudFrontDistribution.DomainName}/?folder=${HomeFolder}
  Password:
    Description: VSCode-Server Password
    Value: !Ref VSCodePassword