Skip to content

Commit

Permalink
feat(ghrunner): Docker image for self-hosted runners (#887)
Browse files Browse the repository at this point in the history
* Initial commit of code to create github runner image

* add missing parts of pipeline yml

* fix some more names

* Update .github/workflows/gh-runner-release.yml

---------

Co-authored-by: tjololo <[email protected]>
  • Loading branch information
tjololo and tjololo authored Oct 1, 2024
1 parent 43a41a1 commit bf4ae3b
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 0 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/gh-runner-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Test build gh-runner

on:
pull_request:
branches:
- main
paths:
- .github/workflows/gh-runner-pr.yml
- infrastructure/images/gh-runner/**

permissions:
contents: read
packages: read

jobs:
build:
name: Test build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get version from tags
id: version
run: |
tag=${GITHUB_REF/refs\/tags\//}
echo "version=${tag%-demo}" >> $GITHUB_OUTPUT
- name: Get Git commit timestamps
run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build
id: docker_build
uses: docker/build-push-action@v6
with:
context: ./infrastructure/images/gh-runner
push: false
platforms: linux/amd64,linux/arm64
tags: ${{ github.repository }}/gh-runner:test
env:
SOURCE_DATE_EPOCH: ${{ env.TIMESTAMP }}
43 changes: 43 additions & 0 deletions .github/workflows/gh-runner-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Release gh-runner

on:
push:
tags:
- "ghrunner-*"

permissions:
contents: read
packages: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get version from tags
id: version
run: |
tag=${GITHUB_REF/refs\/tags\//}
echo "version=${tag%-demo}" >> $GITHUB_OUTPUT
- name: Get Git commit timestamps
run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v6
with:
context: ./infrastructure/images/gh-runner
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository }}/gh-runner:v${{ steps.version.outputs.version }}
env:
SOURCE_DATE_EPOCH: ${{ env.TIMESTAMP }}
26 changes: 26 additions & 0 deletions infrastructure/images/gh-runner/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM ghcr.io/actions/actions-runner:2.319.1
# for latest release, see https://github.com/actions/runner/releases

USER root

# install curl and jq
RUN apt-get update && apt-get install -y curl jq && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

COPY scripts/entrypoint.sh ./entrypoint.sh
COPY scripts/app-token.sh ./app-token.sh
COPY scripts/token.sh ./token.sh
RUN chmod +x ./entrypoint.sh && \
chmod +x ./app-token.sh && \
chmod +x ./token.sh && \
mkdir /_work && \
chown runner:docker \
./entrypoint.sh \
./app-token.sh \
./token.sh \
/_work

USER runner

ENTRYPOINT ["./entrypoint.sh"]
23 changes: 23 additions & 0 deletions infrastructure/images/gh-runner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Altinn Github Runner Image
Default image used for Altinns self-hosted github runners.

This image is maintained by the platform team.

This image is ment to be as small and leightweight as possible so we keep the dependencies at a minum, to reduce the maintenance cost.

If any team needs a custom image they are free to roll their own or extend this, but they will be responsible for maintaining this image.

Example Dockerfile for an image that in addition to what is available in the base image installs netcat:

```Dockerfile
FROM ghcr.io/altinn/altinn-platform-gh-runner-base:1.0.0 ##TODO: Add actual image name when available

USER root

RUN apt-get update && apt-get install -y curl jq && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

USER runner
```

82 changes: 82 additions & 0 deletions infrastructure/images/gh-runner/scripts/app-token.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/bin/bash
#
# Request an ACCESS_TOKEN to be used by a GitHub APP
# Environment variable that need to be set up:
# * APP_ID, the GitHub's app ID
# * APP_PRIVATE_KEY, the content of GitHub app's private key in PEM format.
# * APP_LOGIN, the login name used to install GitHub's app
#

_GITHUB_HOST=${GITHUB_HOST:-"github.com"}

set -o pipefail

# If URL is not github.com then use the enterprise api endpoint
if [[ ${_GITHUB_HOST} = "github.com" ]]; then
URI="https://api.${_GITHUB_HOST}"
else
URI="https://${_GITHUB_HOST}/api/v3"
fi

API_VERSION=v3
API_HEADER="Accept: application/vnd.github.${API_VERSION}+json"
CONTENT_LENGTH_HEADER="Content-Length: 0"
APP_INSTALLATIONS_URI="${URI}/app/installations"

JWT_IAT_DRIFT=60
JWT_EXP_DELTA=600

JWT_JOSE_HEADER='{
"alg": "RS256",
"typ": "JWT"
}'


build_jwt_payload() {
now=$(date +%s)
iat=$((now - JWT_IAT_DRIFT))
jq -c \
--arg iat_str "${iat}" \
--arg exp_delta_str "${JWT_EXP_DELTA}" \
--arg app_id_str "${APP_ID}" \
'
($iat_str | tonumber) as $iat
| ($exp_delta_str | tonumber) as $exp_delta
| ($app_id_str | tonumber) as $app_id
| .iat = $iat
| .exp = ($iat + $exp_delta)
| .iss = $app_id
' <<< "{}" | tr -d '\n'
}

base64url() {
base64 | tr '+/' '-_' | tr -d '=\n'
}

rs256_sign() {
openssl dgst -binary -sha256 -sign <(echo "$1")
}

request_access_token() {
jwt_payload=$(build_jwt_payload)
encoded_jwt_parts=$(base64url <<<"${JWT_JOSE_HEADER}").$(base64url <<<"${jwt_payload}")
encoded_mac=$(echo -n "${encoded_jwt_parts}" | rs256_sign "${APP_PRIVATE_KEY}" | base64url)
generated_jwt="${encoded_jwt_parts}.${encoded_mac}"

auth_header="Authorization: Bearer ${generated_jwt}"

app_installations_response=$(curl -sX GET \
-H "${auth_header}" \
-H "${API_HEADER}" \
"${APP_INSTALLATIONS_URI}" \
)
access_token_url=$(echo "${app_installations_response}" | jq --raw-output '.[] | select (.account.login == "'"${APP_LOGIN}"'" and .app_id == '"${APP_ID}"') .access_tokens_url')
curl -sX POST \
-H "${CONTENT_LENGTH_HEADER}" \
-H "${auth_header}" \
-H "${API_HEADER}" \
"${access_token_url}" | \
jq --raw-output .token
}

request_access_token
74 changes: 74 additions & 0 deletions infrastructure/images/gh-runner/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/bash
# shellcheck shell=bash
export PATH=${PATH}:/actions-runner

##### ENVVARS
# APP_ID ID of app used for registering and setting up runner
# APP_PRIVATE_KEY Private key for app (should be kept in a vault)
# ORG_NAME Name of the org the runner should be added to (For repo runners please supply REPO_NAME as well)
# REPO_NAME (optional) Name of the repository to add this runner to. Leave unset for org runners
# RUNNER_NAME_PREFIX (optional) The name will have random string add after the prefix. Default: github-runner
# RUNNER_WORKDIR (optional) Work dir for the runner. Default: /_work/${RUNNER_NAME}
# LABELS (optional) Runner labels. Default: default
# RUNNER_GROUP (optional) Name of runner group. Default: Default
# RUNNER_GROUP_ID (optional) Id of runner group. Default: 1

# Un-export these, so that they must be passed explicitly to the environment of
# any command that needs them. This may help prevent leaks.
export -n ACCESS_TOKEN
export -n RUNNER_TOKEN
export -n APP_ID
export -n APP_PRIVATE_KEY

_RUNNER_NAME=${RUNNER_NAME_PREFIX:-github-runner}-$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13 ; echo '')
_RUNNER_WORKDIR=${RUNNER_WORKDIR:-/_work/${_RUNNER_NAME}}
_LABELS=${LABELS:-default}
_RUNNER_GROUP=${RUNNER_GROUP:-Default}
_RUNNER_GROUP_ID=${RUNNER_GROUP_ID:-1}
_GITHUB_HOST=${GITHUB_HOST:-"github.com"}
_BASE_URI="https://${_GITHUB_HOST}"

## Unset these, this may help prevent leaks
unset_env() {
unset ACCESS_TOKEN
unset RUNNER_TOKEN
unset APP_ID
unset APP_PRIVATE_KEY
}

[[ -z ${APP_ID} ]] && ( echo "APP_ID is required"; exit 1 )
[[ -z ${APP_PRIVATE_KEY} ]] && (echo "APP_PRIVATE_KEY is required"; exit 1)
[[ -z ${ORG_NAME} ]] && (echo "ORG_NAME is required, to define a Repo runner define REPO_NAME as well"; exit 1)

APP_LOGIN="${ORG_NAME}"

if [[ -z ${REPO_NAME} ]]; then
_REPO_URL="${_BASE_URI}/${ORG_NAME}"
RUNNER_SCOPE="org"
else
_REPO_URL="${_BASE_URI}/${ORG_NAME}/${REPO_NAME}"
RUNNER_SCOPE="repo"
fi

echo "Obtaining access token for app_id ${APP_ID} and login ${APP_LOGIN}"

ACCESS_TOKEN=$(APP_ID="${APP_ID}" APP_PRIVATE_KEY="${APP_PRIVATE_KEY//\\n/${nl}}" APP_LOGIN="${APP_LOGIN}" bash ./app-token.sh)


# Retrieve a short lived runner registration token using the APP_LOGIN
_TOKEN=$(ACCESS_TOKEN="${ACCESS_TOKEN}" REPO_URL="${_REPO_URL}" bash ./token.sh)
RUNNER_TOKEN=$(echo "${_TOKEN}" | jq -r .token)

./config.sh \
--url "${_REPO_URL}" \
--token $RUNNER_TOKEN \
--labels "${_LABELS}" \
--work "${_RUNNER_WORKDIR}" \
--name "${_RUNNER_NAME}" \
--runnergroup "${_RUNNER_GROUP}" \
--unattended \
--replace \
--ephemeral

unset_env
./run.sh
33 changes: 33 additions & 0 deletions infrastructure/images/gh-runner/scripts/token.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash

_GITHUB_HOST=${GITHUB_HOST:-"github.com"}
# If URL is not github.com then use the enterprise api endpoint
if [[ ${_GITHUB_HOST} = "github.com" ]]; then
URI="https://api.${_GITHUB_HOST}"
else
URI="https://${_GITHUB_HOST}/api/v3"
fi

API_VERSION=v3
API_HEADER="Accept: application/vnd.github.${API_VERSION}+json"
AUTH_HEADER="Authorization: token ${ACCESS_TOKEN}"
CONTENT_LENGTH_HEADER="Content-Length: 0"

case ${RUNNER_SCOPE} in
org*)
_FULL_URL="${URI}/orgs/${ORG_NAME}/actions/runners/registration-token"
;;

*)
_FULL_URL="${URI}/repos/${ORG_NAME}/${REPO_NAME}/actions/runners/registration-token"
;;
esac

RUNNER_TOKEN="$(curl -XPOST -fsSL \
-H "${CONTENT_LENGTH_HEADER}" \
-H "${AUTH_HEADER}" \
-H "${API_HEADER}" \
"${_FULL_URL}" \
| jq -r '.token')"

echo "{\"token\": \"${RUNNER_TOKEN}\", \"full_url\": \"${_FULL_URL}\"}"

0 comments on commit bf4ae3b

Please sign in to comment.