Add external providers and an example

Add the ability to externalize providers to a binary on disk.
This commit is contained in:
Gabriel Adrian Samfira 2022-05-09 17:10:40 +00:00
parent ebe7d722c0
commit 0b70a30944
20 changed files with 906 additions and 5 deletions

View file

@ -0,0 +1,5 @@
# OpenStack external provider for GARM
This is an example external provider, written for OpenStack. It is a simple bash script that implements the external provider interface, in order to supply ```garm``` with compute instances. This is just an example, complete with a sample config file.
Not all functions are implemented, just the bare minimum to get it to work with the current feature set of ```garm```. It is not meant for production, as it needs a lot more error checking, retries, and potentially more flexibility to be of any use in a real environment.

View file

@ -0,0 +1,61 @@
#!/bin/bash
set -ex
set -o pipefail
CALLBACK_URL="GARM_CALLBACK_URL"
BEARER_TOKEN="GARM_CALLBACK_TOKEN"
DOWNLOAD_URL="GH_DOWNLOAD_URL"
FILENAME="GH_FILENAME"
TARGET_URL="GH_TARGET_URL"
RUNNER_TOKEN="GH_RUNNER_TOKEN"
RUNNER_NAME="GH_RUNNER_NAME"
RUNNER_LABELS="GH_RUNNER_LABELS"
function call() {
PAYLOAD="$1"
curl -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
}
function sendStatus() {
MSG="$1"
call "{\"status\": \"installing\", \"message\": \"$MSG\"}"
}
function success() {
MSG="$1"
call "{\"status\": \"idle\", \"message\": \"$MSG\"}"
}
function fail() {
MSG="$1"
call "{\"status\": \"failed\", \"message\": \"$MSG\"}"
exit 1
}
sendStatus "downloading tools from ${DOWNLOAD_URL}"
curl -L -o "/home/runner/${FILENAME}" "${DOWNLOAD_URL}" || fail "failed to download tools"
mkdir -p /home/runner/actions-runner || fail "failed to create actions-runner folder"
sendStatus "extracting runner"
tar xf "/home/runner/${FILENAME}" -C /home/runner/actions-runner/ || fail "failed to extract runner"
chown runner:runner -R /home/runner/actions-runner/ || fail "failed to change owner"
sendStatus "installing dependencies"
cd /home/runner/actions-runner
sudo ./bin/installdependencies.sh || fail "failed to install dependencies"
sendStatus "configuring runner"
sudo -u runner -- ./config.sh --unattended --url "${TARGET_URL}" --token "${RUNNER_TOKEN}" --name "${RUNNER_NAME}" --labels "${RUNNER_LABELS}" --ephemeral || fail "failed to configure runner"
sendStatus "installing runner service"
./svc.sh install runner || fail "failed to install service"
sendStatus "starting service"
./svc.sh start || fail "failed to start service"
success "runner successfully installed"

View file

@ -0,0 +1,29 @@
#cloud-config
package_upgrade: true
packages:
- curl
- tar
system_info:
default_user:
name: runner
home: /home/runner
shell: /bin/bash
groups:
- sudo
- adm
- cdrom
- dialout
- dip
- video
- plugdev
- netdev
sudo: ALL=(ALL) NOPASSWD:ALL
runcmd:
- /install_runner.sh
- rm -f /install_runner.sh
write_files:
- encoding: b64
content: RUNNER_INSTALL_B64
owner: root:root
path: /install_runner.sh
permissions: "755"

View file

