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

Parameters:
  ResourceType:
    Description: |
      EC2 resource type.
      Use "ec2" (on-demand) for guaranteed server uptime.
      Use "spot" to save up to 90% of EC2 costs, if an occasional (once in a few months) instance downtime
      of 5-10 minutes is acceptable.
      Make sure to configure a persistent data volume to avoid data loss in spot mode (see below).
    Type: String
    Default: ec2
    AllowedValues:
      - ec2
      - spot
  InstanceType:
    Description: |
      EC2 instance type. Default: t3.small = 2 vCPU, 2GB RAM.
      Instances with < 2GB RAM are not recommended and should only be used for testing the CloudFormation template.
    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"
  ManualAZ:
    Description: "Set instance availability zone"
    Type: AWS::EC2::AvailabilityZone::Name
  VpcId:
    Description: |
      VPC must have internet access (with Internet Gateway or Virtual Private Gateway)
      Leave empty to use the default VPC
    Type: String
    Default: ""
  SubnetId:
    Description: |
      Public subnet ID within the non-default VPC.
      Must be located in the same AZ where the instance is created.
      Required if using non-default VPC.
    Type: String
    Default: ""
  ExistingEIP:
    Description: "Use an existing Elastic IP for the instance (x.x.x.x)"
    Type: String
    Default: ""
  AccessFrom:
    Description: |
      CIDR block to allow ingress access to the instance. Use "0.0.0.0/0" for unrestricted access.
    Type: String
    Default: 0.0.0.0/0
  ExistingDataVolume:
    Description: |
      Use an existing EBS volume for instance persistent storage.
      Must be located in the same AZ where the instance is created.
    # Type: "AWS::EC2::Volume::Id"
    Type: String
    Default: ""
  GitHubToken:
    Description: "Github token with read:org access level"
    Type: String
    Default: ""
  GitHubOrgName:
    Description: "Github organization machine name"
    Type: String
    Default: ""
  GitHubTeamSlug:
    Description: "Github team machine name. Users in this team will have access to Sandbox server"
    Type: String
    Default: ""
  LetsEncryptDomain:
    Description: |
      Domain name for which a wildcard LetsEncrypt certificate will be issued.
      Example: for "example.com", an SNI wildcard cert will be issued covering both "example.com" and "*.example.com".
    Type: String
    Default: ""
  LetsEncryptConfig:
    Description: |
      Configuration for automated LetsEncrypt certificate provisioning (space delimited variable="value" pairs).
      Info about available options: https://github.com/Neilpang/acme.sh/wiki/dnsapi. 
      Example: DSP="dns_aws" AWS_ACCESS_KEY_ID="aws_access_key_id" AWS_SECRET_ACCESS_KEY="aws_secret_key". 
      Note: If the domain is managed in AWS Route53 in the same AWS account, then leave this field empty to give the EC2
      instance the necessary permissions to managed TXT records in Route53.
    Type: String
    Default: ""
  EnableArtifactsBucket:
    Description: "Enable S3 artifacts bucket"
    Type: String
    Default: false
    AllowedValues:
      - false
      - true
  ArtifactsBucketName:
    Description: "Set name for the artifacts bucket. Leave empty to have the bucket name automatically generated."
    Type: String
    Default: ""

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Basic: Required"
        Parameters:
          - ResourceType
          - InstanceType
          - KeyName
          - ManualAZ
      - Label:
          default: "VPC/Network: Optional"
        Parameters:
          - VpcId
          - SubnetId
          - ExistingEIP
          - AccessFrom
      - Label:
          default: "Storage: Optional"
        Parameters:
          - ExistingDataVolume
          - EnableArtifactsBucket
          - ArtifactsBucketName
      - Label:
          default: "Github settings: Optional"
        Parameters:
          - GitHubToken
          - GitHubOrgName
          - GitHubTeamSlug
      - Label:
          default: "LetsEncrypt settings: Optional"
        Parameters:
          - LetsEncryptDomain
          - LetsEncryptConfig
    ParameterLabels:
      ResourceType:
        default: "Resource type"
      InstanceType:
        default: "Instance type"
      KeyName:
        default: "SSH key"
      ManualAZ:
        default: "Availability zone"
      VpcId:
        default: "VPC ID"
      SubnetId:
        default: "Subnet ID"
      ExistingEIP:
        default: "Elastic IP"
      AccessFrom:
        default: "Access from"
      ExistingDataVolume:
        default: "Persistent data volume"
      EnableArtifactsBucket:
        default: "Enable artifacts bucket"
      ArtifactsBucketName:
        default: "Artifacts bucket name"
      GitHubToken:
        default: "Github token"
      GitHubOrgName:
        default: "Github organization"
      GitHubTeamSlug:
        default: "Github team"
      LetsEncryptDomain:
        default: "Sandbox domain name"
      LetsEncryptConfig:
        default: "LetsEncrypt configuration"

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

Conditions:
  CreateEC2: !Equals [!Ref ResourceType, "ec2"]
  CreateSpot: !Equals [!Ref ResourceType, "spot"]
  ArtifactsEnabled: !Equals [!Ref EnableArtifactsBucket, "true"]
  ArtifactsBucketNameDefined: !Not [!Equals [!Ref ArtifactsBucketName, ""]]
  VpcIdDefined: !Not [!Equals [!Ref VpcId, ""]]
  SubnetIdDefined: !Not [!Equals [!Ref SubnetId, ""]]
  ExistingEIPDefined: !Not [!Equals [!Ref ExistingEIP, ""]]
  # When LE domain is set but the LE config is empty, we give the EC2 instance access to manage TXT records in Route53.
  Route53Defined: !And
    - !Not [!Equals [!Ref LetsEncryptDomain, ""]]
    - !Equals [!Ref LetsEncryptConfig, ""]

