From b2dcdc21dc8c540f9e4214992c1d13fff83a7a6c Mon Sep 17 00:00:00 2001 From: Bryan MacFarlane Date: Fri, 17 Apr 2020 11:08:45 -0400 Subject: [PATCH] Sample scripts to automate scaleable runners (#427) --- docs/automate.md | 57 +++++++++++++++ scripts/create-latest-svc.sh | 135 +++++++++++++++++++++++++++++++++++ scripts/delete.sh | 83 +++++++++++++++++++++ scripts/remove-svc.sh | 76 ++++++++++++++++++++ 4 files changed, 351 insertions(+) create mode 100644 docs/automate.md create mode 100755 scripts/create-latest-svc.sh create mode 100755 scripts/delete.sh create mode 100755 scripts/remove-svc.sh diff --git a/docs/automate.md b/docs/automate.md new file mode 100644 index 000000000..11a87a3ec --- /dev/null +++ b/docs/automate.md @@ -0,0 +1,57 @@ +# Automate Configuring Self-Hosted Runners + + +## Export PAT + +Before running any of these sample scripts, create a GitHub PAT and export it before running the script + +```bash +export RUNNER_CFG_PAT=yourPAT +``` + +## Create running as a service + +**Scenario**: Run on a machine or VM (not container) which automates: + + - Resolving latest released runner + - Download and extract latest + - Acquire a registration token + - Configure the runner + - Run as a systemd (linux) or Launchd (osx) service + +:point_right: [Sample script here](../scripts/create-latest-svc.sh) :point_left: + +Run as a one-liner. NOTE: replace with yourorg/yourrepo (repo level) or just yourorg (org level) +```bash +curl -s https://raw.githubusercontent.com/actions/runner/automate/scripts/create-latest-svc.sh | bash -s yourorg/yourrepo +``` + +## Uninstall running as service + +**Scenario**: Run on a machine or VM (not container) which automates: + + - Stops and uninstalls the systemd (linux) or Launchd (osx) service + - Acquires a removal token + - Removes the runner + +:point_right: [Sample script here](../scripts/remove-svc.sh) :point_left: + +Repo level one liner. NOTE: replace with yourorg/yourrepo (repo level) or just yourorg (org level) +```bash +curl -s https://raw.githubusercontent.com/actions/runner/automate/scripts/remove-svc.sh | bash -s yourorg/yourrepo +``` + +### Delete an offline runner + +**Scenario**: Deletes a registered runner that is offline: + + - Ensures the runner is offline + - Resolves id from name + - Deletes the runner + +:point_right: [Sample script here](../scripts/delete.sh) :point_left: + +Repo level one-liner. NOTE: replace with yourorg/yourrepo (repo level) or just yourorg (org level) and replace runnername +```bash +curl -s https://raw.githubusercontent.com/actions/runner/automate/scripts/delete.sh | bash -s yourorg/yourrepo runnername +``` diff --git a/scripts/create-latest-svc.sh b/scripts/create-latest-svc.sh new file mode 100755 index 000000000..f056c51ad --- /dev/null +++ b/scripts/create-latest-svc.sh @@ -0,0 +1,135 @@ +#/bin/bash + +set -e + +# +# Downloads latest releases (not pre-release) runner +# Configures as a service +# +# Examples: +# RUNNER_CFG_PAT= ./create-latest-svc.sh myuser/myrepo +# RUNNER_CFG_PAT= ./create-latest-svc.sh myorg +# +# Usage: +# export RUNNER_CFG_PAT= +# ./create-latest-svc scope [name] [user] +# +# scope required repo (:owner/:repo) or org (:organization) +# name optional defaults to hostname +# user optional user svc will run as. defaults to current +# +# Notes: +# PATS over envvars are more secure +# Should be used on VMs and not containers +# Works on OSX and Linux +# Assumes x64 arch +# + +runner_scope=${1} +runner_name=${2:-$(hostname)} +svc_user=${3:-$USER} + +echo "Configuring runner @ ${runner_scope}" +sudo echo + +#--------------------------------------- +# Validate Environment +#--------------------------------------- +runner_plat=linux +[ ! -z "$(which sw_vers)" ] && runner_plat=osx; + +function fatal() +{ + echo "error: $1" >&2 + exit 1 +} + +if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi +if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi + +which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc" +which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc" + +# bail early if there's already a runner there. also sudo early +if [ -d ./runner ]; then + fatal "Runner already exists. Use a different directory or delete ./runner" +fi + +sudo -u ${svc_user} mkdir runner + +# TODO: validate not in a container +# TODO: validate systemd or osx svc installer + +#-------------------------------------- +# Get a config token +#-------------------------------------- +echo +echo "Generating a registration token..." + +# if the scope has a slash, it's an repo runner +base_api_url="https://api.github.com/orgs" +if [[ "$runner_scope" == *\/* ]]; then + base_api_url="https://api.github.com/repos" +fi + +export RUNNER_TOKEN=$(curl -s -X POST ${base_api_url}/${runner_scope}/actions/runners/registration-token -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" | jq -r '.token') + +if [ -z "$RUNNER_TOKEN" ]; then fatal "Failed to get a token"; fi + +#--------------------------------------- +# Download latest released and extract +#--------------------------------------- +echo +echo "Downloading latest runner ..." + +latest_version_label=$(curl -s -X GET 'https://api.github.com/repos/actions/runner/releases/latest' | jq -r '.tag_name') +latest_version=$(echo ${latest_version_label:1}) +runner_file="actions-runner-${runner_plat}-x64-${latest_version}.tar.gz" + +if [ -f "${runner_file}" ]; then + echo "${runner_file} exists. skipping download." +else + runner_url="https://github.com/actions/runner/releases/download/${latest_version_label}/${runner_file}" + + echo "Downloading ${latest_version_label} for ${runner_plat} ..." + echo $runner_url + + curl -O -L ${runner_url} +fi + +ls -la *.tar.gz + +#--------------------------------------------------- +# extract to runner directory in this directory +#--------------------------------------------------- +echo +echo "Extracting ${runner_file} to ./runner" + +tar xzf "./${runner_file}" -C runner + +# export of pass +sudo chown -R $svc_user ./runner + +pushd ./runner + +#--------------------------------------- +# Unattend config +#--------------------------------------- +runner_url="https://github.com/${runner_scope}" +echo +echo "Configuring ${runner_name} @ $runner_url" +echo "./config.sh --unattended --url $runner_url --token *** --name $runner_name" +sudo -E -u ${svc_user} ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN --name $runner_name + +#--------------------------------------- +# Configuring as a service +#--------------------------------------- +echo +echo "Configuring as a service ..." +prefix="" +if [ "${runner_plat}" == "linux" ]; then + prefix="sudo " +fi + +${prefix}./svc.sh install ${svc_user} +${prefix}./svc.sh start diff --git a/scripts/delete.sh b/scripts/delete.sh new file mode 100755 index 000000000..96cf3a61e --- /dev/null +++ b/scripts/delete.sh @@ -0,0 +1,83 @@ +#/bin/bash + +set -e + +# +# Force deletes a runner from the service +# The caller should have already ensured the runner is gone and/or stopped +# +# Examples: +# RUNNER_CFG_PAT= ./delete.sh myuser/myrepo myname +# RUNNER_CFG_PAT= ./delete.sh myorg +# +# Usage: +# export RUNNER_CFG_PAT= +# ./delete.sh scope name +# +# scope required repo (:owner/:repo) or org (:organization) +# name optional defaults to hostname. name to delete +# +# Notes: +# PATS over envvars are more secure +# Works on OSX and Linux +# Assumes x64 arch +# + +runner_scope=${1} +runner_name=${2} + +echo "Deleting runner ${runner_name} @ ${runner_scope}" + +function fatal() +{ + echo "error: $1" >&2 + exit 1 +} + +if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi +if [ -z "${runner_name}" ]; then fatal "supply name as argument 2"; fi +if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi + +which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc" +which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc" + +base_api_url="https://api.github.com/orgs" +if [[ "$runner_scope" == *\/* ]]; then + base_api_url="https://api.github.com/repos" +fi + + +#-------------------------------------- +# Ensure offline +#-------------------------------------- +runner_status=$(curl -s -X GET ${base_api_url}/${runner_scope}/actions/runners?per_page=100 -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" \ + | jq -M -j ".runners | .[] | [select(.name == \"${runner_name}\")] | .[0].status") + +if [ -z "${runner_status}" ]; then + fatal "Could not find runner with name ${runner_name}" +fi + +echo "Status: ${runner_status}" + +if [ "${runner_status}" != "offline" ]; then + fatal "Runner should be offline before removing" +fi + +#-------------------------------------- +# Get id of runner to remove +#-------------------------------------- +runner_id=$(curl -s -X GET ${base_api_url}/${runner_scope}/actions/runners?per_page=100 -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" \ + | jq -M -j ".runners | .[] | [select(.name == \"${runner_name}\")] | .[0].id") + +if [ -z "${runner_id}" ]; then + fatal "Could not find runner with name ${runner_name}" +fi + +echo "Removing id ${runner_id}" + +#-------------------------------------- +# Remove the runner +#-------------------------------------- +curl -s -X DELETE ${base_api_url}/${runner_scope}/actions/runners/${runner_id} -H "authorization: token ${RUNNER_CFG_PAT}" + +echo "Done." diff --git a/scripts/remove-svc.sh b/scripts/remove-svc.sh new file mode 100755 index 000000000..c55d0075d --- /dev/null +++ b/scripts/remove-svc.sh @@ -0,0 +1,76 @@ +#/bin/bash + +set -e + +# +# Removes a runner running as a service +# Must be run on the machine where the service is run +# +# Examples: +# RUNNER_CFG_PAT= ./remove-svc.sh myuser/myrepo +# RUNNER_CFG_PAT= ./remove-svc.sh myorg +# +# Usage: +# export RUNNER_CFG_PAT= +# ./remove-svc scope name +# +# scope required repo (:owner/:repo) or org (:organization) +# name optional defaults to hostname. name to uninstall and remove +# +# Notes: +# PATS over envvars are more secure +# Should be used on VMs and not containers +# Works on OSX and Linux +# Assumes x64 arch +# + +runner_scope=${1} +runner_name=${2:-$(hostname)} + +echo "Uninstalling runner ${runner_name} @ ${runner_scope}" +sudo echo + +function fatal() +{ + echo "error: $1" >&2 + exit 1 +} + +if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi +if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi + +which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc" +which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc" + +runner_plat=linux +[ ! -z "$(which sw_vers)" ] && runner_plat=osx; + +#-------------------------------------- +# Get a remove token +#-------------------------------------- +echo +echo "Generating a removal token..." + +# if the scope has a slash, it's an repo runner +base_api_url="https://api.github.com/orgs" +if [[ "$runner_scope" == *\/* ]]; then + base_api_url="https://api.github.com/repos" +fi + +export REMOVE_TOKEN=$(curl -s -X POST ${base_api_url}/${runner_scope}/actions/runners/remove-token -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" | jq -r '.token') + +if [ -z "$REMOVE_TOKEN" ]; then fatal "Failed to get a token"; fi + +#--------------------------------------- +# Stop and uninstall the service +#--------------------------------------- +echo +echo "Uninstall the service ..." +pushd ./runner +prefix="" +if [ "${runner_plat}" == "linux" ]; then + prefix="sudo " +fi +${prefix}./svc.sh stop +${prefix}./svc.sh uninstall +${prefix}./config.sh remove --token $REMOVE_TOKEN