AWSTemplateFormatVersion: 2010-09-09
Description: Sandbox server template (basic)

Parameters:
  InstanceType:
    Description: "EC2 instance type (default: t3.small = 2 vCPU, 2GB RAM)"
    Type: String
    # Instances with < 2GB RAM should only be used for testing the CloudFormation template
    # t3.small: 2 vCPU, 2GB RAM
    Default: t3.small
    AllowedValues:
      - t3.nano
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5d.large
      - m5d.xlarge
      - m5d.2xlarge
      - m5d.4xlarge
      - r5.large
      - r5.xlarge
      - r5.2xlarge
      - r5d.large
      - r5d.xlarge
      - r5d.2xlarge
    ConstraintDescription: "Must be a valid EC2 instance type"
  KeyName:
    Description: "Name of an existing EC2 KeyPair to enable SSH access to the instance"
    Type: "AWS::EC2::KeyPair::KeyName"
    ConstraintDescription: "Must be the name of an existing EC2 KeyPair"
  DataDiskSize:
    Description: "Persistent volume size (GiB). Valid range: 20-1024"
    Type: Number
    MinValue: 20
    MaxValue: 1024
    Default: 100

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Basic: Required"
        Parameters:
          - InstanceType
          - KeyName
      - Label:
          default: "Advanced: Optional"
        Parameters:
          - DataDiskSize
    ParameterLabels:
      InstanceType:
        default: "Instance type"
      KeyName:
        default: "SSH key"
      DataDiskSize:
        default: "Data disk size"

Mappings:
  Region2AMI:
    eu-north-1:
      AMI: ami-005bc7d72deb72a3d
    ap-south-1:
      AMI: ami-0245841fc4b40e22f
    eu-west-3:
      AMI: ami-0b70d1460d5c7a299
    eu-west-2:
      AMI: ami-00622b440d92e55c0
    eu-west-1:
      AMI: ami-04c58523038d79132
    ap-northeast-2:
      AMI: ami-02b4a5559ce53a570
    ap-northeast-1:
      AMI: ami-0f6b4f4104d26f399
    sa-east-1:
      AMI: ami-049f5d88d2d436431
    ca-central-1:
      AMI: ami-0972a0d3135cf1fc0
    ap-southeast-1:
      AMI: ami-07febfdfb4080320e
    ap-southeast-2:
      AMI: ami-04a0f7552cff370ba
    eu-central-1:
      AMI: ami-09356619876445425
    us-east-1:
      AMI: ami-00a208c7cdba991ea
    us-east-2:
      AMI: ami-059d836af932792c3
    us-west-1:
      AMI: ami-0f42d8c4eb586ccf7
    us-west-2:
      AMI: ami-0a7d051a1c4b54f65