Resources:
  ArtifactsBucket:
    Type: AWS::S3::Bucket
    Condition: ArtifactsEnabled
    Properties:
      BucketName: !If [ ArtifactsBucketNameDefined, !Ref ArtifactsBucketName, !Ref "AWS::NoValue" ]
      LifecycleConfiguration:
        Rules:
          - Id: "DeleteObjectsOlderThanYear"
            ExpirationInDays: 365
            Status: Enabled

  ArtifactsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Condition: ArtifactsEnabled
    DependsOn: ArtifactsBucket
    Properties:
      Bucket: !Ref ArtifactsBucket
      PolicyDocument:
        Statement:
          - Sid: AllowReadWriteAcces
            Effect: Allow
            Principal:
              AWS:
                - !Sub arn:aws:iam::${AWS::AccountId}:root
                - !If [ CreateEC2, !GetAtt ec2InstanceRole.Arn, !Ref "AWS::NoValue" ]
                - !If [ CreateSpot, !GetAtt spotInstanceRole.Arn, !Ref "AWS::NoValue" ]
            Action:
              - s3:List*
              - s3:Get*
              - s3:*Object*
            Resource:
              - !Sub arn:aws:s3:::${ArtifactsBucket}
              - !Sub arn:aws:s3:::${ArtifactsBucket}/*

  InstanceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      VpcId: !If [ VpcIdDefined, !Ref VpcId, !Ref "AWS::NoValue" ]
      GroupDescription: 'Enable SSH, HTTP, HTTPS'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: !Ref AccessFrom
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: !Ref AccessFrom
        - IpProtocol: tcp
          FromPort: '443'
          ToPort: '443'
          CidrIp: !Ref AccessFrom

  FleetRole:
    Type: AWS::IAM::Role
    Condition: CreateSpot
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - spotfleet.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"

  FleetPolicies:
    Type: AWS::IAM::Policy
    Condition: CreateSpot
    Properties:
      PolicyName: root
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Action:
            - "ec2:DescribeImages"
            - "ec2:DescribeSubnets"
            - "ec2:RequestSpotInstances"
            - "ec2:TerminateInstances"
            - "ec2:DescribeInstanceStatus"
            - "iam:PassRole"
            - "cloudformation:DescribeStacks"
            - "ec2:DescribeInstances"
            - "ec2:CreateTags"
          Resource: "*"
      Roles:
        - !Ref FleetRole

  ec2InstanceRole:
    Type: AWS::IAM::Role
    Condition: CreateEC2
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"

  rootInstancePolicies:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: root
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Action:
            - "cloudformation:DescribeStacks"
            - "ec2:DescribeInstances"
            - "ec2:AttachVolume"
            - "ec2:DetachVolume"
            - "ec2:AssociateAddress"
            - "ec2:DisassociateAddress"
            - "ec2:DescribeVolumes"
          Resource: "*"
      Roles:
        - !If [ CreateEC2, !Ref ec2InstanceRole, !Ref "AWS::NoValue" ]
        - !If [ CreateSpot, !Ref spotInstanceRole, !Ref "AWS::NoValue" ]

  route53InstancePolicies:
    Type: AWS::IAM::Policy
    Condition: Route53Defined
    Properties:
      PolicyName: route53
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Action:
            # Permissions necessary for TXT record updates in Route53
            - "route53:GetHostedZone"
            - "route53:ListHostedZones"
            - "route53:ChangeResourceRecordSets"
            - "route53:ListResourceRecordSets"
            - "route53:GetHostedZoneCount"
            - "route53:ListHostedZonesByName"
          Resource: "*"
      Roles:
        - !If [ CreateEC2, !Ref ec2InstanceRole, !Ref "AWS::NoValue" ]
        - !If [ CreateSpot, !Ref spotInstanceRole, !Ref "AWS::NoValue" ]

  ec2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Condition: CreateEC2
    Properties:
      Path: "/"
      Roles:
        - !Ref ec2InstanceRole

  spotInstanceRole:
    Type: AWS::IAM::Role
    Condition: CreateSpot
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"

  spotInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Condition: CreateSpot
    Properties:
      Path: "/"
      Roles:
        - !Ref spotInstanceRole

  spotInstance:
    Type: 'AWS::EC2::SpotFleet'
    Condition: CreateSpot
    Properties:
      SpotFleetRequestConfigData:
        TargetCapacity: 1
        IamFleetRole: !GetAtt FleetRole.Arn
        LaunchSpecifications:
          - InstanceType: !Ref InstanceType
            IamInstanceProfile:
              Arn:
                Fn::GetAtt:
                  - spotInstanceProfile
                  - Arn
            Placement:
              AvailabilityZone: !Ref ManualAZ
            TagSpecifications:
              - ResourceType: instance
                Tags:
                - Key: StackId
                  Value: !Ref AWS::StackId
            ImageId: !FindInMap [Region2AMI, !Ref "AWS::Region", AMI]
            KeyName: !Ref KeyName
            SecurityGroups:
              - GroupId: !GetAtt InstanceSecurityGroup.GroupId
            BlockDeviceMappings:
            # Root volume
              - DeviceName: /dev/sda1
                Ebs:
                  VolumeSize: '8'
            SubnetId: !If [ SubnetIdDefined, !Ref SubnetId, !Ref "AWS::NoValue" ]
            UserData: !Base64
              'Fn::Join':
                - ''
                - - |
                    Content-Type: multipart/mixed; boundary="//"
                    MIME-Version: 1.0
                    
                    --//
                    Content-Type: text/cloud-config; charset="us-ascii"
                    MIME-Version: 1.0
                    Content-Transfer-Encoding: 7bit
                    Content-Disposition: attachment; filename="cloud-config.txt"
                    
                    #cloud-config
                    cloud_final_modules:
                    - [scripts-user, always]
                    
                    --//
                    Content-Type: text/x-shellscript; charset="us-ascii"
                    MIME-Version: 1.0
                    Content-Transfer-Encoding: 7bit
                    Content-Disposition: attachment; filename="userdata.txt"
                    
                    #!/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}"
                    DOCKSAL_VERSION="master"
                    PROJECT_INACTIVITY_TIMEOUT="0.5h"
                    PROJECT_DANGLING_TIMEOUT="168h"
                    PROJECTS_ROOT="${BUILD_USER_HOME}/builds"
                    ATTACH_VOLUME_AS="/dev/sdp"
                    
                    ##########
                    # Functions begin
                    move_to_data()
                    {
                        local SRC_DIR=$1
                        local DST_DIR=$2
                        if [[ ! -d ${DST_DIR}${SRC_DIR} ]]; then
                            mkdir -p ${DST_DIR}${SRC_DIR}
                            [[ -d ${SRC_DIR} ]] && [[ ! -L ${SRC_DIR} ]] && mv ${SRC_DIR} ${DST_DIR}$(dirname ${SRC_DIR})
                        fi
                        rm -rf ${SRC_DIR} && ln -s ${DST_DIR}${SRC_DIR} ${SRC_DIR}
                    }
                    
                    wait_volume_status()
                    {
                        # Wait for EBS volume status.
                        local volume=$1
                        local status=$2
                        local wait_count=0
                        local wait_max_attempts=30
                        while true
                        do
                            let "wait_count+=1"
                            # Check volume status. Non-empty output means volume in required status and we can proceed.
                            [[ "$(aws ec2 describe-volumes --volume-ids ${volume} --filters "Name=status,Values=${status}" --output text)" != "" ]] && break
                    
                            # Fail if reached maximum attempts
                            if (( ${wait_count} > ${wait_max_attempts} )); then
                                echo "ERROR: Timed out waiting for EBS volume to be ${status}."
                                exit 1
                            fi
                    
                            echo "Waiting for EBS volume to become available (${wait_count})..."
                            sleep 10
                        done
                    }
                    
                    reread_device()
                    {
                        DEVICE=$1
                        hdparm -z ${DEVICE} || true
                        file -s ${DEVICE} || true
                        partprobe ${DEVICE} || true
                        blockdev --rereadpt -v ${DEVICE} || true
                        fdisk -l ${DEVICE} || true
                    }
                    
                    mount_part()
                    {
                        DATA_DISK=$1
                        # mark disk with label
                        tune2fs -L ${DISK_LABEL} ${DATA_DISK}
                        # Mount the data disk
                        mkdir -p ${MOUNT_POINT}
                        mount ${DATA_DISK} ${MOUNT_POINT}
                    }
                    
                    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}
                    }
                    
                    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"
                        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 <<< "$(lsblk -p -n -P -o NAME,TYPE,FSTYPE,LABEL,MOUNTPOINT ${DATA_DISK} 2>&1 | sed 's/ /;/g')"
                        # 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
                    
                    # install aws cli if instance just created
                    if [[ ! -f /root/stack_last_update ]]
                    then
                        apt-get -y update
                        apt-get -y install awscli
                    else
                        old_stack_md5sum="$(cat /root/stack_last_update)"
                    fi
                    
                    # read necessary variables
                    export AWS_DEFAULT_REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/\(.*\)[a-z]/\1/')
                    export INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
                    export STACK_ID=$(aws ec2 describe-instances --instance-id ${INSTANCE_ID} --query 'Reservations[*].Instances[*].Tags[?Key==`StackId`].Value' --output text)
                    export ATTACHED_IP=$(aws ec2 describe-instances --instance-id ${INSTANCE_ID} --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
                    export ATTACHED_VOLUME=$(aws ec2 describe-volumes --filters Name=attachment.instance-id,Values=${INSTANCE_ID} Name=attachment.device,Values=${ATTACH_VOLUME_AS} --query 'Volumes[*].VolumeId' --output text)
                    
                    # wait stack status complete
                    while true
                    do
                      STACK_STATUS=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].StackStatus' --output text)
                      if [[ "${STACK_STATUS}" == *"UPDATE_COMPLETE"* ]] || [[ "${STACK_STATUS}" == *"CREATE_COMPLETE"* ]] || [[ "${STACK_STATUS}" == *"ROLLBACK_COMPLETE"* ]]
                      then
                          stack_md5sum=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters' --output text | sort | md5sum | cut -d' ' -f1)
                          break
                      fi
                      sleep 5
                    done
                    
                    export EIP=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`ExistingEIP`].ParameterValue' --output text)
                    export VOLUME_ID=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`ExistingDataVolume`].ParameterValue' --output text)
                    export GITHUB_TOKEN=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`GitHubToken`].ParameterValue' --output text)
                    export GITHUB_ORG_NAME=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`GitHubOrgName`].ParameterValue' --output text)
                    export GITHUB_TEAM_SLUG=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`GitHubTeamSlug`].ParameterValue' --output text)
                    export LETSENCRYPT_DOMAIN=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`LetsEncryptDomain`].ParameterValue' --output text)
                    export LETSENCRYPT_CONFIG=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`LetsEncryptConfig`].ParameterValue' --output text)
                    export ARTIFACTS_S3_BUCKET=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Outputs[?OutputKey==`ArtifactsBucket`].OutputValue' --output text)
                    
                    # attach/detach elastic ip
                    if [[ "${EIP}" != "${ATTACHED_IP}" ]]
                    then
                        # try to detach attached ip. elastic ip will be detached, but autoassigned ip remain attached
                        aws ec2 disassociate-address --public-ip ${ATTACHED_IP} || true
                        # try to attach elastic ip if defined in template. in case user defined wrong ip, instance will be accessible on autoassigned ip
                        [[ "${EIP}" != "" ]] && aws ec2 associate-address --instance-id ${INSTANCE_ID} --public-ip ${EIP} || true
                    fi
                    
                    # stop docker daemon before partitions manipulation
                    service docker stop || true
                    
                    # back compatibility fix: remove automounting data-volume from /etc/fstab
                    sed -i "/LABEL=${DISK_LABEL}/d" /etc/fstab || true
                    umount -f ${MOUNT_POINT} || true
                    
                    # 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 "" --home ${BUILD_USER_HOME} --uid ${BUILD_USER_UID} ${BUILD_USER}
                        usermod -aG sudo ${BUILD_USER}
                        echo "${BUILD_USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/101-${BUILD_USER}
                        move_to_data ${BUILD_USER_HOME} ${MOUNT_POINT}
                    fi
                    
                    # detach attached volume if template volume parameter changed
                    if [[ "${ATTACHED_VOLUME}" != "${VOLUME_ID}" ]]
                    then
                        if [[ "${ATTACHED_VOLUME}" != "" ]]
                        then
                            aws ec2 detach-volume --volume-id ${ATTACHED_VOLUME} || true
                            # wait detaching attached volume because attaching new volume with existing device name /dev/sdp returns error
                            wait_volume_status ${ATTACHED_VOLUME} "available"
                        fi
                        if [[ "${VOLUME_ID}" != "" ]]
                        then
                            aws ec2 attach-volume --volume-id ${VOLUME_ID} --instance-id ${INSTANCE_ID} --device ${ATTACH_VOLUME_AS} || true
                            wait_volume_status ${VOLUME_ID} "in-use"
                        fi
                    fi
                    
                    # mount volume if exist
                    if [[ "${VOLUME_ID}" != "" ]]
                    then
                        # Wait for data volume attachment to complete
                        wait_count=0
                        wait_max_attempts=6
                        while true
                        do
                            let "wait_count+=1"
                            # Consider additional data disk attached when number of disk attached to instance is > 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}
                            create_part "${disk}"
                            create_fs "${disk}"
                            mount_part "${disk}"
                        done
                    fi
                    
                    # move existing user home directory to /data directory
                    move_to_data ${BUILD_USER_HOME} ${MOUNT_POINT}
                    # copy skelet files to user home if empty. empty home dir will be in case when stack updated with attached data volume without data
                    [ "$(ls -A ${MOUNT_POINT}${BUILD_USER_HOME} 2>/dev/null)" ] || cp -a /etc/skel/. ${MOUNT_POINT}${BUILD_USER_HOME}
                    # move docker files to /data directory
                    move_to_data /var/lib/docker ${MOUNT_POINT}
                    
                    # Create the projects/builds directory, SSH settings: ensure ~/.ssh exists for the build user
                    mkdir -p ${PROJECTS_ROOT} ${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
                    
                    # start docker daemon after partitions manipulation
                    service docker start || true
                    
                    # update/install docksal with new or updated stack
                    if [[ "${old_stack_md5sum}" != "${stack_md5sum}" ]]
                    then
                        # 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"
                    
                        # disable docker autostart
                        systemctl disable docker.service
                        systemctl disable docker.socket
                    fi
                    
                    if [[ "${GITHUB_TOKEN}" != "" ]] && [[ "${GITHUB_ORG_NAME}" != "" ]] && [[ "${GITHUB_TEAM_SLUG}" != "" ]] && [[ "${old_stack_md5sum}" != "${stack_md5sum}" ]]
                    then
                        BACKUP_SSH_PUBLIC_KEY="$(curl -s http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key)"
                        # TODO: move ssh-rake script to separate repo
                        curl -s https://raw.githubusercontent.com/docksal/sandbox-server/develop/aws-cloudformation/scripts/ssh-rake -o /usr/local/bin/ssh-rake
                        sed -i "s|^GITHUB_TOKEN=\".*\"|GITHUB_TOKEN=\"${GITHUB_TOKEN}\"|" /usr/local/bin/ssh-rake
                        sed -i "s|^GITHUB_ORG_NAME=\".*\"|GITHUB_ORG_NAME=\"${GITHUB_ORG_NAME}\"|" /usr/local/bin/ssh-rake
                        sed -i "s|^GITHUB_TEAM_SLUG=\".*\"|GITHUB_TEAM_SLUG=\"${GITHUB_TEAM_SLUG}\"|" /usr/local/bin/ssh-rake
                        sed -i "s|^BACKUP_SSH_PUBLIC_KEY=\".*\"|BACKUP_SSH_PUBLIC_KEY=\"${BACKUP_SSH_PUBLIC_KEY}\"|g" /usr/local/bin/ssh-rake
                        chmod +x /usr/local/bin/ssh-rake
                        /usr/local/bin/ssh-rake install
                    fi
                    
                    if [[ "${old_stack_md5sum}" != "${stack_md5sum}" ]]
                    then
                        ACMESH_CONTAINER="docksal-acme.sh"
                        if [[ "${LETSENCRYPT_DOMAIN}" != "" ]]
                        then
                            ACMESH_PATH="${BUILD_USER_HOME}/letsencrypt/acme.sh/data"
                            CERTOUT_PATH="${BUILD_USER_HOME}/.docksal/certs"
                            DSP="${DSP:-dns_aws}"
                    
                            tmp=$(mktemp)
                            printenv >${tmp}
                            eval "export ${LETSENCRYPT_CONFIG}"
                            printenv | diff -u "${tmp}" - | grep -E "^\+\w+" | sed 's/^+//' >env.file
                            
                            docker rm -vf ${ACMESH_CONTAINER} 2>/dev/null || true
                            docker run -d --restart always \
                                --env-file env.file \
                                --name ${ACMESH_CONTAINER} \
                                -v ${CERTOUT_PATH}:/out \
                                neilpang/acme.sh daemon
                    
                            rm -f "${tmp}" env.file
                    
                            docker exec ${ACMESH_CONTAINER} \
                                --issue \
                                --keylength 4096 \
                                --dns ${DSP} \
                                --domain "${LETSENCRYPT_DOMAIN}" \
                                --domain "*.${LETSENCRYPT_DOMAIN}" \
                                --fullchain-file /out/${LETSENCRYPT_DOMAIN}.crt \
                                --key-file /out/${LETSENCRYPT_DOMAIN}.key \
                                --log /proc/1/fd/1
                    
                            if [[ -f ${CERTOUT_PATH}/${LETSENCRYPT_DOMAIN}.key ]] && [[ -f ${CERTOUT_PATH}/${LETSENCRYPT_DOMAIN}.crt ]]
                            then
                                sed -i '/DOCKSAL_VHOST_PROXY_DEFAULT_CERT/d' "${BUILD_USER_HOME}/.docksal/docksal.env" || true
                                echo DOCKSAL_VHOST_PROXY_DEFAULT_CERT=\"${LETSENCRYPT_DOMAIN}\" | tee -a "${BUILD_USER_HOME}/.docksal/docksal.env"
                                chown -R ${BUILD_USER}:${BUILD_USER} "${BUILD_USER_HOME}/"
                            fi
                        else
                            docker rm -vf ${ACMESH_CONTAINER} 2>/dev/null || true
                        fi
                    fi
                    
                    # prepare directory for artifacts
                    mkdir -p ${BUILD_USER_HOME}/artifacts
                    sed -i '/ARTIFACTS_S3_BUCKET/d' "${BUILD_USER_HOME}/.docksal/docksal.env" || true
                    echo ARTIFACTS_S3_BUCKET=\"${ARTIFACTS_S3_BUCKET}\" | tee -a "${BUILD_USER_HOME}/.docksal/docksal.env"
                    chown ${BUILD_USER}:${BUILD_USER} "${BUILD_USER_HOME}/artifacts"
                    
                    if [[ "${ARTIFACTS_S3_BUCKET}" != "" ]]
                    then
                        apt-get -y install libgcrypt20 s3fs
                        s3fs ${ARTIFACTS_S3_BUCKET} ${BUILD_USER_HOME}/artifacts -o nonempty,allow_other,iam_role,curldbg,endpoint=${AWS_DEFAULT_REGION},url=https://s3.${AWS_DEFAULT_REGION}.amazonaws.com
                    fi
                    
                    su - build-agent -c "fin system reset"
                    echo "${stack_md5sum}" >/root/stack_last_update
                    
                # add to the end of init script echo line with template variable values, for recreate instance on every variable change
                  - !Sub
                    - echo ${ExistingEIP}-${ExistingDataVolume}-${s3name}-${GitHubToken}-${GitHubOrgName}-${GitHubTeamSlug}-${LetsEncryptDomain}-${LetsEncryptConfig} >/dev/null
                    - s3name: !If [ ArtifactsEnabled, !Ref ArtifactsBucket, "" ]

  ec2Instance:
    Type: 'AWS::EC2::Instance'
    Condition: CreateEC2
    Properties:
      IamInstanceProfile: !Ref ec2InstanceProfile
      InstanceType: !Ref InstanceType
      AvailabilityZone: !Ref ManualAZ
      ImageId: !FindInMap [Region2AMI, !Ref "AWS::Region", AMI]
      KeyName: !Ref KeyName
      Tags:
      - Key: StackId
        Value: !Ref AWS::StackId
      SecurityGroupIds:
        - !GetAtt InstanceSecurityGroup.GroupId
      BlockDeviceMappings:
        # Root volume
        - DeviceName: /dev/sda1
          Ebs:
            VolumeSize: '8'
      SubnetId: !If [ SubnetIdDefined, !Ref SubnetId, !Ref "AWS::NoValue" ]
      UserData: !Base64
        'Fn::Join':
          - ''
          - - |
              Content-Type: multipart/mixed; boundary="//"
              MIME-Version: 1.0
              
              --//
              Content-Type: text/cloud-config; charset="us-ascii"
              MIME-Version: 1.0
              Content-Transfer-Encoding: 7bit
              Content-Disposition: attachment; filename="cloud-config.txt"
              
              #cloud-config
              cloud_final_modules:
              - [scripts-user, always]
              
              --//
              Content-Type: text/x-shellscript; charset="us-ascii"
              MIME-Version: 1.0
              Content-Transfer-Encoding: 7bit
              Content-Disposition: attachment; filename="userdata.txt"
              
              #!/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}"
              DOCKSAL_VERSION="master"
              PROJECT_INACTIVITY_TIMEOUT="0.5h"
              PROJECT_DANGLING_TIMEOUT="168h"
              PROJECTS_ROOT="${BUILD_USER_HOME}/builds"
              ATTACH_VOLUME_AS="/dev/sdp"
              
              ##########
              # Functions begin
              move_to_data()
              {
                  local SRC_DIR=$1
                  local DST_DIR=$2
                  if [[ ! -d ${DST_DIR}${SRC_DIR} ]]; then
                      mkdir -p ${DST_DIR}${SRC_DIR}
                      [[ -d ${SRC_DIR} ]] && [[ ! -L ${SRC_DIR} ]] && mv ${SRC_DIR} ${DST_DIR}$(dirname ${SRC_DIR})
                  fi
                  rm -rf ${SRC_DIR} && ln -s ${DST_DIR}${SRC_DIR} ${SRC_DIR}
              }
              
              wait_volume_status()
              {
                  # Wait for EBS volume status.
                  local volume=$1
                  local status=$2
                  local wait_count=0
                  local wait_max_attempts=30
                  while true
                  do
                      let "wait_count+=1"
                      # Check volume status. Non-empty output means volume in required status and we can proceed.
                      [[ "$(aws ec2 describe-volumes --volume-ids ${volume} --filters "Name=status,Values=${status}" --output text)" != "" ]] && break
              
                      # Fail if reached maximum attempts
                      if (( ${wait_count} > ${wait_max_attempts} )); then
                          echo "ERROR: Timed out waiting for EBS volume to be ${status}."
                          exit 1
                      fi
              
                      echo "Waiting for EBS volume to become available (${wait_count})..."
                      sleep 10
                  done
              }
              
              reread_device()
              {
                  DEVICE=$1
                  hdparm -z ${DEVICE} || true
                  file -s ${DEVICE} || true
                  partprobe ${DEVICE} || true
                  blockdev --rereadpt -v ${DEVICE} || true
                  fdisk -l ${DEVICE} || true
              }
              
              mount_part()
              {
                  DATA_DISK=$1
                  # mark disk with label
                  tune2fs -L ${DISK_LABEL} ${DATA_DISK}
                  # Mount the data disk
                  mkdir -p ${MOUNT_POINT}
                  mount ${DATA_DISK} ${MOUNT_POINT}
              }
              
              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}
              }
              
              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"
                  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 <<< "$(lsblk -p -n -P -o NAME,TYPE,FSTYPE,LABEL,MOUNTPOINT ${DATA_DISK} 2>&1 | sed 's/ /;/g')"
                  # 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
              
              # install aws cli if instance just created
              if [[ ! -f /root/stack_last_update ]]
              then
                  apt-get -y update
                  apt-get -y install awscli
              else
                  old_stack_md5sum="$(cat /root/stack_last_update)"
              fi
              
              # read necessary variables
              export AWS_DEFAULT_REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/\(.*\)[a-z]/\1/')
              export INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
              export STACK_ID=$(aws ec2 describe-instances --instance-id ${INSTANCE_ID} --query 'Reservations[*].Instances[*].Tags[?Key==`StackId`].Value' --output text)
              export ATTACHED_IP=$(aws ec2 describe-instances --instance-id ${INSTANCE_ID} --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
              export ATTACHED_VOLUME=$(aws ec2 describe-volumes --filters Name=attachment.instance-id,Values=${INSTANCE_ID} Name=attachment.device,Values=${ATTACH_VOLUME_AS} --query 'Volumes[*].VolumeId' --output text)
              
              # wait stack status complete
              while true
              do
                STACK_STATUS=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].StackStatus' --output text)
                if [[ "${STACK_STATUS}" == *"UPDATE_COMPLETE"* ]] || [[ "${STACK_STATUS}" == *"CREATE_COMPLETE"* ]] || [[ "${STACK_STATUS}" == *"ROLLBACK_COMPLETE"* ]]
                then
                    stack_md5sum=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters' --output text | sort | md5sum | cut -d' ' -f1)
                    break
                fi
                sleep 5
              done
              
              export EIP=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`ExistingEIP`].ParameterValue' --output text)
              export VOLUME_ID=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`ExistingDataVolume`].ParameterValue' --output text)
              export GITHUB_TOKEN=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`GitHubToken`].ParameterValue' --output text)
              export GITHUB_ORG_NAME=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`GitHubOrgName`].ParameterValue' --output text)
              export GITHUB_TEAM_SLUG=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`GitHubTeamSlug`].ParameterValue' --output text)
              export LETSENCRYPT_DOMAIN=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`LetsEncryptDomain`].ParameterValue' --output text)
              export LETSENCRYPT_CONFIG=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Parameters[?ParameterKey==`LetsEncryptConfig`].ParameterValue' --output text)
              export ARTIFACTS_S3_BUCKET=$(aws cloudformation describe-stacks --stack-name=${STACK_ID} --query 'Stacks[*].Outputs[?OutputKey==`ArtifactsBucket`].OutputValue' --output text)
              
              # attach/detach elastic ip
              if [[ "${EIP}" != "${ATTACHED_IP}" ]]
              then
                  # try to detach attached ip. elastic ip will be detached, but autoassigned ip remain attached
                  aws ec2 disassociate-address --public-ip ${ATTACHED_IP} || true
                  # try to attach elastic ip if defined in template. in case user defined wrong ip, instance will be accessible on autoassigned ip
                  [[ "${EIP}" != "" ]] && aws ec2 associate-address --instance-id ${INSTANCE_ID} --public-ip ${EIP} || true
              fi
              
              # stop docker daemon before partitions manipulation
              service docker stop || true
              
              # back compatibility fix: remove automounting data-volume from /etc/fstab
              sed -i "/LABEL=${DISK_LABEL}/d" /etc/fstab || true
              umount -f ${MOUNT_POINT} || true
              
              # 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 "" --home ${BUILD_USER_HOME} --uid ${BUILD_USER_UID} ${BUILD_USER}
                  usermod -aG sudo ${BUILD_USER}
                  echo "${BUILD_USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/101-${BUILD_USER}
                  move_to_data ${BUILD_USER_HOME} ${MOUNT_POINT}
              fi
              
              # detach attached volume if template volume parameter changed
              if [[ "${ATTACHED_VOLUME}" != "${VOLUME_ID}" ]]
              then
                  if [[ "${ATTACHED_VOLUME}" != "" ]]
                  then
                      aws ec2 detach-volume --volume-id ${ATTACHED_VOLUME} || true
                      # wait detaching attached volume because attaching new volume with existing device name /dev/sdp returns error
                      wait_volume_status ${ATTACHED_VOLUME} "available"
                  fi
                  if [[ "${VOLUME_ID}" != "" ]]
                  then
                      aws ec2 attach-volume --volume-id ${VOLUME_ID} --instance-id ${INSTANCE_ID} --device ${ATTACH_VOLUME_AS} || true
                      wait_volume_status ${VOLUME_ID} "in-use"
                  fi
              fi
              
              # mount volume if exist
              if [[ "${VOLUME_ID}" != "" ]]
              then
                  # Wait for data volume attachment to complete
                  wait_count=0
                  wait_max_attempts=6
                  while true
                  do
                      let "wait_count+=1"
                      # Consider additional data disk attached when number of disk attached to instance is > 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}
                      create_part "${disk}"
                      create_fs "${disk}"
                      mount_part "${disk}"
                  done
              fi
              
              # move existing user home directory to /data directory
              move_to_data ${BUILD_USER_HOME} ${MOUNT_POINT}
              # copy skelet files to user home if empty. empty home dir will be in case when stack updated with attached data volume without data
              [ "$(ls -A ${MOUNT_POINT}${BUILD_USER_HOME} 2>/dev/null)" ] || cp -a /etc/skel/. ${MOUNT_POINT}${BUILD_USER_HOME}
              # move docker files to /data directory
              move_to_data /var/lib/docker ${MOUNT_POINT}
              
              # Create the projects/builds directory, SSH settings: ensure ~/.ssh exists for the build user
              mkdir -p ${PROJECTS_ROOT} ${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
              
              # start docker daemon after partitions manipulation
              service docker start || true
              
              # update/install docksal with new or updated stack
              if [[ "${old_stack_md5sum}" != "${stack_md5sum}" ]]
              then
                  # 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"
              
                  # disable docker autostart
                  systemctl disable docker.service
                  systemctl disable docker.socket
              fi
              
              if [[ "${GITHUB_TOKEN}" != "" ]] && [[ "${GITHUB_ORG_NAME}" != "" ]] && [[ "${GITHUB_TEAM_SLUG}" != "" ]] && [[ "${old_stack_md5sum}" != "${stack_md5sum}" ]]
              then
                  BACKUP_SSH_PUBLIC_KEY="$(curl -s http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key)"
                  # TODO: move ssh-rake script to separate repo
                  curl -s https://raw.githubusercontent.com/docksal/sandbox-server/develop/aws-cloudformation/scripts/ssh-rake -o /usr/local/bin/ssh-rake
                  sed -i "s|^GITHUB_TOKEN=\".*\"|GITHUB_TOKEN=\"${GITHUB_TOKEN}\"|" /usr/local/bin/ssh-rake
                  sed -i "s|^GITHUB_ORG_NAME=\".*\"|GITHUB_ORG_NAME=\"${GITHUB_ORG_NAME}\"|" /usr/local/bin/ssh-rake
                  sed -i "s|^GITHUB_TEAM_SLUG=\".*\"|GITHUB_TEAM_SLUG=\"${GITHUB_TEAM_SLUG}\"|" /usr/local/bin/ssh-rake
                  sed -i "s|^BACKUP_SSH_PUBLIC_KEY=\".*\"|BACKUP_SSH_PUBLIC_KEY=\"${BACKUP_SSH_PUBLIC_KEY}\"|g" /usr/local/bin/ssh-rake
                  chmod +x /usr/local/bin/ssh-rake
                  /usr/local/bin/ssh-rake install
              fi
              
              if [[ "${old_stack_md5sum}" != "${stack_md5sum}" ]]
              then
                  ACMESH_CONTAINER="docksal-acme.sh"
                  if [[ "${LETSENCRYPT_DOMAIN}" != "" ]]
                  then
                      ACMESH_PATH="${BUILD_USER_HOME}/letsencrypt/acme.sh/data"
                      CERTOUT_PATH="${BUILD_USER_HOME}/.docksal/certs"
                      DSP="${DSP:-dns_aws}"
              
                      tmp=$(mktemp)
                      printenv >${tmp}
                      eval "export ${LETSENCRYPT_CONFIG}"
                      printenv | diff -u "${tmp}" - | grep -E "^\+\w+" | sed 's/^+//' >env.file
                      
                      docker rm -vf ${ACMESH_CONTAINER} 2>/dev/null || true
                      docker run -d --restart always \
                          --env-file env.file \
                          --name ${ACMESH_CONTAINER} \
                          -v ${CERTOUT_PATH}:/out \
                          neilpang/acme.sh daemon
              
                      rm -f "${tmp}" env.file
              
                      docker exec ${ACMESH_CONTAINER} \
                          --issue \
                          --keylength 4096 \
                          --dns ${DSP} \
                          --domain "${LETSENCRYPT_DOMAIN}" \
                          --domain "*.${LETSENCRYPT_DOMAIN}" \
                          --fullchain-file /out/${LETSENCRYPT_DOMAIN}.crt \
                          --key-file /out/${LETSENCRYPT_DOMAIN}.key \
                          --log /proc/1/fd/1
              
                      if [[ -f ${CERTOUT_PATH}/${LETSENCRYPT_DOMAIN}.key ]] && [[ -f ${CERTOUT_PATH}/${LETSENCRYPT_DOMAIN}.crt ]]
                      then
                          sed -i '/DOCKSAL_VHOST_PROXY_DEFAULT_CERT/d' "${BUILD_USER_HOME}/.docksal/docksal.env" || true
                          echo DOCKSAL_VHOST_PROXY_DEFAULT_CERT=\"${LETSENCRYPT_DOMAIN}\" | tee -a "${BUILD_USER_HOME}/.docksal/docksal.env"
                          chown -R ${BUILD_USER}:${BUILD_USER} "${BUILD_USER_HOME}/"
                      fi
                  else
                      docker rm -vf ${ACMESH_CONTAINER} 2>/dev/null || true
                  fi
              fi
              
              # prepare directory for artifacts
              mkdir -p ${BUILD_USER_HOME}/artifacts
              sed -i '/ARTIFACTS_S3_BUCKET/d' "${BUILD_USER_HOME}/.docksal/docksal.env" || true
              echo ARTIFACTS_S3_BUCKET=\"${ARTIFACTS_S3_BUCKET}\" | tee -a "${BUILD_USER_HOME}/.docksal/docksal.env"
              chown ${BUILD_USER}:${BUILD_USER} "${BUILD_USER_HOME}/artifacts"
              
              if [[ "${ARTIFACTS_S3_BUCKET}" != "" ]]
              then
                  apt-get -y install libgcrypt20 s3fs
                  s3fs ${ARTIFACTS_S3_BUCKET} ${BUILD_USER_HOME}/artifacts -o nonempty,allow_other,iam_role,curldbg,endpoint=${AWS_DEFAULT_REGION},url=https://s3.${AWS_DEFAULT_REGION}.amazonaws.com
              fi
              
              su - build-agent -c "fin system reset"
              echo "${stack_md5sum}" >/root/stack_last_update
              
          # add to the end of init script echo line with template variable values, for recreate instance on every variable change
            - !Sub
              - echo ${ExistingEIP}-${ExistingDataVolume}-${s3name}-${GitHubToken}-${GitHubOrgName}-${GitHubTeamSlug}-${LetsEncryptDomain}-${LetsEncryptConfig} >/dev/null
              - s3name: !If [ ArtifactsEnabled, !Ref ArtifactsBucket, "" ]

Outputs:
  IPAddress:
    Description: If not shown here, IP can be found in EC2 dashboard under instance details
    Value: !If [ ExistingEIPDefined, !Ref ExistingEIP, !If [ CreateEC2, !GetAtt ec2Instance.PublicIp, "" ]]
  InstanceURL:
    Value: !Join [ '', [ 'https://', !If [ ExistingEIPDefined, !Ref ExistingEIP, !If [ CreateEC2, !GetAtt ec2Instance.PublicIp, "" ]], '.nip.io' ] ]
  SSHConnect:
    Description: SSH connection string
    Value: !Join [ '', [ 'ssh build-agent@', !If [ ExistingEIPDefined, !Ref ExistingEIP, !If [ CreateEC2, !GetAtt ec2Instance.PublicIp, "" ] ] ] ]
  PersistentVolume:
    Value: !Ref ExistingDataVolume
  ArtifactsBucket:
    Value: !If [ ArtifactsEnabled, !Ref ArtifactsBucket, "" ]
  GitHubToken:
    Value: !Ref GitHubToken
  GitHubOrgName:
    Value: !Ref GitHubOrgName
  GitHubTeamSlug:
    Value: !Ref GitHubTeamSlug
  LetsEncryptDomain:
    Value: !Ref LetsEncryptDomain
  LetsEncryptConfig:
    Value: !Ref LetsEncryptConfig
