370 lines
12 KiB
Bash
Executable file
370 lines
12 KiB
Bash
Executable file
#!/bin/bash
|
|
|
|
set -e
|
|
set -o pipefail
|
|
|
|
if [ ! -t 0 ]
|
|
then
|
|
INPUT=$(cat -)
|
|
fi
|
|
MYPATH=$(realpath ${BASH_SOURCE[0]})
|
|
MYDIR=$(dirname "${MYPATH}")
|
|
TEMPLATES="$MYDIR/cloudconfig"
|
|
|
|
# Defaults
|
|
LOCATION=${LOCATION:"westeurope"}
|
|
|
|
# END Defaults
|
|
|
|
if [ -z "$GARM_PROVIDER_CONFIG_FILE" ]
|
|
then
|
|
echo "no config file specified in env"
|
|
exit 1
|
|
fi
|
|
|
|
source "$GARM_PROVIDER_CONFIG_FILE"
|
|
|
|
declare -A GARM_TO_GH_ARCH_MAP
|
|
GARM_TO_GH_ARCH_MAP["amd64"]="x64"
|
|
GARM_TO_GH_ARCH_MAP["arm"]="arm"
|
|
GARM_TO_GH_ARCH_MAP["arm64"]="arm64"
|
|
|
|
declare -A AZURE_OS_TO_GH_OS_MAP
|
|
AZURE_OS_TO_GH_OS_MAP["Linux"]="linux"
|
|
AZURE_OS_TO_GH_OS_MAP["Windows"]="win"
|
|
|
|
# https://docs.microsoft.com/en-us/azure/virtual-machines/states-billing#power-states-and-billing
|
|
declare -A AZURE_POWER_STATE_MAP
|
|
AZURE_POWER_STATE_MAP["VM starting"]="pending_create"
|
|
AZURE_POWER_STATE_MAP["VM running"]="running"
|
|
AZURE_POWER_STATE_MAP["VM stopping"]="stopped"
|
|
AZURE_POWER_STATE_MAP["VM stopped"]="stopped"
|
|
AZURE_POWER_STATE_MAP["VM deallocating"]="stopped"
|
|
AZURE_POWER_STATE_MAP["VM deallocated"]="stopped"
|
|
|
|
# https://docs.microsoft.com/en-us/azure/virtual-machines/states-billing#provisioning-states
|
|
declare -A AZURE_PROVISION_STATE_MAP
|
|
AZURE_PROVISION_STATE_MAP["Creating"]="pending_create"
|
|
AZURE_PROVISION_STATE_MAP["Updating"]="pending_create"
|
|
AZURE_PROVISION_STATE_MAP["Migrating"]="pending_create"
|
|
AZURE_PROVISION_STATE_MAP["Failed"]="error"
|
|
AZURE_PROVISION_STATE_MAP["Succeeded"]="running"
|
|
AZURE_PROVISION_STATE_MAP["Deleting"]="pending_delete"
|
|
|
|
function checkValNotNull() {
|
|
if [ -z "$1" -o "$1" == "null" ]; then
|
|
echo "failed to fetch value $2"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
function requestedArch() {
|
|
ARCH=$(echo "$INPUT" | jq -c -r '.arch')
|
|
checkValNotNull "${ARCH}" "arch" || return $?
|
|
echo "${ARCH}"
|
|
}
|
|
|
|
function downloadURL() {
|
|
[ -z "$1" -o -z "$2" ] && return 1
|
|
GH_OS="${AZURE_OS_TO_GH_OS_MAP[$1]}"
|
|
GH_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}"
|
|
URL=$(echo "$INPUT" | jq -c -r --arg OS "$GH_OS" --arg ARCH "$GH_ARCH" '(.tools[] | select( .os == $OS and .architecture == $ARCH)).download_url')
|
|
checkValNotNull "${URL}" "download URL" || return $?
|
|
echo "${URL}"
|
|
}
|
|
|
|
function tempDownloadToken() {
|
|
[ -z "$1" -o -z "$2" ] && return 1
|
|
GH_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}"
|
|
TOKEN=$(echo "$INPUT" | jq -c -r --arg OS "$1" --arg ARCH "$GH_ARCH" '(.tools[] | select( .os == $OS and .architecture == $ARCH)).temp_download_token')
|
|
echo "${TOKEN}"
|
|
}
|
|
|
|
function runnerTokenURL() {
|
|
METADATA_URL=$(echo "$INPUT" | jq -c -r '."metadata-url"')
|
|
checkValNotNull "${METADATA_URL}" "metadata-url" || return $?
|
|
echo "${METADATA_URL}/runner-registration-token/"
|
|
}
|
|
|
|
function downloadFilename() {
|
|
[ -z "$1" -o -z "$2" ] && return 1
|
|
GH_OS="${AZURE_OS_TO_GH_OS_MAP[$1]}"
|
|
GH_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}"
|
|
FN=$(echo "$INPUT" | jq -c -r --arg OS "$GH_OS" --arg ARCH "$GH_ARCH" '(.tools[] | select( .os == $OS and .architecture == $ARCH)).filename')
|
|
checkValNotNull "${FN}" "download filename" || return $?
|
|
echo "${FN}"
|
|
}
|
|
|
|
function poolID() {
|
|
POOL_ID=$(echo "$INPUT" | jq -c -r '.pool_id')
|
|
checkValNotNull "${POOL_ID}" "pool_id" || return $?
|
|
echo "${POOL_ID}"
|
|
}
|
|
|
|
function vmSize() {
|
|
VM_SIZE=$(echo "$INPUT" | jq -c -r '.flavor')
|
|
checkValNotNull "${VM_SIZE}" "flavor" || return $?
|
|
echo "${VM_SIZE}"
|
|
}
|
|
|
|
function imageUrn() {
|
|
IMG=$(echo "$INPUT" | jq -c -r '.image')
|
|
checkValNotNull "${IMG}" "image" || return $?
|
|
echo "${IMG}"
|
|
}
|
|
|
|
function getOSImageDetails() {
|
|
IMAGE=$(echo "$INPUT" | jq -r -c '.image')
|
|
IMAGE_DETAILS=$(az vm image show --urn "$IMAGE" -o json --only-show-errors)
|
|
echo "$IMAGE_DETAILS"
|
|
}
|
|
|
|
function repoURL() {
|
|
REPO=$(echo "$INPUT" | jq -c -r '.repo_url')
|
|
checkValNotNull "${REPO}" "repo_url" || return $?
|
|
echo "${REPO}"
|
|
}
|
|
|
|
function callbackURL() {
|
|
CB_URL=$(echo "$INPUT" | jq -c -r '."callback-url"')
|
|
checkValNotNull "${CB_URL}" "callback-url" || return $?
|
|
echo "${CB_URL}"
|
|
}
|
|
|
|
function callbackToken() {
|
|
CB_TK=$(echo "$INPUT" | jq -c -r '."instance-token"')
|
|
checkValNotNull "${CB_TK}" "instance-token" || return $?
|
|
echo "${CB_TK}"
|
|
}
|
|
|
|
function instanceName() {
|
|
NAME=$(echo "$INPUT" | jq -c -r '.name')
|
|
checkValNotNull "${NAME}" "name" || return $?
|
|
echo "${NAME}"
|
|
}
|
|
|
|
function labels() {
|
|
LBL=$(echo "$INPUT" | jq -c -r '.labels | join(",")')
|
|
checkValNotNull "${LBL}" "labels" || return $?
|
|
echo "${LBL}"
|
|
}
|
|
|
|
function vmStatus() {
|
|
[ -z "$1" -o -z "$2" ] && return 1
|
|
|
|
RG_DETAILS=$(az group show -n "$1" -o json --only-show-errors)
|
|
RG_STATE=$(echo "$RG_DETAILS" | jq -r '.properties.provisioningState')
|
|
STATUS="${AZURE_PROVISION_STATE_MAP[$RG_STATE]}"
|
|
if [[ "$STATUS" != "running" ]]; then
|
|
echo "$STATUS"
|
|
return 0
|
|
fi
|
|
VM_DETAILS=$(az vm show -g "$1" -n "$2" --show-details -o json --only-show-errors)
|
|
VM_STATE=$(echo "$VM_DETAILS" | jq -r '.provisioningState')
|
|
STATUS="${AZURE_PROVISION_STATE_MAP[$VM_STATE]}"
|
|
if [[ "$STATUS" != "running" ]]; then
|
|
echo "$STATUS"
|
|
return 0
|
|
fi
|
|
VM_POWER_STATE=$(echo "$VM_DETAILS" | jq -r '.powerState')
|
|
VM_STATUS="${AZURE_POWER_STATE_MAP[$VM_POWER_STATE]}"
|
|
if [[ -z "${VM_STATUS}" ]]; then
|
|
echo "unknown"
|
|
return 0
|
|
fi
|
|
echo "${VM_STATUS}"
|
|
}
|
|
|
|
function getCloudConfig() {
|
|
IMAGE_DETAILS=$(getOSImageDetails)
|
|
|
|
OS_TYPE=$(echo "${IMAGE_DETAILS}" | jq -c -r '.osDiskImage.operatingSystem')
|
|
checkValNotNull "${OS_TYPE}" "operatingSystem" || return $?
|
|
|
|
ARCH=$(requestedArch)
|
|
DW_URL=$(downloadURL "${OS_TYPE}" "${ARCH}")
|
|
DW_TOKEN=$(tempDownloadToken "${OS_TYPE}" "${ARCH}")
|
|
DW_FILENAME=$(downloadFilename "${OS_TYPE}" "${ARCH}")
|
|
LABELS=$(labels)
|
|
|
|
TMP_SCRIPT=$(mktemp)
|
|
TMP_CC=$(mktemp)
|
|
|
|
INSTALL_TPL=$(cat "${TEMPLATES}/install_runner.tpl")
|
|
CC_TPL=$(cat "${TEMPLATES}/userdata.tpl")
|
|
echo "$INSTALL_TPL" | sed -e "s|GARM_CALLBACK_URL|$(callbackURL)|g" \
|
|
-e "s|GARM_CALLBACK_TOKEN|$(callbackToken)|g" \
|
|
-e "s|GH_DOWNLOAD_URL|${DW_URL}|g" \
|
|
-e "s|GH_FILENAME|${DW_FILENAME}|g" \
|
|
-e "s|GH_TARGET_URL|$(repoURL)|g" \
|
|
-e "s|GARM_METADATA_URL|$(runnerTokenURL)|g" \
|
|
-e "s|GH_RUNNER_NAME|$(instanceName)|g" \
|
|
-e "s|GH_TEMP_DOWNLOAD_TOKEN|${DW_TOKEN}|g" \
|
|
-e "s|GH_RUNNER_LABELS|${LABELS}|g" > ${TMP_SCRIPT}
|
|
|
|
AS_B64=$(base64 -w0 ${TMP_SCRIPT})
|
|
echo "${CC_TPL}" | sed "s|RUNNER_INSTALL_B64|${AS_B64}|g" > ${TMP_CC}
|
|
echo "${TMP_CC}"
|
|
}
|
|
|
|
function CreateInstance() {
|
|
if [ -z "$INPUT" ]; then
|
|
echo "expected build params in stdin"
|
|
exit 1
|
|
fi
|
|
|
|
CC_FILE=$(getCloudConfig)
|
|
VM_SIZE=$(vmSize)
|
|
INSTANCE_NAME=$(instanceName)
|
|
IMAGE_URN=$(imageUrn)
|
|
IMAGE_DETAILS=$(getOSImageDetails)
|
|
|
|
OS_TYPE=$(echo "${IMAGE_DETAILS}" | jq -c -r '.osDiskImage.operatingSystem' | tr '[:upper:]' '[:lower:]')
|
|
checkValNotNull "${OS_TYPE}" "os_type" || return $?
|
|
OS_NAME=$(echo "${IMAGE_URN}" | cut -d ':' -f2)
|
|
OS_VERSION=$(echo "${IMAGE_URN}" | cut -d ':' -f3)
|
|
ARCH="amd64"
|
|
|
|
TAGS="garm_controller_id=${GARM_CONTROLLER_ID} garm_pool_id=${GARM_POOL_ID} os_type=${OS_TYPE} os_name=${OS_NAME} os_version=${OS_VERSION} os_arch=${ARCH}"
|
|
|
|
set +e
|
|
|
|
az group create -n $INSTANCE_NAME -l $LOCATION --tags $TAGS --only-show-errors -o none
|
|
az vm create -g $INSTANCE_NAME -n $INSTANCE_NAME -l $LOCATION --size $VM_SIZE --image $IMAGE_URN --tags $TAGS --nsg-rule none --public-ip-address "" --user-data "${CC_FILE}" -o none --only-show-errors
|
|
if [[ $? -ne 0 ]]; then
|
|
az group delete -n $INSTANCE_NAME --no-wait --y -o none --only-show-errors
|
|
echo "Failed to create Azure VM"
|
|
exit 1
|
|
fi
|
|
rm -f "${CC_FILE}"
|
|
|
|
set -e
|
|
|
|
STATUS=$(vmStatus $INSTANCE_NAME $INSTANCE_NAME)
|
|
FAULT_VAL=""
|
|
|
|
jq -rnc \
|
|
--arg PROVIDER_ID "${INSTANCE_NAME}" \
|
|
--arg NAME "${INSTANCE_NAME}" \
|
|
--arg OS_TYPE "${OS_TYPE}" \
|
|
--arg OS_NAME "${OS_NAME}" \
|
|
--arg OS_VERSION "${OS_VERSION}" \
|
|
--arg ARCH "${ARCH}" \
|
|
--arg STATUS "${STATUS}" \
|
|
--arg POOL_ID "${GARM_POOL_ID}" \
|
|
--arg FAULT "${FAULT_VAL}" \
|
|
'{"provider_id": $PROVIDER_ID, "name": $NAME, "os_type": $OS_TYPE, "os_name": $OS_NAME, "os_version": $OS_VERSION, "os_arch": $ARCH, "status": $STATUS, "pool_id": $POOL_ID, "provider_fault": $FAULT}'
|
|
}
|
|
|
|
function DeleteInstance() {
|
|
local instance_id="${GARM_INSTANCE_ID}"
|
|
if [ -z "${instance_id}" ]; then
|
|
echo "missing instance ID in env"
|
|
return 1
|
|
fi
|
|
|
|
set +e
|
|
rg_info=$(az group show -n "${instance_id}" -o json --only-show-errors 2>&1)
|
|
if [ $? -ne 0 ]; then
|
|
CODE=$?
|
|
set -e
|
|
if echo "${rg_info}" | grep -q "ResourceGroupNotFound"; then
|
|
return 0
|
|
fi
|
|
return $CODE
|
|
fi
|
|
set -e
|
|
az group delete -n "${instance_id}" --no-wait --y --only-show-errors
|
|
}
|
|
|
|
function StartInstance() {
|
|
local instance_id="${GARM_INSTANCE_ID}"
|
|
if [ -z "${instance_id}" ]; then
|
|
echo "missing instance ID in env"
|
|
return 1
|
|
fi
|
|
|
|
az vm start -g "${instance_id}" -n "${instance_id}" -o none --only-show-errors
|
|
}
|
|
|
|
function StopServer() {
|
|
local instance_id="${GARM_INSTANCE_ID}"
|
|
if [ -z "${instance_id}" ]; then
|
|
echo "missing instance ID in env"
|
|
return 1
|
|
fi
|
|
|
|
az vm deallocate -g "${instance_id}" -n "${instance_id}" -o none --only-show-errors
|
|
}
|
|
|
|
function GetInstance() {
|
|
local instance_id="${GARM_INSTANCE_ID}"
|
|
info=$(az vm show -d -n $instance_id -g $instance_id -o json --only-show-errors 2>&1)
|
|
echo $info | jq -r '
|
|
{
|
|
provider_id: .name,
|
|
name: .name,
|
|
os_type: .tags.os_type,
|
|
os_name: .tags.os_name,
|
|
os_version: .tags.os_version,
|
|
os_arch: .tags.os_arch,
|
|
pool_id: .tags.garm_pool_id,
|
|
status: {"VM starting": "pending_create", "VM running": "running", "VM stopping": "stopped", "VM stopped": "stopped", "VM deallocating": "stopped", "VM deallocated": "stopped"}[.powerState]
|
|
}'
|
|
}
|
|
|
|
function ListInstances() {
|
|
INSTANCES=$(az vm list --query "[?tags.garm_pool_id == '${GARM_POOL_ID}']" -o json --only-show-errors 2>&1)
|
|
echo $info | jq -r '[
|
|
.[] | {
|
|
provider_id: .name,
|
|
name: .name,
|
|
os_type: .tags.os_type,
|
|
os_name: .tags.os_name,
|
|
os_version: .tags.os_version,
|
|
os_arch: .tags.os_arch,
|
|
pool_id: .tags.garm_pool_id,
|
|
status: {"Creating": "pending_create", "Migrating": "pending_create", "Failed": "error", "Succeeded": "running", "Deleting": "pending_delete"}[.provisioningState]
|
|
}]'
|
|
}
|
|
|
|
# Login to Azure
|
|
checkValNotNull "${AZURE_SUBSCRIPTION_ID}" "AZURE_SUBSCRIPTION_ID"
|
|
checkValNotNull "${AZURE_TENANT_ID}" "AZURE_TENANT_ID"
|
|
checkValNotNull "${AZURE_CLIENT_ID}" "AZURE_CLIENT_ID"
|
|
checkValNotNull "${AZURE_CLIENT_SECRET}" "AZURE_CLIENT_SECRET"
|
|
|
|
export AZURE_CONFIG_DIR="${MYDIR}/.azure"
|
|
|
|
az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID -o none --only-show-errors
|
|
az account set -s $AZURE_SUBSCRIPTION_ID -o none --only-show-errors
|
|
|
|
case "$GARM_COMMAND" in
|
|
"CreateInstance")
|
|
CreateInstance
|
|
;;
|
|
"DeleteInstance")
|
|
DeleteInstance
|
|
;;
|
|
"GetInstance")
|
|
GetInstance
|
|
;;
|
|
"ListInstances")
|
|
ListInstances
|
|
;;
|
|
"StartInstance")
|
|
StartInstance
|
|
;;
|
|
"StopInstance")
|
|
StopServer
|
|
;;
|
|
"RemoveAllInstances")
|
|
echo "RemoveAllInstances not implemented"
|
|
exit 1
|
|
;;
|
|
*)
|
|
echo "Invalid GARM provider command: \"$GARM_COMMAND\""
|
|
exit 1
|
|
;;
|
|
esac
|