@ -0,0 +1,385 @@
#!/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
# set this variable to 0 in the provider config to disable.
BOOT_FROM_VOLUME=${BOOT_FROM_VOLUME:-1}
# 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 OS_TO_GH_ARCH_MAP
OS_TO_GH_ARCH_MAP["x86_64"]="x64"
OS_TO_GH_ARCH_MAP["armv7l"]="arm64"
OS_TO_GH_ARCH_MAP["mips64"]="arm64"
OS_TO_GH_ARCH_MAP["mips64el"]="arm64"
OS_TO_GH_ARCH_MAP["mips"]="arm"
OS_TO_GH_ARCH_MAP["mipsel"]="arm"
declare -A OS_TO_GARM_ARCH_MAP
OS_TO_GARM_ARCH_MAP["x86_64"]="amd64"
OS_TO_GARM_ARCH_MAP["armv7l"]="arm64"
OS_TO_GARM_ARCH_MAP["mips64"]="arm64"
OS_TO_GARM_ARCH_MAP["mips64el"]="arm64"
OS_TO_GARM_ARCH_MAP["mips"]="arm"
OS_TO_GARM_ARCH_MAP["mipsel"]="arm"
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"
function checkValNotNull() {
if [ -z "$1" -o "$1" == "null" ];then
echo "failed to fetch value $2"
return 1
fi
return 0
}
function getOSImageDetails() {
IMAGE_ID=$(echo "$INPUT" | jq -r -c '.image')
OS_IMAGE=$(openstack image show "$IMAGE_ID" -f json)
echo "$OS_IMAGE"
}
function getOpenStackNetworkID() {
if [ -z "$OPENSTACK_PRIVATE_NETWORK" ]
then
echo "no network specified in config"
return 1
fi
NET_ID=$(openstack network show ${OPENSTACK_PRIVATE_NETWORK} -f value -c id)
if [ -z "$NET_ID" ];then
echo "failed to find network $OPENSTACK_PRIVATE_NETWORK"
fi
echo ${NET_ID}
}
function getVolumeSizeFromFlavor() {
local flavor="$1"
FLAVOR_DETAILS=$(openstack flavor show "${flavor}" -f json)
DISK_SIZE=$(echo "$FLAVOR_DETAILS" | jq -c -r '.disk')
if [ -z "$DISK_SIZE" ];then
echo "failed to get disk size from flavor"
return 1
fi
echo ${DISK_SIZE}
}
function waitForVolume() {
local volumeName=$1
set +e
status=$(openstack volume show "${volumeName}" -f json | jq -r -c '.status')
if [ $? -ne 0 ];then
set -e
return $?
fi
set -e
while [ "${status}" != "available" -a "${status}" != "error" ];do
status=$(openstack volume show "${volumeName}" -f json | jq -r -c '.status')
done
}
function createVolumeFromImage() {
local image="$1"
local disk_size="$2"
local instance_name="$3"
if [ -z ${image} -o -z ${disk_size} -o -z "${instance_name}" ];then
echo "missing image, disk size or instance name in function call"
return 1
fi
# Instance names contain a UUID. It should be safe to create a volume with the same name and
# expect it to be unique.
set +e
VOLUME_INFO=$(openstack volume create -f json --image "${image}" --size "${disk_size}" "${instance_name}")
if [ $? -ne 0 ]; then
CODE=$?
openstack volume delete "${instance_name}" || true
set -e
return $CODE
fi
waitForVolume "${instance_name}"
echo "${VOLUME_INFO}"
}
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_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}"
URL=$(echo "$INPUT" | jq -c -r --arg OS "$1" --arg ARCH "$GH_ARCH" '(.tools[] | select( .os == $OS and .architecture == $ARCH)).download_url')
checkValNotNull "${URL}" "download URL" || return $?
echo "${URL}"
}
function downloadFilename() {
[ -z "$1" -o -z "$2" ] && return 1
GH_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}"
FN=$(echo "$INPUT" | jq -c -r --arg OS "$1" --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 flavor() {
FLAVOR=$(echo "$INPUT" | jq -c -r '.flavor')
checkValNotNull "${FLAVOR}" "flavor" || return $?
echo "${FLAVOR}"
}
function image() {
IMG=$(echo "$INPUT" | jq -c -r '.image')
checkValNotNull "${IMG}" "image" || return $?
echo "${IMG}"
}
function repoURL() {
REPO=$(echo "$INPUT" | jq -c -r '.repo_url')
checkValNotNull "${REPO}" "repo_url" || return $?
echo "${REPO}"
}
function ghAccessToken() {
TOKEN=$(echo "$INPUT" | jq -c -r '.github_runner_access_token')
checkValNotNull "${TOKEN}" "github_runner_access_token" || return $?
echo "${TOKEN}"
}
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 getCloudConfig() {
IMAGE_DETAILS=$(getOSImageDetails)
OS_TYPE=$(echo "${IMAGE_DETAILS}" | jq -c -r '.properties.os_type')
checkValNotNull "${OS_TYPE}" "os_type" || return $?
ARCH=$(requestedArch)
DW_URL=$(downloadURL "${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|GH_RUNNER_TOKEN|$(ghAccessToken)|g" \
-e "s|GH_RUNNER_NAME|$(instanceName)|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 waitForServer() {
local srv_id="$1"
srv_info=$(openstack server show -f json "${srv_id}")
[ $? -ne 0 ] && return $?
status=$(echo "${srv_info}" | jq -r -c '.status')
while [ "${status}" != "ERROR" -a "${status}" != "ACTIVE" ];do
sleep 2
srv_info=$(openstack server show -f json "${srv_id}")
[ $? -ne 0 ] && return $?
status=$(echo "${srv_info}" | jq -r -c '.status')
done
echo "${srv_info}"
}
function CreateInstance() {
if [ -z "$INPUT" ];then
echo "expected build params in stdin"
exit 1
fi
CC_FILE=$(getCloudConfig)
FLAVOR=$(flavor)
IMAGE=$(image)
INSTANCE_NAME=$(instanceName)
NET=$(getOpenStackNetworkID)
IMAGE_DETAILS=$(getOSImageDetails)
OS_TYPE=$(echo "${IMAGE_DETAILS}" | jq -c -r '.properties.os_type')
checkValNotNull "${OS_TYPE}" "os_type" || return $?
DISTRO=$(echo "${IMAGE_DETAILS}" | jq -c -r '.properties.os_distro')
checkValNotNull "${OS_TYPE}" "os_distro" || return $?
VERSION=$(echo "${IMAGE_DETAILS}" | jq -c -r '.properties.os_version')
checkValNotNull "${VERSION}" "os_version" || return $?
ARCH=$(echo "${IMAGE_DETAILS}" | jq -c -r '.properties.architecture')
checkValNotNull "${ARCH}" "architecture" || return $?
SOURCE_ARGS=""
if [ "${BOOT_FROM_VOLUME}" -eq 1 ];then
VOL_SIZE=$(getVolumeSizeFromFlavor "${FLAVOR}")
VOL_INFO=$(createVolumeFromImage "${IMAGE}" "${VOL_SIZE}" "${INSTANCE_NAME}")
if [ $? -ne 0 ];then
openstack volume delete "${INSTANCE_NAME}" || true
fi
SOURCE_ARGS="--volume ${INSTANCE_NAME}"
else
SOURCE_ARGS="--image ${IMAGE}"
fi
set +e
SRV_DETAILS=$(openstack server create ${SOURCE_ARGS} --flavor "${FLAVOR}" --user-data="${CC_FILE}" --network="${NET}" "${INSTANCE_NAME}")
if [ $? -ne 0 ];then
openstack volume delete "${INSTANCE_NAME}" || true
exit 1
fi
SRV_DETAILS=$(waitForServer "${INSTANCE_NAME}")
if [ $? -ne 0 ];then
CODE="$?"
# cleanup
rm -f "${CC_FILE}" || true
openstack server delete "${INSTANCE_NAME}" || true
openstack volume delete "${INSTANCE_NAME}" || true
set -e
return $CODE
fi
set -e
rm -f "${CC_FILE}" || true
SRV_ID=$(echo "${SRV_DETAILS}" | jq -r -c '.id')
STATUS=$(echo "${SRV_DETAILS}" | jq -r -c '.status')
jq -rnc \
--arg PROVIDER_ID ${SRV_ID} \
--arg NAME "${INSTANCE_NAME}" \
--arg OS_TYPE "${OS_TYPE}" \
--arg OS_NAME "${DISTRO}" \
--arg OS_VERSION "${VERSION}" \
--arg ARCH "${ARCH}" \
--arg STATUS "${STATUS}" \
'{"id": "", "provider_id": $PROVIDER_ID, "name": $NAME, "os_type": $OS_TYPE, "os_name": $OS_NAME, "os_version": $OS_VERSION, "os_arch": $ARCH, "status": $STATUS, "runner_status": "", "pool_id": ""}'
}
function DeleteInstance() {
local instance_id="${GARM_INSTANCE_ID}"
if [ -z "${instance_id}" ];then
echo "missing instance ID in env"
return 1
fi
instance_info=$(openstack server show "${instance_id}" -f json)
VOLUMES=$(echo "${instance_info}" | jq -r -c '.volumes_attached[] | .id')
openstack server delete "${instance_id}"
for vol in "$VOLUMES";do
waitForVolume "${vol}"
openstack volume delete $vol || true
done
}
function StartInstance() {
local instance_id="${GARM_INSTANCE_ID}"
if [ -z "${instance_id}" ];then
echo "missing instance ID in env"
return 1
fi
openstack server start "${instance_id}"
}
function StopServer() {
local instance_id="${GARM_INSTANCE_ID}"
if [ -z "${instance_id}" ];then
echo "missing instance ID in env"
return 1
fi
openstack server stop "${instance_id}"
}
case "$GARM_COMMAND" in
"CreateInstance")
CreateInstance
;;
"DeleteInstance")
DeleteInstance
;;
"GetInstance")
echo "GetInstance not implemented"
exit 1
;;
"ListInstances")
echo "ListInstances not implemented"
exit 1
;;
"StartInstance")
StartInstance
;;
"StopInstance")
StopServer
;;
"RemoveAllInstances")
echo "RemoveAllInstances not implemented"
exit 1
;;
*)
echo "Invalid GARM provider command: \"$GARM_COMMAND\""
exit 1
;;
esac

View file

@ -0,0 +1,16 @@
# OpenStack client config
export OS_REGION_NAME=RegionOne
export OS_AUTH_VERSION=3
export OS_AUTH_URL=http://10.0.8.36:5000/v3
export OS_PROJECT_DOMAIN_NAME=admin_domain
export OS_USERNAME=admin
export OS_AUTH_TYPE=password
export OS_USER_DOMAIN_NAME=admin_domain
export OS_PROJECT_NAME=admin
export OS_PASSWORD=Iegeehahth4suSie
export OS_IDENTITY_API_VERSION=3
# GARM config
export OPENSTACK_PRIVATE_NETWORK="int_net"
export BOOT_FROM_VOLUME=1