diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e4acdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.terraform +.credentials +.credentials_rsaparams +.env +.path +.runner +.terraform.lock.hcl +**/externals +**/bin +**/_diag diff --git a/aws-image-pipeline/config.sh b/aws-image-pipeline/config.sh new file mode 100755 index 0000000..14cc6ba --- /dev/null +++ b/aws-image-pipeline/config.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +user_id=`id -u` + +# we want to snapshot the environment of the config user +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run with sudo" + exit 1 +fi + +# Check dotnet Core 6.0 dependencies for Linux +if [[ (`uname` == "Linux") ]] +then + command -v ldd > /dev/null + if [ $? -ne 0 ] + then + echo "Can not find 'ldd'. Please install 'ldd' and try again." + exit 1 + fi + + message="Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies." + + ldd ./bin/libcoreclr.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.Security.Cryptography.Native.OpenSsl.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.IO.Compression.Native.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + if ! [ -x "$(command -v ldconfig)" ]; then + LDCONFIG_COMMAND="/sbin/ldconfig" + if ! [ -x "$LDCONFIG_COMMAND" ]; then + echo "Can not find 'ldconfig' in PATH and '/sbin/ldconfig' doesn't exists either. Please install 'ldconfig' and try again." + exit 1 + fi + else + LDCONFIG_COMMAND="ldconfig" + fi + + libpath=${LD_LIBRARY_PATH:-} + $LDCONFIG_COMMAND -NXv ${libpath//:/ } 2>&1 | grep libicu >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Libicu's dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi +fi + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" +cd "$DIR" + +source ./env.sh + +shopt -s nocasematch +if [[ "$1" == "remove" ]]; then + ./bin/Runner.Listener "$@" +else + ./bin/Runner.Listener configure "$@" +fi diff --git a/aws-image-pipeline/env.sh b/aws-image-pipeline/env.sh new file mode 100755 index 0000000..641d244 --- /dev/null +++ b/aws-image-pipeline/env.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +varCheckList=( + 'LANG' + 'JAVA_HOME' + 'ANT_HOME' + 'M2_HOME' + 'ANDROID_HOME' + 'ANDROID_SDK_ROOT' + 'GRADLE_HOME' + 'NVM_BIN' + 'NVM_PATH' + 'LD_LIBRARY_PATH' + 'PERL5LIB' + ) + +envContents="" + +if [ -f ".env" ]; then + envContents=`cat .env` +else + touch .env +fi + +function writeVar() +{ + checkVar="$1" + checkDelim="${1}=" + if test "${envContents#*$checkDelim}" = "$envContents" + then + if [ ! -z "${!checkVar}" ]; then + echo "${checkVar}=${!checkVar}">>.env + fi + fi +} + +echo $PATH>.path + +for var_name in ${varCheckList[@]} +do + writeVar "${var_name}" +done diff --git a/aws-image-pipeline/run-helper.cmd.template b/aws-image-pipeline/run-helper.cmd.template new file mode 100644 index 0000000..23e4246 --- /dev/null +++ b/aws-image-pipeline/run-helper.cmd.template @@ -0,0 +1,53 @@ +@echo off +SET UPDATEFILE=update.finished +"%~dp0\bin\Runner.Listener.exe" run %* + +rem using `if %ERRORLEVEL% EQU N` insterad of `if ERRORLEVEL N` +rem `if ERRORLEVEL N` means: error level is N or MORE + +if %ERRORLEVEL% EQU 0 ( + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 1 ( + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 2 ( + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + ping 127.0.0.1 -n 6 -w 1000 >NUL + exit /b 1 +) + +if %ERRORLEVEL% EQU 3 ( + rem Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +if %ERRORLEVEL% EQU 4 ( + rem Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +echo "Exiting after unknown error code: %ERRORLEVEL%" +exit /b 0 \ No newline at end of file diff --git a/aws-image-pipeline/run-helper.sh b/aws-image-pipeline/run-helper.sh new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/aws-image-pipeline/run-helper.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/aws-image-pipeline/run-helper.sh.template b/aws-image-pipeline/run-helper.sh.template new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/aws-image-pipeline/run-helper.sh.template @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/aws-image-pipeline/run.sh b/aws-image-pipeline/run.sh new file mode 100755 index 0000000..6b02ea1 --- /dev/null +++ b/aws-image-pipeline/run.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +run() { + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + exit 0 + fi + done +} + +runWithManualTrap() { + # Set job control + set -m + + trap 'kill -INT -$PID' INT TERM + + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* & + PID=$! + wait -f $PID + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + # Unregister signal handling before exit + trap - INT TERM + # wait for last parts to be logged + wait $PID + exit $returnCode + fi + done +} + +function updateCerts() { + local sudo_prefix="" + local user_id=`id -u` + + if [ $user_id -ne 0 ]; then + if [[ ! -x "$(command -v sudo)" ]]; then + echo "Warning: failed to update certificate store: sudo is required but not found" + return 1 + else + sudo_prefix="sudo" + fi + fi + + if [[ -x "$(command -v update-ca-certificates)" ]]; then + eval $sudo_prefix "update-ca-certificates" + elif [[ -x "$(command -v update-ca-trust)" ]]; then + eval $sudo_prefix "update-ca-trust" + else + echo "Warning: failed to update certificate store: update-ca-certificates or update-ca-trust not found. This can happen if you're using a different runner base image." + return 1 + fi +} + +if [[ ! -z "$RUNNER_UPDATE_CA_CERTS" ]]; then + updateCerts +fi + +if [[ -z "$RUNNER_MANUALLY_TRAP_SIG" ]]; then + run $* +else + runWithManualTrap $* +fi \ No newline at end of file diff --git a/aws-image-pipeline/safe_sleep.sh b/aws-image-pipeline/safe_sleep.sh new file mode 100755 index 0000000..7ba5be3 --- /dev/null +++ b/aws-image-pipeline/safe_sleep.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +SECONDS=0 +while [[ $SECONDS != $1 ]]; do + : +done diff --git a/aws-image-pipeline/svc.sh b/aws-image-pipeline/svc.sh new file mode 100755 index 0000000..bf5c2d4 --- /dev/null +++ b/aws-image-pipeline/svc.sh @@ -0,0 +1,179 @@ +#!/bin/bash + +SVC_NAME="actions.runner._services.aws-image-pipeline.service" +SVC_NAME=${SVC_NAME// /_} +SVC_DESCRIPTION="GitHub Actions Runner (_services.aws-image-pipeline)" + +SVC_CMD=$1 +arg_2=${2} + +RUNNER_ROOT=`pwd` + +UNIT_PATH=/etc/systemd/system/${SVC_NAME} +TEMPLATE_PATH=$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE +IS_CUSTOM_TEMPLATE=0 +if [[ -z $TEMPLATE_PATH ]]; then + TEMPLATE_PATH=./bin/actions.runner.service.template +else + IS_CUSTOM_TEMPLATE=1 +fi +TEMP_PATH=./bin/actions.runner.service.temp +CONFIG_PATH=.service + +user_id=`id -u` + +# systemctl must run as sudo +# this script is a convenience wrapper around systemctl +if [ $user_id -ne 0 ]; then + echo "Must run as sudo" + exit 1 +fi + +function failed() +{ + local error=${1:-Undefined error} + echo "Failed: $error" >&2 + exit 1 +} + +if [ ! -f "${TEMPLATE_PATH}" ]; then + if [[ $IS_CUSTOM_TEMPLATE = 0 ]]; then + failed "Must run from runner root or install is corrupt" + else + failed "Service file at '$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE' using GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE env variable is not found" + fi +fi + +#check if we run as root +if [[ $(id -u) != "0" ]]; then + echo "Failed: This script requires to run with sudo." >&2 + exit 1 +fi + +function install() +{ + echo "Creating launch runner in ${UNIT_PATH}" + if [ -f "${UNIT_PATH}" ]; then + failed "error: exists ${UNIT_PATH}" + fi + + if [ -f "${TEMP_PATH}" ]; then + rm "${TEMP_PATH}" || failed "failed to delete ${TEMP_PATH}" + fi + + # can optionally use username supplied + run_as_user=${arg_2:-$SUDO_USER} + echo "Run as user: ${run_as_user}" + + run_as_uid=$(id -u ${run_as_user}) || failed "User does not exist" + echo "Run as uid: ${run_as_uid}" + + run_as_gid=$(id -g ${run_as_user}) || failed "Group not available" + echo "gid: ${run_as_gid}" + + sed "s/{{User}}/${run_as_user}/g; s/{{Description}}/$(echo ${SVC_DESCRIPTION} | sed -e 's/[\/&]/\\&/g')/g; s/{{RunnerRoot}}/$(echo ${RUNNER_ROOT} | sed -e 's/[\/&]/\\&/g')/g;" "${TEMPLATE_PATH}" > "${TEMP_PATH}" || failed "failed to create replacement temp file" + mv "${TEMP_PATH}" "${UNIT_PATH}" || failed "failed to copy unit file" + + # Recent Fedora based Linux (CentOS/Redhat) has SELinux enabled by default + # We need to restore security context on the unit file we added otherwise SystemD have no access to it. + command -v getenforce > /dev/null + if [ $? -eq 0 ] + then + selinuxEnabled=$(getenforce) + if [[ $selinuxEnabled == "Enforcing" ]] + then + # SELinux is enabled, we will need to Restore SELinux Context for the service file + restorecon -r -v "${UNIT_PATH}" || failed "failed to restore SELinux context on ${UNIT_PATH}" + fi + fi + + # unit file should not be executable and world writable + chmod 664 "${UNIT_PATH}" || failed "failed to set permissions on ${UNIT_PATH}" + systemctl daemon-reload || failed "failed to reload daemons" + + # Since we started with sudo, runsvc.sh will be owned by root. Change this to current login user. + cp ./bin/runsvc.sh ./runsvc.sh || failed "failed to copy runsvc.sh" + chown ${run_as_uid}:${run_as_gid} ./runsvc.sh || failed "failed to set owner for runsvc.sh" + chmod 755 ./runsvc.sh || failed "failed to set permission for runsvc.sh" + + systemctl enable ${SVC_NAME} || failed "failed to enable ${SVC_NAME}" + + echo "${SVC_NAME}" > ${CONFIG_PATH} || failed "failed to create .service file" + chown ${run_as_uid}:${run_as_gid} ${CONFIG_PATH} || failed "failed to set permission for ${CONFIG_PATH}" +} + +function start() +{ + systemctl start ${SVC_NAME} || failed "failed to start ${SVC_NAME}" + status +} + +function stop() +{ + systemctl stop ${SVC_NAME} || failed "failed to stop ${SVC_NAME}" + status +} + +function uninstall() +{ + if service_exists; then + stop + systemctl disable ${SVC_NAME} || failed "failed to disable ${SVC_NAME}" + rm "${UNIT_PATH}" || failed "failed to delete ${UNIT_PATH}" + else + echo "Service ${SVC_NAME} is not installed" + fi + if [ -f "${CONFIG_PATH}" ]; then + rm "${CONFIG_PATH}" || failed "failed to delete ${CONFIG_PATH}" + fi + systemctl daemon-reload || failed "failed to reload daemons" +} + +function service_exists() { + if [ -f "${UNIT_PATH}" ]; then + return 0 + else + return 1 + fi +} + +function status() +{ + if service_exists; then + echo + echo "${UNIT_PATH}" + else + echo + echo "not installed" + echo + exit 1 + fi + + systemctl --no-pager status ${SVC_NAME} +} + +function usage() +{ + echo + echo Usage: + echo "./svc.sh [install, start, stop, status, uninstall]" + echo "Commands:" + echo " install [user]: Install runner service as Root or specified user." + echo " start: Manually start the runner service." + echo " stop: Manually stop the runner service." + echo " status: Display status of runner service." + echo " uninstall: Uninstall runner service." + echo +} + +case $SVC_CMD in + "install") install;; + "status") status;; + "uninstall") uninstall;; + "start") start;; + "stop") stop;; + "status") status;; + *) usage;; +esac + +exit 0 diff --git a/image-pipeline-ansible-playbooks/config.sh b/image-pipeline-ansible-playbooks/config.sh new file mode 100755 index 0000000..14cc6ba --- /dev/null +++ b/image-pipeline-ansible-playbooks/config.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +user_id=`id -u` + +# we want to snapshot the environment of the config user +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run with sudo" + exit 1 +fi + +# Check dotnet Core 6.0 dependencies for Linux +if [[ (`uname` == "Linux") ]] +then + command -v ldd > /dev/null + if [ $? -ne 0 ] + then + echo "Can not find 'ldd'. Please install 'ldd' and try again." + exit 1 + fi + + message="Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies." + + ldd ./bin/libcoreclr.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.Security.Cryptography.Native.OpenSsl.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.IO.Compression.Native.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + if ! [ -x "$(command -v ldconfig)" ]; then + LDCONFIG_COMMAND="/sbin/ldconfig" + if ! [ -x "$LDCONFIG_COMMAND" ]; then + echo "Can not find 'ldconfig' in PATH and '/sbin/ldconfig' doesn't exists either. Please install 'ldconfig' and try again." + exit 1 + fi + else + LDCONFIG_COMMAND="ldconfig" + fi + + libpath=${LD_LIBRARY_PATH:-} + $LDCONFIG_COMMAND -NXv ${libpath//:/ } 2>&1 | grep libicu >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Libicu's dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi +fi + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" +cd "$DIR" + +source ./env.sh + +shopt -s nocasematch +if [[ "$1" == "remove" ]]; then + ./bin/Runner.Listener "$@" +else + ./bin/Runner.Listener configure "$@" +fi diff --git a/image-pipeline-ansible-playbooks/env.sh b/image-pipeline-ansible-playbooks/env.sh new file mode 100755 index 0000000..641d244 --- /dev/null +++ b/image-pipeline-ansible-playbooks/env.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +varCheckList=( + 'LANG' + 'JAVA_HOME' + 'ANT_HOME' + 'M2_HOME' + 'ANDROID_HOME' + 'ANDROID_SDK_ROOT' + 'GRADLE_HOME' + 'NVM_BIN' + 'NVM_PATH' + 'LD_LIBRARY_PATH' + 'PERL5LIB' + ) + +envContents="" + +if [ -f ".env" ]; then + envContents=`cat .env` +else + touch .env +fi + +function writeVar() +{ + checkVar="$1" + checkDelim="${1}=" + if test "${envContents#*$checkDelim}" = "$envContents" + then + if [ ! -z "${!checkVar}" ]; then + echo "${checkVar}=${!checkVar}">>.env + fi + fi +} + +echo $PATH>.path + +for var_name in ${varCheckList[@]} +do + writeVar "${var_name}" +done diff --git a/image-pipeline-ansible-playbooks/run-helper.cmd.template b/image-pipeline-ansible-playbooks/run-helper.cmd.template new file mode 100644 index 0000000..23e4246 --- /dev/null +++ b/image-pipeline-ansible-playbooks/run-helper.cmd.template @@ -0,0 +1,53 @@ +@echo off +SET UPDATEFILE=update.finished +"%~dp0\bin\Runner.Listener.exe" run %* + +rem using `if %ERRORLEVEL% EQU N` insterad of `if ERRORLEVEL N` +rem `if ERRORLEVEL N` means: error level is N or MORE + +if %ERRORLEVEL% EQU 0 ( + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 1 ( + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 2 ( + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + ping 127.0.0.1 -n 6 -w 1000 >NUL + exit /b 1 +) + +if %ERRORLEVEL% EQU 3 ( + rem Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +if %ERRORLEVEL% EQU 4 ( + rem Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +echo "Exiting after unknown error code: %ERRORLEVEL%" +exit /b 0 \ No newline at end of file diff --git a/image-pipeline-ansible-playbooks/run-helper.sh b/image-pipeline-ansible-playbooks/run-helper.sh new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/image-pipeline-ansible-playbooks/run-helper.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/image-pipeline-ansible-playbooks/run-helper.sh.template b/image-pipeline-ansible-playbooks/run-helper.sh.template new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/image-pipeline-ansible-playbooks/run-helper.sh.template @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/image-pipeline-ansible-playbooks/run.sh b/image-pipeline-ansible-playbooks/run.sh new file mode 100755 index 0000000..6b02ea1 --- /dev/null +++ b/image-pipeline-ansible-playbooks/run.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +run() { + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + exit 0 + fi + done +} + +runWithManualTrap() { + # Set job control + set -m + + trap 'kill -INT -$PID' INT TERM + + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* & + PID=$! + wait -f $PID + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + # Unregister signal handling before exit + trap - INT TERM + # wait for last parts to be logged + wait $PID + exit $returnCode + fi + done +} + +function updateCerts() { + local sudo_prefix="" + local user_id=`id -u` + + if [ $user_id -ne 0 ]; then + if [[ ! -x "$(command -v sudo)" ]]; then + echo "Warning: failed to update certificate store: sudo is required but not found" + return 1 + else + sudo_prefix="sudo" + fi + fi + + if [[ -x "$(command -v update-ca-certificates)" ]]; then + eval $sudo_prefix "update-ca-certificates" + elif [[ -x "$(command -v update-ca-trust)" ]]; then + eval $sudo_prefix "update-ca-trust" + else + echo "Warning: failed to update certificate store: update-ca-certificates or update-ca-trust not found. This can happen if you're using a different runner base image." + return 1 + fi +} + +if [[ ! -z "$RUNNER_UPDATE_CA_CERTS" ]]; then + updateCerts +fi + +if [[ -z "$RUNNER_MANUALLY_TRAP_SIG" ]]; then + run $* +else + runWithManualTrap $* +fi \ No newline at end of file diff --git a/image-pipeline-ansible-playbooks/safe_sleep.sh b/image-pipeline-ansible-playbooks/safe_sleep.sh new file mode 100755 index 0000000..7ba5be3 --- /dev/null +++ b/image-pipeline-ansible-playbooks/safe_sleep.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +SECONDS=0 +while [[ $SECONDS != $1 ]]; do + : +done diff --git a/image-pipeline-ansible-playbooks/svc.sh b/image-pipeline-ansible-playbooks/svc.sh new file mode 100755 index 0000000..2b1ef58 --- /dev/null +++ b/image-pipeline-ansible-playbooks/svc.sh @@ -0,0 +1,179 @@ +#!/bin/bash + +SVC_NAME="actions.runner._services.image-pipeline-ansible-playbooks.service" +SVC_NAME=${SVC_NAME// /_} +SVC_DESCRIPTION="GitHub Actions Runner (_services.image-pipeline-ansible-playbooks)" + +SVC_CMD=$1 +arg_2=${2} + +RUNNER_ROOT=`pwd` + +UNIT_PATH=/etc/systemd/system/${SVC_NAME} +TEMPLATE_PATH=$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE +IS_CUSTOM_TEMPLATE=0 +if [[ -z $TEMPLATE_PATH ]]; then + TEMPLATE_PATH=./bin/actions.runner.service.template +else + IS_CUSTOM_TEMPLATE=1 +fi +TEMP_PATH=./bin/actions.runner.service.temp +CONFIG_PATH=.service + +user_id=`id -u` + +# systemctl must run as sudo +# this script is a convenience wrapper around systemctl +if [ $user_id -ne 0 ]; then + echo "Must run as sudo" + exit 1 +fi + +function failed() +{ + local error=${1:-Undefined error} + echo "Failed: $error" >&2 + exit 1 +} + +if [ ! -f "${TEMPLATE_PATH}" ]; then + if [[ $IS_CUSTOM_TEMPLATE = 0 ]]; then + failed "Must run from runner root or install is corrupt" + else + failed "Service file at '$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE' using GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE env variable is not found" + fi +fi + +#check if we run as root +if [[ $(id -u) != "0" ]]; then + echo "Failed: This script requires to run with sudo." >&2 + exit 1 +fi + +function install() +{ + echo "Creating launch runner in ${UNIT_PATH}" + if [ -f "${UNIT_PATH}" ]; then + failed "error: exists ${UNIT_PATH}" + fi + + if [ -f "${TEMP_PATH}" ]; then + rm "${TEMP_PATH}" || failed "failed to delete ${TEMP_PATH}" + fi + + # can optionally use username supplied + run_as_user=${arg_2:-$SUDO_USER} + echo "Run as user: ${run_as_user}" + + run_as_uid=$(id -u ${run_as_user}) || failed "User does not exist" + echo "Run as uid: ${run_as_uid}" + + run_as_gid=$(id -g ${run_as_user}) || failed "Group not available" + echo "gid: ${run_as_gid}" + + sed "s/{{User}}/${run_as_user}/g; s/{{Description}}/$(echo ${SVC_DESCRIPTION} | sed -e 's/[\/&]/\\&/g')/g; s/{{RunnerRoot}}/$(echo ${RUNNER_ROOT} | sed -e 's/[\/&]/\\&/g')/g;" "${TEMPLATE_PATH}" > "${TEMP_PATH}" || failed "failed to create replacement temp file" + mv "${TEMP_PATH}" "${UNIT_PATH}" || failed "failed to copy unit file" + + # Recent Fedora based Linux (CentOS/Redhat) has SELinux enabled by default + # We need to restore security context on the unit file we added otherwise SystemD have no access to it. + command -v getenforce > /dev/null + if [ $? -eq 0 ] + then + selinuxEnabled=$(getenforce) + if [[ $selinuxEnabled == "Enforcing" ]] + then + # SELinux is enabled, we will need to Restore SELinux Context for the service file + restorecon -r -v "${UNIT_PATH}" || failed "failed to restore SELinux context on ${UNIT_PATH}" + fi + fi + + # unit file should not be executable and world writable + chmod 664 "${UNIT_PATH}" || failed "failed to set permissions on ${UNIT_PATH}" + systemctl daemon-reload || failed "failed to reload daemons" + + # Since we started with sudo, runsvc.sh will be owned by root. Change this to current login user. + cp ./bin/runsvc.sh ./runsvc.sh || failed "failed to copy runsvc.sh" + chown ${run_as_uid}:${run_as_gid} ./runsvc.sh || failed "failed to set owner for runsvc.sh" + chmod 755 ./runsvc.sh || failed "failed to set permission for runsvc.sh" + + systemctl enable ${SVC_NAME} || failed "failed to enable ${SVC_NAME}" + + echo "${SVC_NAME}" > ${CONFIG_PATH} || failed "failed to create .service file" + chown ${run_as_uid}:${run_as_gid} ${CONFIG_PATH} || failed "failed to set permission for ${CONFIG_PATH}" +} + +function start() +{ + systemctl start ${SVC_NAME} || failed "failed to start ${SVC_NAME}" + status +} + +function stop() +{ + systemctl stop ${SVC_NAME} || failed "failed to stop ${SVC_NAME}" + status +} + +function uninstall() +{ + if service_exists; then + stop + systemctl disable ${SVC_NAME} || failed "failed to disable ${SVC_NAME}" + rm "${UNIT_PATH}" || failed "failed to delete ${UNIT_PATH}" + else + echo "Service ${SVC_NAME} is not installed" + fi + if [ -f "${CONFIG_PATH}" ]; then + rm "${CONFIG_PATH}" || failed "failed to delete ${CONFIG_PATH}" + fi + systemctl daemon-reload || failed "failed to reload daemons" +} + +function service_exists() { + if [ -f "${UNIT_PATH}" ]; then + return 0 + else + return 1 + fi +} + +function status() +{ + if service_exists; then + echo + echo "${UNIT_PATH}" + else + echo + echo "not installed" + echo + exit 1 + fi + + systemctl --no-pager status ${SVC_NAME} +} + +function usage() +{ + echo + echo Usage: + echo "./svc.sh [install, start, stop, status, uninstall]" + echo "Commands:" + echo " install [user]: Install runner service as Root or specified user." + echo " start: Manually start the runner service." + echo " stop: Manually stop the runner service." + echo " status: Display status of runner service." + echo " uninstall: Uninstall runner service." + echo +} + +case $SVC_CMD in + "install") install;; + "status") status;; + "uninstall") uninstall;; + "start") start;; + "stop") stop;; + "status") status;; + *) usage;; +esac + +exit 0 diff --git a/image-pipeline-goss-testing/config.sh b/image-pipeline-goss-testing/config.sh new file mode 100755 index 0000000..14cc6ba --- /dev/null +++ b/image-pipeline-goss-testing/config.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +user_id=`id -u` + +# we want to snapshot the environment of the config user +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run with sudo" + exit 1 +fi + +# Check dotnet Core 6.0 dependencies for Linux +if [[ (`uname` == "Linux") ]] +then + command -v ldd > /dev/null + if [ $? -ne 0 ] + then + echo "Can not find 'ldd'. Please install 'ldd' and try again." + exit 1 + fi + + message="Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies." + + ldd ./bin/libcoreclr.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.Security.Cryptography.Native.OpenSsl.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.IO.Compression.Native.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + if ! [ -x "$(command -v ldconfig)" ]; then + LDCONFIG_COMMAND="/sbin/ldconfig" + if ! [ -x "$LDCONFIG_COMMAND" ]; then + echo "Can not find 'ldconfig' in PATH and '/sbin/ldconfig' doesn't exists either. Please install 'ldconfig' and try again." + exit 1 + fi + else + LDCONFIG_COMMAND="ldconfig" + fi + + libpath=${LD_LIBRARY_PATH:-} + $LDCONFIG_COMMAND -NXv ${libpath//:/ } 2>&1 | grep libicu >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Libicu's dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi +fi + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" +cd "$DIR" + +source ./env.sh + +shopt -s nocasematch +if [[ "$1" == "remove" ]]; then + ./bin/Runner.Listener "$@" +else + ./bin/Runner.Listener configure "$@" +fi diff --git a/image-pipeline-goss-testing/env.sh b/image-pipeline-goss-testing/env.sh new file mode 100755 index 0000000..641d244 --- /dev/null +++ b/image-pipeline-goss-testing/env.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +varCheckList=( + 'LANG' + 'JAVA_HOME' + 'ANT_HOME' + 'M2_HOME' + 'ANDROID_HOME' + 'ANDROID_SDK_ROOT' + 'GRADLE_HOME' + 'NVM_BIN' + 'NVM_PATH' + 'LD_LIBRARY_PATH' + 'PERL5LIB' + ) + +envContents="" + +if [ -f ".env" ]; then + envContents=`cat .env` +else + touch .env +fi + +function writeVar() +{ + checkVar="$1" + checkDelim="${1}=" + if test "${envContents#*$checkDelim}" = "$envContents" + then + if [ ! -z "${!checkVar}" ]; then + echo "${checkVar}=${!checkVar}">>.env + fi + fi +} + +echo $PATH>.path + +for var_name in ${varCheckList[@]} +do + writeVar "${var_name}" +done diff --git a/image-pipeline-goss-testing/run-helper.cmd.template b/image-pipeline-goss-testing/run-helper.cmd.template new file mode 100644 index 0000000..23e4246 --- /dev/null +++ b/image-pipeline-goss-testing/run-helper.cmd.template @@ -0,0 +1,53 @@ +@echo off +SET UPDATEFILE=update.finished +"%~dp0\bin\Runner.Listener.exe" run %* + +rem using `if %ERRORLEVEL% EQU N` insterad of `if ERRORLEVEL N` +rem `if ERRORLEVEL N` means: error level is N or MORE + +if %ERRORLEVEL% EQU 0 ( + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 1 ( + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 2 ( + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + ping 127.0.0.1 -n 6 -w 1000 >NUL + exit /b 1 +) + +if %ERRORLEVEL% EQU 3 ( + rem Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +if %ERRORLEVEL% EQU 4 ( + rem Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +echo "Exiting after unknown error code: %ERRORLEVEL%" +exit /b 0 \ No newline at end of file diff --git a/image-pipeline-goss-testing/run-helper.sh b/image-pipeline-goss-testing/run-helper.sh new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/image-pipeline-goss-testing/run-helper.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/image-pipeline-goss-testing/run-helper.sh.template b/image-pipeline-goss-testing/run-helper.sh.template new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/image-pipeline-goss-testing/run-helper.sh.template @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/image-pipeline-goss-testing/run.sh b/image-pipeline-goss-testing/run.sh new file mode 100755 index 0000000..6b02ea1 --- /dev/null +++ b/image-pipeline-goss-testing/run.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +run() { + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + exit 0 + fi + done +} + +runWithManualTrap() { + # Set job control + set -m + + trap 'kill -INT -$PID' INT TERM + + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* & + PID=$! + wait -f $PID + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + # Unregister signal handling before exit + trap - INT TERM + # wait for last parts to be logged + wait $PID + exit $returnCode + fi + done +} + +function updateCerts() { + local sudo_prefix="" + local user_id=`id -u` + + if [ $user_id -ne 0 ]; then + if [[ ! -x "$(command -v sudo)" ]]; then + echo "Warning: failed to update certificate store: sudo is required but not found" + return 1 + else + sudo_prefix="sudo" + fi + fi + + if [[ -x "$(command -v update-ca-certificates)" ]]; then + eval $sudo_prefix "update-ca-certificates" + elif [[ -x "$(command -v update-ca-trust)" ]]; then + eval $sudo_prefix "update-ca-trust" + else + echo "Warning: failed to update certificate store: update-ca-certificates or update-ca-trust not found. This can happen if you're using a different runner base image." + return 1 + fi +} + +if [[ ! -z "$RUNNER_UPDATE_CA_CERTS" ]]; then + updateCerts +fi + +if [[ -z "$RUNNER_MANUALLY_TRAP_SIG" ]]; then + run $* +else + runWithManualTrap $* +fi \ No newline at end of file diff --git a/image-pipeline-goss-testing/safe_sleep.sh b/image-pipeline-goss-testing/safe_sleep.sh new file mode 100755 index 0000000..7ba5be3 --- /dev/null +++ b/image-pipeline-goss-testing/safe_sleep.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +SECONDS=0 +while [[ $SECONDS != $1 ]]; do + : +done diff --git a/image-pipeline-goss-testing/svc.sh b/image-pipeline-goss-testing/svc.sh new file mode 100755 index 0000000..d59eee2 --- /dev/null +++ b/image-pipeline-goss-testing/svc.sh @@ -0,0 +1,179 @@ +#!/bin/bash + +SVC_NAME="actions.runner._services.image-pipeline-goss-testing.service" +SVC_NAME=${SVC_NAME// /_} +SVC_DESCRIPTION="GitHub Actions Runner (_services.image-pipeline-goss-testing)" + +SVC_CMD=$1 +arg_2=${2} + +RUNNER_ROOT=`pwd` + +UNIT_PATH=/etc/systemd/system/${SVC_NAME} +TEMPLATE_PATH=$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE +IS_CUSTOM_TEMPLATE=0 +if [[ -z $TEMPLATE_PATH ]]; then + TEMPLATE_PATH=./bin/actions.runner.service.template +else + IS_CUSTOM_TEMPLATE=1 +fi +TEMP_PATH=./bin/actions.runner.service.temp +CONFIG_PATH=.service + +user_id=`id -u` + +# systemctl must run as sudo +# this script is a convenience wrapper around systemctl +if [ $user_id -ne 0 ]; then + echo "Must run as sudo" + exit 1 +fi + +function failed() +{ + local error=${1:-Undefined error} + echo "Failed: $error" >&2 + exit 1 +} + +if [ ! -f "${TEMPLATE_PATH}" ]; then + if [[ $IS_CUSTOM_TEMPLATE = 0 ]]; then + failed "Must run from runner root or install is corrupt" + else + failed "Service file at '$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE' using GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE env variable is not found" + fi +fi + +#check if we run as root +if [[ $(id -u) != "0" ]]; then + echo "Failed: This script requires to run with sudo." >&2 + exit 1 +fi + +function install() +{ + echo "Creating launch runner in ${UNIT_PATH}" + if [ -f "${UNIT_PATH}" ]; then + failed "error: exists ${UNIT_PATH}" + fi + + if [ -f "${TEMP_PATH}" ]; then + rm "${TEMP_PATH}" || failed "failed to delete ${TEMP_PATH}" + fi + + # can optionally use username supplied + run_as_user=${arg_2:-$SUDO_USER} + echo "Run as user: ${run_as_user}" + + run_as_uid=$(id -u ${run_as_user}) || failed "User does not exist" + echo "Run as uid: ${run_as_uid}" + + run_as_gid=$(id -g ${run_as_user}) || failed "Group not available" + echo "gid: ${run_as_gid}" + + sed "s/{{User}}/${run_as_user}/g; s/{{Description}}/$(echo ${SVC_DESCRIPTION} | sed -e 's/[\/&]/\\&/g')/g; s/{{RunnerRoot}}/$(echo ${RUNNER_ROOT} | sed -e 's/[\/&]/\\&/g')/g;" "${TEMPLATE_PATH}" > "${TEMP_PATH}" || failed "failed to create replacement temp file" + mv "${TEMP_PATH}" "${UNIT_PATH}" || failed "failed to copy unit file" + + # Recent Fedora based Linux (CentOS/Redhat) has SELinux enabled by default + # We need to restore security context on the unit file we added otherwise SystemD have no access to it. + command -v getenforce > /dev/null + if [ $? -eq 0 ] + then + selinuxEnabled=$(getenforce) + if [[ $selinuxEnabled == "Enforcing" ]] + then + # SELinux is enabled, we will need to Restore SELinux Context for the service file + restorecon -r -v "${UNIT_PATH}" || failed "failed to restore SELinux context on ${UNIT_PATH}" + fi + fi + + # unit file should not be executable and world writable + chmod 664 "${UNIT_PATH}" || failed "failed to set permissions on ${UNIT_PATH}" + systemctl daemon-reload || failed "failed to reload daemons" + + # Since we started with sudo, runsvc.sh will be owned by root. Change this to current login user. + cp ./bin/runsvc.sh ./runsvc.sh || failed "failed to copy runsvc.sh" + chown ${run_as_uid}:${run_as_gid} ./runsvc.sh || failed "failed to set owner for runsvc.sh" + chmod 755 ./runsvc.sh || failed "failed to set permission for runsvc.sh" + + systemctl enable ${SVC_NAME} || failed "failed to enable ${SVC_NAME}" + + echo "${SVC_NAME}" > ${CONFIG_PATH} || failed "failed to create .service file" + chown ${run_as_uid}:${run_as_gid} ${CONFIG_PATH} || failed "failed to set permission for ${CONFIG_PATH}" +} + +function start() +{ + systemctl start ${SVC_NAME} || failed "failed to start ${SVC_NAME}" + status +} + +function stop() +{ + systemctl stop ${SVC_NAME} || failed "failed to stop ${SVC_NAME}" + status +} + +function uninstall() +{ + if service_exists; then + stop + systemctl disable ${SVC_NAME} || failed "failed to disable ${SVC_NAME}" + rm "${UNIT_PATH}" || failed "failed to delete ${UNIT_PATH}" + else + echo "Service ${SVC_NAME} is not installed" + fi + if [ -f "${CONFIG_PATH}" ]; then + rm "${CONFIG_PATH}" || failed "failed to delete ${CONFIG_PATH}" + fi + systemctl daemon-reload || failed "failed to reload daemons" +} + +function service_exists() { + if [ -f "${UNIT_PATH}" ]; then + return 0 + else + return 1 + fi +} + +function status() +{ + if service_exists; then + echo + echo "${UNIT_PATH}" + else + echo + echo "not installed" + echo + exit 1 + fi + + systemctl --no-pager status ${SVC_NAME} +} + +function usage() +{ + echo + echo Usage: + echo "./svc.sh [install, start, stop, status, uninstall]" + echo "Commands:" + echo " install [user]: Install runner service as Root or specified user." + echo " start: Manually start the runner service." + echo " stop: Manually stop the runner service." + echo " status: Display status of runner service." + echo " uninstall: Uninstall runner service." + echo +} + +case $SVC_CMD in + "install") install;; + "status") status;; + "uninstall") uninstall;; + "start") start;; + "stop") stop;; + "status") status;; + *) usage;; +esac + +exit 0 diff --git a/linux-image-pipeline/config.sh b/linux-image-pipeline/config.sh new file mode 100755 index 0000000..14cc6ba --- /dev/null +++ b/linux-image-pipeline/config.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +user_id=`id -u` + +# we want to snapshot the environment of the config user +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run with sudo" + exit 1 +fi + +# Check dotnet Core 6.0 dependencies for Linux +if [[ (`uname` == "Linux") ]] +then + command -v ldd > /dev/null + if [ $? -ne 0 ] + then + echo "Can not find 'ldd'. Please install 'ldd' and try again." + exit 1 + fi + + message="Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies." + + ldd ./bin/libcoreclr.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.Security.Cryptography.Native.OpenSsl.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.IO.Compression.Native.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + if ! [ -x "$(command -v ldconfig)" ]; then + LDCONFIG_COMMAND="/sbin/ldconfig" + if ! [ -x "$LDCONFIG_COMMAND" ]; then + echo "Can not find 'ldconfig' in PATH and '/sbin/ldconfig' doesn't exists either. Please install 'ldconfig' and try again." + exit 1 + fi + else + LDCONFIG_COMMAND="ldconfig" + fi + + libpath=${LD_LIBRARY_PATH:-} + $LDCONFIG_COMMAND -NXv ${libpath//:/ } 2>&1 | grep libicu >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Libicu's dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi +fi + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" +cd "$DIR" + +source ./env.sh + +shopt -s nocasematch +if [[ "$1" == "remove" ]]; then + ./bin/Runner.Listener "$@" +else + ./bin/Runner.Listener configure "$@" +fi diff --git a/linux-image-pipeline/env.sh b/linux-image-pipeline/env.sh new file mode 100755 index 0000000..641d244 --- /dev/null +++ b/linux-image-pipeline/env.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +varCheckList=( + 'LANG' + 'JAVA_HOME' + 'ANT_HOME' + 'M2_HOME' + 'ANDROID_HOME' + 'ANDROID_SDK_ROOT' + 'GRADLE_HOME' + 'NVM_BIN' + 'NVM_PATH' + 'LD_LIBRARY_PATH' + 'PERL5LIB' + ) + +envContents="" + +if [ -f ".env" ]; then + envContents=`cat .env` +else + touch .env +fi + +function writeVar() +{ + checkVar="$1" + checkDelim="${1}=" + if test "${envContents#*$checkDelim}" = "$envContents" + then + if [ ! -z "${!checkVar}" ]; then + echo "${checkVar}=${!checkVar}">>.env + fi + fi +} + +echo $PATH>.path + +for var_name in ${varCheckList[@]} +do + writeVar "${var_name}" +done diff --git a/linux-image-pipeline/run-helper.cmd.template b/linux-image-pipeline/run-helper.cmd.template new file mode 100644 index 0000000..23e4246 --- /dev/null +++ b/linux-image-pipeline/run-helper.cmd.template @@ -0,0 +1,53 @@ +@echo off +SET UPDATEFILE=update.finished +"%~dp0\bin\Runner.Listener.exe" run %* + +rem using `if %ERRORLEVEL% EQU N` insterad of `if ERRORLEVEL N` +rem `if ERRORLEVEL N` means: error level is N or MORE + +if %ERRORLEVEL% EQU 0 ( + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 1 ( + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 2 ( + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + ping 127.0.0.1 -n 6 -w 1000 >NUL + exit /b 1 +) + +if %ERRORLEVEL% EQU 3 ( + rem Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +if %ERRORLEVEL% EQU 4 ( + rem Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +echo "Exiting after unknown error code: %ERRORLEVEL%" +exit /b 0 \ No newline at end of file diff --git a/linux-image-pipeline/run-helper.sh b/linux-image-pipeline/run-helper.sh new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/linux-image-pipeline/run-helper.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/linux-image-pipeline/run-helper.sh.template b/linux-image-pipeline/run-helper.sh.template new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/linux-image-pipeline/run-helper.sh.template @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/linux-image-pipeline/run.sh b/linux-image-pipeline/run.sh new file mode 100755 index 0000000..6b02ea1 --- /dev/null +++ b/linux-image-pipeline/run.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +run() { + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + exit 0 + fi + done +} + +runWithManualTrap() { + # Set job control + set -m + + trap 'kill -INT -$PID' INT TERM + + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* & + PID=$! + wait -f $PID + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + # Unregister signal handling before exit + trap - INT TERM + # wait for last parts to be logged + wait $PID + exit $returnCode + fi + done +} + +function updateCerts() { + local sudo_prefix="" + local user_id=`id -u` + + if [ $user_id -ne 0 ]; then + if [[ ! -x "$(command -v sudo)" ]]; then + echo "Warning: failed to update certificate store: sudo is required but not found" + return 1 + else + sudo_prefix="sudo" + fi + fi + + if [[ -x "$(command -v update-ca-certificates)" ]]; then + eval $sudo_prefix "update-ca-certificates" + elif [[ -x "$(command -v update-ca-trust)" ]]; then + eval $sudo_prefix "update-ca-trust" + else + echo "Warning: failed to update certificate store: update-ca-certificates or update-ca-trust not found. This can happen if you're using a different runner base image." + return 1 + fi +} + +if [[ ! -z "$RUNNER_UPDATE_CA_CERTS" ]]; then + updateCerts +fi + +if [[ -z "$RUNNER_MANUALLY_TRAP_SIG" ]]; then + run $* +else + runWithManualTrap $* +fi \ No newline at end of file diff --git a/linux-image-pipeline/safe_sleep.sh b/linux-image-pipeline/safe_sleep.sh new file mode 100755 index 0000000..7ba5be3 --- /dev/null +++ b/linux-image-pipeline/safe_sleep.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +SECONDS=0 +while [[ $SECONDS != $1 ]]; do + : +done diff --git a/linux-image-pipeline/svc.sh b/linux-image-pipeline/svc.sh new file mode 100755 index 0000000..436ea05 --- /dev/null +++ b/linux-image-pipeline/svc.sh @@ -0,0 +1,179 @@ +#!/bin/bash + +SVC_NAME="actions.runner._services.linux-image-pipeline.service" +SVC_NAME=${SVC_NAME// /_} +SVC_DESCRIPTION="GitHub Actions Runner (_services.linux-image-pipeline)" + +SVC_CMD=$1 +arg_2=${2} + +RUNNER_ROOT=`pwd` + +UNIT_PATH=/etc/systemd/system/${SVC_NAME} +TEMPLATE_PATH=$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE +IS_CUSTOM_TEMPLATE=0 +if [[ -z $TEMPLATE_PATH ]]; then + TEMPLATE_PATH=./bin/actions.runner.service.template +else + IS_CUSTOM_TEMPLATE=1 +fi +TEMP_PATH=./bin/actions.runner.service.temp +CONFIG_PATH=.service + +user_id=`id -u` + +# systemctl must run as sudo +# this script is a convenience wrapper around systemctl +if [ $user_id -ne 0 ]; then + echo "Must run as sudo" + exit 1 +fi + +function failed() +{ + local error=${1:-Undefined error} + echo "Failed: $error" >&2 + exit 1 +} + +if [ ! -f "${TEMPLATE_PATH}" ]; then + if [[ $IS_CUSTOM_TEMPLATE = 0 ]]; then + failed "Must run from runner root or install is corrupt" + else + failed "Service file at '$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE' using GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE env variable is not found" + fi +fi + +#check if we run as root +if [[ $(id -u) != "0" ]]; then + echo "Failed: This script requires to run with sudo." >&2 + exit 1 +fi + +function install() +{ + echo "Creating launch runner in ${UNIT_PATH}" + if [ -f "${UNIT_PATH}" ]; then + failed "error: exists ${UNIT_PATH}" + fi + + if [ -f "${TEMP_PATH}" ]; then + rm "${TEMP_PATH}" || failed "failed to delete ${TEMP_PATH}" + fi + + # can optionally use username supplied + run_as_user=${arg_2:-$SUDO_USER} + echo "Run as user: ${run_as_user}" + + run_as_uid=$(id -u ${run_as_user}) || failed "User does not exist" + echo "Run as uid: ${run_as_uid}" + + run_as_gid=$(id -g ${run_as_user}) || failed "Group not available" + echo "gid: ${run_as_gid}" + + sed "s/{{User}}/${run_as_user}/g; s/{{Description}}/$(echo ${SVC_DESCRIPTION} | sed -e 's/[\/&]/\\&/g')/g; s/{{RunnerRoot}}/$(echo ${RUNNER_ROOT} | sed -e 's/[\/&]/\\&/g')/g;" "${TEMPLATE_PATH}" > "${TEMP_PATH}" || failed "failed to create replacement temp file" + mv "${TEMP_PATH}" "${UNIT_PATH}" || failed "failed to copy unit file" + + # Recent Fedora based Linux (CentOS/Redhat) has SELinux enabled by default + # We need to restore security context on the unit file we added otherwise SystemD have no access to it. + command -v getenforce > /dev/null + if [ $? -eq 0 ] + then + selinuxEnabled=$(getenforce) + if [[ $selinuxEnabled == "Enforcing" ]] + then + # SELinux is enabled, we will need to Restore SELinux Context for the service file + restorecon -r -v "${UNIT_PATH}" || failed "failed to restore SELinux context on ${UNIT_PATH}" + fi + fi + + # unit file should not be executable and world writable + chmod 664 "${UNIT_PATH}" || failed "failed to set permissions on ${UNIT_PATH}" + systemctl daemon-reload || failed "failed to reload daemons" + + # Since we started with sudo, runsvc.sh will be owned by root. Change this to current login user. + cp ./bin/runsvc.sh ./runsvc.sh || failed "failed to copy runsvc.sh" + chown ${run_as_uid}:${run_as_gid} ./runsvc.sh || failed "failed to set owner for runsvc.sh" + chmod 755 ./runsvc.sh || failed "failed to set permission for runsvc.sh" + + systemctl enable ${SVC_NAME} || failed "failed to enable ${SVC_NAME}" + + echo "${SVC_NAME}" > ${CONFIG_PATH} || failed "failed to create .service file" + chown ${run_as_uid}:${run_as_gid} ${CONFIG_PATH} || failed "failed to set permission for ${CONFIG_PATH}" +} + +function start() +{ + systemctl start ${SVC_NAME} || failed "failed to start ${SVC_NAME}" + status +} + +function stop() +{ + systemctl stop ${SVC_NAME} || failed "failed to stop ${SVC_NAME}" + status +} + +function uninstall() +{ + if service_exists; then + stop + systemctl disable ${SVC_NAME} || failed "failed to disable ${SVC_NAME}" + rm "${UNIT_PATH}" || failed "failed to delete ${UNIT_PATH}" + else + echo "Service ${SVC_NAME} is not installed" + fi + if [ -f "${CONFIG_PATH}" ]; then + rm "${CONFIG_PATH}" || failed "failed to delete ${CONFIG_PATH}" + fi + systemctl daemon-reload || failed "failed to reload daemons" +} + +function service_exists() { + if [ -f "${UNIT_PATH}" ]; then + return 0 + else + return 1 + fi +} + +function status() +{ + if service_exists; then + echo + echo "${UNIT_PATH}" + else + echo + echo "not installed" + echo + exit 1 + fi + + systemctl --no-pager status ${SVC_NAME} +} + +function usage() +{ + echo + echo Usage: + echo "./svc.sh [install, start, stop, status, uninstall]" + echo "Commands:" + echo " install [user]: Install runner service as Root or specified user." + echo " start: Manually start the runner service." + echo " stop: Manually stop the runner service." + echo " status: Display status of runner service." + echo " uninstall: Uninstall runner service." + echo +} + +case $SVC_CMD in + "install") install;; + "status") status;; + "uninstall") uninstall;; + "start") start;; + "stop") stop;; + "status") status;; + *) usage;; +esac + +exit 0 diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..36fa2bc --- /dev/null +++ b/main.tf @@ -0,0 +1,17 @@ +module "runner" { + source = "HappyPathway/runner/ghe" + github_base_url = "https://github.e.it.census.gov" + github_owner = "CSVD" + runner_basedir = "/apps/terraform/workspaces/arnol377/ghe-runner" + runner_tarball = "/apps/terraform/workspaces/arnol377/actions-runner-linux-x64-2.304.0.tar.gz" + repos = [ + "aws-image-pipeline", + "linux-image-pipeline", + "windows-image-pipeline", + "image-pipeline-goss-testing", + "image-pipeline-ansible-playbooks", + ] + runner_labels = [ + "image-pipeline" + ] +} diff --git a/terraform.tfstate b/terraform.tfstate new file mode 100644 index 0000000..05ee2b2 --- /dev/null +++ b/terraform.tfstate @@ -0,0 +1,387 @@ +{ + "version": 4, + "terraform_version": "1.9.1", + "serial": 40, + "lineage": "5e2c7a5e-ddf9-7c73-16ae-e0df68665ea7", + "outputs": {}, + "resources": [ + { + "module": "module.runner", + "mode": "data", + "type": "github_actions_registration_token", + "name": "token", + "provider": "provider[\"registry.terraform.io/hashicorp/github\"]", + "instances": [ + { + "index_key": "aws-image-pipeline", + "schema_version": 0, + "attributes": { + "expires_at": 1723228011, + "id": "CSVD/aws-image-pipeline", + "repository": "aws-image-pipeline", + "token": "AAAAEJPIJ7IAYC3ZMAMN3KTGWZPWW" + }, + "sensitive_attributes": [] + }, + { + "index_key": "image-pipeline-ansible-playbooks", + "schema_version": 0, + "attributes": { + "expires_at": 1723228010, + "id": "CSVD/image-pipeline-ansible-playbooks", + "repository": "image-pipeline-ansible-playbooks", + "token": "AAAAEJMCFRBEWF43DDRNRZTGWZPWU" + }, + "sensitive_attributes": [] + }, + { + "index_key": "image-pipeline-goss-testing", + "schema_version": 0, + "attributes": { + "expires_at": 1723228013, + "id": "CSVD/image-pipeline-goss-testing", + "repository": "image-pipeline-goss-testing", + "token": "AAAAEJPETSBDPW4OH6CX6ULGWZPW2" + }, + "sensitive_attributes": [] + }, + { + "index_key": "linux-image-pipeline", + "schema_version": 0, + "attributes": { + "expires_at": 1723228012, + "id": "CSVD/linux-image-pipeline", + "repository": "linux-image-pipeline", + "token": "AAAAEJIL2M2XLWHGQDE2YE3GWZPWY" + }, + "sensitive_attributes": [] + }, + { + "index_key": "windows-image-pipeline", + "schema_version": 0, + "attributes": { + "expires_at": 1723228008, + "id": "CSVD/windows-image-pipeline", + "repository": "windows-image-pipeline", + "token": "AAAAEJKJE4TMXH7I5MLWV2DGWZPWQ" + }, + "sensitive_attributes": [] + } + ] + }, + { + "module": "module.runner", + "mode": "data", + "type": "github_repository", + "name": "repository", + "provider": "provider[\"registry.terraform.io/hashicorp/github\"]", + "instances": [ + { + "index_key": "aws-image-pipeline", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Terraform Workspace for creating and managing AWS Image Pipelines", + "fork": false, + "full_name": "CSVD/aws-image-pipeline", + "git_clone_url": "git://github.e.it.census.gov/CSVD/aws-image-pipeline.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/aws-image-pipeline", + "http_clone_url": "https://github.e.it.census.gov/CSVD/aws-image-pipeline.git", + "id": "aws-image-pipeline", + "is_template": false, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "aws-image-pipeline", + "node_id": "MDEwOlJlcG9zaXRvcnk5MjY=", + "pages": [], + "primary_language": "HCL", + "private": true, + "repo_id": 926, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/aws-image-pipeline.git", + "svn_url": "https://github.e.it.census.gov/CSVD/aws-image-pipeline", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + }, + { + "index_key": "image-pipeline-ansible-playbooks", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Repo for creating and managing Ansible Playbooks for use in Image Pipeline", + "fork": false, + "full_name": "CSVD/image-pipeline-ansible-playbooks", + "git_clone_url": "git://github.e.it.census.gov/CSVD/image-pipeline-ansible-playbooks.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/image-pipeline-ansible-playbooks", + "http_clone_url": "https://github.e.it.census.gov/CSVD/image-pipeline-ansible-playbooks.git", + "id": "image-pipeline-ansible-playbooks", + "is_template": false, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "image-pipeline-ansible-playbooks", + "node_id": "MDEwOlJlcG9zaXRvcnk5ODM=", + "pages": [], + "primary_language": "", + "private": true, + "repo_id": 983, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/image-pipeline-ansible-playbooks.git", + "svn_url": "https://github.e.it.census.gov/CSVD/image-pipeline-ansible-playbooks", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + }, + { + "index_key": "image-pipeline-goss-testing", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Goss testing suite for ec2 instances", + "fork": false, + "full_name": "CSVD/image-pipeline-goss-testing", + "git_clone_url": "git://github.e.it.census.gov/CSVD/image-pipeline-goss-testing.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/image-pipeline-goss-testing", + "http_clone_url": "https://github.e.it.census.gov/CSVD/image-pipeline-goss-testing.git", + "id": "image-pipeline-goss-testing", + "is_template": true, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "image-pipeline-goss-testing", + "node_id": "MDEwOlJlcG9zaXRvcnk5NDI=", + "pages": [], + "primary_language": "HCL", + "private": true, + "repo_id": 942, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/image-pipeline-goss-testing.git", + "svn_url": "https://github.e.it.census.gov/CSVD/image-pipeline-goss-testing", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + }, + { + "index_key": "linux-image-pipeline", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Template repo for windows image pipelines", + "fork": false, + "full_name": "CSVD/linux-image-pipeline", + "git_clone_url": "git://github.e.it.census.gov/CSVD/linux-image-pipeline.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/linux-image-pipeline", + "http_clone_url": "https://github.e.it.census.gov/CSVD/linux-image-pipeline.git", + "id": "linux-image-pipeline", + "is_template": true, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "linux-image-pipeline", + "node_id": "MDEwOlJlcG9zaXRvcnk5OTU=", + "pages": [], + "primary_language": "", + "private": true, + "repo_id": 995, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/linux-image-pipeline.git", + "svn_url": "https://github.e.it.census.gov/CSVD/linux-image-pipeline", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + }, + { + "index_key": "windows-image-pipeline", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Template repo for windows image pipelines", + "fork": false, + "full_name": "CSVD/windows-image-pipeline", + "git_clone_url": "git://github.e.it.census.gov/CSVD/windows-image-pipeline.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/windows-image-pipeline", + "http_clone_url": "https://github.e.it.census.gov/CSVD/windows-image-pipeline.git", + "id": "windows-image-pipeline", + "is_template": true, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "windows-image-pipeline", + "node_id": "MDEwOlJlcG9zaXRvcnk5NzY=", + "pages": [], + "primary_language": "PowerShell", + "private": true, + "repo_id": 976, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/windows-image-pipeline.git", + "svn_url": "https://github.e.it.census.gov/CSVD/windows-image-pipeline", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + } + ] + }, + { + "module": "module.runner", + "mode": "managed", + "type": "null_resource", + "name": "register_runner", + "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", + "instances": [ + { + "index_key": "aws-image-pipeline", + "schema_version": 0, + "attributes": { + "id": "3265801747151091837", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + }, + { + "index_key": "image-pipeline-ansible-playbooks", + "schema_version": 0, + "attributes": { + "id": "194804118598394064", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + }, + { + "index_key": "image-pipeline-goss-testing", + "schema_version": 0, + "attributes": { + "id": "5336565778616734214", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + }, + { + "index_key": "linux-image-pipeline", + "schema_version": 0, + "attributes": { + "id": "2962463554416131553", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + }, + { + "index_key": "windows-image-pipeline", + "schema_version": 0, + "attributes": { + "id": "6846591234767840591", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + } + ] + } + ], + "check_results": null +} diff --git a/terraform.tfstate.backup b/terraform.tfstate.backup new file mode 100644 index 0000000..55b90a1 --- /dev/null +++ b/terraform.tfstate.backup @@ -0,0 +1,397 @@ +{ + "version": 4, + "terraform_version": "1.9.1", + "serial": 35, + "lineage": "5e2c7a5e-ddf9-7c73-16ae-e0df68665ea7", + "outputs": {}, + "resources": [ + { + "module": "module.runner", + "mode": "data", + "type": "github_actions_registration_token", + "name": "token", + "provider": "provider[\"registry.terraform.io/hashicorp/github\"]", + "instances": [ + { + "index_key": "aws-image-pipeline", + "schema_version": 0, + "attributes": { + "expires_at": 1723227946, + "id": "CSVD/aws-image-pipeline", + "repository": "aws-image-pipeline", + "token": "AAAAEJJPILSLCVN2YSYF7F3GWZPSU" + }, + "sensitive_attributes": [] + }, + { + "index_key": "image-pipeline-ansible-playbooks", + "schema_version": 0, + "attributes": { + "expires_at": 1723227947, + "id": "CSVD/image-pipeline-ansible-playbooks", + "repository": "image-pipeline-ansible-playbooks", + "token": "AAAAEJLDJMSSGQ32JUCKTITGWZPSW" + }, + "sensitive_attributes": [] + }, + { + "index_key": "image-pipeline-goss-testing", + "schema_version": 0, + "attributes": { + "expires_at": 1723227942, + "id": "CSVD/image-pipeline-goss-testing", + "repository": "image-pipeline-goss-testing", + "token": "AAAAEJJPPCC3J6BK2AJU7ADGWZPSM" + }, + "sensitive_attributes": [] + }, + { + "index_key": "linux-image-pipeline", + "schema_version": 0, + "attributes": { + "expires_at": 1723227944, + "id": "CSVD/linux-image-pipeline", + "repository": "linux-image-pipeline", + "token": "AAAAEJIASRKNVUVOC7KRSGDGWZPSQ" + }, + "sensitive_attributes": [] + }, + { + "index_key": "windows-image-pipeline", + "schema_version": 0, + "attributes": { + "expires_at": 1723227943, + "id": "CSVD/windows-image-pipeline", + "repository": "windows-image-pipeline", + "token": "AAAAEJPFEAWLHXV7PCV2ITLGWZPSO" + }, + "sensitive_attributes": [] + } + ] + }, + { + "module": "module.runner", + "mode": "data", + "type": "github_repository", + "name": "repository", + "provider": "provider[\"registry.terraform.io/hashicorp/github\"]", + "instances": [ + { + "index_key": "aws-image-pipeline", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Terraform Workspace for creating and managing AWS Image Pipelines", + "fork": false, + "full_name": "CSVD/aws-image-pipeline", + "git_clone_url": "git://github.e.it.census.gov/CSVD/aws-image-pipeline.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/aws-image-pipeline", + "http_clone_url": "https://github.e.it.census.gov/CSVD/aws-image-pipeline.git", + "id": "aws-image-pipeline", + "is_template": false, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "aws-image-pipeline", + "node_id": "MDEwOlJlcG9zaXRvcnk5MjY=", + "pages": [], + "primary_language": "HCL", + "private": true, + "repo_id": 926, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/aws-image-pipeline.git", + "svn_url": "https://github.e.it.census.gov/CSVD/aws-image-pipeline", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + }, + { + "index_key": "image-pipeline-ansible-playbooks", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Repo for creating and managing Ansible Playbooks for use in Image Pipeline", + "fork": false, + "full_name": "CSVD/image-pipeline-ansible-playbooks", + "git_clone_url": "git://github.e.it.census.gov/CSVD/image-pipeline-ansible-playbooks.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/image-pipeline-ansible-playbooks", + "http_clone_url": "https://github.e.it.census.gov/CSVD/image-pipeline-ansible-playbooks.git", + "id": "image-pipeline-ansible-playbooks", + "is_template": false, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "image-pipeline-ansible-playbooks", + "node_id": "MDEwOlJlcG9zaXRvcnk5ODM=", + "pages": [], + "primary_language": "", + "private": true, + "repo_id": 983, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/image-pipeline-ansible-playbooks.git", + "svn_url": "https://github.e.it.census.gov/CSVD/image-pipeline-ansible-playbooks", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + }, + { + "index_key": "image-pipeline-goss-testing", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Goss testing suite for ec2 instances", + "fork": false, + "full_name": "CSVD/image-pipeline-goss-testing", + "git_clone_url": "git://github.e.it.census.gov/CSVD/image-pipeline-goss-testing.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/image-pipeline-goss-testing", + "http_clone_url": "https://github.e.it.census.gov/CSVD/image-pipeline-goss-testing.git", + "id": "image-pipeline-goss-testing", + "is_template": true, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "image-pipeline-goss-testing", + "node_id": "MDEwOlJlcG9zaXRvcnk5NDI=", + "pages": [], + "primary_language": "HCL", + "private": true, + "repo_id": 942, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/image-pipeline-goss-testing.git", + "svn_url": "https://github.e.it.census.gov/CSVD/image-pipeline-goss-testing", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + }, + { + "index_key": "linux-image-pipeline", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Template repo for windows image pipelines", + "fork": false, + "full_name": "CSVD/linux-image-pipeline", + "git_clone_url": "git://github.e.it.census.gov/CSVD/linux-image-pipeline.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/linux-image-pipeline", + "http_clone_url": "https://github.e.it.census.gov/CSVD/linux-image-pipeline.git", + "id": "linux-image-pipeline", + "is_template": true, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "linux-image-pipeline", + "node_id": "MDEwOlJlcG9zaXRvcnk5OTU=", + "pages": [], + "primary_language": "", + "private": true, + "repo_id": 995, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/linux-image-pipeline.git", + "svn_url": "https://github.e.it.census.gov/CSVD/linux-image-pipeline", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + }, + { + "index_key": "windows-image-pipeline", + "schema_version": 0, + "attributes": { + "allow_auto_merge": false, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "archived": false, + "default_branch": "main", + "description": "Template repo for windows image pipelines", + "fork": false, + "full_name": "CSVD/windows-image-pipeline", + "git_clone_url": "git://github.e.it.census.gov/CSVD/windows-image-pipeline.git", + "has_discussions": false, + "has_downloads": false, + "has_issues": false, + "has_projects": true, + "has_wiki": true, + "homepage_url": "", + "html_url": "https://github.e.it.census.gov/CSVD/windows-image-pipeline", + "http_clone_url": "https://github.e.it.census.gov/CSVD/windows-image-pipeline.git", + "id": "windows-image-pipeline", + "is_template": true, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "name": "windows-image-pipeline", + "node_id": "MDEwOlJlcG9zaXRvcnk5NzY=", + "pages": [], + "primary_language": "PowerShell", + "private": true, + "repo_id": 976, + "repository_license": [], + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_clone_url": "git@github.e.it.census.gov:CSVD/windows-image-pipeline.git", + "svn_url": "https://github.e.it.census.gov/CSVD/windows-image-pipeline", + "template": [], + "topics": [ + "terraform" + ], + "visibility": "private" + }, + "sensitive_attributes": [] + } + ] + }, + { + "module": "module.runner", + "mode": "managed", + "type": "github_actions_runner_group", + "name": "rg", + "provider": "provider[\"registry.terraform.io/hashicorp/github\"]", + "instances": [] + }, + { + "module": "module.runner", + "mode": "managed", + "type": "null_resource", + "name": "register_runner", + "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", + "instances": [ + { + "index_key": "aws-image-pipeline", + "status": "tainted", + "schema_version": 0, + "attributes": { + "id": "2120434582617306576", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + }, + { + "index_key": "image-pipeline-ansible-playbooks", + "schema_version": 0, + "attributes": { + "id": "194804118598394064", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + }, + { + "index_key": "image-pipeline-goss-testing", + "schema_version": 0, + "attributes": { + "id": "5336565778616734214", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + }, + { + "index_key": "linux-image-pipeline", + "status": "tainted", + "schema_version": 0, + "attributes": { + "id": "4994079606812454375", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + }, + { + "index_key": "windows-image-pipeline", + "schema_version": 0, + "attributes": { + "id": "6846591234767840591", + "triggers": null + }, + "sensitive_attributes": [], + "dependencies": [ + "module.runner.data.github_actions_registration_token.token", + "module.runner.data.github_repository.repository", + "module.runner.github_actions_runner_group.rg" + ] + } + ] + } + ], + "check_results": null +} diff --git a/windows-image-pipeline/config.sh b/windows-image-pipeline/config.sh new file mode 100755 index 0000000..14cc6ba --- /dev/null +++ b/windows-image-pipeline/config.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +user_id=`id -u` + +# we want to snapshot the environment of the config user +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run with sudo" + exit 1 +fi + +# Check dotnet Core 6.0 dependencies for Linux +if [[ (`uname` == "Linux") ]] +then + command -v ldd > /dev/null + if [ $? -ne 0 ] + then + echo "Can not find 'ldd'. Please install 'ldd' and try again." + exit 1 + fi + + message="Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies." + + ldd ./bin/libcoreclr.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.Security.Cryptography.Native.OpenSsl.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + ldd ./bin/libSystem.IO.Compression.Native.so | grep 'not found' + if [ $? -eq 0 ]; then + echo "Dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi + + if ! [ -x "$(command -v ldconfig)" ]; then + LDCONFIG_COMMAND="/sbin/ldconfig" + if ! [ -x "$LDCONFIG_COMMAND" ]; then + echo "Can not find 'ldconfig' in PATH and '/sbin/ldconfig' doesn't exists either. Please install 'ldconfig' and try again." + exit 1 + fi + else + LDCONFIG_COMMAND="ldconfig" + fi + + libpath=${LD_LIBRARY_PATH:-} + $LDCONFIG_COMMAND -NXv ${libpath//:/ } 2>&1 | grep libicu >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Libicu's dependencies is missing for Dotnet Core 6.0" + echo $message + exit 1 + fi +fi + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" +cd "$DIR" + +source ./env.sh + +shopt -s nocasematch +if [[ "$1" == "remove" ]]; then + ./bin/Runner.Listener "$@" +else + ./bin/Runner.Listener configure "$@" +fi diff --git a/windows-image-pipeline/env.sh b/windows-image-pipeline/env.sh new file mode 100755 index 0000000..641d244 --- /dev/null +++ b/windows-image-pipeline/env.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +varCheckList=( + 'LANG' + 'JAVA_HOME' + 'ANT_HOME' + 'M2_HOME' + 'ANDROID_HOME' + 'ANDROID_SDK_ROOT' + 'GRADLE_HOME' + 'NVM_BIN' + 'NVM_PATH' + 'LD_LIBRARY_PATH' + 'PERL5LIB' + ) + +envContents="" + +if [ -f ".env" ]; then + envContents=`cat .env` +else + touch .env +fi + +function writeVar() +{ + checkVar="$1" + checkDelim="${1}=" + if test "${envContents#*$checkDelim}" = "$envContents" + then + if [ ! -z "${!checkVar}" ]; then + echo "${checkVar}=${!checkVar}">>.env + fi + fi +} + +echo $PATH>.path + +for var_name in ${varCheckList[@]} +do + writeVar "${var_name}" +done diff --git a/windows-image-pipeline/run-helper.cmd.template b/windows-image-pipeline/run-helper.cmd.template new file mode 100644 index 0000000..23e4246 --- /dev/null +++ b/windows-image-pipeline/run-helper.cmd.template @@ -0,0 +1,53 @@ +@echo off +SET UPDATEFILE=update.finished +"%~dp0\bin\Runner.Listener.exe" run %* + +rem using `if %ERRORLEVEL% EQU N` insterad of `if ERRORLEVEL N` +rem `if ERRORLEVEL N` means: error level is N or MORE + +if %ERRORLEVEL% EQU 0 ( + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 1 ( + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit /b 0 +) + +if %ERRORLEVEL% EQU 2 ( + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + ping 127.0.0.1 -n 6 -w 1000 >NUL + exit /b 1 +) + +if %ERRORLEVEL% EQU 3 ( + rem Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +if %ERRORLEVEL% EQU 4 ( + rem Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + FOR /L %%G IN (1,1,30) DO ( + IF EXIST %UPDATEFILE% ( + echo "Update finished successfully." + del %FILE% + exit /b 1 + ) + ping 127.0.0.1 -n 2 -w 1000 >NUL + ) + exit /b 1 +) + +echo "Exiting after unknown error code: %ERRORLEVEL%" +exit /b 0 \ No newline at end of file diff --git a/windows-image-pipeline/run-helper.sh b/windows-image-pipeline/run-helper.sh new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/windows-image-pipeline/run-helper.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/windows-image-pipeline/run-helper.sh.template b/windows-image-pipeline/run-helper.sh.template new file mode 100755 index 0000000..743fd8b --- /dev/null +++ b/windows-image-pipeline/run-helper.sh.template @@ -0,0 +1,76 @@ +#!/bin/bash + +# Validate not sudo +user_id=`id -u` +if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then + echo "Must not run interactively with sudo" + exit 1 +fi + +# Run +shopt -s nocasematch + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +# Wait for docker to start +if [ ! -z "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" ]; then + if [ "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS" -gt 0 ]; then + echo "Waiting for docker to be ready." + for i in $(seq "$RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"); do + if docker ps > /dev/null 2>&1; then + echo "Docker is ready." + break + fi + "$DIR"/safe_sleep.sh 1 + done + fi +fi + +updateFile="update.finished" +"$DIR"/bin/Runner.Listener run $* + +returnCode=$? +if [[ $returnCode == 0 ]]; then + echo "Runner listener exit with 0 return code, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 1 ]]; then + echo "Runner listener exit with terminated error, stop the service, no retry needed." + exit 0 +elif [[ $returnCode == 2 ]]; then + echo "Runner listener exit with retryable error, re-launch runner in 5 seconds." + "$DIR"/safe_sleep.sh 5 + exit 2 +elif [[ $returnCode == 3 ]]; then + # Wait for 30 seconds or for flag file to exists for the runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +elif [[ $returnCode == 4 ]]; then + # Wait for 30 seconds or for flag file to exists for the ephemeral runner update process finish + echo "Runner listener exit because of updating, re-launch runner after successful update" + for i in {0..30}; do + if test -f "$updateFile"; then + echo "Update finished successfully." + rm "$updateFile" + break + fi + "$DIR"/safe_sleep.sh 1 + done + exit 2 +else + echo "Exiting with unknown error code: ${returnCode}" + exit 0 +fi diff --git a/windows-image-pipeline/run.sh b/windows-image-pipeline/run.sh new file mode 100755 index 0000000..6b02ea1 --- /dev/null +++ b/windows-image-pipeline/run.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Change directory to the script root directory +# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +run() { + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + exit 0 + fi + done +} + +runWithManualTrap() { + # Set job control + set -m + + trap 'kill -INT -$PID' INT TERM + + # run the helper process which keep the listener alive + while :; + do + cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh + "$DIR"/run-helper.sh $* & + PID=$! + wait -f $PID + returnCode=$? + if [[ $returnCode -eq 2 ]]; then + echo "Restarting runner..." + else + echo "Exiting runner..." + # Unregister signal handling before exit + trap - INT TERM + # wait for last parts to be logged + wait $PID + exit $returnCode + fi + done +} + +function updateCerts() { + local sudo_prefix="" + local user_id=`id -u` + + if [ $user_id -ne 0 ]; then + if [[ ! -x "$(command -v sudo)" ]]; then + echo "Warning: failed to update certificate store: sudo is required but not found" + return 1 + else + sudo_prefix="sudo" + fi + fi + + if [[ -x "$(command -v update-ca-certificates)" ]]; then + eval $sudo_prefix "update-ca-certificates" + elif [[ -x "$(command -v update-ca-trust)" ]]; then + eval $sudo_prefix "update-ca-trust" + else + echo "Warning: failed to update certificate store: update-ca-certificates or update-ca-trust not found. This can happen if you're using a different runner base image." + return 1 + fi +} + +if [[ ! -z "$RUNNER_UPDATE_CA_CERTS" ]]; then + updateCerts +fi + +if [[ -z "$RUNNER_MANUALLY_TRAP_SIG" ]]; then + run $* +else + runWithManualTrap $* +fi \ No newline at end of file diff --git a/windows-image-pipeline/safe_sleep.sh b/windows-image-pipeline/safe_sleep.sh new file mode 100755 index 0000000..7ba5be3 --- /dev/null +++ b/windows-image-pipeline/safe_sleep.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +SECONDS=0 +while [[ $SECONDS != $1 ]]; do + : +done diff --git a/windows-image-pipeline/svc.sh b/windows-image-pipeline/svc.sh new file mode 100755 index 0000000..5efd832 --- /dev/null +++ b/windows-image-pipeline/svc.sh @@ -0,0 +1,179 @@ +#!/bin/bash + +SVC_NAME="actions.runner._services.windows-image-pipeline.service" +SVC_NAME=${SVC_NAME// /_} +SVC_DESCRIPTION="GitHub Actions Runner (_services.windows-image-pipeline)" + +SVC_CMD=$1 +arg_2=${2} + +RUNNER_ROOT=`pwd` + +UNIT_PATH=/etc/systemd/system/${SVC_NAME} +TEMPLATE_PATH=$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE +IS_CUSTOM_TEMPLATE=0 +if [[ -z $TEMPLATE_PATH ]]; then + TEMPLATE_PATH=./bin/actions.runner.service.template +else + IS_CUSTOM_TEMPLATE=1 +fi +TEMP_PATH=./bin/actions.runner.service.temp +CONFIG_PATH=.service + +user_id=`id -u` + +# systemctl must run as sudo +# this script is a convenience wrapper around systemctl +if [ $user_id -ne 0 ]; then + echo "Must run as sudo" + exit 1 +fi + +function failed() +{ + local error=${1:-Undefined error} + echo "Failed: $error" >&2 + exit 1 +} + +if [ ! -f "${TEMPLATE_PATH}" ]; then + if [[ $IS_CUSTOM_TEMPLATE = 0 ]]; then + failed "Must run from runner root or install is corrupt" + else + failed "Service file at '$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE' using GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE env variable is not found" + fi +fi + +#check if we run as root +if [[ $(id -u) != "0" ]]; then + echo "Failed: This script requires to run with sudo." >&2 + exit 1 +fi + +function install() +{ + echo "Creating launch runner in ${UNIT_PATH}" + if [ -f "${UNIT_PATH}" ]; then + failed "error: exists ${UNIT_PATH}" + fi + + if [ -f "${TEMP_PATH}" ]; then + rm "${TEMP_PATH}" || failed "failed to delete ${TEMP_PATH}" + fi + + # can optionally use username supplied + run_as_user=${arg_2:-$SUDO_USER} + echo "Run as user: ${run_as_user}" + + run_as_uid=$(id -u ${run_as_user}) || failed "User does not exist" + echo "Run as uid: ${run_as_uid}" + + run_as_gid=$(id -g ${run_as_user}) || failed "Group not available" + echo "gid: ${run_as_gid}" + + sed "s/{{User}}/${run_as_user}/g; s/{{Description}}/$(echo ${SVC_DESCRIPTION} | sed -e 's/[\/&]/\\&/g')/g; s/{{RunnerRoot}}/$(echo ${RUNNER_ROOT} | sed -e 's/[\/&]/\\&/g')/g;" "${TEMPLATE_PATH}" > "${TEMP_PATH}" || failed "failed to create replacement temp file" + mv "${TEMP_PATH}" "${UNIT_PATH}" || failed "failed to copy unit file" + + # Recent Fedora based Linux (CentOS/Redhat) has SELinux enabled by default + # We need to restore security context on the unit file we added otherwise SystemD have no access to it. + command -v getenforce > /dev/null + if [ $? -eq 0 ] + then + selinuxEnabled=$(getenforce) + if [[ $selinuxEnabled == "Enforcing" ]] + then + # SELinux is enabled, we will need to Restore SELinux Context for the service file + restorecon -r -v "${UNIT_PATH}" || failed "failed to restore SELinux context on ${UNIT_PATH}" + fi + fi + + # unit file should not be executable and world writable + chmod 664 "${UNIT_PATH}" || failed "failed to set permissions on ${UNIT_PATH}" + systemctl daemon-reload || failed "failed to reload daemons" + + # Since we started with sudo, runsvc.sh will be owned by root. Change this to current login user. + cp ./bin/runsvc.sh ./runsvc.sh || failed "failed to copy runsvc.sh" + chown ${run_as_uid}:${run_as_gid} ./runsvc.sh || failed "failed to set owner for runsvc.sh" + chmod 755 ./runsvc.sh || failed "failed to set permission for runsvc.sh" + + systemctl enable ${SVC_NAME} || failed "failed to enable ${SVC_NAME}" + + echo "${SVC_NAME}" > ${CONFIG_PATH} || failed "failed to create .service file" + chown ${run_as_uid}:${run_as_gid} ${CONFIG_PATH} || failed "failed to set permission for ${CONFIG_PATH}" +} + +function start() +{ + systemctl start ${SVC_NAME} || failed "failed to start ${SVC_NAME}" + status +} + +function stop() +{ + systemctl stop ${SVC_NAME} || failed "failed to stop ${SVC_NAME}" + status +} + +function uninstall() +{ + if service_exists; then + stop + systemctl disable ${SVC_NAME} || failed "failed to disable ${SVC_NAME}" + rm "${UNIT_PATH}" || failed "failed to delete ${UNIT_PATH}" + else + echo "Service ${SVC_NAME} is not installed" + fi + if [ -f "${CONFIG_PATH}" ]; then + rm "${CONFIG_PATH}" || failed "failed to delete ${CONFIG_PATH}" + fi + systemctl daemon-reload || failed "failed to reload daemons" +} + +function service_exists() { + if [ -f "${UNIT_PATH}" ]; then + return 0 + else + return 1 + fi +} + +function status() +{ + if service_exists; then + echo + echo "${UNIT_PATH}" + else + echo + echo "not installed" + echo + exit 1 + fi + + systemctl --no-pager status ${SVC_NAME} +} + +function usage() +{ + echo + echo Usage: + echo "./svc.sh [install, start, stop, status, uninstall]" + echo "Commands:" + echo " install [user]: Install runner service as Root or specified user." + echo " start: Manually start the runner service." + echo " stop: Manually stop the runner service." + echo " status: Display status of runner service." + echo " uninstall: Uninstall runner service." + echo +} + +case $SVC_CMD in + "install") install;; + "status") status;; + "uninstall") uninstall;; + "start") start;; + "stop") stop;; + "status") status;; + *) usage;; +esac + +exit 0