Resources:
  InstanceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: 'Enable SSH, HTTP, HTTPS'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: '443'
          ToPort: '443'
          CidrIp: 0.0.0.0/0

  IPAddress:
    Type: 'AWS::EC2::EIP'

  IPAssoc:
    Type: 'AWS::EC2::EIPAssociation'
    Properties:
      InstanceId: !Ref ec2Instance
      EIP: !Ref IPAddress

  PersistentVolume:
    Type: "AWS::EC2::Volume"
    Properties:
      Size: !Ref DataDiskSize
      AvailabilityZone: !GetAtt ec2Instance.AvailabilityZone

  PersistentVolumeMount:
    Type: "AWS::EC2::VolumeAttachment"
    Properties:
      InstanceId: !Ref ec2Instance
      VolumeId: !Ref PersistentVolume
      Device: /dev/sdp

  ec2Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      InstanceType: !Ref InstanceType
      AvailabilityZone: !Ref "AWS::NoValue"
      ImageId: !FindInMap [Region2AMI, !Ref "AWS::Region", AMI]
      KeyName: !Ref KeyName
      SecurityGroups:
        - !Ref InstanceSecurityGroup
      BlockDeviceMappings:
        # Root volume
        - DeviceName: /dev/sda1
          Ebs:
            VolumeSize: '8'
      UserData: !Base64
        'Fn::Join':
          - ''
          - - |
              #!/bin/bash
              
              # This is a startup script for a Docksal Sandbox server in GCP.
              # It installs and configures Docksal on a bare Ubuntu machine (tested with Ubuntu 18.04 Minimal).
              #
              # The startup script log can be views via "gcloud compute ssh vm-sandbox-test -- tail -f /var/log/syslog"
              
              set -x  # Print commands
              set -e  # Fail on errors
              
              # Persistent disk settings
              DISK_LABEL="data-volume"
              MOUNT_POINT="/data"
              BUILD_USER="build-agent"
              BUILD_USER_UID="1100"
              BUILD_USER_HOME="/home/${BUILD_USER}"
              DATA_BUILD_USER_HOME="${MOUNT_POINT}${BUILD_USER_HOME}"
              DOCKSAL_VERSION="master"
              PROJECT_INACTIVITY_TIMEOUT="0.5h"
              PROJECT_DANGLING_TIMEOUT="168h"
              PROJECTS_ROOT="${BUILD_USER_HOME}/builds"
              
              ##########
              # Functions begin
              reread_device()
              {
                  DEVICE=$1
                  hdparm -z ${DEVICE} >>/var/log/device.log 2>&1 || true
                  file -s ${DEVICE} >>/var/log/device.log 2>&1 || true
                  partprobe ${DEVICE} >>/var/log/device.log 2>&1 || true
                  blockdev --rereadpt -v ${DEVICE} >>/var/log/device.log 2>&1 || true
                  fdisk -l ${DEVICE} >>/var/log/device.log 2>&1 || true
              }
              
              mount_part()
              {
                  DATA_DISK=$1
                  # mark disk with label
                  tune2fs -L ${DISK_LABEL} ${DATA_DISK} >/dev/null 2>&1
                  # Mount the data disk
                  mkdir -p ${MOUNT_POINT}
                  cp /etc/fstab /etc/fstab.backup
                  # Write disk mount to /etc/fstab (so that it persists on reboots)
                  # Equivalent of `mount /dev/sdb /mnt/data`
                  echo "LABEL=${DISK_LABEL} ${MOUNT_POINT}  ext4  defaults,nofail  0 2" | tee -a /etc/fstab
                  mount -a
              }
              
              create_fs()
              {
                  # creating ext4 fs and add label
                  DATA_DISK=$1
                  mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard ${DATA_DISK} -L ${DISK_LABEL} >/dev/null 2>&1
              }
              
              get_disk_info()
              {
                  # get disk/partitions info.
                  # the result will contain strings: NAME="/dev/nvme1n1";TYPE="disk";FSTYPE="";LABEL="";MOUNTPOINT="" NAME="/dev/nvme0n1";TYPE="disk";FSTYPE="";LABEL="";MOUNTPOINT=""
                  DATA_DISK=$1
                  reread_device "$DATA_DISK"
                  device_list="$(lsblk -p -n -P -o NAME,TYPE,FSTYPE,LABEL,MOUNTPOINT ${DATA_DISK} 2>&1 | sed 's/ /;/g')"
                  while read device_info
                  do
                    eval ${device_info}
                    # exit if device already mounted
                    [[ ${MOUNTPOINT} != "" ]] && return
                    # exit if device has ext4 fs
                    [[ ${FSTYPE} == "ext4" ]] && return
                  done <<< ${device_list}
                  # if not found ext4 fs return empty data for creating new fs
                  NAME=""; TYPE=""; FSTYPE=""; LABEL=""; MOUNTPOINT=""
              }
              
              create_part()
              {
                  # create msdos partition table and create primary partition used 100% disk size
                  DATA_DISK=$1
                  /sbin/parted ${DATA_DISK} -s mklabel msdos
              }
              
              ##########
              # Functions end
              
              # Create build-agent user with no-password sudo access
              # Forcing the uid to avoid race conditions with GCP creating project level users at the same time.
              # (Otherwise, we may run into something like "useradd: UID 1001 is not unique")
              if [[ "$(id -u ${BUILD_USER})" != "${BUILD_USER_UID}" ]]; then
              	adduser --disabled-password --gecos "" --uid ${BUILD_USER_UID} ${BUILD_USER}
              	usermod -aG sudo ${BUILD_USER}
              	echo "${BUILD_USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/101-${BUILD_USER}
              fi
              
              # Wait for data volume attachment (necessary with AWS EBS)
              wait_count=0
              wait_max_attempts=6
              while true
              do
                  let "wait_count+=1"
                  # additional data disk is considered attached when number of disk attached to instance more than 1
                  [[ "$(lsblk -p -n -o NAME,TYPE | grep disk | wc -l)" > 1 ]] && break
              
              	# Fail if reached maximum attempts
              	if (( ${wait_count} > ${wait_max_attempts} )); then
              		echo "ERROR: Timed out waiting for EBS volume to attach."
              		exit 1
              	fi
              
                  echo "Waiting for EBS volume to attach (${wait_count})..."
                  sleep 10
              done
              
              # find additional data disk, format it and mount
              for disk in $(lsblk -d -p -n -o NAME,TYPE | grep disk | cut -d' ' -f1)
              do
                  # get partition info
                  get_disk_info "${disk}"
                  [[ "$MOUNTPOINT" != "" ]] && { echo "Disk $NAME already mounted! Skipping..."; continue; }
                  [[ "$FSTYPE" == "ext4" ]] && { echo "Disk $NAME have ext4 filesystem but not mounted! Mounting..."; mount_part "$NAME"; continue; }
                  echo "Disk ${disk} is clean! Creating partition..."
                  wipefs -fa ${disk} >/dev/null
                  create_part "${disk}"
                  create_fs "${disk}"
                  mount_part "${disk}"
              done
              
              if [ -d $MOUNT_POINT ]
              then
              	# Move BUILD_USER_HOME to the data disk
              	# E.g. /home/build-agent => /mnt/data/home/build-agent
              	if [[ ! -d ${DATA_BUILD_USER_HOME} ]]; then
              		mkdir -p $(dirname ${DATA_BUILD_USER_HOME})
              		mv ${BUILD_USER_HOME} $(dirname ${DATA_BUILD_USER_HOME})
              	else
              		rm -rf ${BUILD_USER_HOME}
              	fi
              	ln -s ${DATA_BUILD_USER_HOME} ${BUILD_USER_HOME}
              
              	# Symlink /var/lib/docker (should not yet exist when this script runs) to the data volume
              	mkdir -p ${MOUNT_POINT}/var/lib/docker
              	ln -s ${MOUNT_POINT}/var/lib/docker /var/lib/docker
              else
              	echo "WARNING: data volume not found. Using instance-only storage"
              fi
              
              # Create the projects/builds directory
              mkdir -p ${PROJECTS_ROOT}
              
              # SSH settings: ensure ~/.ssh exists for the build user
              mkdir -p ${BUILD_USER_HOME}/.ssh
              
              # SSH settings: authorized_keys
              # If ~/.ssh/authorized_keys does not exist for the build user, reuse the one from the default user account (ubuntu)
              if [[ ! -f "${BUILD_USER_HOME}/.ssh/authorized_keys" ]]; then
              	cp "/home/ubuntu/.ssh/authorized_keys" "${BUILD_USER_HOME}/.ssh/authorized_keys"
              	chown ${BUILD_USER}:${BUILD_USER} "${BUILD_USER_HOME}/.ssh/authorized_keys"
              fi
              
              # SSH settings: disable the host key check
              if [[ ! -f "${BUILD_USER_HOME}/.ssh/config" ]]; then
              	tee "${BUILD_USER_HOME}/.ssh/config" <<EOF
              Host *
                StrictHostKeyChecking no
                UserKnownHostsFile=/dev/null
                LogLevel ERROR
              EOF
              	chmod 600 "${BUILD_USER_HOME}/.ssh/config"
              fi
              
              # Sandbox settings (set these before installing Docksal)
              # Note: do nothing if docksal.env exists from a previos installation (persistent data disk)
              if [[ ! -f ${BUILD_USER_HOME}/.docksal/docksal.env ]]; then
              	mkdir -p ${BUILD_USER_HOME}/.docksal
              	tee ${BUILD_USER_HOME}/.docksal/docksal.env <<EOF
              CI=true
              PROJECT_INACTIVITY_TIMEOUT="${PROJECT_INACTIVITY_TIMEOUT}"
              PROJECT_DANGLING_TIMEOUT="${PROJECT_DANGLING_TIMEOUT}"
              PROJECTS_ROOT="${PROJECTS_ROOT}"
              EOF
              	# Fix permissions
              	#chown ${BUILD_USER}:${BUILD_USER} ${BUILD_USER_HOME}/.docksal/docksal.env
              fi
              
              # Fix permissions, since we are running as root here
              # Trailing slash in necessary here, since BUILD_USER_HOME is a symlink
              chown -R ${BUILD_USER}:${BUILD_USER} "${BUILD_USER_HOME}/"
              
              # Unlock updates
              # Necessary with existing installations (persistent data disk)
              sed -i '/DOCKSAL_LOCK_UPDATES/d' "${BUILD_USER_HOME}/.docksal/docksal.env" || true
              
              # Install/update Docksal and dependencies
              su - ${BUILD_USER} -c "curl -fsSL https://get.docksal.io | DOCKSAL_VERSION=${DOCKSAL_VERSION} bash"
              
              # Lock updates (protect against unintentional updates in builds)
              echo "DOCKSAL_LOCK_UPDATES=1" | tee -a "${BUILD_USER_HOME}/.docksal/docksal.env"

Outputs:
  InstanceID:
    Value: !Ref ec2Instance
  IPAddress:
    Value: !GetAtt ec2Instance.PublicIp
  InstanceURL:
    Value: !Join [ '', [ 'https://', !GetAtt ec2Instance.PublicIp, '.nip.io' ] ]
