Compare commits

...

34 Commits

Author SHA1 Message Date
Nikola Jokic
ded39bede6 Prepare 0.12.1 release (#4153) 2025-06-27 13:49:47 +02:00
Nikola Jokic
9890c0592d Explicitly requeue during backoff ephemeral runner (#4152) 2025-06-27 12:05:43 +02:00
Nikola Jokic
3b5693eecb Remove check if runner exists after exit code 0 (#4142) 2025-06-27 11:11:39 +02:00
calx
e6e621a50a Remove duplicate float64 call (#4139) 2025-06-24 11:26:20 +02:00
Mark Huijgen
0b2534ebc9 Fix dind sidecar template (#4128) 2025-06-16 12:14:18 +02:00
Jeev B
e858d67926 Fix indentation of startupProbe attributes in dind sidecar (#4126) 2025-06-14 21:05:53 +02:00
Nikola Jokic
bc6c23609a Remove cache for build-push-action (#4124) 2025-06-13 15:26:55 +02:00
Nikola Jokic
666d0c52c4 Bump build-push-action to 6.18.0 (#4123) 2025-06-13 15:09:27 +02:00
Nikola Jokic
d9826e5244 Prepare 0.12.0 release (#4122) 2025-06-13 14:23:26 +02:00
dependabot[bot]
6f3882c482 Bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2 (#4120)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 21:43:46 +02:00
Nikola Jokic
e46c929241 Azure Key Vault integration to resolve secrets (#4090) 2025-06-11 15:53:33 +02:00
Wim Fournier
d4af75d82e Delete config secret when listener pod gets deleted (#4033)
Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com>
2025-06-11 15:53:04 +02:00
Nash Luffman
e335f53037 Add response body to error when fetching access token (#4005)
Co-authored-by: mluffman <mluffman@thoughtmachine.net>
Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com>
2025-06-11 15:52:38 +02:00
Tingluo Huang
c359d14e69 Avoid nil point when config.Metrics is nil and expose all metrics if none are configured (#4101)
Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com>
2025-06-11 15:51:26 +02:00
Nikola Jokic
9d8c59aeb3 Add startup probe to dind side-car (#4117) 2025-06-11 15:50:30 +02:00
dependabot[bot]
eef57e1a77 Bump github.com/cloudflare/circl from 1.6.0 to 1.6.1 (#4118)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 11:46:20 +02:00
Ryo Sakamoto
97697e80b4 Add job_workflow_ref label to listener metrics (#4054)
Signed-off-by: rskmm0chang <rskmm0chang@hatena.ne.jp>
2025-06-05 08:33:30 +02:00
github-actions[bot]
27b292bdd3 Updates: runner to v2.325.0 (#4109)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-03 11:05:40 -04:00
Nikola Jokic
1dbb88cb9e Allow use of client id as an app id (#4057) 2025-05-16 16:21:06 +02:00
Nikola Jokic
43f1cd0dac Refactor resource naming removing unnecessary calculations (#4076) 2025-05-15 10:56:34 +02:00
Nikola Jokic
389d842a30 Relax version requirements to allow patch version mismatch (#4080)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-14 21:38:16 +02:00
Nikola Jokic
f6f42dd4c1 Fix docker lint warnings (#4074) 2025-05-14 19:57:39 +02:00
github-actions[bot]
20e157fa72 Updates: runner to v2.324.0 container-hooks to v0.7.0 (#4086)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-14 10:05:33 -04:00
Nikola Jokic
cae7efa2c6 Create backoff mechanism for failed runners and allow re-creation of failed ephemeral runners (#4059) 2025-05-14 15:38:50 +02:00
Nikola Jokic
d6e2790db5 Bump go version (#4075) 2025-05-14 13:51:47 +02:00
scodef
a1a8dc5606 Add missing backtick to metrics.serviceMonitor.namespace line to correct formatting (#3790) 2025-05-07 14:52:42 +02:00
Mosè Giordano
16304b5ce7 Fix code block fences (#3140)
Co-authored-by: Mosè Giordano <giordano@users.noreply.github.com>
2025-05-06 13:47:13 +02:00
Borislav Velkov
32f19acc66 feat(helm): move dind to sidecar (#3842) 2025-04-23 14:51:01 +02:00
Ken Muse
46ee5cf9a2 Revised dashboard (#4022) 2025-04-23 11:36:05 +02:00
Ryosei Karaki
f832b0b254 upgrade(golangci-lint): v2.1.2 (#4023)
Signed-off-by: karamaru-alpha <mrnk3078@gmail.com>
2025-04-17 16:14:31 +02:00
Nikola Jokic
a33d34a036 Pin third party actions (#3981) 2025-04-17 12:19:15 +02:00
Nikola Jokic
15990d492d Include more context to errors raised by github/actions client (#4032)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-11 11:36:15 +02:00
dependabot[bot]
462db4dfc8 Bump the gomod group across 1 directory with 7 updates (#4008)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com>
2025-04-07 16:51:07 +02:00
David Maxwell
ea27448da5 Fix busy runners metric (#4016) 2025-04-04 17:17:09 +02:00
109 changed files with 5813 additions and 2969 deletions

View File

@@ -1,9 +1,9 @@
name: 'Setup ARC E2E Test Action' name: "Setup ARC E2E Test Action"
description: 'Build controller image, create kind cluster, load the image, and exchange ARC configure token.' description: "Build controller image, create kind cluster, load the image, and exchange ARC configure token."
inputs: inputs:
app-id: app-id:
description: 'GitHub App Id for exchange access token' description: "GitHub App Id for exchange access token"
required: true required: true
app-pk: app-pk:
description: "GitHub App private key for exchange access token" description: "GitHub App private key for exchange access token"
@@ -20,30 +20,31 @@ inputs:
outputs: outputs:
token: token:
description: 'Token to use for configure ARC' description: "Token to use for configure ARC"
value: ${{steps.config-token.outputs.token}} value: ${{steps.config-token.outputs.token}}
runs: runs:
using: "composite" using: "composite"
steps: steps:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2
with: with:
# Pinning v0.9.1 for Buildx and BuildKit v0.10.6 # Pinning v0.9.1 for Buildx and BuildKit v0.10.6
# BuildKit v0.11 which has a bug causing intermittent # BuildKit v0.11 which has a bug causing intermittent
# failures pushing images to GHCR # failures pushing images to GHCR
version: v0.9.1 version: v0.9.1
driver-opts: image=moby/buildkit:v0.10.6 driver-opts: image=moby/buildkit:v0.10.6
- name: Build controller image - name: Build controller image
uses: docker/build-push-action@v5 # https://github.com/docker/build-push-action/releases/tag/v6.18.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with: with:
file: Dockerfile file: Dockerfile
platforms: linux/amd64 platforms: linux/amd64
load: true load: true
build-args: | build-args: |
DOCKER_IMAGE_NAME=${{inputs.image-name}} DOCKER_IMAGE_NAME=${{inputs.image-name}}
VERSION=${{inputs.image-tag}} VERSION=${{inputs.image-tag}}
tags: | tags: |
${{inputs.image-name}}:${{inputs.image-tag}} ${{inputs.image-name}}:${{inputs.image-tag}}
no-cache: true no-cache: true
@@ -56,8 +57,9 @@ runs:
- name: Get configure token - name: Get configure token
id: config-token id: config-token
# https://github.com/peter-murray/workflow-application-token-action/releases/tag/v3.0.0
uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3 uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3
with: with:
application_id: ${{ inputs.app-id }} application_id: ${{ inputs.app-id }}
application_private_key: ${{ inputs.app-pk }} application_private_key: ${{ inputs.app-pk }}
organization: ${{ inputs.target-org}} organization: ${{ inputs.target-org}}

View File

@@ -24,23 +24,27 @@ runs:
shell: bash shell: bash
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 # https://github.com/docker/setup-qemu-action/releases/tag/v3.6.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 # https://github.com/docker/setup-buildx-action/releases/tag/v3.10.0
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2
with: with:
version: latest version: latest
- name: Login to DockerHub - name: Login to DockerHub
if: ${{ github.event_name == 'release' || github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.password != '' }} if: ${{ github.event_name == 'release' || github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.password != '' }}
uses: docker/login-action@v3 # https://github.com/docker/login-action/releases/tag/v3.4.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with: with:
username: ${{ inputs.username }} username: ${{ inputs.username }}
password: ${{ inputs.password }} password: ${{ inputs.password }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: ${{ github.event_name == 'release' || github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ghcr_password != '' }} if: ${{ github.event_name == 'release' || github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ghcr_password != '' }}
uses: docker/login-action@v3 # https://github.com/docker/login-action/releases/tag/v3.4.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ inputs.ghcr_username }} username: ${{ inputs.ghcr_username }}

View File

@@ -5,18 +5,18 @@ name: Publish ARC Helm Charts
on: on:
push: push:
branches: branches:
- master - master
paths: paths:
- 'charts/**' - "charts/**"
- '.github/workflows/arc-publish-chart.yaml' - ".github/workflows/arc-publish-chart.yaml"
- '!charts/actions-runner-controller/docs/**' - "!charts/actions-runner-controller/docs/**"
- '!charts/gha-runner-scale-set-controller/**' - "!charts/gha-runner-scale-set-controller/**"
- '!charts/gha-runner-scale-set/**' - "!charts/gha-runner-scale-set/**"
- '!**.md' - "!**.md"
workflow_dispatch: workflow_dispatch:
inputs: inputs:
force: force:
description: 'Force publish even if the chart version is not bumped' description: "Force publish even if the chart version is not bumped"
type: boolean type: boolean
required: true required: true
default: false default: false
@@ -39,86 +39,89 @@ jobs:
outputs: outputs:
publish-chart: ${{ steps.publish-chart-step.outputs.publish }} publish-chart: ${{ steps.publish-chart-step.outputs.publish }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Helm - name: Set up Helm
uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # Using https://github.com/Azure/setup-helm/releases/tag/v4.2.0
with: uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814
version: ${{ env.HELM_VERSION }} with:
version: ${{ env.HELM_VERSION }}
- name: Set up kube-score - name: Set up kube-score
run: | run: |
wget https://github.com/zegl/kube-score/releases/download/v${{ env.KUBE_SCORE_VERSION }}/kube-score_${{ env.KUBE_SCORE_VERSION }}_linux_amd64 -O kube-score wget https://github.com/zegl/kube-score/releases/download/v${{ env.KUBE_SCORE_VERSION }}/kube-score_${{ env.KUBE_SCORE_VERSION }}_linux_amd64 -O kube-score
chmod 755 kube-score chmod 755 kube-score
- name: Kube-score generated manifests - name: Kube-score generated manifests
run: helm template --values charts/.ci/values-kube-score.yaml charts/* | ./kube-score score - --ignore-test pod-networkpolicy --ignore-test deployment-has-poddisruptionbudget --ignore-test deployment-has-host-podantiaffinity --ignore-test container-security-context --ignore-test pod-probes --ignore-test container-image-tag --enable-optional-test container-security-context-privileged --enable-optional-test container-security-context-readonlyrootfilesystem run: helm template --values charts/.ci/values-kube-score.yaml charts/* | ./kube-score score - --ignore-test pod-networkpolicy --ignore-test deployment-has-poddisruptionbudget --ignore-test deployment-has-host-podantiaffinity --ignore-test container-security-context --ignore-test pod-probes --ignore-test container-image-tag --enable-optional-test container-security-context-privileged --enable-optional-test container-security-context-readonlyrootfilesystem
# python is a requirement for the chart-testing action below (supports yamllint among other tests) # python is a requirement for the chart-testing action below (supports yamllint among other tests)
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.11' python-version: "3.11"
- name: Set up chart-testing - name: Set up chart-testing
uses: helm/chart-testing-action@v2.6.0 # https://github.com/helm/chart-testing-action/releases/tag/v2.7.0
uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b
- name: Run chart-testing (list-changed) - name: Run chart-testing (list-changed)
id: list-changed id: list-changed
run: | run: |
changed=$(ct list-changed --config charts/.ci/ct-config.yaml) changed=$(ct list-changed --config charts/.ci/ct-config.yaml)
if [[ -n "$changed" ]]; then if [[ -n "$changed" ]]; then
echo "changed=true" >> $GITHUB_OUTPUT echo "changed=true" >> $GITHUB_OUTPUT
fi fi
- name: Run chart-testing (lint) - name: Run chart-testing (lint)
run: | run: |
ct lint --config charts/.ci/ct-config.yaml ct lint --config charts/.ci/ct-config.yaml
- name: Create kind cluster - name: Create kind cluster
if: steps.list-changed.outputs.changed == 'true' if: steps.list-changed.outputs.changed == 'true'
uses: helm/kind-action@v1.4.0 # https://github.com/helm/kind-action/releases/tag/v1.12.0
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3
# We need cert-manager already installed in the cluster because we assume the CRDs exist # We need cert-manager already installed in the cluster because we assume the CRDs exist
- name: Install cert-manager - name: Install cert-manager
if: steps.list-changed.outputs.changed == 'true' if: steps.list-changed.outputs.changed == 'true'
run: | run: |
helm repo add jetstack https://charts.jetstack.io --force-update helm repo add jetstack https://charts.jetstack.io --force-update
helm install cert-manager jetstack/cert-manager --set installCRDs=true --wait helm install cert-manager jetstack/cert-manager --set installCRDs=true --wait
- name: Run chart-testing (install) - name: Run chart-testing (install)
if: steps.list-changed.outputs.changed == 'true' if: steps.list-changed.outputs.changed == 'true'
run: ct install --config charts/.ci/ct-config.yaml run: ct install --config charts/.ci/ct-config.yaml
# WARNING: This relies on the latest release being at the top of the JSON from GitHub and a clean chart.yaml # WARNING: This relies on the latest release being at the top of the JSON from GitHub and a clean chart.yaml
- name: Check if Chart Publish is Needed - name: Check if Chart Publish is Needed
id: publish-chart-step id: publish-chart-step
run: | run: |
CHART_TEXT=$(curl -fs https://raw.githubusercontent.com/${{ github.repository }}/master/charts/actions-runner-controller/Chart.yaml) CHART_TEXT=$(curl -fs https://raw.githubusercontent.com/${{ github.repository }}/master/charts/actions-runner-controller/Chart.yaml)
NEW_CHART_VERSION=$(echo "$CHART_TEXT" | grep version: | cut -d ' ' -f 2) NEW_CHART_VERSION=$(echo "$CHART_TEXT" | grep version: | cut -d ' ' -f 2)
RELEASE_LIST=$(curl -fs https://api.github.com/repos/${{ github.repository }}/releases | jq .[].tag_name | grep actions-runner-controller | cut -d '"' -f 2 | cut -d '-' -f 4) RELEASE_LIST=$(curl -fs https://api.github.com/repos/${{ github.repository }}/releases | jq .[].tag_name | grep actions-runner-controller | cut -d '"' -f 2 | cut -d '-' -f 4)
LATEST_RELEASED_CHART_VERSION=$(echo $RELEASE_LIST | cut -d ' ' -f 1) LATEST_RELEASED_CHART_VERSION=$(echo $RELEASE_LIST | cut -d ' ' -f 1)
echo "CHART_VERSION_IN_MASTER=$NEW_CHART_VERSION" >> $GITHUB_ENV echo "CHART_VERSION_IN_MASTER=$NEW_CHART_VERSION" >> $GITHUB_ENV
echo "LATEST_CHART_VERSION=$LATEST_RELEASED_CHART_VERSION" >> $GITHUB_ENV echo "LATEST_CHART_VERSION=$LATEST_RELEASED_CHART_VERSION" >> $GITHUB_ENV
# Always publish if force is true # Always publish if force is true
if [[ $NEW_CHART_VERSION != $LATEST_RELEASED_CHART_VERSION || "${{ inputs.force }}" == "true" ]]; then if [[ $NEW_CHART_VERSION != $LATEST_RELEASED_CHART_VERSION || "${{ inputs.force }}" == "true" ]]; then
echo "publish=true" >> $GITHUB_OUTPUT echo "publish=true" >> $GITHUB_OUTPUT
else else
echo "publish=false" >> $GITHUB_OUTPUT echo "publish=false" >> $GITHUB_OUTPUT
fi fi
- name: Job summary - name: Job summary
run: | run: |
echo "Chart linting has been completed." >> $GITHUB_STEP_SUMMARY echo "Chart linting has been completed." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "**Status:**" >> $GITHUB_STEP_SUMMARY echo "**Status:**" >> $GITHUB_STEP_SUMMARY
echo "- chart version in master: ${{ env.CHART_VERSION_IN_MASTER }}" >> $GITHUB_STEP_SUMMARY echo "- chart version in master: ${{ env.CHART_VERSION_IN_MASTER }}" >> $GITHUB_STEP_SUMMARY
echo "- latest chart version: ${{ env.LATEST_CHART_VERSION }}" >> $GITHUB_STEP_SUMMARY echo "- latest chart version: ${{ env.LATEST_CHART_VERSION }}" >> $GITHUB_STEP_SUMMARY
echo "- publish new chart: ${{ steps.publish-chart-step.outputs.publish }}" >> $GITHUB_STEP_SUMMARY echo "- publish new chart: ${{ steps.publish-chart-step.outputs.publish }}" >> $GITHUB_STEP_SUMMARY
publish-chart: publish-chart:
if: needs.lint-chart.outputs.publish-chart == 'true' if: needs.lint-chart.outputs.publish-chart == 'true'
@@ -133,80 +136,81 @@ jobs:
CHART_TARGET_BRANCH: master CHART_TARGET_BRANCH: master
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Configure Git - name: Configure Git
run: | run: |
git config user.name "$GITHUB_ACTOR" git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com" git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Get Token - name: Get Token
id: get_workflow_token id: get_workflow_token
uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3 # https://github.com/peter-murray/workflow-application-token-action/releases/tag/v3.0.0
with: uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3
application_id: ${{ secrets.ACTIONS_ACCESS_APP_ID }} with:
application_private_key: ${{ secrets.ACTIONS_ACCESS_PK }} application_id: ${{ secrets.ACTIONS_ACCESS_APP_ID }}
organization: ${{ env.CHART_TARGET_ORG }} application_private_key: ${{ secrets.ACTIONS_ACCESS_PK }}
organization: ${{ env.CHART_TARGET_ORG }}
- name: Install chart-releaser - name: Install chart-releaser
uses: helm/chart-releaser-action@v1.4.1 uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f
with: with:
install_only: true install_only: true
install_dir: ${{ github.workspace }}/bin install_dir: ${{ github.workspace }}/bin
- name: Package and upload release assets - name: Package and upload release assets
run: | run: |
cr package \ cr package \
${{ github.workspace }}/charts/actions-runner-controller/ \ ${{ github.workspace }}/charts/actions-runner-controller/ \
--package-path .cr-release-packages --package-path .cr-release-packages
cr upload \ cr upload \
--owner "$(echo ${{ github.repository }} | cut -d '/' -f 1)" \ --owner "$(echo ${{ github.repository }} | cut -d '/' -f 1)" \
--git-repo "$(echo ${{ github.repository }} | cut -d '/' -f 2)" \ --git-repo "$(echo ${{ github.repository }} | cut -d '/' -f 2)" \
--package-path .cr-release-packages \ --package-path .cr-release-packages \
--token ${{ secrets.GITHUB_TOKEN }} --token ${{ secrets.GITHUB_TOKEN }}
- name: Generate updated index.yaml - name: Generate updated index.yaml
run: | run: |
cr index \ cr index \
--owner "$(echo ${{ github.repository }} | cut -d '/' -f 1)" \ --owner "$(echo ${{ github.repository }} | cut -d '/' -f 1)" \
--git-repo "$(echo ${{ github.repository }} | cut -d '/' -f 2)" \ --git-repo "$(echo ${{ github.repository }} | cut -d '/' -f 2)" \
--index-path ${{ github.workspace }}/index.yaml \ --index-path ${{ github.workspace }}/index.yaml \
--token ${{ secrets.GITHUB_TOKEN }} \ --token ${{ secrets.GITHUB_TOKEN }} \
--push \ --push \
--pages-branch 'gh-pages' \ --pages-branch 'gh-pages' \
--pages-index-path 'index.yaml' --pages-index-path 'index.yaml'
# Chart Release was never intended to publish to a different repo # Chart Release was never intended to publish to a different repo
# this workaround is intended to move the index.yaml to the target repo # this workaround is intended to move the index.yaml to the target repo
# where the github pages are hosted # where the github pages are hosted
- name: Checkout target repository - name: Checkout target repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: ${{ env.CHART_TARGET_ORG }}/${{ env.CHART_TARGET_REPO }} repository: ${{ env.CHART_TARGET_ORG }}/${{ env.CHART_TARGET_REPO }}
path: ${{ env.CHART_TARGET_REPO }} path: ${{ env.CHART_TARGET_REPO }}
ref: ${{ env.CHART_TARGET_BRANCH }} ref: ${{ env.CHART_TARGET_BRANCH }}
token: ${{ steps.get_workflow_token.outputs.token }} token: ${{ steps.get_workflow_token.outputs.token }}
- name: Copy index.yaml - name: Copy index.yaml
run: | run: |
cp ${{ github.workspace }}/index.yaml ${{ env.CHART_TARGET_REPO }}/actions-runner-controller/index.yaml cp ${{ github.workspace }}/index.yaml ${{ env.CHART_TARGET_REPO }}/actions-runner-controller/index.yaml
- name: Commit and push to target repository - name: Commit and push to target repository
run: | run: |
git config user.name "$GITHUB_ACTOR" git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com" git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
git add . git add .
git commit -m "Update index.yaml" git commit -m "Update index.yaml"
git push git push
working-directory: ${{ github.workspace }}/${{ env.CHART_TARGET_REPO }} working-directory: ${{ github.workspace }}/${{ env.CHART_TARGET_REPO }}
- name: Job summary - name: Job summary
run: | run: |
echo "New helm chart has been published" >> $GITHUB_STEP_SUMMARY echo "New helm chart has been published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "**Status:**" >> $GITHUB_STEP_SUMMARY echo "**Status:**" >> $GITHUB_STEP_SUMMARY
echo "- New [index.yaml](https://github.com/${{ env.CHART_TARGET_ORG }}/${{ env.CHART_TARGET_REPO }}/tree/master/actions-runner-controller) pushed" >> $GITHUB_STEP_SUMMARY echo "- New [index.yaml](https://github.com/${{ env.CHART_TARGET_ORG }}/${{ env.CHART_TARGET_REPO }}/tree/master/actions-runner-controller) pushed" >> $GITHUB_STEP_SUMMARY

View File

@@ -9,17 +9,17 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
release_tag_name: release_tag_name:
description: 'Tag name of the release to publish' description: "Tag name of the release to publish"
required: true required: true
push_to_registries: push_to_registries:
description: 'Push images to registries' description: "Push images to registries"
required: true required: true
type: boolean type: boolean
default: false default: false
permissions: permissions:
contents: write contents: write
packages: write packages: write
env: env:
TARGET_ORG: actions-runner-controller TARGET_ORG: actions-runner-controller
@@ -43,7 +43,7 @@ jobs:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: "go.mod"
- name: Install tools - name: Install tools
run: | run: |
@@ -73,6 +73,7 @@ jobs:
- name: Get Token - name: Get Token
id: get_workflow_token id: get_workflow_token
# https://github.com/peter-murray/workflow-application-token-action/releases/tag/v3.0.0
uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3 uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3
with: with:
application_id: ${{ secrets.ACTIONS_ACCESS_APP_ID }} application_id: ${{ secrets.ACTIONS_ACCESS_APP_ID }}

View File

@@ -7,10 +7,10 @@ on:
# are available to the workflow run # are available to the workflow run
push: push:
branches: branches:
- 'master' - "master"
paths: paths:
- 'runner/VERSION' - "runner/VERSION"
- '.github/workflows/arc-release-runners.yaml' - ".github/workflows/arc-release-runners.yaml"
env: env:
# Safeguard to prevent pushing images to registeries after build # Safeguard to prevent pushing images to registeries after build
@@ -39,6 +39,7 @@ jobs:
- name: Get Token - name: Get Token
id: get_workflow_token id: get_workflow_token
# https://github.com/peter-murray/workflow-application-token-action/releases/tag/v3.0.0
uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3 uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3
with: with:
application_id: ${{ secrets.ACTIONS_ACCESS_APP_ID }} application_id: ${{ secrets.ACTIONS_ACCESS_APP_ID }}

View File

@@ -5,20 +5,20 @@ on:
branches: branches:
- master - master
paths: paths:
- 'charts/**' - "charts/**"
- '.github/workflows/arc-validate-chart.yaml' - ".github/workflows/arc-validate-chart.yaml"
- '!charts/actions-runner-controller/docs/**' - "!charts/actions-runner-controller/docs/**"
- '!**.md' - "!**.md"
- '!charts/gha-runner-scale-set-controller/**' - "!charts/gha-runner-scale-set-controller/**"
- '!charts/gha-runner-scale-set/**' - "!charts/gha-runner-scale-set/**"
push: push:
paths: paths:
- 'charts/**' - "charts/**"
- '.github/workflows/arc-validate-chart.yaml' - ".github/workflows/arc-validate-chart.yaml"
- '!charts/actions-runner-controller/docs/**' - "!charts/actions-runner-controller/docs/**"
- '!**.md' - "!**.md"
- '!charts/gha-runner-scale-set-controller/**' - "!charts/gha-runner-scale-set-controller/**"
- '!charts/gha-runner-scale-set/**' - "!charts/gha-runner-scale-set/**"
workflow_dispatch: workflow_dispatch:
env: env:
KUBE_SCORE_VERSION: 1.10.0 KUBE_SCORE_VERSION: 1.10.0
@@ -45,34 +45,19 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Helm - name: Set up Helm
# Using https://github.com/Azure/setup-helm/releases/tag/v4.2 # Using https://github.com/Azure/setup-helm/releases/tag/v4.2.0
uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814
with: with:
version: ${{ env.HELM_VERSION }} version: ${{ env.HELM_VERSION }}
- name: Set up kube-score
run: |
wget https://github.com/zegl/kube-score/releases/download/v${{ env.KUBE_SCORE_VERSION }}/kube-score_${{ env.KUBE_SCORE_VERSION }}_linux_amd64 -O kube-score
chmod 755 kube-score
- name: Kube-score generated manifests
run: helm template --values charts/.ci/values-kube-score.yaml charts/* | ./kube-score score -
--ignore-test pod-networkpolicy
--ignore-test deployment-has-poddisruptionbudget
--ignore-test deployment-has-host-podantiaffinity
--ignore-test container-security-context
--ignore-test pod-probes
--ignore-test container-image-tag
--enable-optional-test container-security-context-privileged
--enable-optional-test container-security-context-readonlyrootfilesystem
# python is a requirement for the chart-testing action below (supports yamllint among other tests) # python is a requirement for the chart-testing action below (supports yamllint among other tests)
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.11' python-version: "3.11"
- name: Set up chart-testing - name: Set up chart-testing
uses: helm/chart-testing-action@v2.6.0 # https://github.com/helm/chart-testing-action/releases/tag/v2.7.0
uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b
- name: Run chart-testing (list-changed) - name: Run chart-testing (list-changed)
id: list-changed id: list-changed
@@ -87,7 +72,8 @@ jobs:
ct lint --config charts/.ci/ct-config.yaml ct lint --config charts/.ci/ct-config.yaml
- name: Create kind cluster - name: Create kind cluster
uses: helm/kind-action@v1.4.0 # https://github.com/helm/kind-action/releases/tag/v1.12.0
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3
if: steps.list-changed.outputs.changed == 'true' if: steps.list-changed.outputs.changed == 'true'
# We need cert-manager already installed in the cluster because we assume the CRDs exist # We need cert-manager already installed in the cluster because we assume the CRDs exist

View File

@@ -3,17 +3,17 @@ name: Validate ARC Runners
on: on:
pull_request: pull_request:
branches: branches:
- '**' - "**"
paths: paths:
- 'runner/**' - "runner/**"
- 'test/startup/**' - "test/startup/**"
- '!**.md' - "!**.md"
permissions: permissions:
contents: read contents: read
concurrency: concurrency:
# This will make sure we only apply the concurrency limits on pull requests # This will make sure we only apply the concurrency limits on pull requests
# but not pushes to master branch by making the concurrency group name unique # but not pushes to master branch by making the concurrency group name unique
# for pushes # for pushes
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -25,28 +25,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: shellcheck - name: "Run shellcheck"
uses: reviewdog/action-shellcheck@v1 run: make shellcheck
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
path: "./runner"
pattern: |
*.sh
*.bash
update-status
# Make this consistent with `make shellsheck`
shellcheck_flags: "--shell bash --source-path runner"
exclude: "./.git/*"
check_all_files_with_shebangs: "false"
# Set this to "true" once we addressed all the shellcheck findings
fail_on_error: "false"
test-runner-entrypoint: test-runner-entrypoint:
name: Test entrypoint name: Test entrypoint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run tests - name: Run tests
run: | run: |
make acceptance/runner/startup make acceptance/runner/startup

View File

@@ -16,7 +16,7 @@ env:
TARGET_ORG: actions-runner-controller TARGET_ORG: actions-runner-controller
TARGET_REPO: arc_e2e_test_dummy TARGET_REPO: arc_e2e_test_dummy
IMAGE_NAME: "arc-test-image" IMAGE_NAME: "arc-test-image"
IMAGE_VERSION: "0.11.0" IMAGE_VERSION: "0.12.1"
concurrency: concurrency:
# This will make sure we only apply the concurrency limits on pull requests # This will make sure we only apply the concurrency limits on pull requests

View File

@@ -4,27 +4,27 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
ref: ref:
description: 'The branch, tag or SHA to cut a release from' description: "The branch, tag or SHA to cut a release from"
required: false required: false
type: string type: string
default: '' default: ""
release_tag_name: release_tag_name:
description: 'The name to tag the controller image with' description: "The name to tag the controller image with"
required: true required: true
type: string type: string
default: 'canary' default: "canary"
push_to_registries: push_to_registries:
description: 'Push images to registries' description: "Push images to registries"
required: true required: true
type: boolean type: boolean
default: false default: false
publish_gha_runner_scale_set_controller_chart: publish_gha_runner_scale_set_controller_chart:
description: 'Publish new helm chart for gha-runner-scale-set-controller' description: "Publish new helm chart for gha-runner-scale-set-controller"
required: true required: true
type: boolean type: boolean
default: false default: false
publish_gha_runner_scale_set_chart: publish_gha_runner_scale_set_chart:
description: 'Publish new helm chart for gha-runner-scale-set' description: "Publish new helm chart for gha-runner-scale-set"
required: true required: true
type: boolean type: boolean
default: false default: false
@@ -72,10 +72,11 @@ jobs:
echo "repository_owner=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT echo "repository_owner=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 # https://github.com/docker/setup-qemu-action/releases/tag/v3.6.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2
with: with:
# Pinning v0.9.1 for Buildx and BuildKit v0.10.6 # Pinning v0.9.1 for Buildx and BuildKit v0.10.6
# BuildKit v0.11 which has a bug causing intermittent # BuildKit v0.11 which has a bug causing intermittent
@@ -84,14 +85,16 @@ jobs:
driver-opts: image=moby/buildkit:v0.10.6 driver-opts: image=moby/buildkit:v0.10.6
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 # https://github.com/docker/login-action/releases/tag/v3.4.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push controller image - name: Build & push controller image
uses: docker/build-push-action@v5 # https://github.com/docker/build-push-action/releases/tag/v6.18.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with: with:
file: Dockerfile file: Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
@@ -100,8 +103,6 @@ jobs:
tags: | tags: |
ghcr.io/${{ steps.resolve_parameters.outputs.repository_owner }}/gha-runner-scale-set-controller:${{ inputs.release_tag_name }} ghcr.io/${{ steps.resolve_parameters.outputs.repository_owner }}/gha-runner-scale-set-controller:${{ inputs.release_tag_name }}
ghcr.io/${{ steps.resolve_parameters.outputs.repository_owner }}/gha-runner-scale-set-controller:${{ inputs.release_tag_name }}-${{ steps.resolve_parameters.outputs.short_sha }} ghcr.io/${{ steps.resolve_parameters.outputs.repository_owner }}/gha-runner-scale-set-controller:${{ inputs.release_tag_name }}-${{ steps.resolve_parameters.outputs.short_sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Job summary - name: Job summary
run: | run: |
@@ -140,7 +141,7 @@ jobs:
echo "repository_owner=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT echo "repository_owner=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Set up Helm - name: Set up Helm
# Using https://github.com/Azure/setup-helm/releases/tag/v4.2 # Using https://github.com/Azure/setup-helm/releases/tag/v4.2.0
uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814
with: with:
version: ${{ env.HELM_VERSION }} version: ${{ env.HELM_VERSION }}
@@ -188,7 +189,7 @@ jobs:
echo "repository_owner=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT echo "repository_owner=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Set up Helm - name: Set up Helm
# Using https://github.com/Azure/setup-helm/releases/tag/v4.2 # Using https://github.com/Azure/setup-helm/releases/tag/v4.2.0
uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814
with: with:
version: ${{ env.HELM_VERSION }} version: ${{ env.HELM_VERSION }}

View File

@@ -5,16 +5,16 @@ on:
branches: branches:
- master - master
paths: paths:
- 'charts/**' - "charts/**"
- '.github/workflows/gha-validate-chart.yaml' - ".github/workflows/gha-validate-chart.yaml"
- '!charts/actions-runner-controller/**' - "!charts/actions-runner-controller/**"
- '!**.md' - "!**.md"
push: push:
paths: paths:
- 'charts/**' - "charts/**"
- '.github/workflows/gha-validate-chart.yaml' - ".github/workflows/gha-validate-chart.yaml"
- '!charts/actions-runner-controller/**' - "!charts/actions-runner-controller/**"
- '!**.md' - "!**.md"
workflow_dispatch: workflow_dispatch:
env: env:
KUBE_SCORE_VERSION: 1.16.1 KUBE_SCORE_VERSION: 1.16.1
@@ -41,7 +41,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Helm - name: Set up Helm
# Using https://github.com/Azure/setup-helm/releases/tag/v4.2 # Using https://github.com/Azure/setup-helm/releases/tag/v4.2.0
uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814
with: with:
version: ${{ env.HELM_VERSION }} version: ${{ env.HELM_VERSION }}
@@ -49,10 +49,11 @@ jobs:
# python is a requirement for the chart-testing action below (supports yamllint among other tests) # python is a requirement for the chart-testing action below (supports yamllint among other tests)
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.11' python-version: "3.11"
- name: Set up chart-testing - name: Set up chart-testing
uses: helm/chart-testing-action@v2.6.0 # https://github.com/helm/chart-testing-action/releases/tag/v2.7.0
uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b
- name: Run chart-testing (list-changed) - name: Run chart-testing (list-changed)
id: list-changed id: list-changed
@@ -68,13 +69,14 @@ jobs:
ct lint --config charts/.ci/ct-config-gha.yaml ct lint --config charts/.ci/ct-config-gha.yaml
- name: Set up docker buildx - name: Set up docker buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2
if: steps.list-changed.outputs.changed == 'true' if: steps.list-changed.outputs.changed == 'true'
with: with:
version: latest version: latest
- name: Build controller image - name: Build controller image
uses: docker/build-push-action@v5 # https://github.com/docker/build-push-action/releases/tag/v6.18.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
if: steps.list-changed.outputs.changed == 'true' if: steps.list-changed.outputs.changed == 'true'
with: with:
file: Dockerfile file: Dockerfile
@@ -89,7 +91,8 @@ jobs:
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
- name: Create kind cluster - name: Create kind cluster
uses: helm/kind-action@v1.4.0 # https://github.com/helm/kind-action/releases/tag/v1.12.0
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3
if: steps.list-changed.outputs.changed == 'true' if: steps.list-changed.outputs.changed == 'true'
with: with:
cluster_name: chart-testing cluster_name: chart-testing
@@ -97,11 +100,11 @@ jobs:
- name: Load image into cluster - name: Load image into cluster
if: steps.list-changed.outputs.changed == 'true' if: steps.list-changed.outputs.changed == 'true'
run: | run: |
export DOCKER_IMAGE_NAME=test-arc export DOCKER_IMAGE_NAME=test-arc
export VERSION=dev export VERSION=dev
export IMG_RESULT=load export IMG_RESULT=load
make docker-buildx make docker-buildx
kind load docker-image test-arc:dev --name chart-testing kind load docker-image test-arc:dev --name chart-testing
- name: Run chart-testing (install) - name: Run chart-testing (install)
if: steps.list-changed.outputs.changed == 'true' if: steps.list-changed.outputs.changed == 'true'

View File

@@ -7,30 +7,30 @@ on:
branches: branches:
- master - master
paths-ignore: paths-ignore:
- '**.md' - "**.md"
- '.github/actions/**' - ".github/actions/**"
- '.github/ISSUE_TEMPLATE/**' - ".github/ISSUE_TEMPLATE/**"
- '.github/workflows/e2e-test-dispatch-workflow.yaml' - ".github/workflows/e2e-test-dispatch-workflow.yaml"
- '.github/workflows/gha-e2e-tests.yaml' - ".github/workflows/gha-e2e-tests.yaml"
- '.github/workflows/arc-publish.yaml' - ".github/workflows/arc-publish.yaml"
- '.github/workflows/arc-publish-chart.yaml' - ".github/workflows/arc-publish-chart.yaml"
- '.github/workflows/gha-publish-chart.yaml' - ".github/workflows/gha-publish-chart.yaml"
- '.github/workflows/arc-release-runners.yaml' - ".github/workflows/arc-release-runners.yaml"
- '.github/workflows/global-run-codeql.yaml' - ".github/workflows/global-run-codeql.yaml"
- '.github/workflows/global-run-first-interaction.yaml' - ".github/workflows/global-run-first-interaction.yaml"
- '.github/workflows/global-run-stale.yaml' - ".github/workflows/global-run-stale.yaml"
- '.github/workflows/arc-update-runners-scheduled.yaml' - ".github/workflows/arc-update-runners-scheduled.yaml"
- '.github/workflows/validate-arc.yaml' - ".github/workflows/validate-arc.yaml"
- '.github/workflows/arc-validate-chart.yaml' - ".github/workflows/arc-validate-chart.yaml"
- '.github/workflows/gha-validate-chart.yaml' - ".github/workflows/gha-validate-chart.yaml"
- '.github/workflows/arc-validate-runners.yaml' - ".github/workflows/arc-validate-runners.yaml"
- '.github/dependabot.yml' - ".github/dependabot.yml"
- '.github/RELEASE_NOTE_TEMPLATE.md' - ".github/RELEASE_NOTE_TEMPLATE.md"
- 'runner/**' - "runner/**"
- '.gitignore' - ".gitignore"
- 'PROJECT' - "PROJECT"
- 'LICENSE' - "LICENSE"
- 'Makefile' - "Makefile"
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps # https://docs.github.com/en/rest/overview/permissions-required-for-github-apps
permissions: permissions:
@@ -59,6 +59,7 @@ jobs:
- name: Get Token - name: Get Token
id: get_workflow_token id: get_workflow_token
# https://github.com/peter-murray/workflow-application-token-action/releases/tag/v3.0.0
uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3 uses: peter-murray/workflow-application-token-action@dc0413987a085fa17d19df9e47d4677cf81ffef3
with: with:
application_id: ${{ secrets.ACTIONS_ACCESS_APP_ID }} application_id: ${{ secrets.ACTIONS_ACCESS_APP_ID }}
@@ -93,7 +94,8 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 # https://github.com/docker/login-action/releases/tag/v3.4.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -110,16 +112,19 @@ jobs:
echo "repository_owner=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT echo "repository_owner=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 # https://github.com/docker/setup-qemu-action/releases/tag/v3.6.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 # https://github.com/docker/setup-buildx-action/releases/tag/v3.10.0
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2
with: with:
version: latest version: latest
# Unstable builds - run at your own risk # Unstable builds - run at your own risk
- name: Build and Push - name: Build and Push
uses: docker/build-push-action@v5 # https://github.com/docker/build-push-action/releases/tag/v6.18.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile

View File

@@ -4,16 +4,16 @@ on:
branches: branches:
- master - master
paths: paths:
- '.github/workflows/go.yaml' - ".github/workflows/go.yaml"
- '**.go' - "**.go"
- 'go.mod' - "go.mod"
- 'go.sum' - "go.sum"
pull_request: pull_request:
paths: paths:
- '.github/workflows/go.yaml' - ".github/workflows/go.yaml"
- '**.go' - "**.go"
- 'go.mod' - "go.mod"
- 'go.sum' - "go.sum"
permissions: permissions:
contents: read contents: read
@@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: "go.mod"
cache: false cache: false
- name: fmt - name: fmt
run: go fmt ./... run: go fmt ./...
@@ -45,13 +45,14 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: "go.mod"
cache: false cache: false
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 # https://github.com/golangci/golangci-lint-action/releases/tag/v7.0.0
uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd
with: with:
only-new-issues: true only-new-issues: true
version: v1.55.2 version: v2.1.2
generate: generate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -59,7 +60,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: "go.mod"
cache: false cache: false
- name: Generate - name: Generate
run: make generate run: make generate
@@ -72,7 +73,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: "go.mod"
- run: make manifests - run: make manifests
- name: Check diff - name: Check diff
run: git diff --exit-code run: git diff --exit-code

View File

@@ -1,19 +1,14 @@
version: "2"
run: run:
timeout: 3m timeout: 5m
output: linters:
formats: settings:
- format: github-actions errcheck:
path: stdout exclude-functions:
linters-settings: - (net/http.ResponseWriter).Write
errcheck: - (*net/http.Server).Shutdown
exclude-functions: - (*github.com/actions/actions-runner-controller/simulator.VisibleRunnerGroups).Add
- (net/http.ResponseWriter).Write - (*github.com/actions/actions-runner-controller/testing.Kind).Stop
- (*net/http.Server).Shutdown exclusions:
- (*github.com/actions/actions-runner-controller/simulator.VisibleRunnerGroups).Add presets:
- (*github.com/actions/actions-runner-controller/testing.Kind).Stop - std-error-handling
issues:
exclude-rules:
- path: controllers/suite_test.go
linters:
- staticcheck
text: "SA1019"

View File

@@ -1,5 +1,5 @@
# Build the manager binary # Build the manager binary
FROM --platform=$BUILDPLATFORM golang:1.24.0 as builder FROM --platform=$BUILDPLATFORM golang:1.24.3 AS builder
WORKDIR /workspace WORKDIR /workspace
@@ -30,7 +30,7 @@ ARG TARGETPLATFORM TARGETOS TARGETARCH TARGETVARIANT VERSION=dev COMMIT_SHA=dev
# to avoid https://github.com/moby/buildkit/issues/2334 # to avoid https://github.com/moby/buildkit/issues/2334
# We can use docker layer cache so the build is fast enogh anyway # We can use docker layer cache so the build is fast enogh anyway
# We also use per-platform GOCACHE for the same reason. # We also use per-platform GOCACHE for the same reason.
ENV GOCACHE /build/${TARGETPLATFORM}/root/.cache/go-build ENV GOCACHE="/build/${TARGETPLATFORM}/root/.cache/go-build"
# Build # Build
RUN --mount=target=. \ RUN --mount=target=. \

View File

@@ -6,7 +6,7 @@ endif
DOCKER_USER ?= $(shell echo ${DOCKER_IMAGE_NAME} | cut -d / -f1) DOCKER_USER ?= $(shell echo ${DOCKER_IMAGE_NAME} | cut -d / -f1)
VERSION ?= dev VERSION ?= dev
COMMIT_SHA = $(shell git rev-parse HEAD) COMMIT_SHA = $(shell git rev-parse HEAD)
RUNNER_VERSION ?= 2.323.0 RUNNER_VERSION ?= 2.325.0
TARGETPLATFORM ?= $(shell arch) TARGETPLATFORM ?= $(shell arch)
RUNNER_NAME ?= ${DOCKER_USER}/actions-runner RUNNER_NAME ?= ${DOCKER_USER}/actions-runner
RUNNER_TAG ?= ${VERSION} RUNNER_TAG ?= ${VERSION}
@@ -20,7 +20,7 @@ KUBECONTEXT ?= kind-acceptance
CLUSTER ?= acceptance CLUSTER ?= acceptance
CERT_MANAGER_VERSION ?= v1.1.1 CERT_MANAGER_VERSION ?= v1.1.1
KUBE_RBAC_PROXY_VERSION ?= v0.11.0 KUBE_RBAC_PROXY_VERSION ?= v0.11.0
SHELLCHECK_VERSION ?= 0.8.0 SHELLCHECK_VERSION ?= 0.10.0
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion) # Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:generateEmbeddedObjectMeta=true,allowDangerousTypes=true" CRD_OPTIONS ?= "crd:generateEmbeddedObjectMeta=true,allowDangerousTypes=true"
@@ -68,7 +68,7 @@ endif
all: manager all: manager
lint: lint:
docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.57.2 golangci-lint run docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:v2.1.2 golangci-lint run
GO_TEST_ARGS ?= -short GO_TEST_ARGS ?= -short
@@ -204,7 +204,7 @@ generate: controller-gen
# Run shellcheck on runner scripts # Run shellcheck on runner scripts
shellcheck: shellcheck-install shellcheck: shellcheck-install
$(TOOLS_PATH)/shellcheck --shell bash --source-path runner runner/*.sh hack/*.sh $(TOOLS_PATH)/shellcheck --shell bash --source-path runner runner/*.sh runner/update-status hack/*.sh
docker-buildx: docker-buildx:
export DOCKER_CLI_EXPERIMENTAL=enabled ;\ export DOCKER_CLI_EXPERIMENTAL=enabled ;\
@@ -310,7 +310,7 @@ github-release: release
# Otherwise we get errors like the below: # Otherwise we get errors like the below:
# Error: failed to install CRD crds/actions.summerwind.dev_runnersets.yaml: CustomResourceDefinition.apiextensions.k8s.io "runnersets.actions.summerwind.dev" is invalid: [spec.validation.openAPIV3Schema.properties[spec].properties[template].properties[spec].properties[containers].items.properties[ports].items.properties[protocol].default: Required value: this property is in x-kubernetes-list-map-keys, so it must have a default or be a required property, spec.validation.openAPIV3Schema.properties[spec].properties[template].properties[spec].properties[initContainers].items.properties[ports].items.properties[protocol].default: Required value: this property is in x-kubernetes-list-map-keys, so it must have a default or be a required property] # Error: failed to install CRD crds/actions.summerwind.dev_runnersets.yaml: CustomResourceDefinition.apiextensions.k8s.io "runnersets.actions.summerwind.dev" is invalid: [spec.validation.openAPIV3Schema.properties[spec].properties[template].properties[spec].properties[containers].items.properties[ports].items.properties[protocol].default: Required value: this property is in x-kubernetes-list-map-keys, so it must have a default or be a required property, spec.validation.openAPIV3Schema.properties[spec].properties[template].properties[spec].properties[initContainers].items.properties[ports].items.properties[protocol].default: Required value: this property is in x-kubernetes-list-map-keys, so it must have a default or be a required property]
# #
# Note that controller-gen newer than 0.6.2 is needed due to https://github.com/kubernetes-sigs/controller-tools/issues/448 # Note that controller-gen newer than 0.7.0 is needed due to https://github.com/kubernetes-sigs/controller-tools/issues/448
# Otherwise ObjectMeta embedded in Spec results in empty on the storage. # Otherwise ObjectMeta embedded in Spec results in empty on the storage.
controller-gen: controller-gen:
ifeq (, $(shell which controller-gen)) ifeq (, $(shell which controller-gen))

View File

@@ -5,22 +5,23 @@ on:
env: env:
IRSA_ROLE_ARN: IRSA_ROLE_ARN:
ASSUME_ROLE_ARN: ASSUME_ROLE_ARN:
AWS_REGION: AWS_REGION:
jobs: jobs:
assume-role-in-runner-test: assume-role-in-runner-test:
runs-on: ['self-hosted', 'Linux'] runs-on: ["self-hosted", "Linux"]
steps: steps:
- name: Test aws-actions/configure-aws-credentials Action - name: Test aws-actions/configure-aws-credentials Action
uses: aws-actions/configure-aws-credentials@v1 # https://github.com/aws-actions/configure-aws-credentials/releases/tag/v4.1.0
uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722
with: with:
aws-region: ${{ env.AWS_REGION }} aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ env.ASSUME_ROLE_ARN }} role-to-assume: ${{ env.ASSUME_ROLE_ARN }}
role-duration-seconds: 900 role-duration-seconds: 900
assume-role-in-container-test: assume-role-in-container-test:
runs-on: ['self-hosted', 'Linux'] runs-on: ["self-hosted", "Linux"]
container: container:
image: amazon/aws-cli image: amazon/aws-cli
env: env:
AWS_WEB_IDENTITY_TOKEN_FILE: /var/run/secrets/eks.amazonaws.com/serviceaccount/token AWS_WEB_IDENTITY_TOKEN_FILE: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
@@ -29,7 +30,8 @@ jobs:
- /var/run/secrets/eks.amazonaws.com/serviceaccount/token:/var/run/secrets/eks.amazonaws.com/serviceaccount/token - /var/run/secrets/eks.amazonaws.com/serviceaccount/token:/var/run/secrets/eks.amazonaws.com/serviceaccount/token
steps: steps:
- name: Test aws-actions/configure-aws-credentials Action in container - name: Test aws-actions/configure-aws-credentials Action in container
uses: aws-actions/configure-aws-credentials@v1 # https://github.com/aws-actions/configure-aws-credentials/releases/tag/v4.1.0
uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722
with: with:
aws-region: ${{ env.AWS_REGION }} aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ env.ASSUME_ROLE_ARN }} role-to-assume: ${{ env.ASSUME_ROLE_ARN }}

View File

@@ -8,8 +8,8 @@ env:
jobs: jobs:
run-step-in-container-test: run-step-in-container-test:
runs-on: ['self-hosted', 'Linux'] runs-on: ["self-hosted", "Linux"]
container: container:
image: alpine image: alpine
steps: steps:
- name: Test we are working in the container - name: Test we are working in the container
@@ -21,7 +21,7 @@ jobs:
exit 1 exit 1
fi fi
setup-python-test: setup-python-test:
runs-on: ['self-hosted', 'Linux'] runs-on: ["self-hosted", "Linux"]
steps: steps:
- name: Print native Python environment - name: Print native Python environment
run: | run: |
@@ -41,12 +41,12 @@ jobs:
echo "Python version detected : $(python --version 2>&1)" echo "Python version detected : $(python --version 2>&1)"
fi fi
setup-node-test: setup-node-test:
runs-on: ['self-hosted', 'Linux'] runs-on: ["self-hosted", "Linux"]
steps: steps:
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '12' node-version: "12"
- name: Test actions/setup-node works - name: Test actions/setup-node works
run: | run: |
VERSION=$(node --version | cut -c 2- | cut -d '.' -f1) VERSION=$(node --version | cut -c 2- | cut -d '.' -f1)
if [[ $VERSION != '12' ]]; then if [[ $VERSION != '12' ]]; then
@@ -57,13 +57,14 @@ jobs:
echo "Node version detected : $(node --version 2>&1)" echo "Node version detected : $(node --version 2>&1)"
fi fi
setup-ruby-test: setup-ruby-test:
runs-on: ['self-hosted', 'Linux'] runs-on: ["self-hosted", "Linux"]
steps: steps:
- uses: ruby/setup-ruby@v1 # https://github.com/ruby/setup-ruby/releases/tag/v1.227.0
- uses: ruby/setup-ruby@1a615958ad9d422dd932dc1d5823942ee002799f
with: with:
ruby-version: 3.0 ruby-version: 3.0
bundler-cache: true bundler-cache: true
- name: Test ruby/setup-ruby works - name: Test ruby/setup-ruby works
run: | run: |
VERSION=$(ruby --version | cut -d ' ' -f2 | cut -d '.' -f1-2) VERSION=$(ruby --version | cut -d ' ' -f2 | cut -d '.' -f1-2)
if [[ $VERSION != '3.0' ]]; then if [[ $VERSION != '3.0' ]]; then
@@ -74,8 +75,8 @@ jobs:
echo "Ruby version detected : $(ruby --version 2>&1)" echo "Ruby version detected : $(ruby --version 2>&1)"
fi fi
python-shell-test: python-shell-test:
runs-on: ['self-hosted', 'Linux'] runs-on: ["self-hosted", "Linux"]
steps: steps:
- name: Test Python shell works - name: Test Python shell works
run: | run: |
import os import os

View File

@@ -0,0 +1,89 @@
package appconfig
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
corev1 "k8s.io/api/core/v1"
)
type AppConfig struct {
AppID string `json:"github_app_id"`
AppInstallationID int64 `json:"github_app_installation_id"`
AppPrivateKey string `json:"github_app_private_key"`
Token string `json:"github_token"`
}
func (c *AppConfig) tidy() *AppConfig {
if len(c.Token) > 0 {
return &AppConfig{
Token: c.Token,
}
}
return &AppConfig{
AppID: c.AppID,
AppInstallationID: c.AppInstallationID,
AppPrivateKey: c.AppPrivateKey,
}
}
func (c *AppConfig) Validate() error {
if c == nil {
return fmt.Errorf("missing app config")
}
hasToken := len(c.Token) > 0
hasGitHubAppAuth := c.hasGitHubAppAuth()
if hasToken && hasGitHubAppAuth {
return fmt.Errorf("both PAT and GitHub App credentials provided. should only provide one")
}
if !hasToken && !hasGitHubAppAuth {
return fmt.Errorf("no credentials provided: either a PAT or GitHub App credentials should be provided")
}
return nil
}
func (c *AppConfig) hasGitHubAppAuth() bool {
return len(c.AppID) > 0 && c.AppInstallationID > 0 && len(c.AppPrivateKey) > 0
}
func FromSecret(secret *corev1.Secret) (*AppConfig, error) {
var appInstallationID int64
if v := string(secret.Data["github_app_installation_id"]); v != "" {
val, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, err
}
appInstallationID = val
}
cfg := &AppConfig{
Token: string(secret.Data["github_token"]),
AppID: string(secret.Data["github_app_id"]),
AppInstallationID: appInstallationID,
AppPrivateKey: string(secret.Data["github_app_private_key"]),
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate config: %v", err)
}
return cfg.tidy(), nil
}
func FromJSONString(v string) (*AppConfig, error) {
var appConfig AppConfig
if err := json.NewDecoder(bytes.NewBufferString(v)).Decode(&appConfig); err != nil {
return nil, err
}
if err := appConfig.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate app config decoded from string: %w", err)
}
return appConfig.tidy(), nil
}

View File

@@ -0,0 +1,152 @@
package appconfig
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
)
func TestAppConfigValidate_invalid(t *testing.T) {
tt := map[string]*AppConfig{
"empty": {},
"token and app config": {
AppID: "1",
AppInstallationID: 2,
AppPrivateKey: "private key",
Token: "token",
},
"app id not set": {
AppInstallationID: 2,
AppPrivateKey: "private key",
},
"app installation id not set": {
AppID: "2",
AppPrivateKey: "private key",
},
"private key empty": {
AppID: "2",
AppInstallationID: 1,
AppPrivateKey: "",
},
}
for name, cfg := range tt {
t.Run(name, func(t *testing.T) {
err := cfg.Validate()
require.Error(t, err)
})
}
}
func TestAppConfigValidate_valid(t *testing.T) {
tt := map[string]*AppConfig{
"token": {
Token: "token",
},
"app ID": {
AppID: "1",
AppInstallationID: 2,
AppPrivateKey: "private key",
},
}
for name, cfg := range tt {
t.Run(name, func(t *testing.T) {
err := cfg.Validate()
require.NoError(t, err)
})
}
}
func TestAppConfigFromSecret_invalid(t *testing.T) {
tt := map[string]map[string]string{
"empty": {},
"token and app provided": {
"github_token": "token",
"github_app_id": "2",
"githu_app_installation_id": "3",
"github_app_private_key": "private key",
},
"invalid app id": {
"github_app_id": "abc",
"githu_app_installation_id": "3",
"github_app_private_key": "private key",
},
"invalid app installation_id": {
"github_app_id": "1",
"githu_app_installation_id": "abc",
"github_app_private_key": "private key",
},
"empty private key": {
"github_app_id": "1",
"githu_app_installation_id": "2",
"github_app_private_key": "",
},
}
for name, data := range tt {
t.Run(name, func(t *testing.T) {
secret := &corev1.Secret{
StringData: data,
}
appConfig, err := FromSecret(secret)
assert.Error(t, err)
assert.Nil(t, appConfig)
})
}
}
func TestAppConfigFromSecret_valid(t *testing.T) {
tt := map[string]map[string]string{
"with token": {
"github_token": "token",
},
"app config": {
"github_app_id": "2",
"githu_app_installation_id": "3",
"github_app_private_key": "private key",
},
}
for name, data := range tt {
t.Run(name, func(t *testing.T) {
secret := &corev1.Secret{
StringData: data,
}
appConfig, err := FromSecret(secret)
assert.Error(t, err)
assert.Nil(t, appConfig)
})
}
}
func TestAppConfigFromString_valid(t *testing.T) {
tt := map[string]*AppConfig{
"token": {
Token: "token",
},
"app ID": {
AppID: "1",
AppInstallationID: 2,
AppPrivateKey: "private key",
},
}
for name, cfg := range tt {
t.Run(name, func(t *testing.T) {
bytes, err := json.Marshal(cfg)
require.NoError(t, err)
got, err := FromJSONString(string(bytes))
require.NoError(t, err)
want := cfg.tidy()
assert.Equal(t, want, got)
})
}
}

View File

@@ -59,7 +59,10 @@ type AutoscalingListenerSpec struct {
Proxy *ProxyConfig `json:"proxy,omitempty"` Proxy *ProxyConfig `json:"proxy,omitempty"`
// +optional // +optional
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"` GitHubServerTLS *TLSConfig `json:"githubServerTLS,omitempty"`
// +optional
VaultConfig *VaultConfig `json:"vaultConfig,omitempty"`
// +optional // +optional
Metrics *MetricsConfig `json:"metrics,omitempty"` Metrics *MetricsConfig `json:"metrics,omitempty"`
@@ -87,7 +90,6 @@ type AutoscalingListener struct {
} }
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
// AutoscalingListenerList contains a list of AutoscalingListener // AutoscalingListenerList contains a list of AutoscalingListener
type AutoscalingListenerList struct { type AutoscalingListenerList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`

View File

@@ -24,6 +24,7 @@ import (
"strings" "strings"
"github.com/actions/actions-runner-controller/hash" "github.com/actions/actions-runner-controller/hash"
"github.com/actions/actions-runner-controller/vault"
"golang.org/x/net/http/httpproxy" "golang.org/x/net/http/httpproxy"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -69,7 +70,10 @@ type AutoscalingRunnerSetSpec struct {
Proxy *ProxyConfig `json:"proxy,omitempty"` Proxy *ProxyConfig `json:"proxy,omitempty"`
// +optional // +optional
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"` GitHubServerTLS *TLSConfig `json:"githubServerTLS,omitempty"`
// +optional
VaultConfig *VaultConfig `json:"vaultConfig,omitempty"`
// Required // Required
Template corev1.PodTemplateSpec `json:"template,omitempty"` Template corev1.PodTemplateSpec `json:"template,omitempty"`
@@ -89,12 +93,12 @@ type AutoscalingRunnerSetSpec struct {
MinRunners *int `json:"minRunners,omitempty"` MinRunners *int `json:"minRunners,omitempty"`
} }
type GitHubServerTLSConfig struct { type TLSConfig struct {
// Required // Required
CertificateFrom *TLSCertificateSource `json:"certificateFrom,omitempty"` CertificateFrom *TLSCertificateSource `json:"certificateFrom,omitempty"`
} }
func (c *GitHubServerTLSConfig) ToCertPool(keyFetcher func(name, key string) ([]byte, error)) (*x509.CertPool, error) { func (c *TLSConfig) ToCertPool(keyFetcher func(name, key string) ([]byte, error)) (*x509.CertPool, error) {
if c.CertificateFrom == nil { if c.CertificateFrom == nil {
return nil, fmt.Errorf("certificateFrom not specified") return nil, fmt.Errorf("certificateFrom not specified")
} }
@@ -142,7 +146,7 @@ type ProxyConfig struct {
NoProxy []string `json:"noProxy,omitempty"` NoProxy []string `json:"noProxy,omitempty"`
} }
func (c *ProxyConfig) toHTTPProxyConfig(secretFetcher func(string) (*corev1.Secret, error)) (*httpproxy.Config, error) { func (c *ProxyConfig) ToHTTPProxyConfig(secretFetcher func(string) (*corev1.Secret, error)) (*httpproxy.Config, error) {
config := &httpproxy.Config{ config := &httpproxy.Config{
NoProxy: strings.Join(c.NoProxy, ","), NoProxy: strings.Join(c.NoProxy, ","),
} }
@@ -201,7 +205,7 @@ func (c *ProxyConfig) toHTTPProxyConfig(secretFetcher func(string) (*corev1.Secr
} }
func (c *ProxyConfig) ToSecretData(secretFetcher func(string) (*corev1.Secret, error)) (map[string][]byte, error) { func (c *ProxyConfig) ToSecretData(secretFetcher func(string) (*corev1.Secret, error)) (map[string][]byte, error) {
config, err := c.toHTTPProxyConfig(secretFetcher) config, err := c.ToHTTPProxyConfig(secretFetcher)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -215,7 +219,7 @@ func (c *ProxyConfig) ToSecretData(secretFetcher func(string) (*corev1.Secret, e
} }
func (c *ProxyConfig) ProxyFunc(secretFetcher func(string) (*corev1.Secret, error)) (func(*http.Request) (*url.URL, error), error) { func (c *ProxyConfig) ProxyFunc(secretFetcher func(string) (*corev1.Secret, error)) (func(*http.Request) (*url.URL, error), error) {
config, err := c.toHTTPProxyConfig(secretFetcher) config, err := c.ToHTTPProxyConfig(secretFetcher)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -235,6 +239,26 @@ type ProxyServerConfig struct {
CredentialSecretRef string `json:"credentialSecretRef,omitempty"` CredentialSecretRef string `json:"credentialSecretRef,omitempty"`
} }
type VaultConfig struct {
// +optional
Type vault.VaultType `json:"type,omitempty"`
// +optional
AzureKeyVault *AzureKeyVaultConfig `json:"azureKeyVault,omitempty"`
// +optional
Proxy *ProxyConfig `json:"proxy,omitempty"`
}
type AzureKeyVaultConfig struct {
// +required
URL string `json:"url,omitempty"`
// +required
TenantID string `json:"tenantId,omitempty"`
// +required
ClientID string `json:"clientId,omitempty"`
// +required
CertificatePath string `json:"certificatePath,omitempty"`
}
// MetricsConfig holds configuration parameters for each metric type // MetricsConfig holds configuration parameters for each metric type
type MetricsConfig struct { type MetricsConfig struct {
// +optional // +optional
@@ -285,6 +309,33 @@ func (ars *AutoscalingRunnerSet) ListenerSpecHash() string {
return hash.ComputeTemplateHash(&spec) return hash.ComputeTemplateHash(&spec)
} }
func (ars *AutoscalingRunnerSet) GitHubConfigSecret() string {
return ars.Spec.GitHubConfigSecret
}
func (ars *AutoscalingRunnerSet) GitHubConfigUrl() string {
return ars.Spec.GitHubConfigUrl
}
func (ars *AutoscalingRunnerSet) GitHubProxy() *ProxyConfig {
return ars.Spec.Proxy
}
func (ars *AutoscalingRunnerSet) GitHubServerTLS() *TLSConfig {
return ars.Spec.GitHubServerTLS
}
func (ars *AutoscalingRunnerSet) VaultConfig() *VaultConfig {
return ars.Spec.VaultConfig
}
func (ars *AutoscalingRunnerSet) VaultProxy() *ProxyConfig {
if ars.Spec.VaultConfig != nil {
return ars.Spec.VaultConfig.Proxy
}
return nil
}
func (ars *AutoscalingRunnerSet) RunnerSetSpecHash() string { func (ars *AutoscalingRunnerSet) RunnerSetSpecHash() string {
type runnerSetSpec struct { type runnerSetSpec struct {
GitHubConfigUrl string GitHubConfigUrl string
@@ -292,7 +343,7 @@ func (ars *AutoscalingRunnerSet) RunnerSetSpecHash() string {
RunnerGroup string RunnerGroup string
RunnerScaleSetName string RunnerScaleSetName string
Proxy *ProxyConfig Proxy *ProxyConfig
GitHubServerTLS *GitHubServerTLSConfig GitHubServerTLS *TLSConfig
Template corev1.PodTemplateSpec Template corev1.PodTemplateSpec
} }
spec := &runnerSetSpec{ spec := &runnerSetSpec{

View File

@@ -67,6 +67,33 @@ func (er *EphemeralRunner) HasContainerHookConfigured() bool {
return false return false
} }
func (er *EphemeralRunner) GitHubConfigSecret() string {
return er.Spec.GitHubConfigSecret
}
func (er *EphemeralRunner) GitHubConfigUrl() string {
return er.Spec.GitHubConfigUrl
}
func (er *EphemeralRunner) GitHubProxy() *ProxyConfig {
return er.Spec.Proxy
}
func (er *EphemeralRunner) GitHubServerTLS() *TLSConfig {
return er.Spec.GitHubServerTLS
}
func (er *EphemeralRunner) VaultConfig() *VaultConfig {
return er.Spec.VaultConfig
}
func (er *EphemeralRunner) VaultProxy() *ProxyConfig {
if er.Spec.VaultConfig != nil {
return er.Spec.VaultConfig.Proxy
}
return nil
}
// EphemeralRunnerSpec defines the desired state of EphemeralRunner // EphemeralRunnerSpec defines the desired state of EphemeralRunner
type EphemeralRunnerSpec struct { type EphemeralRunnerSpec struct {
// +required // +required
@@ -75,6 +102,9 @@ type EphemeralRunnerSpec struct {
// +required // +required
GitHubConfigSecret string `json:"githubConfigSecret,omitempty"` GitHubConfigSecret string `json:"githubConfigSecret,omitempty"`
// +optional
GitHubServerTLS *TLSConfig `json:"githubServerTLS,omitempty"`
// +required // +required
RunnerScaleSetId int `json:"runnerScaleSetId,omitempty"` RunnerScaleSetId int `json:"runnerScaleSetId,omitempty"`
@@ -85,7 +115,7 @@ type EphemeralRunnerSpec struct {
ProxySecretRef string `json:"proxySecretRef,omitempty"` ProxySecretRef string `json:"proxySecretRef,omitempty"`
// +optional // +optional
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"` VaultConfig *VaultConfig `json:"vaultConfig,omitempty"`
corev1.PodTemplateSpec `json:",inline"` corev1.PodTemplateSpec `json:",inline"`
} }
@@ -119,7 +149,7 @@ type EphemeralRunnerStatus struct {
RunnerJITConfig string `json:"runnerJITConfig,omitempty"` RunnerJITConfig string `json:"runnerJITConfig,omitempty"`
// +optional // +optional
Failures map[string]bool `json:"failures,omitempty"` Failures map[string]metav1.Time `json:"failures,omitempty"`
// +optional // +optional
JobRequestId int64 `json:"jobRequestId,omitempty"` JobRequestId int64 `json:"jobRequestId,omitempty"`
@@ -137,6 +167,20 @@ type EphemeralRunnerStatus struct {
JobDisplayName string `json:"jobDisplayName,omitempty"` JobDisplayName string `json:"jobDisplayName,omitempty"`
} }
func (s *EphemeralRunnerStatus) LastFailure() metav1.Time {
var maxTime metav1.Time
if len(s.Failures) == 0 {
return maxTime
}
for _, ts := range s.Failures {
if ts.After(maxTime.Time) {
maxTime = ts
}
}
return maxTime
}
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
// EphemeralRunnerList contains a list of EphemeralRunner // EphemeralRunnerList contains a list of EphemeralRunner

View File

@@ -60,9 +60,35 @@ type EphemeralRunnerSet struct {
Status EphemeralRunnerSetStatus `json:"status,omitempty"` Status EphemeralRunnerSetStatus `json:"status,omitempty"`
} }
// +kubebuilder:object:root=true func (ers *EphemeralRunnerSet) GitHubConfigSecret() string {
return ers.Spec.EphemeralRunnerSpec.GitHubConfigSecret
}
func (ers *EphemeralRunnerSet) GitHubConfigUrl() string {
return ers.Spec.EphemeralRunnerSpec.GitHubConfigUrl
}
func (ers *EphemeralRunnerSet) GitHubProxy() *ProxyConfig {
return ers.Spec.EphemeralRunnerSpec.Proxy
}
func (ers *EphemeralRunnerSet) GitHubServerTLS() *TLSConfig {
return ers.Spec.EphemeralRunnerSpec.GitHubServerTLS
}
func (ers *EphemeralRunnerSet) VaultConfig() *VaultConfig {
return ers.Spec.EphemeralRunnerSpec.VaultConfig
}
func (ers *EphemeralRunnerSet) VaultProxy() *ProxyConfig {
if ers.Spec.EphemeralRunnerSpec.VaultConfig != nil {
return ers.Spec.EphemeralRunnerSpec.VaultConfig.Proxy
}
return nil
}
// EphemeralRunnerSetList contains a list of EphemeralRunnerSet // EphemeralRunnerSetList contains a list of EphemeralRunnerSet
// +kubebuilder:object:root=true
type EphemeralRunnerSetList struct { type EphemeralRunnerSetList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"` metav1.ListMeta `json:"metadata,omitempty"`

View File

@@ -17,7 +17,7 @@ import (
func TestGitHubServerTLSConfig_ToCertPool(t *testing.T) { func TestGitHubServerTLSConfig_ToCertPool(t *testing.T) {
t.Run("returns an error if CertificateFrom not specified", func(t *testing.T) { t.Run("returns an error if CertificateFrom not specified", func(t *testing.T) {
c := &v1alpha1.GitHubServerTLSConfig{ c := &v1alpha1.TLSConfig{
CertificateFrom: nil, CertificateFrom: nil,
} }
@@ -29,7 +29,7 @@ func TestGitHubServerTLSConfig_ToCertPool(t *testing.T) {
}) })
t.Run("returns an error if CertificateFrom.ConfigMapKeyRef not specified", func(t *testing.T) { t.Run("returns an error if CertificateFrom.ConfigMapKeyRef not specified", func(t *testing.T) {
c := &v1alpha1.GitHubServerTLSConfig{ c := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{}, CertificateFrom: &v1alpha1.TLSCertificateSource{},
} }
@@ -41,7 +41,7 @@ func TestGitHubServerTLSConfig_ToCertPool(t *testing.T) {
}) })
t.Run("returns a valid cert pool with correct configuration", func(t *testing.T) { t.Run("returns a valid cert pool with correct configuration", func(t *testing.T) {
c := &v1alpha1.GitHubServerTLSConfig{ c := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{ ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{ LocalObjectReference: v1.LocalObjectReference{

View File

@@ -0,0 +1,72 @@
package v1alpha1
import "strings"
func IsVersionAllowed(resourceVersion, buildVersion string) bool {
if buildVersion == "dev" || resourceVersion == buildVersion || strings.HasPrefix(buildVersion, "canary-") {
return true
}
rv, ok := parseSemver(resourceVersion)
if !ok {
return false
}
bv, ok := parseSemver(buildVersion)
if !ok {
return false
}
return rv.major == bv.major && rv.minor == bv.minor
}
type semver struct {
major string
minor string
}
func parseSemver(v string) (p semver, ok bool) {
if v == "" {
return
}
p.major, v, ok = parseInt(v)
if !ok {
return p, false
}
if v == "" {
p.minor = "0"
return p, true
}
if v[0] != '.' {
return p, false
}
p.minor, v, ok = parseInt(v[1:])
if !ok {
return p, false
}
if v == "" {
return p, true
}
if v[0] != '.' {
return p, false
}
if _, _, ok = parseInt(v[1:]); !ok {
return p, false
}
return p, true
}
func parseInt(v string) (t, rest string, ok bool) {
if v == "" {
return
}
if v[0] < '0' || '9' < v[0] {
return
}
i := 1
for i < len(v) && '0' <= v[i] && v[i] <= '9' {
i++
}
if v[0] == '0' && i != 1 {
return
}
return v[:i], v[i:], true
}

View File

@@ -0,0 +1,60 @@
package v1alpha1_test
import (
"testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/stretchr/testify/assert"
)
func TestIsVersionAllowed(t *testing.T) {
t.Parallel()
tt := map[string]struct {
resourceVersion string
buildVersion string
want bool
}{
"dev should always be allowed": {
resourceVersion: "0.11.0",
buildVersion: "dev",
want: true,
},
"resourceVersion is not semver": {
resourceVersion: "dev",
buildVersion: "0.11.0",
want: false,
},
"buildVersion is not semver": {
resourceVersion: "0.11.0",
buildVersion: "NA",
want: false,
},
"major version mismatch": {
resourceVersion: "0.11.0",
buildVersion: "1.11.0",
want: false,
},
"minor version mismatch": {
resourceVersion: "0.11.0",
buildVersion: "0.10.0",
want: false,
},
"patch version mismatch": {
resourceVersion: "0.11.1",
buildVersion: "0.11.0",
want: true,
},
"arbitrary version match": {
resourceVersion: "abc",
buildVersion: "abc",
want: true,
},
}
for name, tc := range tt {
t.Run(name, func(t *testing.T) {
got := v1alpha1.IsVersionAllowed(tc.resourceVersion, tc.buildVersion)
assert.Equal(t, tc.want, got)
})
}
}

View File

@@ -22,6 +22,7 @@ package v1alpha1
import ( import (
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )
@@ -99,7 +100,12 @@ func (in *AutoscalingListenerSpec) DeepCopyInto(out *AutoscalingListenerSpec) {
} }
if in.GitHubServerTLS != nil { if in.GitHubServerTLS != nil {
in, out := &in.GitHubServerTLS, &out.GitHubServerTLS in, out := &in.GitHubServerTLS, &out.GitHubServerTLS
*out = new(GitHubServerTLSConfig) *out = new(TLSConfig)
(*in).DeepCopyInto(*out)
}
if in.VaultConfig != nil {
in, out := &in.VaultConfig, &out.VaultConfig
*out = new(VaultConfig)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.Metrics != nil { if in.Metrics != nil {
@@ -208,7 +214,12 @@ func (in *AutoscalingRunnerSetSpec) DeepCopyInto(out *AutoscalingRunnerSetSpec)
} }
if in.GitHubServerTLS != nil { if in.GitHubServerTLS != nil {
in, out := &in.GitHubServerTLS, &out.GitHubServerTLS in, out := &in.GitHubServerTLS, &out.GitHubServerTLS
*out = new(GitHubServerTLSConfig) *out = new(TLSConfig)
(*in).DeepCopyInto(*out)
}
if in.VaultConfig != nil {
in, out := &in.VaultConfig, &out.VaultConfig
*out = new(VaultConfig)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
in.Template.DeepCopyInto(&out.Template) in.Template.DeepCopyInto(&out.Template)
@@ -259,6 +270,21 @@ func (in *AutoscalingRunnerSetStatus) DeepCopy() *AutoscalingRunnerSetStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AzureKeyVaultConfig) DeepCopyInto(out *AzureKeyVaultConfig) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureKeyVaultConfig.
func (in *AzureKeyVaultConfig) DeepCopy() *AzureKeyVaultConfig {
if in == nil {
return nil
}
out := new(AzureKeyVaultConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CounterMetric) DeepCopyInto(out *CounterMetric) { func (in *CounterMetric) DeepCopyInto(out *CounterMetric) {
*out = *in *out = *in
@@ -431,14 +457,19 @@ func (in *EphemeralRunnerSetStatus) DeepCopy() *EphemeralRunnerSetStatus {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EphemeralRunnerSpec) DeepCopyInto(out *EphemeralRunnerSpec) { func (in *EphemeralRunnerSpec) DeepCopyInto(out *EphemeralRunnerSpec) {
*out = *in *out = *in
if in.GitHubServerTLS != nil {
in, out := &in.GitHubServerTLS, &out.GitHubServerTLS
*out = new(TLSConfig)
(*in).DeepCopyInto(*out)
}
if in.Proxy != nil { if in.Proxy != nil {
in, out := &in.Proxy, &out.Proxy in, out := &in.Proxy, &out.Proxy
*out = new(ProxyConfig) *out = new(ProxyConfig)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.GitHubServerTLS != nil { if in.VaultConfig != nil {
in, out := &in.GitHubServerTLS, &out.GitHubServerTLS in, out := &in.VaultConfig, &out.VaultConfig
*out = new(GitHubServerTLSConfig) *out = new(VaultConfig)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
in.PodTemplateSpec.DeepCopyInto(&out.PodTemplateSpec) in.PodTemplateSpec.DeepCopyInto(&out.PodTemplateSpec)
@@ -459,9 +490,9 @@ func (in *EphemeralRunnerStatus) DeepCopyInto(out *EphemeralRunnerStatus) {
*out = *in *out = *in
if in.Failures != nil { if in.Failures != nil {
in, out := &in.Failures, &out.Failures in, out := &in.Failures, &out.Failures
*out = make(map[string]bool, len(*in)) *out = make(map[string]metav1.Time, len(*in))
for key, val := range *in { for key, val := range *in {
(*out)[key] = val (*out)[key] = *val.DeepCopy()
} }
} }
} }
@@ -496,26 +527,6 @@ func (in *GaugeMetric) DeepCopy() *GaugeMetric {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GitHubServerTLSConfig) DeepCopyInto(out *GitHubServerTLSConfig) {
*out = *in
if in.CertificateFrom != nil {
in, out := &in.CertificateFrom, &out.CertificateFrom
*out = new(TLSCertificateSource)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubServerTLSConfig.
func (in *GitHubServerTLSConfig) DeepCopy() *GitHubServerTLSConfig {
if in == nil {
return nil
}
out := new(GitHubServerTLSConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HistogramMetric) DeepCopyInto(out *HistogramMetric) { func (in *HistogramMetric) DeepCopyInto(out *HistogramMetric) {
*out = *in *out = *in
@@ -668,3 +679,48 @@ func (in *TLSCertificateSource) DeepCopy() *TLSCertificateSource {
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSConfig) DeepCopyInto(out *TLSConfig) {
*out = *in
if in.CertificateFrom != nil {
in, out := &in.CertificateFrom, &out.CertificateFrom
*out = new(TLSCertificateSource)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSConfig.
func (in *TLSConfig) DeepCopy() *TLSConfig {
if in == nil {
return nil
}
out := new(TLSConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultConfig) DeepCopyInto(out *VaultConfig) {
*out = *in
if in.AzureKeyVault != nil {
in, out := &in.AzureKeyVault, &out.AzureKeyVault
*out = new(AzureKeyVaultConfig)
**out = **in
}
if in.Proxy != nil {
in, out := &in.Proxy, &out.Proxy
*out = new(ProxyConfig)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultConfig.
func (in *VaultConfig) DeepCopy() *VaultConfig {
if in == nil {
return nil
}
out := new(VaultConfig)
in.DeepCopyInto(out)
return out
}

View File

@@ -215,10 +215,10 @@ func (rs *RunnerSpec) validateRepository() error {
foundCount += 1 foundCount += 1
} }
if foundCount == 0 { if foundCount == 0 {
return errors.New("Spec needs enterprise, organization or repository") return errors.New("spec needs enterprise, organization or repository")
} }
if foundCount > 1 { if foundCount > 1 {
return errors.New("Spec cannot have many fields defined enterprise, organization and repository") return errors.New("spec cannot have many fields defined enterprise, organization and repository")
} }
return nil return nil

View File

@@ -1,9 +1,11 @@
# This file defines the config for "ct" (chart tester) used by the helm linting GitHub workflow # This file defines the config for "ct" (chart tester) used by the helm linting GitHub workflow
remote: origin
target-branch: master
lint-conf: charts/.ci/lint-config.yaml lint-conf: charts/.ci/lint-config.yaml
chart-repos: chart-repos:
- jetstack=https://charts.jetstack.io - jetstack=https://charts.jetstack.io
check-version-increment: false # Disable checking that the chart version has been bumped check-version-increment: false # Disable checking that the chart version has been bumped
charts: charts:
- charts/gha-runner-scale-set-controller - charts/gha-runner-scale-set-controller
- charts/gha-runner-scale-set - charts/gha-runner-scale-set
skip-clean-up: true skip-clean-up: true

View File

@@ -1,7 +1,9 @@
# This file defines the config for "ct" (chart tester) used by the helm linting GitHub workflow # This file defines the config for "ct" (chart tester) used by the helm linting GitHub workflow
remote: origin
target-branch: master
lint-conf: charts/.ci/lint-config.yaml lint-conf: charts/.ci/lint-config.yaml
chart-repos: chart-repos:
- jetstack=https://charts.jetstack.io - jetstack=https://charts.jetstack.io
check-version-increment: false # Disable checking that the chart version has been bumped check-version-increment: false # Disable checking that the chart version has been bumped
charts: charts:
- charts/actions-runner-controller - charts/actions-runner-controller

View File

@@ -1,6 +1,5 @@
#!/bin/bash #!/bin/bash
for chart in `ls charts`; for chart in `ls charts`;
do do
helm template --values charts/$chart/ci/ci-values.yaml charts/$chart | kube-score score - \ helm template --values charts/$chart/ci/ci-values.yaml charts/$chart | kube-score score - \
@@ -12,4 +11,4 @@ helm template --values charts/$chart/ci/ci-values.yaml charts/$chart | kube-scor
--enable-optional-test container-security-context-privileged \ --enable-optional-test container-security-context-privileged \
--enable-optional-test container-security-context-readonlyrootfilesystem \ --enable-optional-test container-security-context-readonlyrootfilesystem \
--ignore-test container-security-context --ignore-test container-security-context
done done

View File

@@ -44,7 +44,7 @@ All additional docs are kept in the `docs/` folder, this README is solely for do
| `image.pullPolicy` | The pull policy of the controller image | IfNotPresent | | `image.pullPolicy` | The pull policy of the controller image | IfNotPresent |
| `metrics.serviceMonitor.enable` | Deploy serviceMonitor kind for for use with prometheus-operator CRDs | false | | `metrics.serviceMonitor.enable` | Deploy serviceMonitor kind for for use with prometheus-operator CRDs | false |
| `metrics.serviceMonitor.interval` | Configure the interval that Prometheus should scrap the controller's metrics | 1m | | `metrics.serviceMonitor.interval` | Configure the interval that Prometheus should scrap the controller's metrics | 1m |
| `metrics.serviceMonitor.namespace | Namespace which Prometheus is running in | `Release.Namespace` (the default namespace of the helm chart). | | `metrics.serviceMonitor.namespace` | Namespace which Prometheus is running in | `Release.Namespace` (the default namespace of the helm chart). |
| `metrics.serviceMonitor.timeout` | Configure the timeout the timeout of Prometheus scrapping. | 30s | | `metrics.serviceMonitor.timeout` | Configure the timeout the timeout of Prometheus scrapping. | 30s |
| `metrics.serviceAnnotations` | Set annotations for the provisioned metrics service resource | | | `metrics.serviceAnnotations` | Set annotations for the provisioned metrics service resource | |
| `metrics.port` | Set port of metrics service | 8443 | | `metrics.port` | Set port of metrics service | 8443 |

View File

@@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.11.0 version: 0.12.1
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "0.11.0" appVersion: "0.12.1"
home: https://github.com/actions/actions-runner-controller home: https://github.com/actions/actions-runner-controller

View File

@@ -7863,6 +7863,53 @@ spec:
- containers - containers
type: object type: object
type: object type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
type: object type: object
status: status:
description: AutoscalingListenerStatus defines the observed state of AutoscalingListener description: AutoscalingListenerStatus defines the observed state of AutoscalingListener

View File

@@ -15504,6 +15504,53 @@ spec:
- containers - containers
type: object type: object
type: object type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
type: object type: object
status: status:
description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet

View File

@@ -7784,6 +7784,53 @@ spec:
required: required:
- containers - containers
type: object type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
required: required:
- githubConfigSecret - githubConfigSecret
- githubConfigUrl - githubConfigUrl
@@ -7794,7 +7841,8 @@ spec:
properties: properties:
failures: failures:
additionalProperties: additionalProperties:
type: boolean format: date-time
type: string
type: object type: object
jobDisplayName: jobDisplayName:
type: string type: string

View File

@@ -7778,6 +7778,53 @@ spec:
required: required:
- containers - containers
type: object type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
required: required:
- githubConfigSecret - githubConfigSecret
- githubConfigUrl - githubConfigUrl

View File

@@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.11.0 version: 0.12.1
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "0.11.0" appVersion: "0.12.1"
home: https://github.com/actions/actions-runner-controller home: https://github.com/actions/actions-runner-controller

View File

@@ -106,6 +106,17 @@ env:
value: "123" value: "123"
securityContext: securityContext:
privileged: true privileged: true
{{- if (ge (.Capabilities.KubeVersion.Minor | int) 29) }}
restartPolicy: Always
startupProbe:
exec:
command:
- docker
- info
initialDelaySeconds: 0
failureThreshold: 24
periodSeconds: 5
{{- end }}
volumeMounts: volumeMounts:
- name: work - name: work
mountPath: /home/runner/_work mountPath: /home/runner/_work

View File

@@ -45,6 +45,7 @@ metadata:
{{- if and (ne $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} {{- if and (ne $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }}
actions.github.com/cleanup-no-permission-service-account-name: {{ include "gha-runner-scale-set.noPermissionServiceAccountName" . }} actions.github.com/cleanup-no-permission-service-account-name: {{ include "gha-runner-scale-set.noPermissionServiceAccountName" . }}
{{- end }} {{- end }}
spec: spec:
githubConfigUrl: {{ required ".Values.githubConfigUrl is required" (trimSuffix "/" .Values.githubConfigUrl) }} githubConfigUrl: {{ required ".Values.githubConfigUrl is required" (trimSuffix "/" .Values.githubConfigUrl) }}
githubConfigSecret: {{ include "gha-runner-scale-set.githubsecret" . }} githubConfigSecret: {{ include "gha-runner-scale-set.githubsecret" . }}
@@ -65,6 +66,24 @@ spec:
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- if and .Values.keyVault .Values.keyVault.type }}
vaultConfig:
type: {{ .Values.keyVault.type }}
{{- if .Values.keyVault.proxy }}
proxy: {{- toYaml .Values.keyVault.proxy | nindent 6 }}
{{- end }}
{{- if eq .Values.keyVault.type "azure_key_vault" }}
azureKeyVault:
url: {{ .Values.keyVault.azureKeyVault.url }}
tenantId: {{ .Values.keyVault.azureKeyVault.tenantId }}
clientId: {{ .Values.keyVault.azureKeyVault.clientId }}
certificatePath: {{ .Values.keyVault.azureKeyVault.certificatePath }}
secretKey: {{ .Values.keyVault.azureKeyVault.secretKey }}
{{- else }}
{{- fail "Unsupported keyVault type: " .Values.keyVault.type }}
{{- end }}
{{- end }}
{{- if .Values.proxy }} {{- if .Values.proxy }}
proxy: proxy:
{{- if .Values.proxy.http }} {{- if .Values.proxy.http }}
@@ -147,7 +166,11 @@ spec:
initContainers: initContainers:
{{- if eq $containerMode.type "dind" }} {{- if eq $containerMode.type "dind" }}
- name: init-dind-externals - name: init-dind-externals
{{- include "gha-runner-scale-set.dind-init-container" . | nindent 8 }} {{- include "gha-runner-scale-set.dind-init-container" . | nindent 8 }}
{{- if (ge (.Capabilities.KubeVersion.Minor | int) 29) }}
- name: dind
{{- include "gha-runner-scale-set.dind-container" . | nindent 8 }}
{{- end }}
{{- end }} {{- end }}
{{- with .Values.template.spec.initContainers }} {{- with .Values.template.spec.initContainers }}
{{- toYaml . | nindent 6 }} {{- toYaml . | nindent 6 }}
@@ -157,8 +180,10 @@ spec:
{{- if eq $containerMode.type "dind" }} {{- if eq $containerMode.type "dind" }}
- name: runner - name: runner
{{- include "gha-runner-scale-set.dind-runner-container" . | nindent 8 }} {{- include "gha-runner-scale-set.dind-runner-container" . | nindent 8 }}
{{- if not (ge (.Capabilities.KubeVersion.Minor | int) 29) }}
- name: dind - name: dind
{{- include "gha-runner-scale-set.dind-container" . | nindent 8 }} {{- include "gha-runner-scale-set.dind-container" . | nindent 8 }}
{{- end }}
{{- include "gha-runner-scale-set.non-runner-non-dind-containers" . | nindent 6 }} {{- include "gha-runner-scale-set.non-runner-non-dind-containers" . | nindent 6 }}
{{- else if eq $containerMode.type "kubernetes" }} {{- else if eq $containerMode.type "kubernetes" }}
- name: runner - name: runner

View File

@@ -728,20 +728,20 @@ func TestTemplateRenderedAutoScalingRunnerSet_DinD_ExtraInitContainers(t *testin
var ars v1alpha1.AutoscalingRunnerSet var ars v1alpha1.AutoscalingRunnerSet
helm.UnmarshalK8SYaml(t, output, &ars) helm.UnmarshalK8SYaml(t, output, &ars)
assert.Len(t, ars.Spec.Template.Spec.InitContainers, 3, "InitContainers should be 3") assert.Len(t, ars.Spec.Template.Spec.InitContainers, 4, "InitContainers should be 4")
assert.Equal(t, "kube-init", ars.Spec.Template.Spec.InitContainers[1].Name, "InitContainers[1] Name should be kube-init") assert.Equal(t, "kube-init", ars.Spec.Template.Spec.InitContainers[2].Name, "InitContainers[1] Name should be kube-init")
assert.Equal(t, "runner-image:latest", ars.Spec.Template.Spec.InitContainers[1].Image, "InitContainers[1] Image should be runner-image:latest") assert.Equal(t, "runner-image:latest", ars.Spec.Template.Spec.InitContainers[2].Image, "InitContainers[1] Image should be runner-image:latest")
assert.Equal(t, "sudo", ars.Spec.Template.Spec.InitContainers[1].Command[0], "InitContainers[1] Command[0] should be sudo") assert.Equal(t, "sudo", ars.Spec.Template.Spec.InitContainers[2].Command[0], "InitContainers[1] Command[0] should be sudo")
assert.Equal(t, "chown", ars.Spec.Template.Spec.InitContainers[1].Command[1], "InitContainers[1] Command[1] should be chown") assert.Equal(t, "chown", ars.Spec.Template.Spec.InitContainers[2].Command[1], "InitContainers[1] Command[1] should be chown")
assert.Equal(t, "-R", ars.Spec.Template.Spec.InitContainers[1].Command[2], "InitContainers[1] Command[2] should be -R") assert.Equal(t, "-R", ars.Spec.Template.Spec.InitContainers[2].Command[2], "InitContainers[1] Command[2] should be -R")
assert.Equal(t, "1001:123", ars.Spec.Template.Spec.InitContainers[1].Command[3], "InitContainers[1] Command[3] should be 1001:123") assert.Equal(t, "1001:123", ars.Spec.Template.Spec.InitContainers[2].Command[3], "InitContainers[1] Command[3] should be 1001:123")
assert.Equal(t, "/home/runner/_work", ars.Spec.Template.Spec.InitContainers[1].Command[4], "InitContainers[1] Command[4] should be /home/runner/_work") assert.Equal(t, "/home/runner/_work", ars.Spec.Template.Spec.InitContainers[2].Command[4], "InitContainers[1] Command[4] should be /home/runner/_work")
assert.Equal(t, "work", ars.Spec.Template.Spec.InitContainers[1].VolumeMounts[0].Name, "InitContainers[1] VolumeMounts[0] Name should be work") assert.Equal(t, "work", ars.Spec.Template.Spec.InitContainers[2].VolumeMounts[0].Name, "InitContainers[1] VolumeMounts[0] Name should be work")
assert.Equal(t, "/home/runner/_work", ars.Spec.Template.Spec.InitContainers[1].VolumeMounts[0].MountPath, "InitContainers[1] VolumeMounts[0] MountPath should be /home/runner/_work") assert.Equal(t, "/home/runner/_work", ars.Spec.Template.Spec.InitContainers[2].VolumeMounts[0].MountPath, "InitContainers[1] VolumeMounts[0] MountPath should be /home/runner/_work")
assert.Equal(t, "ls", ars.Spec.Template.Spec.InitContainers[2].Name, "InitContainers[2] Name should be ls") assert.Equal(t, "ls", ars.Spec.Template.Spec.InitContainers[3].Name, "InitContainers[2] Name should be ls")
assert.Equal(t, "ubuntu:latest", ars.Spec.Template.Spec.InitContainers[2].Image, "InitContainers[2] Image should be ubuntu:latest") assert.Equal(t, "ubuntu:latest", ars.Spec.Template.Spec.InitContainers[3].Image, "InitContainers[2] Image should be ubuntu:latest")
assert.Equal(t, "ls", ars.Spec.Template.Spec.InitContainers[2].Command[0], "InitContainers[2] Command[0] should be ls") assert.Equal(t, "ls", ars.Spec.Template.Spec.InitContainers[3].Command[0], "InitContainers[2] Command[0] should be ls")
} }
func TestTemplateRenderedAutoScalingRunnerSet_DinD_ExtraVolumes(t *testing.T) { func TestTemplateRenderedAutoScalingRunnerSet_DinD_ExtraVolumes(t *testing.T) {
@@ -860,13 +860,26 @@ func TestTemplateRenderedAutoScalingRunnerSet_EnableDinD(t *testing.T) {
assert.NotNil(t, ars.Spec.Template.Spec, "Template.Spec should not be nil") assert.NotNil(t, ars.Spec.Template.Spec, "Template.Spec should not be nil")
assert.Len(t, ars.Spec.Template.Spec.InitContainers, 1, "Template.Spec should have 1 init container") assert.Len(t, ars.Spec.Template.Spec.InitContainers, 2, "Template.Spec should have 2 init container")
assert.Equal(t, "init-dind-externals", ars.Spec.Template.Spec.InitContainers[0].Name) assert.Equal(t, "init-dind-externals", ars.Spec.Template.Spec.InitContainers[0].Name)
assert.Equal(t, "ghcr.io/actions/actions-runner:latest", ars.Spec.Template.Spec.InitContainers[0].Image) assert.Equal(t, "ghcr.io/actions/actions-runner:latest", ars.Spec.Template.Spec.InitContainers[0].Image)
assert.Equal(t, "cp", ars.Spec.Template.Spec.InitContainers[0].Command[0]) assert.Equal(t, "cp", ars.Spec.Template.Spec.InitContainers[0].Command[0])
assert.Equal(t, "-r /home/runner/externals/. /home/runner/tmpDir/", strings.Join(ars.Spec.Template.Spec.InitContainers[0].Args, " ")) assert.Equal(t, "-r /home/runner/externals/. /home/runner/tmpDir/", strings.Join(ars.Spec.Template.Spec.InitContainers[0].Args, " "))
assert.Len(t, ars.Spec.Template.Spec.Containers, 2, "Template.Spec should have 2 container") assert.Equal(t, "dind", ars.Spec.Template.Spec.InitContainers[1].Name)
assert.Equal(t, "docker:dind", ars.Spec.Template.Spec.InitContainers[1].Image)
assert.True(t, *ars.Spec.Template.Spec.InitContainers[1].SecurityContext.Privileged)
assert.Len(t, ars.Spec.Template.Spec.InitContainers[1].VolumeMounts, 3, "The dind container should have 3 volume mounts, dind-sock, work and externals")
assert.Equal(t, "work", ars.Spec.Template.Spec.InitContainers[1].VolumeMounts[0].Name)
assert.Equal(t, "/home/runner/_work", ars.Spec.Template.Spec.InitContainers[1].VolumeMounts[0].MountPath)
assert.Equal(t, "dind-sock", ars.Spec.Template.Spec.InitContainers[1].VolumeMounts[1].Name)
assert.Equal(t, "/var/run", ars.Spec.Template.Spec.InitContainers[1].VolumeMounts[1].MountPath)
assert.Equal(t, "dind-externals", ars.Spec.Template.Spec.InitContainers[1].VolumeMounts[2].Name)
assert.Equal(t, "/home/runner/externals", ars.Spec.Template.Spec.InitContainers[1].VolumeMounts[2].MountPath)
assert.Len(t, ars.Spec.Template.Spec.Containers, 1, "Template.Spec should have 1 container")
assert.Equal(t, "runner", ars.Spec.Template.Spec.Containers[0].Name) assert.Equal(t, "runner", ars.Spec.Template.Spec.Containers[0].Name)
assert.Equal(t, "ghcr.io/actions/actions-runner:latest", ars.Spec.Template.Spec.Containers[0].Image) assert.Equal(t, "ghcr.io/actions/actions-runner:latest", ars.Spec.Template.Spec.Containers[0].Image)
assert.Len(t, ars.Spec.Template.Spec.Containers[0].Env, 2, "The runner container should have 2 env vars, DOCKER_HOST and RUNNER_WAIT_FOR_DOCKER_IN_SECONDS") assert.Len(t, ars.Spec.Template.Spec.Containers[0].Env, 2, "The runner container should have 2 env vars, DOCKER_HOST and RUNNER_WAIT_FOR_DOCKER_IN_SECONDS")
@@ -883,19 +896,6 @@ func TestTemplateRenderedAutoScalingRunnerSet_EnableDinD(t *testing.T) {
assert.Equal(t, "dind-sock", ars.Spec.Template.Spec.Containers[0].VolumeMounts[1].Name) assert.Equal(t, "dind-sock", ars.Spec.Template.Spec.Containers[0].VolumeMounts[1].Name)
assert.Equal(t, "/var/run", ars.Spec.Template.Spec.Containers[0].VolumeMounts[1].MountPath) assert.Equal(t, "/var/run", ars.Spec.Template.Spec.Containers[0].VolumeMounts[1].MountPath)
assert.Equal(t, "dind", ars.Spec.Template.Spec.Containers[1].Name)
assert.Equal(t, "docker:dind", ars.Spec.Template.Spec.Containers[1].Image)
assert.True(t, *ars.Spec.Template.Spec.Containers[1].SecurityContext.Privileged)
assert.Len(t, ars.Spec.Template.Spec.Containers[1].VolumeMounts, 3, "The dind container should have 3 volume mounts, dind-sock, work and externals")
assert.Equal(t, "work", ars.Spec.Template.Spec.Containers[1].VolumeMounts[0].Name)
assert.Equal(t, "/home/runner/_work", ars.Spec.Template.Spec.Containers[1].VolumeMounts[0].MountPath)
assert.Equal(t, "dind-sock", ars.Spec.Template.Spec.Containers[1].VolumeMounts[1].Name)
assert.Equal(t, "/var/run", ars.Spec.Template.Spec.Containers[1].VolumeMounts[1].MountPath)
assert.Equal(t, "dind-externals", ars.Spec.Template.Spec.Containers[1].VolumeMounts[2].Name)
assert.Equal(t, "/home/runner/externals", ars.Spec.Template.Spec.Containers[1].VolumeMounts[2].MountPath)
assert.Len(t, ars.Spec.Template.Spec.Volumes, 3, "Volumes should be 3") assert.Len(t, ars.Spec.Template.Spec.Volumes, 3, "Volumes should be 3")
assert.Equal(t, "dind-sock", ars.Spec.Template.Spec.Volumes[0].Name, "Volume name should be dind-sock") assert.Equal(t, "dind-sock", ars.Spec.Template.Spec.Volumes[0].Name, "Volume name should be dind-sock")
assert.Equal(t, "dind-externals", ars.Spec.Template.Spec.Volumes[1].Name, "Volume name should be dind-externals") assert.Equal(t, "dind-externals", ars.Spec.Template.Spec.Volumes[1].Name, "Volume name should be dind-externals")
@@ -1158,7 +1158,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options) ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS) require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{ expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1178,7 +1178,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
} }
} }
require.NotNil(t, volume) require.NotNil(t, volume)
assert.Equal(t, "certs-configmap", volume.ConfigMap.LocalObjectReference.Name) assert.Equal(t, "certs-configmap", volume.ConfigMap.Name)
assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Key) assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Key)
assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Path) assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Path)
@@ -1218,7 +1218,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options) ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS) require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{ expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1238,7 +1238,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
} }
} }
require.NotNil(t, volume) require.NotNil(t, volume)
assert.Equal(t, "certs-configmap", volume.ConfigMap.LocalObjectReference.Name) assert.Equal(t, "certs-configmap", volume.ConfigMap.Name)
assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Key) assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Key)
assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Path) assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Path)
@@ -1278,7 +1278,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options) ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS) require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{ expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1298,7 +1298,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
} }
} }
require.NotNil(t, volume) require.NotNil(t, volume)
assert.Equal(t, "certs-configmap", volume.ConfigMap.LocalObjectReference.Name) assert.Equal(t, "certs-configmap", volume.ConfigMap.Name)
assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Key) assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Key)
assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Path) assert.Equal(t, "cert.pem", volume.ConfigMap.Items[0].Path)
@@ -1338,7 +1338,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options) ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS) require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{ expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1394,7 +1394,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options) ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS) require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{ expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1450,7 +1450,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options) ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS) require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{ expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1826,7 +1826,7 @@ func TestTemplateRenderedAutoScalingRunnerSet_DinDMergePodSpec(t *testing.T) {
var ars v1alpha1.AutoscalingRunnerSet var ars v1alpha1.AutoscalingRunnerSet
helm.UnmarshalK8SYaml(t, output, &ars) helm.UnmarshalK8SYaml(t, output, &ars)
assert.Len(t, ars.Spec.Template.Spec.Containers, 2, "There should be 2 containers") assert.Len(t, ars.Spec.Template.Spec.Containers, 1, "There should be 1 containers")
assert.Equal(t, "runner", ars.Spec.Template.Spec.Containers[0].Name, "Container name should be runner") assert.Equal(t, "runner", ars.Spec.Template.Spec.Containers[0].Name, "Container name should be runner")
assert.Equal(t, "250m", ars.Spec.Template.Spec.Containers[0].Resources.Limits.Cpu().String(), "CPU Limit should be set") assert.Equal(t, "250m", ars.Spec.Template.Spec.Containers[0].Resources.Limits.Cpu().String(), "CPU Limit should be set")
assert.Equal(t, "64Mi", ars.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().String(), "Memory Limit should be set") assert.Equal(t, "64Mi", ars.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().String(), "Memory Limit should be set")
@@ -2468,3 +2468,43 @@ func TestNamespaceOverride(t *testing.T) {
}) })
} }
} }
func TestAutoscalingRunnerSetCustomAnnotationsAndLabelsApplied(t *testing.T) {
t.Parallel()
// Path to the helm chart we will test
helmChartPath, err := filepath.Abs("../../gha-runner-scale-set")
require.NoError(t, err)
releaseName := "test-runners"
namespaceName := "test-" + strings.ToLower(random.UniqueId())
options := &helm.Options{
Logger: logger.Discard,
SetValues: map[string]string{
"githubConfigUrl": "https://github.com/actions",
"githubConfigSecret.github_token": "gh_token12345",
"controllerServiceAccount.name": "arc",
"controllerServiceAccount.namespace": "arc-system",
"annotations.actions\\.github\\.com/vault": "azure_key_vault",
"annotations.actions\\.github\\.com/cleanup-manager-role-name": "not-propagated",
"labels.custom": "custom",
"labels.app\\.kubernetes\\.io/component": "not-propagated",
},
KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
}
output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"})
var autoscalingRunnerSet v1alpha1.AutoscalingRunnerSet
helm.UnmarshalK8SYaml(t, output, &autoscalingRunnerSet)
vault := autoscalingRunnerSet.Annotations["actions.github.com/vault"]
assert.Equal(t, "azure_key_vault", vault)
custom := autoscalingRunnerSet.Labels["custom"]
assert.Equal(t, "custom", custom)
assert.NotEqual(t, "not-propagated", autoscalingRunnerSet.Annotations["actions.github.com/cleanup-manager-role-name"])
assert.NotEqual(t, "not-propagated", autoscalingRunnerSet.Labels["app.kubernetes.io/component"])
}

View File

@@ -6,7 +6,7 @@ githubConfigUrl: ""
## You can choose to supply: ## You can choose to supply:
## A) a PAT token, ## A) a PAT token,
## B) a GitHub App, or ## B) a GitHub App, or
## C) a pre-defined Kubernetes secret. ## C) a pre-defined secret.
## The syntax for each of these variations is documented below. ## The syntax for each of these variations is documented below.
## (Variation A) When using a PAT token, the syntax is as follows: ## (Variation A) When using a PAT token, the syntax is as follows:
githubConfigSecret: githubConfigSecret:
@@ -17,6 +17,7 @@ githubConfigSecret:
## (Variation B) When using a GitHub App, the syntax is as follows: ## (Variation B) When using a GitHub App, the syntax is as follows:
# githubConfigSecret: # githubConfigSecret:
# # NOTE: IDs MUST be strings, use quotes # # NOTE: IDs MUST be strings, use quotes
# # The github_app_id can be an app_id or the client_id
# github_app_id: "" # github_app_id: ""
# github_app_installation_id: "" # github_app_installation_id: ""
# github_app_private_key: | # github_app_private_key: |
@@ -27,8 +28,11 @@ githubConfigSecret:
# . # .
# private key line N # private key line N
# #
## (Variation C) When using a pre-defined Kubernetes secret in the same namespace that the gha-runner-scale-set is going to deploy, ## (Variation C) When using a pre-defined secret.
## the syntax is as follows: ## The secret can be pulled either directly from Kubernetes, or from the vault, depending on configuration.
## Kubernetes secret in the same namespace that the gha-runner-scale-set is going to deploy.
## On the other hand, if the vault is configured, secret name will be used to fetch the app configuration.
## The syntax is as follows:
# githubConfigSecret: pre-defined-secret # githubConfigSecret: pre-defined-secret
## Notes on using pre-defined Kubernetes secrets: ## Notes on using pre-defined Kubernetes secrets:
## You need to make sure your predefined secret has all the required secret data set properly. ## You need to make sure your predefined secret has all the required secret data set properly.
@@ -84,6 +88,26 @@ githubConfigSecret:
# key: ca.crt # key: ca.crt
# runnerMountPath: /usr/local/share/ca-certificates/ # runnerMountPath: /usr/local/share/ca-certificates/
# keyVault:
# Available values: "azure_key_vault"
# type: ""
# Configuration related to azure key vault
# azure_key_vault:
# url: ""
# client_id: ""
# tenant_id: ""
# certificate_path: ""
# proxy:
# http:
# url: http://proxy.com:1234
# credentialSecretRef: proxy-auth # a secret with `username` and `password` keys
# https:
# url: http://proxy.com:1234
# credentialSecretRef: proxy-auth # a secret with `username` and `password` keys
# noProxy:
# - example.com
# - example.org
## Container mode is an object that provides out-of-box configuration ## Container mode is an object that provides out-of-box configuration
## for dind and kubernetes mode. Template will be modified as documented under the ## for dind and kubernetes mode. Template will be modified as documented under the
## template object. ## template object.
@@ -130,7 +154,7 @@ githubConfigSecret:
# counters: # counters:
# gha_started_jobs_total: # gha_started_jobs_total:
# labels: # labels:
# ["repository", "organization", "enterprise", "job_name", "event_name"] # ["repository", "organization", "enterprise", "job_name", "event_name", "job_workflow_ref"]
# gha_completed_jobs_total: # gha_completed_jobs_total:
# labels: # labels:
# [ # [
@@ -140,6 +164,7 @@ githubConfigSecret:
# "job_name", # "job_name",
# "event_name", # "event_name",
# "job_result", # "job_result",
# "job_workflow_ref",
# ] # ]
# gauges: # gauges:
# gha_assigned_jobs: # gha_assigned_jobs:
@@ -161,7 +186,7 @@ githubConfigSecret:
# histograms: # histograms:
# gha_job_startup_duration_seconds: # gha_job_startup_duration_seconds:
# labels: # labels:
# ["repository", "organization", "enterprise", "job_name", "event_name"] # ["repository", "organization", "enterprise", "job_name", "event_name","job_workflow_ref"]
# buckets: # buckets:
# [ # [
# 0.01, # 0.01,
@@ -219,6 +244,7 @@ githubConfigSecret:
# "job_name", # "job_name",
# "event_name", # "event_name",
# "job_result", # "job_result",
# "job_workflow_ref"
# ] # ]
# buckets: # buckets:
# [ # [
@@ -283,18 +309,6 @@ template:
## volumeMounts: ## volumeMounts:
## - name: dind-externals ## - name: dind-externals
## mountPath: /home/runner/tmpDir ## mountPath: /home/runner/tmpDir
## containers:
## - name: runner
## image: ghcr.io/actions/actions-runner:latest
## command: ["/home/runner/run.sh"]
## env:
## - name: DOCKER_HOST
## value: unix:///var/run/docker.sock
## volumeMounts:
## - name: work
## mountPath: /home/runner/_work
## - name: dind-sock
## mountPath: /var/run
## - name: dind ## - name: dind
## image: docker:dind ## image: docker:dind
## args: ## args:
@@ -306,6 +320,15 @@ template:
## value: "123" ## value: "123"
## securityContext: ## securityContext:
## privileged: true ## privileged: true
## restartPolicy: Always
## startupProbe:
## exec:
## command:
## - docker
## - info
## initialDelaySeconds: 0
## failureThreshold: 24
## periodSeconds: 5
## volumeMounts: ## volumeMounts:
## - name: work ## - name: work
## mountPath: /home/runner/_work ## mountPath: /home/runner/_work
@@ -313,6 +336,20 @@ template:
## mountPath: /var/run ## mountPath: /var/run
## - name: dind-externals ## - name: dind-externals
## mountPath: /home/runner/externals ## mountPath: /home/runner/externals
## containers:
## - name: runner
## image: ghcr.io/actions/actions-runner:latest
## command: ["/home/runner/run.sh"]
## env:
## - name: DOCKER_HOST
## value: unix:///var/run/docker.sock
## - name: RUNNER_WAIT_FOR_DOCKER_IN_SECONDS
## value: "120"
## volumeMounts:
## - name: work
## mountPath: /home/runner/_work
## - name: dind-sock
## mountPath: /var/run
## volumes: ## volumes:
## - name: work ## - name: work
## emptyDir: {} ## emptyDir: {}

View File

@@ -17,7 +17,7 @@ import (
// App is responsible for initializing required components and running the app. // App is responsible for initializing required components and running the app.
type App struct { type App struct {
// configured fields // configured fields
config config.Config config *config.Config
logger logr.Logger logger logr.Logger
// initialized fields // initialized fields
@@ -38,8 +38,12 @@ type Worker interface {
} }
func New(config config.Config) (*App, error) { func New(config config.Config) (*App, error) {
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate config: %w", err)
}
app := &App{ app := &App{
config: config, config: &config,
} }
ghConfig, err := actions.ParseGitHubConfigFromURL(config.ConfigureUrl) ghConfig, err := actions.ParseGitHubConfigFromURL(config.ConfigureUrl)
@@ -69,8 +73,8 @@ func New(config config.Config) (*App, error) {
Repository: ghConfig.Repository, Repository: ghConfig.Repository,
ServerAddr: config.MetricsAddr, ServerAddr: config.MetricsAddr,
ServerEndpoint: config.MetricsEndpoint, ServerEndpoint: config.MetricsEndpoint,
Metrics: config.Metrics,
Logger: app.logger.WithName("metrics exporter"), Logger: app.logger.WithName("metrics exporter"),
Metrics: *config.Metrics,
}) })
} }

View File

@@ -1,6 +1,7 @@
package config package config
import ( import (
"context"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -9,19 +10,26 @@ import (
"os" "os"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/build" "github.com/actions/actions-runner-controller/build"
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/logging" "github.com/actions/actions-runner-controller/logging"
"github.com/actions/actions-runner-controller/vault"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"golang.org/x/net/http/httpproxy" "golang.org/x/net/http/httpproxy"
) )
type Config struct { type Config struct {
ConfigureUrl string `json:"configure_url"` ConfigureUrl string `json:"configure_url"`
AppID int64 `json:"app_id"` VaultType vault.VaultType `json:"vault_type"`
AppInstallationID int64 `json:"app_installation_id"` VaultLookupKey string `json:"vault_lookup_key"`
AppPrivateKey string `json:"app_private_key"` // If the VaultType is set to "azure_key_vault", this field must be populated.
Token string `json:"token"` AzureKeyVaultConfig *azurekeyvault.Config `json:"azure_key_vault,omitempty"`
// AppConfig contains the GitHub App configuration.
// It is initially set to nil if VaultType is set.
// Otherwise, it is populated with the GitHub App credentials from the GitHub secret.
*appconfig.AppConfig
EphemeralRunnerSetNamespace string `json:"ephemeral_runner_set_namespace"` EphemeralRunnerSetNamespace string `json:"ephemeral_runner_set_namespace"`
EphemeralRunnerSetName string `json:"ephemeral_runner_set_name"` EphemeralRunnerSetName string `json:"ephemeral_runner_set_name"`
MaxRunners int `json:"max_runners"` MaxRunners int `json:"max_runners"`
@@ -36,23 +44,58 @@ type Config struct {
Metrics *v1alpha1.MetricsConfig `json:"metrics"` Metrics *v1alpha1.MetricsConfig `json:"metrics"`
} }
func Read(path string) (Config, error) { func Read(ctx context.Context, configPath string) (*Config, error) {
f, err := os.Open(path) f, err := os.Open(configPath)
if err != nil { if err != nil {
return Config{}, err return nil, err
} }
defer f.Close() defer f.Close()
var config Config var config Config
if err := json.NewDecoder(f).Decode(&config); err != nil { if err := json.NewDecoder(f).Decode(&config); err != nil {
return Config{}, fmt.Errorf("failed to decode config: %w", err) return nil, fmt.Errorf("failed to decode config: %w", err)
} }
var vault vault.Vault
switch config.VaultType {
case "":
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate configuration: %v", err)
}
return &config, nil
case "azure_key_vault":
akv, err := azurekeyvault.New(*config.AzureKeyVaultConfig)
if err != nil {
return nil, fmt.Errorf("failed to create Azure Key Vault client: %w", err)
}
vault = akv
default:
return nil, fmt.Errorf("unsupported vault type: %s", config.VaultType)
}
appConfigRaw, err := vault.GetSecret(ctx, config.VaultLookupKey)
if err != nil {
return nil, fmt.Errorf("failed to get app config from vault: %w", err)
}
appConfig, err := appconfig.FromJSONString(appConfigRaw)
if err != nil {
return nil, fmt.Errorf("failed to read app config from string: %v", err)
}
config.AppConfig = appConfig
if err := config.Validate(); err != nil { if err := config.Validate(); err != nil {
return Config{}, fmt.Errorf("failed to validate config: %w", err) return nil, fmt.Errorf("config validation failed: %w", err)
} }
return config, nil if ctx.Err() != nil {
return nil, ctx.Err()
}
return &config, nil
} }
// Validate checks the configuration for errors. // Validate checks the configuration for errors.
@@ -62,26 +105,30 @@ func (c *Config) Validate() error {
} }
if len(c.EphemeralRunnerSetNamespace) == 0 || len(c.EphemeralRunnerSetName) == 0 { if len(c.EphemeralRunnerSetNamespace) == 0 || len(c.EphemeralRunnerSetName) == 0 {
return fmt.Errorf("EphemeralRunnerSetNamespace '%s' or EphemeralRunnerSetName '%s' is missing", c.EphemeralRunnerSetNamespace, c.EphemeralRunnerSetName) return fmt.Errorf("EphemeralRunnerSetNamespace %q or EphemeralRunnerSetName %q is missing", c.EphemeralRunnerSetNamespace, c.EphemeralRunnerSetName)
} }
if c.RunnerScaleSetId == 0 { if c.RunnerScaleSetId == 0 {
return fmt.Errorf("RunnerScaleSetId '%d' is missing", c.RunnerScaleSetId) return fmt.Errorf(`RunnerScaleSetId "%d" is missing`, c.RunnerScaleSetId)
} }
if c.MaxRunners < c.MinRunners { if c.MaxRunners < c.MinRunners {
return fmt.Errorf("MinRunners '%d' cannot be greater than MaxRunners '%d'", c.MinRunners, c.MaxRunners) return fmt.Errorf(`MinRunners "%d" cannot be greater than MaxRunners "%d"`, c.MinRunners, c.MaxRunners)
} }
hasToken := len(c.Token) > 0 if c.VaultType != "" {
hasPrivateKeyConfig := c.AppID > 0 && c.AppPrivateKey != "" if err := c.VaultType.Validate(); err != nil {
return fmt.Errorf("VaultType validation failed: %w", err)
if !hasToken && !hasPrivateKeyConfig { }
return fmt.Errorf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(c.Token), c.AppID, c.AppInstallationID, len(c.AppPrivateKey)) if c.VaultLookupKey == "" {
return fmt.Errorf("VaultLookupKey is required when VaultType is set to %q", c.VaultType)
}
} }
if hasToken && hasPrivateKeyConfig { if c.VaultType == "" && c.VaultLookupKey == "" {
return fmt.Errorf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(c.Token), c.AppID, c.AppInstallationID, len(c.AppPrivateKey)) if err := c.AppConfig.Validate(); err != nil {
return fmt.Errorf("AppConfig validation failed: %w", err)
}
} }
return nil return nil

View File

@@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/cmd/ghalistener/config" "github.com/actions/actions-runner-controller/cmd/ghalistener/config"
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/github/actions/testserver" "github.com/actions/actions-runner-controller/github/actions/testserver"
@@ -53,7 +54,9 @@ func TestCustomerServerRootCA(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: server.ConfigURLForOrg("myorg"), ConfigureUrl: server.ConfigURLForOrg("myorg"),
ServerRootCA: certsString, ServerRootCA: certsString,
Token: "token", AppConfig: &appconfig.AppConfig{
Token: "token",
},
} }
client, err := config.ActionsClient(logr.Discard()) client, err := config.ActionsClient(logr.Discard())
@@ -80,7 +83,9 @@ func TestProxySettings(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: "https://github.com/org/repo", ConfigureUrl: "https://github.com/org/repo",
Token: "token", AppConfig: &appconfig.AppConfig{
Token: "token",
},
} }
client, err := config.ActionsClient(logr.Discard()) client, err := config.ActionsClient(logr.Discard())
@@ -110,7 +115,9 @@ func TestProxySettings(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: "https://github.com/org/repo", ConfigureUrl: "https://github.com/org/repo",
Token: "token", AppConfig: &appconfig.AppConfig{
Token: "token",
},
} }
client, err := config.ActionsClient(logr.Discard(), actions.WithRetryMax(0)) client, err := config.ActionsClient(logr.Discard(), actions.WithRetryMax(0))
@@ -145,7 +152,9 @@ func TestProxySettings(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: "https://github.com/org/repo", ConfigureUrl: "https://github.com/org/repo",
Token: "token", AppConfig: &appconfig.AppConfig{
Token: "token",
},
} }
client, err := config.ActionsClient(logr.Discard()) client, err := config.ActionsClient(logr.Discard())

View File

@@ -1,92 +0,0 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfigValidationMinMax(t *testing.T) {
config := &Config{
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 5,
MaxRunners: 2,
Token: "token",
}
err := config.Validate()
assert.ErrorContains(t, err, "MinRunners '5' cannot be greater than MaxRunners '2", "Expected error about MinRunners > MaxRunners")
}
func TestConfigValidationMissingToken(t *testing.T) {
config := &Config{
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
}
func TestConfigValidationAppKey(t *testing.T) {
config := &Config{
AppID: 1,
AppInstallationID: 10,
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
}
func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
config := &Config{
AppID: 1,
AppInstallationID: 10,
AppPrivateKey: "asdf",
Token: "asdf",
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
expectedError := fmt.Sprintf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
}
func TestConfigValidation(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 1,
MaxRunners: 5,
Token: "asdf",
}
err := config.Validate()
assert.NoError(t, err, "Expected no error")
}
func TestConfigValidationConfigUrl(t *testing.T) {
config := &Config{
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
assert.ErrorContains(t, err, "GitHubConfigUrl is not provided", "Expected error about missing ConfigureUrl")
}

View File

@@ -0,0 +1,170 @@
package config
import (
"testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/vault"
"github.com/stretchr/testify/assert"
)
func TestConfigValidationMinMax(t *testing.T) {
config := &Config{
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 5,
MaxRunners: 2,
AppConfig: &appconfig.AppConfig{
Token: "token",
},
}
err := config.Validate()
assert.ErrorContains(t, err, `MinRunners "5" cannot be greater than MaxRunners "2"`, "Expected error about MinRunners > MaxRunners")
}
func TestConfigValidationMissingToken(t *testing.T) {
config := &Config{
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
expectedError := "AppConfig validation failed: missing app config"
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
}
func TestConfigValidationAppKey(t *testing.T) {
t.Parallel()
t.Run("app id integer", func(t *testing.T) {
t.Parallel()
config := &Config{
AppConfig: &appconfig.AppConfig{
AppID: "1",
AppInstallationID: 10,
},
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
expectedError := "AppConfig validation failed: no credentials provided: either a PAT or GitHub App credentials should be provided"
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
})
t.Run("app id as client id", func(t *testing.T) {
t.Parallel()
config := &Config{
AppConfig: &appconfig.AppConfig{
AppID: "Iv23f8doAlphaNumer1c",
AppInstallationID: 10,
},
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
expectedError := "AppConfig validation failed: no credentials provided: either a PAT or GitHub App credentials should be provided"
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
})
}
func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
config := &Config{
AppConfig: &appconfig.AppConfig{
AppID: "1",
AppInstallationID: 10,
AppPrivateKey: "asdf",
Token: "asdf",
},
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
expectedError := "AppConfig validation failed: both PAT and GitHub App credentials provided. should only provide one"
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
}
func TestConfigValidation(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 1,
MaxRunners: 5,
AppConfig: &appconfig.AppConfig{
Token: "asdf",
},
}
err := config.Validate()
assert.NoError(t, err, "Expected no error")
}
func TestConfigValidationConfigUrl(t *testing.T) {
config := &Config{
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
assert.ErrorContains(t, err, "GitHubConfigUrl is not provided", "Expected error about missing ConfigureUrl")
}
func TestConfigValidationWithVaultConfig(t *testing.T) {
t.Run("valid", func(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 1,
MaxRunners: 5,
VaultType: vault.VaultTypeAzureKeyVault,
VaultLookupKey: "testkey",
}
err := config.Validate()
assert.NoError(t, err, "Expected no error for valid vault type")
})
t.Run("invalid vault type", func(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 1,
MaxRunners: 5,
VaultType: vault.VaultType("invalid_vault_type"),
VaultLookupKey: "testkey",
}
err := config.Validate()
assert.ErrorContains(t, err, `unknown vault type: "invalid_vault_type"`, "Expected error for invalid vault type")
})
t.Run("vault type set without lookup key", func(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 1,
MaxRunners: 5,
VaultType: vault.VaultTypeAzureKeyVault,
VaultLookupKey: "",
}
err := config.Validate()
assert.ErrorContains(t, err, `VaultLookupKey is required when VaultType is set to "azure_key_vault"`, "Expected error for vault type without lookup key")
})
}

View File

@@ -13,26 +13,27 @@ import (
) )
func main() { func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
configPath, ok := os.LookupEnv("LISTENER_CONFIG_PATH") configPath, ok := os.LookupEnv("LISTENER_CONFIG_PATH")
if !ok { if !ok {
fmt.Fprintf(os.Stderr, "Error: LISTENER_CONFIG_PATH environment variable is not set\n") fmt.Fprintf(os.Stderr, "Error: LISTENER_CONFIG_PATH environment variable is not set\n")
os.Exit(1) os.Exit(1)
} }
config, err := config.Read(configPath)
config, err := config.Read(ctx, configPath)
if err != nil { if err != nil {
log.Printf("Failed to read config: %v", err) log.Printf("Failed to read config: %v", err)
os.Exit(1) os.Exit(1)
} }
app, err := app.New(config) app, err := app.New(*config)
if err != nil { if err != nil {
log.Printf("Failed to initialize app: %v", err) log.Printf("Failed to initialize app: %v", err)
os.Exit(1) os.Exit(1)
} }
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := app.Run(ctx); err != nil { if err := app.Run(ctx); err != nil {
log.Printf("Application returned an error: %v", err) log.Printf("Application returned an error: %v", err)
os.Exit(1) os.Exit(1)

View File

@@ -21,6 +21,7 @@ const (
labelKeyOrganization = "organization" labelKeyOrganization = "organization"
labelKeyRepository = "repository" labelKeyRepository = "repository"
labelKeyJobName = "job_name" labelKeyJobName = "job_name"
labelKeyJobWorkflowRef = "job_workflow_ref"
labelKeyEventName = "event_name" labelKeyEventName = "event_name"
labelKeyJobResult = "job_result" labelKeyJobResult = "job_result"
) )
@@ -75,11 +76,12 @@ var metricsHelp = metricsHelpRegistry{
func (e *exporter) jobLabels(jobBase *actions.JobMessageBase) prometheus.Labels { func (e *exporter) jobLabels(jobBase *actions.JobMessageBase) prometheus.Labels {
return prometheus.Labels{ return prometheus.Labels{
labelKeyEnterprise: e.scaleSetLabels[labelKeyEnterprise], labelKeyEnterprise: e.scaleSetLabels[labelKeyEnterprise],
labelKeyOrganization: jobBase.OwnerName, labelKeyOrganization: jobBase.OwnerName,
labelKeyRepository: jobBase.RepositoryName, labelKeyRepository: jobBase.RepositoryName,
labelKeyJobName: jobBase.JobDisplayName, labelKeyJobName: jobBase.JobDisplayName,
labelKeyEventName: jobBase.EventName, labelKeyJobWorkflowRef: jobBase.JobWorkflowRef,
labelKeyEventName: jobBase.EventName,
} }
} }
@@ -152,13 +154,148 @@ type ExporterConfig struct {
ServerAddr string ServerAddr string
ServerEndpoint string ServerEndpoint string
Logger logr.Logger Logger logr.Logger
Metrics v1alpha1.MetricsConfig Metrics *v1alpha1.MetricsConfig
}
var defaultMetrics = v1alpha1.MetricsConfig{
Counters: map[string]*v1alpha1.CounterMetric{
MetricStartedJobsTotal: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyJobName,
labelKeyEventName,
},
},
MetricCompletedJobsTotal: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyJobName,
labelKeyEventName,
labelKeyJobResult,
},
},
},
Gauges: map[string]*v1alpha1.GaugeMetric{
MetricAssignedJobs: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyRunnerScaleSetName,
labelKeyRunnerScaleSetNamespace,
},
},
MetricRunningJobs: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyRunnerScaleSetName,
labelKeyRunnerScaleSetNamespace,
},
},
MetricRegisteredRunners: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyRunnerScaleSetName,
labelKeyRunnerScaleSetNamespace,
},
},
MetricBusyRunners: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyRunnerScaleSetName,
labelKeyRunnerScaleSetNamespace,
},
},
MetricMinRunners: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyRunnerScaleSetName,
labelKeyRunnerScaleSetNamespace,
},
},
MetricMaxRunners: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyRunnerScaleSetName,
labelKeyRunnerScaleSetNamespace,
},
},
MetricDesiredRunners: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyRunnerScaleSetName,
labelKeyRunnerScaleSetNamespace,
},
},
MetricIdleRunners: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyRunnerScaleSetName,
labelKeyRunnerScaleSetNamespace,
},
},
},
Histograms: map[string]*v1alpha1.HistogramMetric{
MetricJobStartupDurationSeconds: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyJobName,
labelKeyEventName,
},
Buckets: defaultRuntimeBuckets,
},
MetricJobExecutionDurationSeconds: {
Labels: []string{
labelKeyEnterprise,
labelKeyOrganization,
labelKeyRepository,
labelKeyJobName,
labelKeyEventName,
labelKeyJobResult,
},
Buckets: defaultRuntimeBuckets,
},
},
}
func (e *ExporterConfig) defaults() {
if e.ServerAddr == "" {
e.ServerAddr = ":8080"
}
if e.ServerEndpoint == "" {
e.ServerEndpoint = "/metrics"
}
if e.Metrics == nil {
defaultMetrics := defaultMetrics
e.Metrics = &defaultMetrics
}
} }
func NewExporter(config ExporterConfig) ServerExporter { func NewExporter(config ExporterConfig) ServerExporter {
config.defaults()
reg := prometheus.NewRegistry() reg := prometheus.NewRegistry()
metrics := installMetrics(config.Metrics, reg, config.Logger) metrics := installMetrics(*config.Metrics, reg, config.Logger)
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle( mux.Handle(
@@ -287,7 +424,7 @@ func (e *exporter) ListenAndServe(ctx context.Context) error {
} }
func (e *exporter) setGauge(name string, allLabels prometheus.Labels, val float64) { func (e *exporter) setGauge(name string, allLabels prometheus.Labels, val float64) {
m, ok := e.metrics.gauges[name] m, ok := e.gauges[name]
if !ok { if !ok {
return return
} }
@@ -299,7 +436,7 @@ func (e *exporter) setGauge(name string, allLabels prometheus.Labels, val float6
} }
func (e *exporter) incCounter(name string, allLabels prometheus.Labels) { func (e *exporter) incCounter(name string, allLabels prometheus.Labels) {
m, ok := e.metrics.counters[name] m, ok := e.counters[name]
if !ok { if !ok {
return return
} }
@@ -311,7 +448,7 @@ func (e *exporter) incCounter(name string, allLabels prometheus.Labels) {
} }
func (e *exporter) observeHistogram(name string, allLabels prometheus.Labels, val float64) { func (e *exporter) observeHistogram(name string, allLabels prometheus.Labels, val float64) {
m, ok := e.metrics.histograms[name] m, ok := e.histograms[name]
if !ok { if !ok {
return return
} }
@@ -331,7 +468,7 @@ func (e *exporter) PublishStatistics(stats *actions.RunnerScaleSetStatistic) {
e.setGauge(MetricAssignedJobs, e.scaleSetLabels, float64(stats.TotalAssignedJobs)) e.setGauge(MetricAssignedJobs, e.scaleSetLabels, float64(stats.TotalAssignedJobs))
e.setGauge(MetricRunningJobs, e.scaleSetLabels, float64(stats.TotalRunningJobs)) e.setGauge(MetricRunningJobs, e.scaleSetLabels, float64(stats.TotalRunningJobs))
e.setGauge(MetricRegisteredRunners, e.scaleSetLabels, float64(stats.TotalRegisteredRunners)) e.setGauge(MetricRegisteredRunners, e.scaleSetLabels, float64(stats.TotalRegisteredRunners))
e.setGauge(MetricBusyRunners, e.scaleSetLabels, float64(float64(stats.TotalRegisteredRunners))) e.setGauge(MetricBusyRunners, e.scaleSetLabels, float64(stats.TotalBusyRunners))
e.setGauge(MetricIdleRunners, e.scaleSetLabels, float64(stats.TotalIdleRunners)) e.setGauge(MetricIdleRunners, e.scaleSetLabels, float64(stats.TotalIdleRunners))
} }
@@ -339,7 +476,7 @@ func (e *exporter) PublishJobStarted(msg *actions.JobStarted) {
l := e.startedJobLabels(msg) l := e.startedJobLabels(msg)
e.incCounter(MetricStartedJobsTotal, l) e.incCounter(MetricStartedJobsTotal, l)
startupDuration := msg.JobMessageBase.RunnerAssignTime.Unix() - msg.JobMessageBase.ScaleSetAssignTime.Unix() startupDuration := msg.RunnerAssignTime.Unix() - msg.ScaleSetAssignTime.Unix()
e.observeHistogram(MetricJobStartupDurationSeconds, l, float64(startupDuration)) e.observeHistogram(MetricJobStartupDurationSeconds, l, float64(startupDuration))
} }
@@ -347,7 +484,7 @@ func (e *exporter) PublishJobCompleted(msg *actions.JobCompleted) {
l := e.completedJobLabels(msg) l := e.completedJobLabels(msg)
e.incCounter(MetricCompletedJobsTotal, l) e.incCounter(MetricCompletedJobsTotal, l)
executionDuration := msg.JobMessageBase.FinishTime.Unix() - msg.JobMessageBase.RunnerAssignTime.Unix() executionDuration := msg.FinishTime.Unix() - msg.RunnerAssignTime.Unix()
e.observeHistogram(MetricJobExecutionDurationSeconds, l, float64(executionDuration)) e.observeHistogram(MetricJobExecutionDurationSeconds, l, float64(executionDuration))
} }

View File

@@ -7,6 +7,7 @@ import (
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestInstallMetrics(t *testing.T) { func TestInstallMetrics(t *testing.T) {
@@ -86,3 +87,179 @@ func TestInstallMetrics(t *testing.T) {
assert.Equal(t, duration.config.Labels, metricsConfig.Histograms[MetricJobStartupDurationSeconds].Labels) assert.Equal(t, duration.config.Labels, metricsConfig.Histograms[MetricJobStartupDurationSeconds].Labels)
assert.Equal(t, duration.config.Buckets, defaultRuntimeBuckets) assert.Equal(t, duration.config.Buckets, defaultRuntimeBuckets)
} }
func TestNewExporter(t *testing.T) {
t.Run("with defaults metrics applied", func(t *testing.T) {
config := ExporterConfig{
ScaleSetName: "test-scale-set",
ScaleSetNamespace: "test-namespace",
Enterprise: "",
Organization: "org",
Repository: "repo",
ServerAddr: ":6060",
ServerEndpoint: "/metrics",
Logger: logr.Discard(),
Metrics: nil, // when metrics is nil, all default metrics should be registered
}
exporter, ok := NewExporter(config).(*exporter)
require.True(t, ok, "expected exporter to be of type *exporter")
require.NotNil(t, exporter)
reg := prometheus.NewRegistry()
wantMetrics := installMetrics(defaultMetrics, reg, config.Logger)
assert.Equal(t, len(wantMetrics.counters), len(exporter.counters))
for k, v := range wantMetrics.counters {
assert.Contains(t, exporter.counters, k)
assert.Equal(t, v.config, exporter.counters[k].config)
}
assert.Equal(t, len(wantMetrics.gauges), len(exporter.gauges))
for k, v := range wantMetrics.gauges {
assert.Contains(t, exporter.gauges, k)
assert.Equal(t, v.config, exporter.gauges[k].config)
}
assert.Equal(t, len(wantMetrics.histograms), len(exporter.histograms))
for k, v := range wantMetrics.histograms {
assert.Contains(t, exporter.histograms, k)
assert.Equal(t, v.config, exporter.histograms[k].config)
}
require.NotNil(t, exporter.srv)
assert.Equal(t, config.ServerAddr, exporter.srv.Addr)
})
t.Run("with default server URL", func(t *testing.T) {
config := ExporterConfig{
ScaleSetName: "test-scale-set",
ScaleSetNamespace: "test-namespace",
Enterprise: "",
Organization: "org",
Repository: "repo",
ServerAddr: "", // empty ServerAddr should default to ":8080"
ServerEndpoint: "",
Logger: logr.Discard(),
Metrics: nil, // when metrics is nil, all default metrics should be registered
}
exporter, ok := NewExporter(config).(*exporter)
require.True(t, ok, "expected exporter to be of type *exporter")
require.NotNil(t, exporter)
reg := prometheus.NewRegistry()
wantMetrics := installMetrics(defaultMetrics, reg, config.Logger)
assert.Equal(t, len(wantMetrics.counters), len(exporter.counters))
for k, v := range wantMetrics.counters {
assert.Contains(t, exporter.counters, k)
assert.Equal(t, v.config, exporter.counters[k].config)
}
assert.Equal(t, len(wantMetrics.gauges), len(exporter.gauges))
for k, v := range wantMetrics.gauges {
assert.Contains(t, exporter.gauges, k)
assert.Equal(t, v.config, exporter.gauges[k].config)
}
assert.Equal(t, len(wantMetrics.histograms), len(exporter.histograms))
for k, v := range wantMetrics.histograms {
assert.Contains(t, exporter.histograms, k)
assert.Equal(t, v.config, exporter.histograms[k].config)
}
require.NotNil(t, exporter.srv)
assert.Equal(t, exporter.srv.Addr, ":8080")
})
t.Run("with metrics configured", func(t *testing.T) {
metricsConfig := v1alpha1.MetricsConfig{
Counters: map[string]*v1alpha1.CounterMetric{
MetricStartedJobsTotal: {
Labels: []string{labelKeyRepository},
},
},
Gauges: map[string]*v1alpha1.GaugeMetric{
MetricAssignedJobs: {
Labels: []string{labelKeyRepository},
},
},
Histograms: map[string]*v1alpha1.HistogramMetric{
MetricJobExecutionDurationSeconds: {
Labels: []string{labelKeyRepository},
Buckets: []float64{0.1, 1},
},
},
}
config := ExporterConfig{
ScaleSetName: "test-scale-set",
ScaleSetNamespace: "test-namespace",
Enterprise: "",
Organization: "org",
Repository: "repo",
ServerAddr: ":6060",
ServerEndpoint: "/metrics",
Logger: logr.Discard(),
Metrics: &metricsConfig,
}
exporter, ok := NewExporter(config).(*exporter)
require.True(t, ok, "expected exporter to be of type *exporter")
require.NotNil(t, exporter)
reg := prometheus.NewRegistry()
wantMetrics := installMetrics(metricsConfig, reg, config.Logger)
assert.Equal(t, len(wantMetrics.counters), len(exporter.counters))
for k, v := range wantMetrics.counters {
assert.Contains(t, exporter.counters, k)
assert.Equal(t, v.config, exporter.counters[k].config)
}
assert.Equal(t, len(wantMetrics.gauges), len(exporter.gauges))
for k, v := range wantMetrics.gauges {
assert.Contains(t, exporter.gauges, k)
assert.Equal(t, v.config, exporter.gauges[k].config)
}
assert.Equal(t, len(wantMetrics.histograms), len(exporter.histograms))
for k, v := range wantMetrics.histograms {
assert.Contains(t, exporter.histograms, k)
assert.Equal(t, v.config, exporter.histograms[k].config)
}
require.NotNil(t, exporter.srv)
assert.Equal(t, config.ServerAddr, exporter.srv.Addr)
})
}
func TestExporterConfigDefaults(t *testing.T) {
config := ExporterConfig{
ScaleSetName: "test-scale-set",
ScaleSetNamespace: "test-namespace",
Enterprise: "",
Organization: "org",
Repository: "repo",
ServerAddr: "",
ServerEndpoint: "",
Logger: logr.Discard(),
Metrics: nil, // when metrics is nil, all default metrics should be registered
}
config.defaults()
want := ExporterConfig{
ScaleSetName: "test-scale-set",
ScaleSetNamespace: "test-namespace",
Enterprise: "",
Organization: "org",
Repository: "repo",
ServerAddr: ":8080", // default server address
ServerEndpoint: "/metrics", // default server endpoint
Logger: logr.Discard(),
Metrics: &defaultMetrics, // when metrics is nil, all default metrics should be registered
}
assert.Equal(t, want, config)
}

View File

@@ -7863,6 +7863,53 @@ spec:
- containers - containers
type: object type: object
type: object type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
type: object type: object
status: status:
description: AutoscalingListenerStatus defines the observed state of AutoscalingListener description: AutoscalingListenerStatus defines the observed state of AutoscalingListener

View File

@@ -15504,6 +15504,53 @@ spec:
- containers - containers
type: object type: object
type: object type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
type: object type: object
status: status:
description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet

View File

@@ -7784,6 +7784,53 @@ spec:
required: required:
- containers - containers
type: object type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
required: required:
- githubConfigSecret - githubConfigSecret
- githubConfigUrl - githubConfigUrl
@@ -7794,7 +7841,8 @@ spec:
properties: properties:
failures: failures:
additionalProperties: additionalProperties:
type: boolean format: date-time
type: string
type: object type: object
jobDisplayName: jobDisplayName:
type: string type: string

View File

@@ -7778,6 +7778,53 @@ spec:
required: required:
- containers - containers
type: object type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
required: required:
- githubConfigSecret - githubConfigSecret
- githubConfigUrl - githubConfigUrl

View File

@@ -32,6 +32,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/reconcile"
v1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" v1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/metrics" "github.com/actions/actions-runner-controller/controllers/actions.github.com/metrics"
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
hash "github.com/actions/actions-runner-controller/hash" hash "github.com/actions/actions-runner-controller/hash"
@@ -77,7 +78,7 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if !autoscalingListener.ObjectMeta.DeletionTimestamp.IsZero() { if !autoscalingListener.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(autoscalingListener, autoscalingListenerFinalizerName) { if !controllerutil.ContainsFinalizer(autoscalingListener, autoscalingListenerFinalizerName) {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@@ -128,41 +129,24 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// Check if the GitHub config secret exists appConfig, err := r.GetAppConfig(ctx, &autoscalingRunnerSet)
secret := new(corev1.Secret) if err != nil {
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: autoscalingListener.Spec.GitHubConfigSecret}, secret); err != nil { log.Error(
log.Error(err, "Failed to find GitHub config secret.", err,
"namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "Failed to get app config for AutoscalingRunnerSet.",
"name", autoscalingListener.Spec.GitHubConfigSecret) "namespace",
autoscalingRunnerSet.Namespace,
"name",
autoscalingRunnerSet.GitHubConfigSecret,
)
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// Create a mirror secret in the same namespace as the AutoscalingListener
mirrorSecret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerSecretMirrorName(autoscalingListener)}, mirrorSecret); err != nil {
if !kerrors.IsNotFound(err) {
log.Error(err, "Unable to get listener secret mirror", "namespace", autoscalingListener.Namespace, "name", scaleSetListenerSecretMirrorName(autoscalingListener))
return ctrl.Result{}, err
}
// Create a mirror secret for the listener pod in the Controller namespace for listener pod to use
log.Info("Creating a mirror listener secret for the listener pod")
return r.createSecretsForListener(ctx, autoscalingListener, secret, log)
}
// make sure the mirror secret is up to date
mirrorSecretDataHash := mirrorSecret.Labels["secret-data-hash"]
secretDataHash := hash.ComputeTemplateHash(secret.Data)
if mirrorSecretDataHash != secretDataHash {
log.Info("Updating mirror listener secret for the listener pod", "mirrorSecretDataHash", mirrorSecretDataHash, "secretDataHash", secretDataHash)
return r.updateSecretsForListener(ctx, secret, mirrorSecret, log)
}
// Make sure the runner scale set listener service account is created for the listener pod in the controller namespace // Make sure the runner scale set listener service account is created for the listener pod in the controller namespace
serviceAccount := new(corev1.ServiceAccount) serviceAccount := new(corev1.ServiceAccount)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerServiceAccountName(autoscalingListener)}, serviceAccount); err != nil { if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: autoscalingListener.Name}, serviceAccount); err != nil {
if !kerrors.IsNotFound(err) { if !kerrors.IsNotFound(err) {
log.Error(err, "Unable to get listener service accounts", "namespace", autoscalingListener.Namespace, "name", scaleSetListenerServiceAccountName(autoscalingListener)) log.Error(err, "Unable to get listener service accounts", "namespace", autoscalingListener.Namespace, "name", autoscalingListener.Name)
return ctrl.Result{}, err return ctrl.Result{}, err
} }
@@ -175,9 +159,9 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
// Make sure the runner scale set listener role is created in the AutoscalingRunnerSet namespace // Make sure the runner scale set listener role is created in the AutoscalingRunnerSet namespace
listenerRole := new(rbacv1.Role) listenerRole := new(rbacv1.Role)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: scaleSetListenerRoleName(autoscalingListener)}, listenerRole); err != nil { if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: autoscalingListener.Name}, listenerRole); err != nil {
if !kerrors.IsNotFound(err) { if !kerrors.IsNotFound(err) {
log.Error(err, "Unable to get listener role", "namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "name", scaleSetListenerRoleName(autoscalingListener)) log.Error(err, "Unable to get listener role", "namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "name", autoscalingListener.Name)
return ctrl.Result{}, err return ctrl.Result{}, err
} }
@@ -197,9 +181,9 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
// Make sure the runner scale set listener role binding is created // Make sure the runner scale set listener role binding is created
listenerRoleBinding := new(rbacv1.RoleBinding) listenerRoleBinding := new(rbacv1.RoleBinding)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: scaleSetListenerRoleName(autoscalingListener)}, listenerRoleBinding); err != nil { if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: autoscalingListener.Name}, listenerRoleBinding); err != nil {
if !kerrors.IsNotFound(err) { if !kerrors.IsNotFound(err) {
log.Error(err, "Unable to get listener role binding", "namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "name", scaleSetListenerRoleName(autoscalingListener)) log.Error(err, "Unable to get listener role binding", "namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "name", autoscalingListener.Name)
return ctrl.Result{}, err return ctrl.Result{}, err
} }
@@ -239,7 +223,7 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
// Create a listener pod in the controller namespace // Create a listener pod in the controller namespace
log.Info("Creating a listener pod") log.Info("Creating a listener pod")
return r.createListenerPod(ctx, &autoscalingRunnerSet, autoscalingListener, serviceAccount, mirrorSecret, log) return r.createListenerPod(ctx, &autoscalingRunnerSet, autoscalingListener, serviceAccount, appConfig, log)
} }
cs := listenerContainerStatus(listenerPod) cs := listenerContainerStatus(listenerPod)
@@ -260,6 +244,19 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
log.Error(err, "Unable to delete the listener pod", "namespace", listenerPod.Namespace, "name", listenerPod.Name) log.Error(err, "Unable to delete the listener pod", "namespace", listenerPod.Namespace, "name", listenerPod.Name)
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// delete the listener config secret as well, so it gets recreated when the listener pod is recreated, with any new data if it exists
var configSecret corev1.Secret
err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerConfigName(autoscalingListener)}, &configSecret)
switch {
case err == nil && configSecret.DeletionTimestamp.IsZero():
log.Info("Deleting the listener config secret")
if err := r.Delete(ctx, &configSecret); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to delete listener config secret: %w", err)
}
case !kerrors.IsNotFound(err):
return ctrl.Result{}, fmt.Errorf("failed to get the listener config secret: %w", err)
}
} }
return ctrl.Result{}, nil return ctrl.Result{}, nil
case cs.State.Running != nil: case cs.State.Running != nil:
@@ -281,7 +278,7 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au
err = r.Get(ctx, types.NamespacedName{Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace}, listenerPod) err = r.Get(ctx, types.NamespacedName{Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace}, listenerPod)
switch { switch {
case err == nil: case err == nil:
if listenerPod.ObjectMeta.DeletionTimestamp.IsZero() { if listenerPod.DeletionTimestamp.IsZero() {
logger.Info("Deleting the listener pod") logger.Info("Deleting the listener pod")
if err := r.Delete(ctx, listenerPod); err != nil { if err := r.Delete(ctx, listenerPod); err != nil {
return false, fmt.Errorf("failed to delete listener pod: %w", err) return false, fmt.Errorf("failed to delete listener pod: %w", err)
@@ -299,7 +296,7 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au
err = r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerConfigName(autoscalingListener)}, &secret) err = r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerConfigName(autoscalingListener)}, &secret)
switch { switch {
case err == nil: case err == nil:
if secret.ObjectMeta.DeletionTimestamp.IsZero() { if secret.DeletionTimestamp.IsZero() {
logger.Info("Deleting the listener config secret") logger.Info("Deleting the listener config secret")
if err := r.Delete(ctx, &secret); err != nil { if err := r.Delete(ctx, &secret); err != nil {
return false, fmt.Errorf("failed to delete listener config secret: %w", err) return false, fmt.Errorf("failed to delete listener config secret: %w", err)
@@ -316,7 +313,7 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au
err = r.Get(ctx, types.NamespacedName{Name: proxyListenerSecretName(autoscalingListener), Namespace: autoscalingListener.Namespace}, proxySecret) err = r.Get(ctx, types.NamespacedName{Name: proxyListenerSecretName(autoscalingListener), Namespace: autoscalingListener.Namespace}, proxySecret)
switch { switch {
case err == nil: case err == nil:
if proxySecret.ObjectMeta.DeletionTimestamp.IsZero() { if proxySecret.DeletionTimestamp.IsZero() {
logger.Info("Deleting the listener proxy secret") logger.Info("Deleting the listener proxy secret")
if err := r.Delete(ctx, proxySecret); err != nil { if err := r.Delete(ctx, proxySecret); err != nil {
return false, fmt.Errorf("failed to delete listener proxy secret: %w", err) return false, fmt.Errorf("failed to delete listener proxy secret: %w", err)
@@ -330,10 +327,10 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au
} }
listenerRoleBinding := new(rbacv1.RoleBinding) listenerRoleBinding := new(rbacv1.RoleBinding)
err = r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: scaleSetListenerRoleName(autoscalingListener)}, listenerRoleBinding) err = r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: autoscalingListener.Name}, listenerRoleBinding)
switch { switch {
case err == nil: case err == nil:
if listenerRoleBinding.ObjectMeta.DeletionTimestamp.IsZero() { if listenerRoleBinding.DeletionTimestamp.IsZero() {
logger.Info("Deleting the listener role binding") logger.Info("Deleting the listener role binding")
if err := r.Delete(ctx, listenerRoleBinding); err != nil { if err := r.Delete(ctx, listenerRoleBinding); err != nil {
return false, fmt.Errorf("failed to delete listener role binding: %w", err) return false, fmt.Errorf("failed to delete listener role binding: %w", err)
@@ -346,10 +343,10 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au
logger.Info("Listener role binding is deleted") logger.Info("Listener role binding is deleted")
listenerRole := new(rbacv1.Role) listenerRole := new(rbacv1.Role)
err = r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: scaleSetListenerRoleName(autoscalingListener)}, listenerRole) err = r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: autoscalingListener.Name}, listenerRole)
switch { switch {
case err == nil: case err == nil:
if listenerRole.ObjectMeta.DeletionTimestamp.IsZero() { if listenerRole.DeletionTimestamp.IsZero() {
logger.Info("Deleting the listener role") logger.Info("Deleting the listener role")
if err := r.Delete(ctx, listenerRole); err != nil { if err := r.Delete(ctx, listenerRole); err != nil {
return false, fmt.Errorf("failed to delete listener role: %w", err) return false, fmt.Errorf("failed to delete listener role: %w", err)
@@ -363,10 +360,10 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au
logger.Info("Cleaning up the listener service account") logger.Info("Cleaning up the listener service account")
listenerSa := new(corev1.ServiceAccount) listenerSa := new(corev1.ServiceAccount)
err = r.Get(ctx, types.NamespacedName{Name: scaleSetListenerServiceAccountName(autoscalingListener), Namespace: autoscalingListener.Namespace}, listenerSa) err = r.Get(ctx, types.NamespacedName{Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace}, listenerSa)
switch { switch {
case err == nil: case err == nil:
if listenerSa.ObjectMeta.DeletionTimestamp.IsZero() { if listenerSa.DeletionTimestamp.IsZero() {
logger.Info("Deleting the listener service account") logger.Info("Deleting the listener service account")
if err := r.Delete(ctx, listenerSa); err != nil { if err := r.Delete(ctx, listenerSa); err != nil {
return false, fmt.Errorf("failed to delete listener service account: %w", err) return false, fmt.Errorf("failed to delete listener service account: %w", err)
@@ -382,7 +379,7 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au
} }
func (r *AutoscalingListenerReconciler) createServiceAccountForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) { func (r *AutoscalingListenerReconciler) createServiceAccountForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) {
newServiceAccount := r.ResourceBuilder.newScaleSetListenerServiceAccount(autoscalingListener) newServiceAccount := r.newScaleSetListenerServiceAccount(autoscalingListener)
if err := ctrl.SetControllerReference(autoscalingListener, newServiceAccount, r.Scheme); err != nil { if err := ctrl.SetControllerReference(autoscalingListener, newServiceAccount, r.Scheme); err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -398,7 +395,7 @@ func (r *AutoscalingListenerReconciler) createServiceAccountForListener(ctx cont
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, autoscalingListener *v1alpha1.AutoscalingListener, serviceAccount *corev1.ServiceAccount, secret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) { func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, autoscalingListener *v1alpha1.AutoscalingListener, serviceAccount *corev1.ServiceAccount, appConfig *appconfig.AppConfig, logger logr.Logger) (ctrl.Result, error) {
var envs []corev1.EnvVar var envs []corev1.EnvVar
if autoscalingListener.Spec.Proxy != nil { if autoscalingListener.Spec.Proxy != nil {
httpURL := corev1.EnvVar{ httpURL := corev1.EnvVar{
@@ -467,7 +464,7 @@ func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, a
logger.Info("Creating listener config secret") logger.Info("Creating listener config secret")
podConfig, err := r.ResourceBuilder.newScaleSetListenerConfig(autoscalingListener, secret, metricsConfig, cert) podConfig, err := r.newScaleSetListenerConfig(autoscalingListener, appConfig, metricsConfig, cert)
if err != nil { if err != nil {
logger.Error(err, "Failed to build listener config secret") logger.Error(err, "Failed to build listener config secret")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -486,7 +483,7 @@ func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, a
return ctrl.Result{Requeue: true}, nil return ctrl.Result{Requeue: true}, nil
} }
newPod, err := r.ResourceBuilder.newScaleSetListenerPod(autoscalingListener, &podConfig, serviceAccount, secret, metricsConfig, envs...) newPod, err := r.newScaleSetListenerPod(autoscalingListener, &podConfig, serviceAccount, metricsConfig, envs...)
if err != nil { if err != nil {
logger.Error(err, "Failed to build listener pod") logger.Error(err, "Failed to build listener pod")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -545,23 +542,6 @@ func (r *AutoscalingListenerReconciler) certificate(ctx context.Context, autosca
return certificate, nil return certificate, nil
} }
func (r *AutoscalingListenerReconciler) createSecretsForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, secret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
newListenerSecret := r.ResourceBuilder.newScaleSetListenerSecretMirror(autoscalingListener, secret)
if err := ctrl.SetControllerReference(autoscalingListener, newListenerSecret, r.Scheme); err != nil {
return ctrl.Result{}, err
}
logger.Info("Creating listener secret", "namespace", newListenerSecret.Namespace, "name", newListenerSecret.Name)
if err := r.Create(ctx, newListenerSecret); err != nil {
logger.Error(err, "Unable to create listener secret", "namespace", newListenerSecret.Namespace, "name", newListenerSecret.Name)
return ctrl.Result{}, err
}
logger.Info("Created listener secret", "namespace", newListenerSecret.Namespace, "name", newListenerSecret.Name)
return ctrl.Result{Requeue: true}, nil
}
func (r *AutoscalingListenerReconciler) createProxySecret(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) { func (r *AutoscalingListenerReconciler) createProxySecret(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) {
data, err := autoscalingListener.Spec.Proxy.ToSecretData(func(s string) (*corev1.Secret, error) { data, err := autoscalingListener.Spec.Proxy.ToSecretData(func(s string) (*corev1.Secret, error) {
var secret corev1.Secret var secret corev1.Secret
@@ -601,24 +581,8 @@ func (r *AutoscalingListenerReconciler) createProxySecret(ctx context.Context, a
return ctrl.Result{Requeue: true}, nil return ctrl.Result{Requeue: true}, nil
} }
func (r *AutoscalingListenerReconciler) updateSecretsForListener(ctx context.Context, secret *corev1.Secret, mirrorSecret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
dataHash := hash.ComputeTemplateHash(secret.Data)
updatedMirrorSecret := mirrorSecret.DeepCopy()
updatedMirrorSecret.Labels["secret-data-hash"] = dataHash
updatedMirrorSecret.Data = secret.Data
logger.Info("Updating listener mirror secret", "namespace", updatedMirrorSecret.Namespace, "name", updatedMirrorSecret.Name, "hash", dataHash)
if err := r.Update(ctx, updatedMirrorSecret); err != nil {
logger.Error(err, "Unable to update listener mirror secret", "namespace", updatedMirrorSecret.Namespace, "name", updatedMirrorSecret.Name)
return ctrl.Result{}, err
}
logger.Info("Updated listener mirror secret", "namespace", updatedMirrorSecret.Namespace, "name", updatedMirrorSecret.Name, "hash", dataHash)
return ctrl.Result{Requeue: true}, nil
}
func (r *AutoscalingListenerReconciler) createRoleForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) { func (r *AutoscalingListenerReconciler) createRoleForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) {
newRole := r.ResourceBuilder.newScaleSetListenerRole(autoscalingListener) newRole := r.newScaleSetListenerRole(autoscalingListener)
logger.Info("Creating listener role", "namespace", newRole.Namespace, "name", newRole.Name, "rules", newRole.Rules) logger.Info("Creating listener role", "namespace", newRole.Namespace, "name", newRole.Name, "rules", newRole.Rules)
if err := r.Create(ctx, newRole); err != nil { if err := r.Create(ctx, newRole); err != nil {
@@ -646,7 +610,7 @@ func (r *AutoscalingListenerReconciler) updateRoleForListener(ctx context.Contex
} }
func (r *AutoscalingListenerReconciler) createRoleBindingForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, listenerRole *rbacv1.Role, serviceAccount *corev1.ServiceAccount, logger logr.Logger) (ctrl.Result, error) { func (r *AutoscalingListenerReconciler) createRoleBindingForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, listenerRole *rbacv1.Role, serviceAccount *corev1.ServiceAccount, logger logr.Logger) (ctrl.Result, error) {
newRoleBinding := r.ResourceBuilder.newScaleSetListenerRoleBinding(autoscalingListener, listenerRole, serviceAccount) newRoleBinding := r.newScaleSetListenerRoleBinding(autoscalingListener, listenerRole, serviceAccount)
logger.Info("Creating listener role binding", logger.Info("Creating listener role binding",
"namespace", newRoleBinding.Namespace, "namespace", newRoleBinding.Namespace,

View File

@@ -14,7 +14,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
listenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config" ghalistenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config"
"github.com/actions/actions-runner-controller/github/actions/fake"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
kerrors "k8s.io/apimachinery/pkg/api/errors" kerrors "k8s.io/apimachinery/pkg/api/errors"
@@ -43,10 +44,17 @@ var _ = Describe("Test AutoScalingListener controller", func() {
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
rb := ResourceBuilder{
SecretResolver: secretResolver,
}
controller := &AutoscalingListenerReconciler{ controller := &AutoscalingListenerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: rb,
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -134,37 +142,25 @@ var _ = Describe("Test AutoScalingListener controller", func() {
autoscalingListenerTestTimeout, autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(autoscalingListenerFinalizerName), "AutoScalingListener should have a finalizer") autoscalingListenerTestInterval).Should(BeEquivalentTo(autoscalingListenerFinalizerName), "AutoScalingListener should have a finalizer")
// Check if secret is created
mirrorSecret := new(corev1.Secret)
Eventually(
func() (string, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerSecretMirrorName(autoscalingListener), Namespace: autoscalingListener.Namespace}, mirrorSecret)
if err != nil {
return "", err
}
return string(mirrorSecret.Data["github_token"]), nil
},
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(autoscalingListenerTestGitHubToken), "Mirror secret should be created")
// Check if service account is created // Check if service account is created
serviceAccount := new(corev1.ServiceAccount) serviceAccount := new(corev1.ServiceAccount)
Eventually( Eventually(
func() (string, error) { func() (string, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerServiceAccountName(autoscalingListener), Namespace: autoscalingListener.Namespace}, serviceAccount) err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace}, serviceAccount)
if err != nil { if err != nil {
return "", err return "", err
} }
return serviceAccount.Name, nil return serviceAccount.Name, nil
}, },
autoscalingListenerTestTimeout, autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(scaleSetListenerServiceAccountName(autoscalingListener)), "Service account should be created") autoscalingListenerTestInterval,
).Should(BeEquivalentTo(autoscalingListener.Name), "Service account should be created")
// Check if role is created // Check if role is created
role := new(rbacv1.Role) role := new(rbacv1.Role)
Eventually( Eventually(
func() ([]rbacv1.PolicyRule, error) { func() ([]rbacv1.PolicyRule, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerRoleName(autoscalingListener), Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, role) err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, role)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -178,7 +174,7 @@ var _ = Describe("Test AutoScalingListener controller", func() {
roleBinding := new(rbacv1.RoleBinding) roleBinding := new(rbacv1.RoleBinding)
Eventually( Eventually(
func() (string, error) { func() (string, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerRoleName(autoscalingListener), Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, roleBinding) err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, roleBinding)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -186,7 +182,7 @@ var _ = Describe("Test AutoScalingListener controller", func() {
return roleBinding.RoleRef.Name, nil return roleBinding.RoleRef.Name, nil
}, },
autoscalingListenerTestTimeout, autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(scaleSetListenerRoleName(autoscalingListener)), "Rolebinding should be created") autoscalingListenerTestInterval).Should(BeEquivalentTo(autoscalingListener.Name), "Rolebinding should be created")
// Check if pod is created // Check if pod is created
pod := new(corev1.Pod) pod := new(corev1.Pod)
@@ -248,7 +244,7 @@ var _ = Describe("Test AutoScalingListener controller", func() {
Eventually( Eventually(
func() bool { func() bool {
roleBinding := new(rbacv1.RoleBinding) roleBinding := new(rbacv1.RoleBinding)
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerRoleName(autoscalingListener), Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, roleBinding) err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, roleBinding)
return kerrors.IsNotFound(err) return kerrors.IsNotFound(err)
}, },
autoscalingListenerTestTimeout, autoscalingListenerTestTimeout,
@@ -259,7 +255,7 @@ var _ = Describe("Test AutoScalingListener controller", func() {
Eventually( Eventually(
func() bool { func() bool {
role := new(rbacv1.Role) role := new(rbacv1.Role)
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerRoleName(autoscalingListener), Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, role) err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, role)
return kerrors.IsNotFound(err) return kerrors.IsNotFound(err)
}, },
autoscalingListenerTestTimeout, autoscalingListenerTestTimeout,
@@ -340,7 +336,7 @@ var _ = Describe("Test AutoScalingListener controller", func() {
role := new(rbacv1.Role) role := new(rbacv1.Role)
Eventually( Eventually(
func() ([]rbacv1.PolicyRule, error) { func() ([]rbacv1.PolicyRule, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerRoleName(autoscalingListener), Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, role) err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace}, role)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -351,7 +347,7 @@ var _ = Describe("Test AutoScalingListener controller", func() {
autoscalingListenerTestInterval).Should(BeEquivalentTo(rulesForListenerRole([]string{updated.Spec.EphemeralRunnerSetName})), "Role should be updated") autoscalingListenerTestInterval).Should(BeEquivalentTo(rulesForListenerRole([]string{updated.Spec.EphemeralRunnerSetName})), "Role should be updated")
}) })
It("It should re-create pod whenever listener container is terminated", func() { It("It should re-create pod and config secret whenever listener container is terminated", func() {
// Waiting for the pod is created // Waiting for the pod is created
pod := new(corev1.Pod) pod := new(corev1.Pod)
Eventually( Eventually(
@@ -367,7 +363,18 @@ var _ = Describe("Test AutoScalingListener controller", func() {
autoscalingListenerTestInterval, autoscalingListenerTestInterval,
).Should(BeEquivalentTo(autoscalingListener.Name), "Pod should be created") ).Should(BeEquivalentTo(autoscalingListener.Name), "Pod should be created")
secret := new(corev1.Secret)
Eventually(
func() error {
return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerConfigName(autoscalingListener), Namespace: autoscalingListener.Namespace}, secret)
},
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval,
).Should(Succeed(), "Config secret should be created")
oldPodUID := string(pod.UID) oldPodUID := string(pod.UID)
oldSecretUID := string(secret.UID)
updated := pod.DeepCopy() updated := pod.DeepCopy()
updated.Status.ContainerStatuses = []corev1.ContainerStatus{ updated.Status.ContainerStatuses = []corev1.ContainerStatus{
{ {
@@ -396,75 +403,21 @@ var _ = Describe("Test AutoScalingListener controller", func() {
autoscalingListenerTestTimeout, autoscalingListenerTestTimeout,
autoscalingListenerTestInterval, autoscalingListenerTestInterval,
).ShouldNot(BeEquivalentTo(oldPodUID), "Pod should be re-created") ).ShouldNot(BeEquivalentTo(oldPodUID), "Pod should be re-created")
})
It("It should update mirror secrets to match secret used by AutoScalingRunnerSet", func() { // Check if config secret is re-created
// Waiting for the pod is created
pod := new(corev1.Pod)
Eventually( Eventually(
func() (string, error) { func() (string, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace}, pod) secret := new(corev1.Secret)
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerConfigName(autoscalingListener), Namespace: autoscalingListener.Namespace}, secret)
if err != nil { if err != nil {
return "", err return "", err
} }
return pod.Name, nil return string(secret.UID), nil
}, },
autoscalingListenerTestTimeout, autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(autoscalingListener.Name), "Pod should be created") autoscalingListenerTestInterval,
).ShouldNot(BeEquivalentTo(oldSecretUID), "Config secret should be re-created")
// Update the secret
updatedSecret := configSecret.DeepCopy()
updatedSecret.Data["github_token"] = []byte(autoscalingListenerTestGitHubToken + "_updated")
err := k8sClient.Update(ctx, updatedSecret)
Expect(err).NotTo(HaveOccurred(), "failed to update test secret")
updatedPod := pod.DeepCopy()
// Ignore status running and consult the container state
updatedPod.Status.Phase = corev1.PodRunning
updatedPod.Status.ContainerStatuses = []corev1.ContainerStatus{
{
Name: autoscalingListenerContainerName,
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 1,
},
},
},
}
err = k8sClient.Status().Update(ctx, updatedPod)
Expect(err).NotTo(HaveOccurred(), "failed to update test pod to failed")
// Check if mirror secret is updated with right data
mirrorSecret := new(corev1.Secret)
Eventually(
func() (map[string][]byte, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerSecretMirrorName(autoscalingListener), Namespace: autoscalingListener.Namespace}, mirrorSecret)
if err != nil {
return nil, err
}
return mirrorSecret.Data, nil
},
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(updatedSecret.Data), "Mirror secret should be updated")
// Check if we re-created a new pod
Eventually(
func() error {
latestPod := new(corev1.Pod)
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace}, latestPod)
if err != nil {
return err
}
if latestPod.UID == pod.UID {
return fmt.Errorf("Pod should be recreated")
}
return nil
},
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(Succeed(), "Pod should be recreated")
}) })
}) })
}) })
@@ -507,10 +460,17 @@ var _ = Describe("Test AutoScalingListener customization", func() {
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
rb := ResourceBuilder{
SecretResolver: secretResolver,
}
controller := &AutoscalingListenerReconciler{ controller := &AutoscalingListenerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: rb,
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -780,11 +740,17 @@ var _ = Describe("Test AutoScalingListener controller with proxy", func() {
ctx = context.Background() ctx = context.Background()
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
rb := ResourceBuilder{
SecretResolver: secretResolver,
}
controller := &AutoscalingListenerReconciler{ controller := &AutoscalingListenerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: rb,
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -977,10 +943,17 @@ var _ = Describe("Test AutoScalingListener controller with template modification
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
rb := ResourceBuilder{
SecretResolver: secretResolver,
}
controller := &AutoscalingListenerReconciler{ controller := &AutoscalingListenerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: rb,
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -1073,6 +1046,12 @@ var _ = Describe("Test GitHub Server TLS configuration", func() {
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
rb := ResourceBuilder{
SecretResolver: secretResolver,
}
cert, err := os.ReadFile(filepath.Join( cert, err := os.ReadFile(filepath.Join(
"../../", "../../",
"github", "github",
@@ -1094,9 +1073,10 @@ var _ = Describe("Test GitHub Server TLS configuration", func() {
Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs") Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs")
controller := &AutoscalingListenerReconciler{ controller := &AutoscalingListenerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: rb,
} }
err = controller.SetupWithManager(mgr) err = controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -1111,7 +1091,7 @@ var _ = Describe("Test GitHub Server TLS configuration", func() {
Spec: v1alpha1.AutoscalingRunnerSetSpec{ Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: "https://github.com/owner/repo", GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name, GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{ GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1147,7 +1127,7 @@ var _ = Describe("Test GitHub Server TLS configuration", func() {
Spec: v1alpha1.AutoscalingListenerSpec{ Spec: v1alpha1.AutoscalingListenerSpec{
GitHubConfigUrl: "https://github.com/owner/repo", GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name, GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{ GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1191,7 +1171,7 @@ var _ = Describe("Test GitHub Server TLS configuration", func() {
g.Expect(config.Data["config.json"]).ToNot(BeEmpty(), "listener configuration file should not be empty") g.Expect(config.Data["config.json"]).ToNot(BeEmpty(), "listener configuration file should not be empty")
var listenerConfig listenerconfig.Config var listenerConfig ghalistenerconfig.Config
err = json.Unmarshal(config.Data["config.json"], &listenerConfig) err = json.Unmarshal(config.Data["config.json"], &listenerConfig)
g.Expect(err).NotTo(HaveOccurred(), "failed to parse listener configuration file") g.Expect(err).NotTo(HaveOccurred(), "failed to parse listener configuration file")

View File

@@ -99,7 +99,7 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if !autoscalingRunnerSet.ObjectMeta.DeletionTimestamp.IsZero() { if !autoscalingRunnerSet.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(autoscalingRunnerSet, autoscalingRunnerSetFinalizerName) { if !controllerutil.ContainsFinalizer(autoscalingRunnerSet, autoscalingRunnerSetFinalizerName) {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@@ -151,7 +151,7 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
if autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion] != build.Version { if !v1alpha1.IsVersionAllowed(autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion], build.Version) {
if err := r.Delete(ctx, autoscalingRunnerSet); err != nil { if err := r.Delete(ctx, autoscalingRunnerSet); err != nil {
log.Error(err, "Failed to delete autoscaling runner set on version mismatch", log.Error(err, "Failed to delete autoscaling runner set on version mismatch",
"buildVersion", build.Version, "buildVersion", build.Version,
@@ -207,14 +207,6 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
return r.updateRunnerScaleSetName(ctx, autoscalingRunnerSet, log) return r.updateRunnerScaleSetName(ctx, autoscalingRunnerSet, log)
} }
secret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingRunnerSet.Namespace, Name: autoscalingRunnerSet.Spec.GitHubConfigSecret}, secret); err != nil {
log.Error(err, "Failed to find GitHub config secret.",
"namespace", autoscalingRunnerSet.Namespace,
"name", autoscalingRunnerSet.Spec.GitHubConfigSecret)
return ctrl.Result{}, err
}
existingRunnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet) existingRunnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet)
if err != nil { if err != nil {
log.Error(err, "Failed to list existing ephemeral runner sets") log.Error(err, "Failed to list existing ephemeral runner sets")
@@ -332,7 +324,7 @@ func (r *AutoscalingRunnerSetReconciler) cleanupListener(ctx context.Context, au
err = r.Get(ctx, client.ObjectKey{Namespace: r.ControllerNamespace, Name: scaleSetListenerName(autoscalingRunnerSet)}, &listener) err = r.Get(ctx, client.ObjectKey{Namespace: r.ControllerNamespace, Name: scaleSetListenerName(autoscalingRunnerSet)}, &listener)
switch { switch {
case err == nil: case err == nil:
if listener.ObjectMeta.DeletionTimestamp.IsZero() { if listener.DeletionTimestamp.IsZero() {
logger.Info("Deleting the listener") logger.Info("Deleting the listener")
if err := r.Delete(ctx, &listener); err != nil { if err := r.Delete(ctx, &listener); err != nil {
return false, fmt.Errorf("failed to delete listener: %w", err) return false, fmt.Errorf("failed to delete listener: %w", err)
@@ -369,7 +361,7 @@ func (r *AutoscalingRunnerSetReconciler) deleteEphemeralRunnerSets(ctx context.C
for i := range oldRunnerSets { for i := range oldRunnerSets {
rs := &oldRunnerSets[i] rs := &oldRunnerSets[i]
// already deleted but contains finalizer so it still exists // already deleted but contains finalizer so it still exists
if !rs.ObjectMeta.DeletionTimestamp.IsZero() { if !rs.DeletionTimestamp.IsZero() {
logger.Info("Skip ephemeral runner set since it is already marked for deletion", "name", rs.Name) logger.Info("Skip ephemeral runner set since it is already marked for deletion", "name", rs.Name)
continue continue
} }
@@ -402,12 +394,12 @@ func (r *AutoscalingRunnerSetReconciler) removeFinalizersFromDependentResources(
func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, logger logr.Logger) (ctrl.Result, error) { func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, logger logr.Logger) (ctrl.Result, error) {
logger.Info("Creating a new runner scale set") logger.Info("Creating a new runner scale set")
actionsClient, err := r.actionsClientFor(ctx, autoscalingRunnerSet) actionsClient, err := r.GetActionsService(ctx, autoscalingRunnerSet)
if len(autoscalingRunnerSet.Spec.RunnerScaleSetName) == 0 { if len(autoscalingRunnerSet.Spec.RunnerScaleSetName) == 0 {
autoscalingRunnerSet.Spec.RunnerScaleSetName = autoscalingRunnerSet.Name autoscalingRunnerSet.Spec.RunnerScaleSetName = autoscalingRunnerSet.Name
} }
if err != nil { if err != nil {
logger.Error(err, "Failed to initialize Actions service client for creating a new runner scale set") logger.Error(err, "Failed to initialize Actions service client for creating a new runner scale set", "error", err.Error())
return ctrl.Result{}, err return ctrl.Result{}, err
} }
@@ -498,7 +490,7 @@ func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetRunnerGroup(ctx con
return ctrl.Result{}, err return ctrl.Result{}, err
} }
actionsClient, err := r.actionsClientFor(ctx, autoscalingRunnerSet) actionsClient, err := r.GetActionsService(ctx, autoscalingRunnerSet)
if err != nil { if err != nil {
logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set") logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -546,7 +538,7 @@ func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetName(ctx context.Co
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
actionsClient, err := r.actionsClientFor(ctx, autoscalingRunnerSet) actionsClient, err := r.GetActionsService(ctx, autoscalingRunnerSet)
if err != nil { if err != nil {
logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set") logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -597,7 +589,7 @@ func (r *AutoscalingRunnerSetReconciler) deleteRunnerScaleSet(ctx context.Contex
return nil return nil
} }
actionsClient, err := r.actionsClientFor(ctx, autoscalingRunnerSet) actionsClient, err := r.GetActionsService(ctx, autoscalingRunnerSet)
if err != nil { if err != nil {
logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set") logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set")
return err return err
@@ -622,7 +614,7 @@ func (r *AutoscalingRunnerSetReconciler) deleteRunnerScaleSet(ctx context.Contex
} }
func (r *AutoscalingRunnerSetReconciler) createEphemeralRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, log logr.Logger) (ctrl.Result, error) { func (r *AutoscalingRunnerSetReconciler) createEphemeralRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, log logr.Logger) (ctrl.Result, error) {
desiredRunnerSet, err := r.ResourceBuilder.newEphemeralRunnerSet(autoscalingRunnerSet) desiredRunnerSet, err := r.newEphemeralRunnerSet(autoscalingRunnerSet)
if err != nil { if err != nil {
log.Error(err, "Could not create EphemeralRunnerSet") log.Error(err, "Could not create EphemeralRunnerSet")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -651,7 +643,7 @@ func (r *AutoscalingRunnerSetReconciler) createAutoScalingListenerForRunnerSet(c
}) })
} }
autoscalingListener, err := r.ResourceBuilder.newAutoScalingListener(autoscalingRunnerSet, ephemeralRunnerSet, r.ControllerNamespace, r.DefaultRunnerScaleSetListenerImage, imagePullSecrets) autoscalingListener, err := r.newAutoScalingListener(autoscalingRunnerSet, ephemeralRunnerSet, r.ControllerNamespace, r.DefaultRunnerScaleSetListenerImage, imagePullSecrets)
if err != nil { if err != nil {
log.Error(err, "Could not create AutoscalingListener spec") log.Error(err, "Could not create AutoscalingListener spec")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -676,74 +668,6 @@ func (r *AutoscalingRunnerSetReconciler) listEphemeralRunnerSets(ctx context.Con
return &EphemeralRunnerSets{list: list}, nil return &EphemeralRunnerSets{list: list}, nil
} }
func (r *AutoscalingRunnerSetReconciler) actionsClientFor(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (actions.ActionsService, error) {
var configSecret corev1.Secret
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingRunnerSet.Namespace, Name: autoscalingRunnerSet.Spec.GitHubConfigSecret}, &configSecret); err != nil {
return nil, fmt.Errorf("failed to find GitHub config secret: %w", err)
}
opts, err := r.actionsClientOptionsFor(ctx, autoscalingRunnerSet)
if err != nil {
return nil, fmt.Errorf("failed to get actions client options: %w", err)
}
return r.ActionsClient.GetClientFromSecret(
ctx,
autoscalingRunnerSet.Spec.GitHubConfigUrl,
autoscalingRunnerSet.Namespace,
configSecret.Data,
opts...,
)
}
func (r *AutoscalingRunnerSetReconciler) actionsClientOptionsFor(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) ([]actions.ClientOption, error) {
var options []actions.ClientOption
if autoscalingRunnerSet.Spec.Proxy != nil {
proxyFunc, err := autoscalingRunnerSet.Spec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
var secret corev1.Secret
err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingRunnerSet.Namespace, Name: s}, &secret)
if err != nil {
return nil, fmt.Errorf("failed to get proxy secret %s: %w", s, err)
}
return &secret, nil
})
if err != nil {
return nil, fmt.Errorf("failed to get proxy func: %w", err)
}
options = append(options, actions.WithProxy(proxyFunc))
}
tlsConfig := autoscalingRunnerSet.Spec.GitHubServerTLS
if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap
err := r.Get(
ctx,
types.NamespacedName{
Namespace: autoscalingRunnerSet.Namespace,
Name: name,
},
&configmap,
)
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
}
return []byte(configmap.Data[key]), nil
})
if err != nil {
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
options = append(options, actions.WithRootCAs(pool))
}
return options, nil
}
// SetupWithManager sets up the controller with the Manager. // SetupWithManager sets up the controller with the Manager.
func (r *AutoscalingRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *AutoscalingRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr). return ctrl.NewControllerManagedBy(mgr).

View File

@@ -70,7 +70,12 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
Log: logf.Log, Log: logf.Log,
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -280,10 +285,10 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
// This should trigger re-creation of EphemeralRunnerSet and Listener // This should trigger re-creation of EphemeralRunnerSet and Listener
patched := autoscalingRunnerSet.DeepCopy() patched := autoscalingRunnerSet.DeepCopy()
patched.Spec.Template.Spec.PriorityClassName = "test-priority-class" patched.Spec.Template.Spec.PriorityClassName = "test-priority-class"
if patched.ObjectMeta.Annotations == nil { if patched.Annotations == nil {
patched.ObjectMeta.Annotations = make(map[string]string) patched.Annotations = make(map[string]string)
} }
patched.ObjectMeta.Annotations[annotationKeyValuesHash] = "test-hash" patched.Annotations[annotationKeyValuesHash] = "test-hash"
err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet)) err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet") Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")
autoscalingRunnerSet = patched.DeepCopy() autoscalingRunnerSet = patched.DeepCopy()
@@ -383,7 +388,7 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
Expect(err).NotTo(HaveOccurred(), "failed to get Listener") Expect(err).NotTo(HaveOccurred(), "failed to get Listener")
patched = autoscalingRunnerSet.DeepCopy() patched = autoscalingRunnerSet.DeepCopy()
patched.ObjectMeta.Annotations[annotationKeyValuesHash] = "hash-changes" patched.Annotations[annotationKeyValuesHash] = "hash-changes"
err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet)) err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet") Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")
@@ -546,10 +551,10 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
// Patch the AutoScalingRunnerSet image which should trigger // Patch the AutoScalingRunnerSet image which should trigger
// the recreation of the Listener and EphemeralRunnerSet // the recreation of the Listener and EphemeralRunnerSet
patched := autoscalingRunnerSet.DeepCopy() patched := autoscalingRunnerSet.DeepCopy()
if patched.ObjectMeta.Annotations == nil { if patched.Annotations == nil {
patched.ObjectMeta.Annotations = make(map[string]string) patched.Annotations = make(map[string]string)
} }
patched.ObjectMeta.Annotations[annotationKeyValuesHash] = "testgroup2" patched.Annotations[annotationKeyValuesHash] = "testgroup2"
patched.Spec.Template.Spec = corev1.PodSpec{ patched.Spec.Template.Spec = corev1.PodSpec{
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {
@@ -677,33 +682,40 @@ var _ = Describe("Test AutoScalingController updates", Ordered, func() {
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
multiClient := fake.NewMultiClient(
fake.WithDefaultClient(
fake.NewFakeClient(
fake.WithUpdateRunnerScaleSet(
&actions.RunnerScaleSet{
Id: 1,
Name: "testset_update",
RunnerGroupId: 1,
RunnerGroupName: "testgroup",
Labels: []actions.Label{{Type: "test", Name: "test"}},
RunnerSetting: actions.RunnerSetting{},
CreatedOn: time.Now(),
RunnerJitConfigUrl: "test.test.test",
Statistics: nil,
},
nil,
),
),
nil,
),
)
controller := &AutoscalingRunnerSetReconciler{ controller := &AutoscalingRunnerSetReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ActionsClient: fake.NewMultiClient( ResourceBuilder: ResourceBuilder{
fake.WithDefaultClient( SecretResolver: &SecretResolver{
fake.NewFakeClient( k8sClient: k8sClient,
fake.WithUpdateRunnerScaleSet( multiClient: multiClient,
&actions.RunnerScaleSet{ },
Id: 1, },
Name: "testset_update",
RunnerGroupId: 1,
RunnerGroupName: "testgroup",
Labels: []actions.Label{{Type: "test", Name: "test"}},
RunnerSetting: actions.RunnerSetting{},
CreatedOn: time.Now(),
RunnerJitConfigUrl: "test.test.test",
Statistics: nil,
},
nil,
),
),
nil,
),
),
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -818,7 +830,12 @@ var _ = Describe("Test AutoscalingController creation failures", Ordered, func()
Log: logf.Log, Log: logf.Log,
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -875,7 +892,7 @@ var _ = Describe("Test AutoscalingController creation failures", Ordered, func()
autoscalingRunnerSetTestInterval, autoscalingRunnerSetTestInterval,
).Should(BeEquivalentTo(autoscalingRunnerSetFinalizerName), "AutoScalingRunnerSet should have a finalizer") ).Should(BeEquivalentTo(autoscalingRunnerSetFinalizerName), "AutoScalingRunnerSet should have a finalizer")
ars.ObjectMeta.Annotations = make(map[string]string) ars.Annotations = make(map[string]string)
err = k8sClient.Update(ctx, ars) err = k8sClient.Update(ctx, ars)
Expect(err).NotTo(HaveOccurred(), "Update autoscaling runner set without annotation should be successful") Expect(err).NotTo(HaveOccurred(), "Update autoscaling runner set without annotation should be successful")
@@ -937,14 +954,19 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
ctx = context.Background() ctx = context.Background()
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
multiClient := actions.NewMultiClient(logr.Discard())
controller = &AutoscalingRunnerSetReconciler{ controller = &AutoscalingRunnerSetReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ActionsClient: actions.NewMultiClient(logr.Discard()), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: multiClient,
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
@@ -1127,7 +1149,12 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
Log: logf.Log, Log: logf.Log,
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
},
} }
err = controller.SetupWithManager(mgr) err = controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -1136,7 +1163,10 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
}) })
It("should be able to make requests to a server using root CAs", func() { It("should be able to make requests to a server using root CAs", func() {
controller.ActionsClient = actions.NewMultiClient(logr.Discard()) controller.SecretResolver = &SecretResolver{
k8sClient: k8sClient,
multiClient: actions.NewMultiClient(logr.Discard()),
}
certsFolder := filepath.Join( certsFolder := filepath.Join(
"../../", "../../",
@@ -1171,7 +1201,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
Spec: v1alpha1.AutoscalingRunnerSetSpec{ Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: server.ConfigURLForOrg("my-org"), GitHubConfigUrl: server.ConfigURLForOrg("my-org"),
GitHubConfigSecret: configSecret.Name, GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{ GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1224,7 +1254,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
Spec: v1alpha1.AutoscalingRunnerSetSpec{ Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: "https://github.com/owner/repo", GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name, GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{ GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1288,7 +1318,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
Spec: v1alpha1.AutoscalingRunnerSetSpec{ Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: "https://github.com/owner/repo", GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name, GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{ GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{
@@ -1361,7 +1391,12 @@ var _ = Describe("Test external permissions cleanup", Ordered, func() {
Log: logf.Log, Log: logf.Log,
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -1519,7 +1554,12 @@ var _ = Describe("Test external permissions cleanup", Ordered, func() {
Log: logf.Log, Log: logf.Log,
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -1727,7 +1767,12 @@ var _ = Describe("Test resource version and build version mismatch", func() {
Log: logf.Log, Log: logf.Log,
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

View File

@@ -28,6 +28,7 @@ import (
"github.com/go-logr/logr" "github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors" kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
@@ -44,12 +45,24 @@ const (
// EphemeralRunnerReconciler reconciles a EphemeralRunner object // EphemeralRunnerReconciler reconciles a EphemeralRunner object
type EphemeralRunnerReconciler struct { type EphemeralRunnerReconciler struct {
client.Client client.Client
Log logr.Logger Log logr.Logger
Scheme *runtime.Scheme Scheme *runtime.Scheme
ActionsClient actions.MultiClient
ResourceBuilder ResourceBuilder
} }
// precompute backoff durations for failed ephemeral runners
// the len(failedRunnerBackoff) must be equal to maxFailures + 1
var failedRunnerBackoff = []time.Duration{
0,
5 * time.Second,
10 * time.Second,
20 * time.Second,
40 * time.Second,
80 * time.Second,
}
const maxFailures = 5
// +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunners,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunners,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunners/status,verbs=get;update;patch // +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunners/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunners/finalizers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunners/finalizers,verbs=get;list;watch;create;update;patch;delete
@@ -70,7 +83,7 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if !ephemeralRunner.ObjectMeta.DeletionTimestamp.IsZero() { if !ephemeralRunner.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(ephemeralRunner, ephemeralRunnerFinalizerName) { if !controllerutil.ContainsFinalizer(ephemeralRunner, ephemeralRunnerFinalizerName) {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@@ -173,6 +186,33 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
} }
} }
if len(ephemeralRunner.Status.Failures) > maxFailures {
log.Info(fmt.Sprintf("EphemeralRunner has failed more than %d times. Deleting ephemeral runner so it can be re-created", maxFailures))
if err := r.Delete(ctx, ephemeralRunner); err != nil {
log.Error(fmt.Errorf("failed to delete ephemeral runner after %d failures: %w", maxFailures, err), "Failed to delete ephemeral runner")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
now := metav1.Now()
lastFailure := ephemeralRunner.Status.LastFailure()
backoffDuration := failedRunnerBackoff[len(ephemeralRunner.Status.Failures)]
nextReconciliation := lastFailure.Add(backoffDuration)
if !lastFailure.IsZero() && now.Before(&metav1.Time{Time: nextReconciliation}) {
requeueAfter := nextReconciliation.Sub(now.Time)
log.Info("Backing off the next reconciliation due to failure",
"lastFailure", lastFailure,
"nextReconciliation", nextReconciliation,
"requeueAfter", requeueAfter,
)
return ctrl.Result{
Requeue: true,
RequeueAfter: requeueAfter,
}, nil
}
secret := new(corev1.Secret) secret := new(corev1.Secret)
if err := r.Get(ctx, req.NamespacedName, secret); err != nil { if err := r.Get(ctx, req.NamespacedName, secret); err != nil {
if !kerrors.IsNotFound(err) { if !kerrors.IsNotFound(err) {
@@ -196,39 +236,28 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
pod := new(corev1.Pod) pod := new(corev1.Pod)
if err := r.Get(ctx, req.NamespacedName, pod); err != nil { if err := r.Get(ctx, req.NamespacedName, pod); err != nil {
switch { if !kerrors.IsNotFound(err) {
case !kerrors.IsNotFound(err):
log.Error(err, "Failed to fetch the pod") log.Error(err, "Failed to fetch the pod")
return ctrl.Result{}, err return ctrl.Result{}, err
}
case len(ephemeralRunner.Status.Failures) > 5: // Pod was not found. Create if the pod has never been created
log.Info("EphemeralRunner has failed more than 5 times. Marking it as failed") log.Info("Creating new EphemeralRunner pod.")
errMessage := fmt.Sprintf("Pod has failed to start more than 5 times: %s", pod.Status.Message) result, err := r.createPod(ctx, ephemeralRunner, secret, log)
if err := r.markAsFailed(ctx, ephemeralRunner, errMessage, ReasonTooManyPodFailures, log); err != nil { switch {
case err == nil:
return result, nil
case kerrors.IsInvalid(err) || kerrors.IsForbidden(err):
log.Error(err, "Failed to create a pod due to unrecoverable failure")
errMessage := fmt.Sprintf("Failed to create the pod: %v", err)
if err := r.markAsFailed(ctx, ephemeralRunner, errMessage, ReasonInvalidPodFailure, log); err != nil {
log.Error(err, "Failed to set ephemeral runner to phase Failed") log.Error(err, "Failed to set ephemeral runner to phase Failed")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
return ctrl.Result{}, nil return ctrl.Result{}, nil
default: default:
// Pod was not found. Create if the pod has never been created log.Error(err, "Failed to create the pod")
log.Info("Creating new EphemeralRunner pod.") return ctrl.Result{}, err
result, err := r.createPod(ctx, ephemeralRunner, secret, log)
switch {
case err == nil:
return result, nil
case kerrors.IsInvalid(err) || kerrors.IsForbidden(err):
log.Error(err, "Failed to create a pod due to unrecoverable failure")
errMessage := fmt.Sprintf("Failed to create the pod: %v", err)
if err := r.markAsFailed(ctx, ephemeralRunner, errMessage, ReasonInvalidPodFailure, log); err != nil {
log.Error(err, "Failed to set ephemeral runner to phase Failed")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
default:
log.Error(err, "Failed to create the pod")
return ctrl.Result{}, err
}
} }
} }
@@ -268,28 +297,10 @@ func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Requ
} }
return ctrl.Result{}, nil return ctrl.Result{}, nil
default: default: // succeeded
// pod succeeded. We double-check with the service if the runner exists. log.Info("Ephemeral runner has finished successfully")
// The reason is that image can potentially finish with status 0, but not pick up the job. if err := r.markAsFinished(ctx, ephemeralRunner, log); err != nil {
existsInService, err := r.runnerRegisteredWithService(ctx, ephemeralRunner.DeepCopy(), log) log.Error(err, "Failed to mark ephemeral runner as finished")
if err != nil {
log.Error(err, "Failed to check if runner is registered with the service")
return ctrl.Result{}, err
}
if !existsInService {
// the runner does not exist in the service, so it must be done
log.Info("Ephemeral runner has finished since it does not exist in the service anymore")
if err := r.markAsFinished(ctx, ephemeralRunner, log); err != nil {
log.Error(err, "Failed to mark ephemeral runner as finished")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
// The runner still exists. This can happen if the pod exited with 0 but fails to start
log.Info("Ephemeral runner pod has finished, but the runner still exists in the service. Deleting the pod to restart it.")
if err := r.deletePodAsFailed(ctx, ephemeralRunner, pod, log); err != nil {
log.Error(err, "failed to delete a pod that still exists in the service")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
return ctrl.Result{}, nil return ctrl.Result{}, nil
@@ -319,7 +330,7 @@ func (r *EphemeralRunnerReconciler) cleanupResources(ctx context.Context, epheme
err := r.Get(ctx, types.NamespacedName{Namespace: ephemeralRunner.Namespace, Name: ephemeralRunner.Name}, pod) err := r.Get(ctx, types.NamespacedName{Namespace: ephemeralRunner.Namespace, Name: ephemeralRunner.Name}, pod)
switch { switch {
case err == nil: case err == nil:
if pod.ObjectMeta.DeletionTimestamp.IsZero() { if pod.DeletionTimestamp.IsZero() {
log.Info("Deleting the runner pod") log.Info("Deleting the runner pod")
if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) { if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) {
return fmt.Errorf("failed to delete pod: %w", err) return fmt.Errorf("failed to delete pod: %w", err)
@@ -339,7 +350,7 @@ func (r *EphemeralRunnerReconciler) cleanupResources(ctx context.Context, epheme
err = r.Get(ctx, types.NamespacedName{Namespace: ephemeralRunner.Namespace, Name: ephemeralRunner.Name}, secret) err = r.Get(ctx, types.NamespacedName{Namespace: ephemeralRunner.Namespace, Name: ephemeralRunner.Name}, secret)
switch { switch {
case err == nil: case err == nil:
if secret.ObjectMeta.DeletionTimestamp.IsZero() { if secret.DeletionTimestamp.IsZero() {
log.Info("Deleting the jitconfig secret") log.Info("Deleting the jitconfig secret")
if err := r.Delete(ctx, secret); err != nil && !kerrors.IsNotFound(err) { if err := r.Delete(ctx, secret); err != nil && !kerrors.IsNotFound(err) {
return fmt.Errorf("failed to delete secret: %w", err) return fmt.Errorf("failed to delete secret: %w", err)
@@ -393,7 +404,7 @@ func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedPods(ctx context.Context,
var errs []error var errs []error
for i := range runnerLinkedPodList.Items { for i := range runnerLinkedPodList.Items {
linkedPod := &runnerLinkedPodList.Items[i] linkedPod := &runnerLinkedPodList.Items[i]
if !linkedPod.ObjectMeta.DeletionTimestamp.IsZero() { if !linkedPod.DeletionTimestamp.IsZero() {
continue continue
} }
@@ -409,7 +420,7 @@ func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedPods(ctx context.Context,
func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedSecrets(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error { func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedSecrets(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error {
runnerLinkedLabels := client.MatchingLabels( runnerLinkedLabels := client.MatchingLabels(
map[string]string{ map[string]string{
"runner-pod": ephemeralRunner.ObjectMeta.Name, "runner-pod": ephemeralRunner.Name,
}, },
) )
var runnerLinkedSecretList corev1.SecretList var runnerLinkedSecretList corev1.SecretList
@@ -427,7 +438,7 @@ func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedSecrets(ctx context.Conte
var errs []error var errs []error
for i := range runnerLinkedSecretList.Items { for i := range runnerLinkedSecretList.Items {
s := &runnerLinkedSecretList.Items[i] s := &runnerLinkedSecretList.Items[i]
if !s.ObjectMeta.DeletionTimestamp.IsZero() { if !s.DeletionTimestamp.IsZero() {
continue continue
} }
@@ -474,7 +485,7 @@ func (r *EphemeralRunnerReconciler) markAsFinished(ctx context.Context, ephemera
// deletePodAsFailed is responsible for deleting the pod and updating the .Status.Failures for tracking failure count. // deletePodAsFailed is responsible for deleting the pod and updating the .Status.Failures for tracking failure count.
// It should not be responsible for setting the status to Failed. // It should not be responsible for setting the status to Failed.
func (r *EphemeralRunnerReconciler) deletePodAsFailed(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, pod *corev1.Pod, log logr.Logger) error { func (r *EphemeralRunnerReconciler) deletePodAsFailed(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, pod *corev1.Pod, log logr.Logger) error {
if pod.ObjectMeta.DeletionTimestamp.IsZero() { if pod.DeletionTimestamp.IsZero() {
log.Info("Deleting the ephemeral runner pod", "podId", pod.UID) log.Info("Deleting the ephemeral runner pod", "podId", pod.UID)
if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) { if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) {
return fmt.Errorf("failed to delete pod with status failed: %w", err) return fmt.Errorf("failed to delete pod with status failed: %w", err)
@@ -484,9 +495,9 @@ func (r *EphemeralRunnerReconciler) deletePodAsFailed(ctx context.Context, ephem
log.Info("Updating ephemeral runner status to track the failure count") log.Info("Updating ephemeral runner status to track the failure count")
if err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) { if err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
if obj.Status.Failures == nil { if obj.Status.Failures == nil {
obj.Status.Failures = make(map[string]bool) obj.Status.Failures = make(map[string]metav1.Time)
} }
obj.Status.Failures[string(pod.UID)] = true obj.Status.Failures[string(pod.UID)] = metav1.Now()
obj.Status.Ready = false obj.Status.Ready = false
obj.Status.Reason = pod.Status.Reason obj.Status.Reason = pod.Status.Reason
obj.Status.Message = pod.Status.Message obj.Status.Message = pod.Status.Message
@@ -503,7 +514,7 @@ func (r *EphemeralRunnerReconciler) deletePodAsFailed(ctx context.Context, ephem
func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (*ctrl.Result, error) { func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (*ctrl.Result, error) {
// Runner is not registered with the service. We need to register it first // Runner is not registered with the service. We need to register it first
log.Info("Creating ephemeral runner JIT config") log.Info("Creating ephemeral runner JIT config")
actionsClient, err := r.actionsClientFor(ctx, ephemeralRunner) actionsClient, err := r.GetActionsService(ctx, ephemeralRunner)
if err != nil { if err != nil {
return &ctrl.Result{}, fmt.Errorf("failed to get actions client for generating JIT config: %w", err) return &ctrl.Result{}, fmt.Errorf("failed to get actions client for generating JIT config: %w", err)
} }
@@ -640,7 +651,7 @@ func (r *EphemeralRunnerReconciler) createPod(ctx context.Context, runner *v1alp
} }
log.Info("Creating new pod for ephemeral runner") log.Info("Creating new pod for ephemeral runner")
newPod := r.ResourceBuilder.newEphemeralRunnerPod(ctx, runner, secret, envs...) newPod := r.newEphemeralRunnerPod(ctx, runner, secret, envs...)
if err := ctrl.SetControllerReference(runner, newPod, r.Scheme); err != nil { if err := ctrl.SetControllerReference(runner, newPod, r.Scheme); err != nil {
log.Error(err, "Failed to set controller reference to a new pod") log.Error(err, "Failed to set controller reference to a new pod")
@@ -665,7 +676,7 @@ func (r *EphemeralRunnerReconciler) createPod(ctx context.Context, runner *v1alp
func (r *EphemeralRunnerReconciler) createSecret(ctx context.Context, runner *v1alpha1.EphemeralRunner, log logr.Logger) (*ctrl.Result, error) { func (r *EphemeralRunnerReconciler) createSecret(ctx context.Context, runner *v1alpha1.EphemeralRunner, log logr.Logger) (*ctrl.Result, error) {
log.Info("Creating new secret for ephemeral runner") log.Info("Creating new secret for ephemeral runner")
jitSecret := r.ResourceBuilder.newEphemeralRunnerJitSecret(runner) jitSecret := r.newEphemeralRunnerJitSecret(runner)
if err := ctrl.SetControllerReference(runner, jitSecret, r.Scheme); err != nil { if err := ctrl.SetControllerReference(runner, jitSecret, r.Scheme); err != nil {
return &ctrl.Result{}, fmt.Errorf("failed to set controller reference: %w", err) return &ctrl.Result{}, fmt.Errorf("failed to set controller reference: %w", err)
@@ -727,104 +738,8 @@ func (r *EphemeralRunnerReconciler) updateRunStatusFromPod(ctx context.Context,
return nil return nil
} }
func (r *EphemeralRunnerReconciler) actionsClientFor(ctx context.Context, runner *v1alpha1.EphemeralRunner) (actions.ActionsService, error) {
secret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: runner.Namespace, Name: runner.Spec.GitHubConfigSecret}, secret); err != nil {
return nil, fmt.Errorf("failed to get secret: %w", err)
}
opts, err := r.actionsClientOptionsFor(ctx, runner)
if err != nil {
return nil, fmt.Errorf("failed to get actions client options: %w", err)
}
return r.ActionsClient.GetClientFromSecret(
ctx,
runner.Spec.GitHubConfigUrl,
runner.Namespace,
secret.Data,
opts...,
)
}
func (r *EphemeralRunnerReconciler) actionsClientOptionsFor(ctx context.Context, runner *v1alpha1.EphemeralRunner) ([]actions.ClientOption, error) {
var opts []actions.ClientOption
if runner.Spec.Proxy != nil {
proxyFunc, err := runner.Spec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
var secret corev1.Secret
err := r.Get(ctx, types.NamespacedName{Namespace: runner.Namespace, Name: s}, &secret)
if err != nil {
return nil, fmt.Errorf("failed to get proxy secret %s: %w", s, err)
}
return &secret, nil
})
if err != nil {
return nil, fmt.Errorf("failed to get proxy func: %w", err)
}
opts = append(opts, actions.WithProxy(proxyFunc))
}
tlsConfig := runner.Spec.GitHubServerTLS
if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap
err := r.Get(
ctx,
types.NamespacedName{
Namespace: runner.Namespace,
Name: name,
},
&configmap,
)
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
}
return []byte(configmap.Data[key]), nil
})
if err != nil {
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
opts = append(opts, actions.WithRootCAs(pool))
}
return opts, nil
}
// runnerRegisteredWithService checks if the runner is still registered with the service
// Returns found=false and err=nil if ephemeral runner does not exist in GitHub service and should be deleted
func (r EphemeralRunnerReconciler) runnerRegisteredWithService(ctx context.Context, runner *v1alpha1.EphemeralRunner, log logr.Logger) (found bool, err error) {
actionsClient, err := r.actionsClientFor(ctx, runner)
if err != nil {
return false, fmt.Errorf("failed to get Actions client for ScaleSet: %w", err)
}
log.Info("Checking if runner exists in GitHub service", "runnerId", runner.Status.RunnerId)
_, err = actionsClient.GetRunner(ctx, int64(runner.Status.RunnerId))
if err != nil {
actionsError := &actions.ActionsError{}
if !errors.As(err, &actionsError) {
return false, err
}
if actionsError.StatusCode != http.StatusNotFound ||
!actionsError.IsException("AgentNotFoundException") {
return false, fmt.Errorf("failed to check if runner exists in GitHub service: %w", err)
}
log.Info("Runner does not exist in GitHub service", "runnerId", runner.Status.RunnerId)
return false, nil
}
log.Info("Runner exists in GitHub service", "runnerId", runner.Status.RunnerId)
return true, nil
}
func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error { func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error {
client, err := r.actionsClientFor(ctx, ephemeralRunner) client, err := r.GetActionsService(ctx, ephemeralRunner)
if err != nil { if err != nil {
return fmt.Errorf("failed to get actions client for runner: %w", err) return fmt.Errorf("failed to get actions client for runner: %w", err)
} }

View File

@@ -30,7 +30,7 @@ import (
const ( const (
ephemeralRunnerTimeout = time.Second * 20 ephemeralRunnerTimeout = time.Second * 20
ephemeralRunnerInterval = time.Millisecond * 250 ephemeralRunnerInterval = time.Millisecond * 10
runnerImage = "ghcr.io/actions/actions-runner:latest" runnerImage = "ghcr.io/actions/actions-runner:latest"
) )
@@ -107,10 +107,15 @@ var _ = Describe("EphemeralRunner", func() {
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
controller = &EphemeralRunnerReconciler{ controller = &EphemeralRunnerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
@@ -528,44 +533,26 @@ var _ = Describe("EphemeralRunner", func() {
).Should(BeEquivalentTo("")) ).Should(BeEquivalentTo(""))
}) })
It("It should not re-create pod indefinitely", func() { It("It should eventually delete ephemeral runner after consecutive failures", func() {
updated := new(v1alpha1.EphemeralRunner) updated := new(v1alpha1.EphemeralRunner)
pod := new(corev1.Pod)
Eventually( Eventually(
func() (bool, error) { func() error {
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated) return k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
if err != nil {
return false, err
}
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
if err != nil {
if kerrors.IsNotFound(err) && len(updated.Status.Failures) > 5 {
return true, nil
}
return false, err
}
pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
Name: v1alpha1.EphemeralRunnerContainerName,
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 1,
},
},
})
err = k8sClient.Status().Update(ctx, pod)
Expect(err).To(BeNil(), "Failed to update pod status")
return false, fmt.Errorf("pod haven't failed for 5 times.")
}, },
ephemeralRunnerTimeout, ephemeralRunnerTimeout,
ephemeralRunnerInterval, ephemeralRunnerInterval,
).Should(BeEquivalentTo(true), "we should stop creating pod after 5 failures") ).Should(Succeed(), "failed to get ephemeral runner")
failEphemeralRunnerPod := func() *corev1.Pod {
pod := new(corev1.Pod)
Eventually(
func() error {
return k8sClient.Get(ctx, client.ObjectKey{Name: updated.Name, Namespace: updated.Namespace}, pod)
},
ephemeralRunnerTimeout,
ephemeralRunnerInterval,
).Should(Succeed(), "failed to get ephemeral runner pod")
// In case we still have pod created due to controller-runtime cache delay, mark the container as exited
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
if err == nil {
pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{ pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
Name: v1alpha1.EphemeralRunnerContainerName, Name: v1alpha1.EphemeralRunnerContainerName,
State: corev1.ContainerState{ State: corev1.ContainerState{
@@ -576,25 +563,70 @@ var _ = Describe("EphemeralRunner", func() {
}) })
err := k8sClient.Status().Update(ctx, pod) err := k8sClient.Status().Update(ctx, pod)
Expect(err).To(BeNil(), "Failed to update pod status") Expect(err).To(BeNil(), "Failed to update pod status")
return pod
} }
// EphemeralRunner should failed with reason TooManyPodFailures for i := range 5 {
Eventually(func() (string, error) { pod := failEphemeralRunnerPod()
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
if err != nil {
return "", err
}
return updated.Status.Reason, nil
}, ephemeralRunnerTimeout, ephemeralRunnerInterval).Should(BeEquivalentTo("TooManyPodFailures"), "Reason should be TooManyPodFailures")
// EphemeralRunner should not have any pod Eventually(
Eventually(func() (bool, error) { func() (int, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod) err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
if err == nil { if err != nil {
return false, nil return 0, err
} }
return kerrors.IsNotFound(err), nil return len(updated.Status.Failures), nil
}, ephemeralRunnerTimeout, ephemeralRunnerInterval).Should(BeEquivalentTo(true)) },
ephemeralRunnerTimeout,
ephemeralRunnerInterval,
).Should(BeEquivalentTo(i + 1))
Eventually(
func() error {
nextPod := new(corev1.Pod)
err := k8sClient.Get(ctx, client.ObjectKey{Name: pod.Name, Namespace: pod.Namespace}, nextPod)
if err != nil {
return err
}
if nextPod.UID != pod.UID {
return nil
}
return fmt.Errorf("pod not recreated")
},
).WithTimeout(20*time.Second).WithPolling(10*time.Millisecond).Should(Succeed(), "pod should be recreated")
Eventually(
func() (bool, error) {
pod := new(corev1.Pod)
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
if err != nil {
return false, err
}
for _, cs := range pod.Status.ContainerStatuses {
if cs.Name == v1alpha1.EphemeralRunnerContainerName {
return cs.State.Terminated == nil, nil
}
}
return true, nil
},
).WithTimeout(20*time.Second).WithPolling(10*time.Millisecond).Should(BeEquivalentTo(true), "pod should be terminated")
}
failEphemeralRunnerPod()
Eventually(
func() (bool, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
if kerrors.IsNotFound(err) {
return true, nil
}
return false, err
},
ephemeralRunnerTimeout,
ephemeralRunnerInterval,
).Should(BeTrue(), "Ephemeral runner should eventually be deleted")
}) })
It("It should re-create pod on eviction", func() { It("It should re-create pod on eviction", func() {
@@ -643,53 +675,6 @@ var _ = Describe("EphemeralRunner", func() {
).Should(BeEquivalentTo(true)) ).Should(BeEquivalentTo(true))
}) })
It("It should re-create pod on exit status 0, but runner exists within the service", func() {
pod := new(corev1.Pod)
Eventually(
func() (bool, error) {
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
return false, err
}
return true, nil
},
ephemeralRunnerTimeout,
ephemeralRunnerInterval,
).Should(BeEquivalentTo(true))
pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
Name: v1alpha1.EphemeralRunnerContainerName,
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 0,
},
},
})
err := k8sClient.Status().Update(ctx, pod)
Expect(err).To(BeNil(), "failed to update pod status")
updated := new(v1alpha1.EphemeralRunner)
Eventually(func() (bool, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
if err != nil {
return false, err
}
return len(updated.Status.Failures) == 1, nil
}, ephemeralRunnerTimeout, ephemeralRunnerInterval).Should(BeEquivalentTo(true))
// should re-create after failure
Eventually(
func() (bool, error) {
pod := new(corev1.Pod)
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
return false, err
}
return true, nil
},
ephemeralRunnerTimeout,
ephemeralRunnerInterval,
).Should(BeEquivalentTo(true))
})
It("It should not set the phase to succeeded without pod termination status", func() { It("It should not set the phase to succeeded without pod termination status", func() {
pod := new(corev1.Pod) pod := new(corev1.Pod)
Eventually( Eventually(
@@ -762,22 +747,27 @@ var _ = Describe("EphemeralRunner", func() {
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: fake.NewMultiClient( ResourceBuilder: ResourceBuilder{
fake.WithDefaultClient( SecretResolver: &SecretResolver{
fake.NewFakeClient( k8sClient: mgr.GetClient(),
fake.WithGetRunner( multiClient: fake.NewMultiClient(
fake.WithDefaultClient(
fake.NewFakeClient(
fake.WithGetRunner(
nil,
&actions.ActionsError{
StatusCode: http.StatusNotFound,
Err: &actions.ActionsExceptionError{
ExceptionName: "AgentNotFoundException",
},
},
),
),
nil, nil,
&actions.ActionsError{
StatusCode: http.StatusNotFound,
Err: &actions.ActionsExceptionError{
ExceptionName: "AgentNotFoundException",
},
},
), ),
), ),
nil, },
), },
),
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).To(BeNil(), "failed to setup controller") Expect(err).To(BeNil(), "failed to setup controller")
@@ -834,10 +824,15 @@ var _ = Describe("EphemeralRunner", func() {
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoScalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoScalingNS.Name)
controller = &EphemeralRunnerReconciler{ controller = &EphemeralRunnerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).To(BeNil(), "failed to setup controller") Expect(err).To(BeNil(), "failed to setup controller")
@@ -847,7 +842,12 @@ var _ = Describe("EphemeralRunner", func() {
It("uses an actions client with proxy transport", func() { It("uses an actions client with proxy transport", func() {
// Use an actual client // Use an actual client
controller.ActionsClient = actions.NewMultiClient(logr.Discard()) controller.ResourceBuilder = ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
}
proxySuccessfulllyCalled := false proxySuccessfulllyCalled := false
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -998,10 +998,15 @@ var _ = Describe("EphemeralRunner", func() {
Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs") Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs")
controller = &EphemeralRunnerReconciler{ controller = &EphemeralRunnerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
},
} }
err = controller.SetupWithManager(mgr) err = controller.SetupWithManager(mgr)
@@ -1032,11 +1037,16 @@ var _ = Describe("EphemeralRunner", func() {
server.StartTLS() server.StartTLS()
// Use an actual client // Use an actual client
controller.ActionsClient = actions.NewMultiClient(logr.Discard()) controller.ResourceBuilder = ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
}
ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name) ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name)
ephemeralRunner.Spec.GitHubConfigUrl = server.ConfigURLForOrg("my-org") ephemeralRunner.Spec.GitHubConfigUrl = server.ConfigURLForOrg("my-org")
ephemeralRunner.Spec.GitHubServerTLS = &v1alpha1.GitHubServerTLSConfig{ ephemeralRunner.Spec.GitHubServerTLS = &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{

View File

@@ -83,7 +83,7 @@ func (r *EphemeralRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.R
} }
// Requested deletion does not need reconciled. // Requested deletion does not need reconciled.
if !ephemeralRunnerSet.ObjectMeta.DeletionTimestamp.IsZero() { if !ephemeralRunnerSet.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(ephemeralRunnerSet, ephemeralRunnerSetFinalizerName) { if !controllerutil.ContainsFinalizer(ephemeralRunnerSet, ephemeralRunnerSetFinalizerName) {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@@ -331,7 +331,7 @@ func (r *EphemeralRunnerSetReconciler) cleanUpEphemeralRunners(ctx context.Conte
return false, nil return false, nil
} }
actionsClient, err := r.actionsClientFor(ctx, ephemeralRunnerSet) actionsClient, err := r.GetActionsService(ctx, ephemeralRunnerSet)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -360,7 +360,7 @@ func (r *EphemeralRunnerSetReconciler) createEphemeralRunners(ctx context.Contex
// Track multiple errors at once and return the bundle. // Track multiple errors at once and return the bundle.
errs := make([]error, 0) errs := make([]error, 0)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
ephemeralRunner := r.ResourceBuilder.newEphemeralRunner(runnerSet) ephemeralRunner := r.newEphemeralRunner(runnerSet)
if runnerSet.Spec.EphemeralRunnerSpec.Proxy != nil { if runnerSet.Spec.EphemeralRunnerSpec.Proxy != nil {
ephemeralRunner.Spec.ProxySecretRef = proxyEphemeralRunnerSetSecretName(runnerSet) ephemeralRunner.Spec.ProxySecretRef = proxyEphemeralRunnerSetSecretName(runnerSet)
} }
@@ -439,7 +439,7 @@ func (r *EphemeralRunnerSetReconciler) deleteIdleEphemeralRunners(ctx context.Co
log.Info("No pending or running ephemeral runners running at this time for scale down") log.Info("No pending or running ephemeral runners running at this time for scale down")
return nil return nil
} }
actionsClient, err := r.actionsClientFor(ctx, ephemeralRunnerSet) actionsClient, err := r.GetActionsService(ctx, ephemeralRunnerSet)
if err != nil { if err != nil {
return fmt.Errorf("failed to create actions client for ephemeral runner replica set: %w", err) return fmt.Errorf("failed to create actions client for ephemeral runner replica set: %w", err)
} }
@@ -502,73 +502,6 @@ func (r *EphemeralRunnerSetReconciler) deleteEphemeralRunnerWithActionsClient(ct
return true, nil return true, nil
} }
func (r *EphemeralRunnerSetReconciler) actionsClientFor(ctx context.Context, rs *v1alpha1.EphemeralRunnerSet) (actions.ActionsService, error) {
secret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: rs.Namespace, Name: rs.Spec.EphemeralRunnerSpec.GitHubConfigSecret}, secret); err != nil {
return nil, fmt.Errorf("failed to get secret: %w", err)
}
opts, err := r.actionsClientOptionsFor(ctx, rs)
if err != nil {
return nil, fmt.Errorf("failed to get actions client options: %w", err)
}
return r.ActionsClient.GetClientFromSecret(
ctx,
rs.Spec.EphemeralRunnerSpec.GitHubConfigUrl,
rs.Namespace,
secret.Data,
opts...,
)
}
func (r *EphemeralRunnerSetReconciler) actionsClientOptionsFor(ctx context.Context, rs *v1alpha1.EphemeralRunnerSet) ([]actions.ClientOption, error) {
var opts []actions.ClientOption
if rs.Spec.EphemeralRunnerSpec.Proxy != nil {
proxyFunc, err := rs.Spec.EphemeralRunnerSpec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
var secret corev1.Secret
err := r.Get(ctx, types.NamespacedName{Namespace: rs.Namespace, Name: s}, &secret)
if err != nil {
return nil, fmt.Errorf("failed to get secret %s: %w", s, err)
}
return &secret, nil
})
if err != nil {
return nil, fmt.Errorf("failed to get proxy func: %w", err)
}
opts = append(opts, actions.WithProxy(proxyFunc))
}
tlsConfig := rs.Spec.EphemeralRunnerSpec.GitHubServerTLS
if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap
err := r.Get(
ctx,
types.NamespacedName{
Namespace: rs.Namespace,
Name: name,
},
&configmap,
)
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
}
return []byte(configmap.Data[key]), nil
})
if err != nil {
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
opts = append(opts, actions.WithRootCAs(pool))
}
return opts, nil
}
// SetupWithManager sets up the controller with the Manager. // SetupWithManager sets up the controller with the Manager.
func (r *EphemeralRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *EphemeralRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr). return ctrl.NewControllerManagedBy(mgr).
@@ -641,7 +574,7 @@ func newEphemeralRunnerState(ephemeralRunnerList *v1alpha1.EphemeralRunnerList)
if err == nil && patchID > ephemeralRunnerState.latestPatchID { if err == nil && patchID > ephemeralRunnerState.latestPatchID {
ephemeralRunnerState.latestPatchID = patchID ephemeralRunnerState.latestPatchID = patchID
} }
if !r.ObjectMeta.DeletionTimestamp.IsZero() { if !r.DeletionTimestamp.IsZero() {
ephemeralRunnerState.deleting = append(ephemeralRunnerState.deleting, r) ephemeralRunnerState.deleting = append(ephemeralRunnerState.deleting, r)
continue continue
} }

View File

@@ -10,6 +10,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing"
"time" "time"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@@ -21,6 +22,7 @@ import (
"github.com/go-logr/logr" "github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
@@ -35,6 +37,10 @@ const (
ephemeralRunnerSetTestGitHubToken = "gh_token" ephemeralRunnerSetTestGitHubToken = "gh_token"
) )
func TestPrecomputedConstants(t *testing.T) {
require.Equal(t, len(failedRunnerBackoff), maxFailures+1)
}
var _ = Describe("Test EphemeralRunnerSet controller", func() { var _ = Describe("Test EphemeralRunnerSet controller", func() {
var ctx context.Context var ctx context.Context
var mgr ctrl.Manager var mgr ctrl.Manager
@@ -48,10 +54,15 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
controller := &EphemeralRunnerSetReconciler{ controller := &EphemeralRunnerSetReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -1098,10 +1109,15 @@ var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func(
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
controller := &EphemeralRunnerSetReconciler{ controller := &EphemeralRunnerSetReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: actions.NewMultiClient(logr.Discard()), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -1397,10 +1413,15 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs") Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs")
controller := &EphemeralRunnerSetReconciler{ controller := &EphemeralRunnerSetReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: actions.NewMultiClient(logr.Discard()), ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
},
} }
err = controller.SetupWithManager(mgr) err = controller.SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred(), "failed to setup controller") Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
@@ -1439,7 +1460,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{ EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigUrl: server.ConfigURLForOrg("my-org"), GitHubConfigUrl: server.ConfigURLForOrg("my-org"),
GitHubConfigSecret: configSecret.Name, GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{ GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{ CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ LocalObjectReference: corev1.LocalObjectReference{

View File

@@ -5,17 +5,20 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"math" "math"
"net" "net"
"strconv" "strconv"
"strings" "strings"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/build" "github.com/actions/actions-runner-controller/build"
listenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config" ghalistenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config"
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/hash" "github.com/actions/actions-runner-controller/hash"
"github.com/actions/actions-runner-controller/logging" "github.com/actions/actions-runner-controller/logging"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -71,6 +74,7 @@ func SetListenerEntrypoint(entrypoint string) {
type ResourceBuilder struct { type ResourceBuilder struct {
ExcludeLabelPropagationPrefixes []string ExcludeLabelPropagationPrefixes []string
*SecretResolver
} }
// boolPtr returns a pointer to a bool value // boolPtr returns a pointer to a bool value
@@ -120,6 +124,7 @@ func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.
Spec: v1alpha1.AutoscalingListenerSpec{ Spec: v1alpha1.AutoscalingListenerSpec{
GitHubConfigUrl: autoscalingRunnerSet.Spec.GitHubConfigUrl, GitHubConfigUrl: autoscalingRunnerSet.Spec.GitHubConfigUrl,
GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret, GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret,
VaultConfig: autoscalingRunnerSet.VaultConfig(),
RunnerScaleSetId: runnerScaleSetId, RunnerScaleSetId: runnerScaleSetId,
AutoscalingRunnerSetNamespace: autoscalingRunnerSet.Namespace, AutoscalingRunnerSetNamespace: autoscalingRunnerSet.Namespace,
AutoscalingRunnerSetName: autoscalingRunnerSet.Name, AutoscalingRunnerSetName: autoscalingRunnerSet.Name,
@@ -159,7 +164,7 @@ func (lm *listenerMetricsServerConfig) containerPort() (corev1.ContainerPort, er
}, nil }, nil
} }
func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha1.AutoscalingListener, secret *corev1.Secret, metricsConfig *listenerMetricsServerConfig, cert string) (*corev1.Secret, error) { func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha1.AutoscalingListener, appConfig *appconfig.AppConfig, metricsConfig *listenerMetricsServerConfig, cert string) (*corev1.Secret, error) {
var ( var (
metricsAddr = "" metricsAddr = ""
metricsEndpoint = "" metricsEndpoint = ""
@@ -169,30 +174,8 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
metricsEndpoint = metricsConfig.endpoint metricsEndpoint = metricsConfig.endpoint
} }
var appID int64 config := ghalistenerconfig.Config{
if id, ok := secret.Data["github_app_id"]; ok {
var err error
appID, err = strconv.ParseInt(string(id), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to convert github_app_id to int: %v", err)
}
}
var appInstallationID int64
if id, ok := secret.Data["github_app_installation_id"]; ok {
var err error
appInstallationID, err = strconv.ParseInt(string(id), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to convert github_app_installation_id to int: %v", err)
}
}
config := listenerconfig.Config{
ConfigureUrl: autoscalingListener.Spec.GitHubConfigUrl, ConfigureUrl: autoscalingListener.Spec.GitHubConfigUrl,
AppID: appID,
AppInstallationID: appInstallationID,
AppPrivateKey: string(secret.Data["github_app_private_key"]),
Token: string(secret.Data["github_token"]),
EphemeralRunnerSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, EphemeralRunnerSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
EphemeralRunnerSetName: autoscalingListener.Spec.EphemeralRunnerSetName, EphemeralRunnerSetName: autoscalingListener.Spec.EphemeralRunnerSetName,
MaxRunners: autoscalingListener.Spec.MaxRunners, MaxRunners: autoscalingListener.Spec.MaxRunners,
@@ -207,6 +190,24 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
Metrics: autoscalingListener.Spec.Metrics, Metrics: autoscalingListener.Spec.Metrics,
} }
vault := autoscalingListener.Spec.VaultConfig
if vault == nil {
config.AppConfig = appConfig
} else {
config.VaultType = vault.Type
config.VaultLookupKey = autoscalingListener.Spec.GitHubConfigSecret
config.AzureKeyVaultConfig = &azurekeyvault.Config{
TenantID: vault.AzureKeyVault.TenantID,
ClientID: vault.AzureKeyVault.ClientID,
URL: vault.AzureKeyVault.URL,
CertificatePath: vault.AzureKeyVault.CertificatePath,
}
}
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid listener config: %w", err)
}
var buf bytes.Buffer var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(config); err != nil { if err := json.NewEncoder(&buf).Encode(config); err != nil {
return nil, fmt.Errorf("failed to encode config: %w", err) return nil, fmt.Errorf("failed to encode config: %w", err)
@@ -223,7 +224,7 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
}, nil }, nil
} }
func (b *ResourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.AutoscalingListener, podConfig *corev1.Secret, serviceAccount *corev1.ServiceAccount, secret *corev1.Secret, metricsConfig *listenerMetricsServerConfig, envs ...corev1.EnvVar) (*corev1.Pod, error) { func (b *ResourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.AutoscalingListener, podConfig *corev1.Secret, serviceAccount *corev1.ServiceAccount, metricsConfig *listenerMetricsServerConfig, envs ...corev1.EnvVar) (*corev1.Pod, error) {
listenerEnv := []corev1.EnvVar{ listenerEnv := []corev1.EnvVar{
{ {
Name: "LISTENER_CONFIG_PATH", Name: "LISTENER_CONFIG_PATH",
@@ -278,9 +279,7 @@ func (b *ResourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.A
} }
labels := make(map[string]string, len(autoscalingListener.Labels)) labels := make(map[string]string, len(autoscalingListener.Labels))
for key, val := range autoscalingListener.Labels { maps.Copy(labels, autoscalingListener.Labels)
labels[key] = val
}
newRunnerScaleSetListenerPod := &corev1.Pod{ newRunnerScaleSetListenerPod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
@@ -429,7 +428,7 @@ func mergeListenerContainer(base, from *corev1.Container) {
func (b *ResourceBuilder) newScaleSetListenerServiceAccount(autoscalingListener *v1alpha1.AutoscalingListener) *corev1.ServiceAccount { func (b *ResourceBuilder) newScaleSetListenerServiceAccount(autoscalingListener *v1alpha1.AutoscalingListener) *corev1.ServiceAccount {
return &corev1.ServiceAccount{ return &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: scaleSetListenerServiceAccountName(autoscalingListener), Name: autoscalingListener.Name,
Namespace: autoscalingListener.Namespace, Namespace: autoscalingListener.Namespace,
Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{ Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{
LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
@@ -444,7 +443,7 @@ func (b *ResourceBuilder) newScaleSetListenerRole(autoscalingListener *v1alpha1.
rulesHash := hash.ComputeTemplateHash(&rules) rulesHash := hash.ComputeTemplateHash(&rules)
newRole := &rbacv1.Role{ newRole := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: scaleSetListenerRoleName(autoscalingListener), Name: autoscalingListener.Name,
Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{ Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{
LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
@@ -478,7 +477,7 @@ func (b *ResourceBuilder) newScaleSetListenerRoleBinding(autoscalingListener *v1
newRoleBinding := &rbacv1.RoleBinding{ newRoleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: scaleSetListenerRoleName(autoscalingListener), Name: autoscalingListener.Name,
Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{ Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{
LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
@@ -496,25 +495,6 @@ func (b *ResourceBuilder) newScaleSetListenerRoleBinding(autoscalingListener *v1
return newRoleBinding return newRoleBinding
} }
func (b *ResourceBuilder) newScaleSetListenerSecretMirror(autoscalingListener *v1alpha1.AutoscalingListener, secret *corev1.Secret) *corev1.Secret {
dataHash := hash.ComputeTemplateHash(&secret.Data)
newListenerSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: scaleSetListenerSecretMirrorName(autoscalingListener),
Namespace: autoscalingListener.Namespace,
Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{
LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
LabelKeyGitHubScaleSetName: autoscalingListener.Spec.AutoscalingRunnerSetName,
"secret-data-hash": dataHash,
}),
},
Data: secret.DeepCopy().Data,
}
return newListenerSecret
}
func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (*v1alpha1.EphemeralRunnerSet, error) { func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (*v1alpha1.EphemeralRunnerSet, error) {
runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdAnnotationKey]) runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdAnnotationKey])
if err != nil { if err != nil {
@@ -543,8 +523,8 @@ func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.A
newEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{ newEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
TypeMeta: metav1.TypeMeta{}, TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
GenerateName: autoscalingRunnerSet.ObjectMeta.Name + "-", GenerateName: autoscalingRunnerSet.Name + "-",
Namespace: autoscalingRunnerSet.ObjectMeta.Namespace, Namespace: autoscalingRunnerSet.Namespace,
Labels: labels, Labels: labels,
Annotations: newAnnotations, Annotations: newAnnotations,
OwnerReferences: []metav1.OwnerReference{ OwnerReferences: []metav1.OwnerReference{
@@ -567,6 +547,7 @@ func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.A
Proxy: autoscalingRunnerSet.Spec.Proxy, Proxy: autoscalingRunnerSet.Spec.Proxy,
GitHubServerTLS: autoscalingRunnerSet.Spec.GitHubServerTLS, GitHubServerTLS: autoscalingRunnerSet.Spec.GitHubServerTLS,
PodTemplateSpec: autoscalingRunnerSet.Spec.Template, PodTemplateSpec: autoscalingRunnerSet.Spec.Template,
VaultConfig: autoscalingRunnerSet.VaultConfig(),
}, },
}, },
} }
@@ -588,6 +569,7 @@ func (b *ResourceBuilder) newEphemeralRunner(ephemeralRunnerSet *v1alpha1.Epheme
for key, val := range ephemeralRunnerSet.Annotations { for key, val := range ephemeralRunnerSet.Annotations {
annotations[key] = val annotations[key] = val
} }
annotations[AnnotationKeyPatchID] = strconv.Itoa(ephemeralRunnerSet.Spec.PatchID) annotations[AnnotationKeyPatchID] = strconv.Itoa(ephemeralRunnerSet.Spec.PatchID)
return &v1alpha1.EphemeralRunner{ return &v1alpha1.EphemeralRunner{
TypeMeta: metav1.TypeMeta{}, TypeMeta: metav1.TypeMeta{},
@@ -617,18 +599,18 @@ func (b *ResourceBuilder) newEphemeralRunnerPod(ctx context.Context, runner *v1a
labels := map[string]string{} labels := map[string]string{}
annotations := map[string]string{} annotations := map[string]string{}
for k, v := range runner.ObjectMeta.Labels { for k, v := range runner.Labels {
labels[k] = v labels[k] = v
} }
for k, v := range runner.Spec.PodTemplateSpec.Labels { for k, v := range runner.Spec.Labels {
labels[k] = v labels[k] = v
} }
labels["actions-ephemeral-runner"] = string(corev1.ConditionTrue) labels["actions-ephemeral-runner"] = string(corev1.ConditionTrue)
for k, v := range runner.ObjectMeta.Annotations { for k, v := range runner.Annotations {
annotations[k] = v annotations[k] = v
} }
for k, v := range runner.Spec.PodTemplateSpec.Annotations { for k, v := range runner.Spec.Annotations {
annotations[k] = v annotations[k] = v
} }
@@ -640,8 +622,8 @@ func (b *ResourceBuilder) newEphemeralRunnerPod(ctx context.Context, runner *v1a
) )
objectMeta := metav1.ObjectMeta{ objectMeta := metav1.ObjectMeta{
Name: runner.ObjectMeta.Name, Name: runner.Name,
Namespace: runner.ObjectMeta.Namespace, Namespace: runner.Namespace,
Labels: labels, Labels: labels,
Annotations: annotations, Annotations: annotations,
OwnerReferences: []metav1.OwnerReference{ OwnerReferences: []metav1.OwnerReference{
@@ -657,10 +639,10 @@ func (b *ResourceBuilder) newEphemeralRunnerPod(ctx context.Context, runner *v1a
} }
newPod.ObjectMeta = objectMeta newPod.ObjectMeta = objectMeta
newPod.Spec = runner.Spec.PodTemplateSpec.Spec newPod.Spec = runner.Spec.Spec
newPod.Spec.Containers = make([]corev1.Container, 0, len(runner.Spec.PodTemplateSpec.Spec.Containers)) newPod.Spec.Containers = make([]corev1.Container, 0, len(runner.Spec.Spec.Containers))
for _, c := range runner.Spec.PodTemplateSpec.Spec.Containers { for _, c := range runner.Spec.Spec.Containers {
if c.Name == v1alpha1.EphemeralRunnerContainerName { if c.Name == v1alpha1.EphemeralRunnerContainerName {
c.Env = append( c.Env = append(
c.Env, c.Env,
@@ -713,30 +695,6 @@ func scaleSetListenerName(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) s
return fmt.Sprintf("%v-%v-listener", autoscalingRunnerSet.Name, namespaceHash) return fmt.Sprintf("%v-%v-listener", autoscalingRunnerSet.Name, namespaceHash)
} }
func scaleSetListenerServiceAccountName(autoscalingListener *v1alpha1.AutoscalingListener) string {
namespaceHash := hash.FNVHashString(autoscalingListener.Spec.AutoscalingRunnerSetNamespace)
if len(namespaceHash) > 8 {
namespaceHash = namespaceHash[:8]
}
return fmt.Sprintf("%v-%v-listener", autoscalingListener.Spec.AutoscalingRunnerSetName, namespaceHash)
}
func scaleSetListenerRoleName(autoscalingListener *v1alpha1.AutoscalingListener) string {
namespaceHash := hash.FNVHashString(autoscalingListener.Spec.AutoscalingRunnerSetNamespace)
if len(namespaceHash) > 8 {
namespaceHash = namespaceHash[:8]
}
return fmt.Sprintf("%v-%v-listener", autoscalingListener.Spec.AutoscalingRunnerSetName, namespaceHash)
}
func scaleSetListenerSecretMirrorName(autoscalingListener *v1alpha1.AutoscalingListener) string {
namespaceHash := hash.FNVHashString(autoscalingListener.Spec.AutoscalingRunnerSetNamespace)
if len(namespaceHash) > 8 {
namespaceHash = namespaceHash[:8]
}
return fmt.Sprintf("%v-%v-listener", autoscalingListener.Spec.AutoscalingRunnerSetName, namespaceHash)
}
func proxyListenerSecretName(autoscalingListener *v1alpha1.AutoscalingListener) string { func proxyListenerSecretName(autoscalingListener *v1alpha1.AutoscalingListener) string {
namespaceHash := hash.FNVHashString(autoscalingListener.Spec.AutoscalingRunnerSetNamespace) namespaceHash := hash.FNVHashString(autoscalingListener.Spec.AutoscalingRunnerSetNamespace)
if len(namespaceHash) > 8 { if len(namespaceHash) > 8 {

View File

@@ -82,12 +82,7 @@ func TestLabelPropagation(t *testing.T) {
Name: "test", Name: "test",
}, },
} }
listenerSecret := &corev1.Secret{ listenerPod, err := b.newScaleSetListenerPod(listener, &corev1.Secret{}, listenerServiceAccount, nil)
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
}
listenerPod, err := b.newScaleSetListenerPod(listener, &corev1.Secret{}, listenerServiceAccount, listenerSecret, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, listenerPod.Labels, listener.Labels) assert.Equal(t, listenerPod.Labels, listener.Labels)

View File

@@ -0,0 +1,280 @@
package actionsgithubcom
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/vault"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
"golang.org/x/net/http/httpproxy"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type SecretResolver struct {
k8sClient client.Client
multiClient actions.MultiClient
}
type SecretResolverOption func(*SecretResolver)
func NewSecretResolver(k8sClient client.Client, multiClient actions.MultiClient, opts ...SecretResolverOption) *SecretResolver {
if k8sClient == nil {
panic("k8sClient must not be nil")
}
secretResolver := &SecretResolver{
k8sClient: k8sClient,
multiClient: multiClient,
}
for _, opt := range opts {
opt(secretResolver)
}
return secretResolver
}
type ActionsGitHubObject interface {
client.Object
GitHubConfigUrl() string
GitHubConfigSecret() string
GitHubProxy() *v1alpha1.ProxyConfig
GitHubServerTLS() *v1alpha1.TLSConfig
VaultConfig() *v1alpha1.VaultConfig
VaultProxy() *v1alpha1.ProxyConfig
}
func (sr *SecretResolver) GetAppConfig(ctx context.Context, obj ActionsGitHubObject) (*appconfig.AppConfig, error) {
resolver, err := sr.resolverForObject(ctx, obj)
if err != nil {
return nil, fmt.Errorf("failed to get resolver for object: %v", err)
}
appConfig, err := resolver.appConfig(ctx, obj.GitHubConfigSecret())
if err != nil {
return nil, fmt.Errorf("failed to resolve app config: %v", err)
}
return appConfig, nil
}
func (sr *SecretResolver) GetActionsService(ctx context.Context, obj ActionsGitHubObject) (actions.ActionsService, error) {
resolver, err := sr.resolverForObject(ctx, obj)
if err != nil {
return nil, fmt.Errorf("failed to get resolver for object: %v", err)
}
appConfig, err := resolver.appConfig(ctx, obj.GitHubConfigSecret())
if err != nil {
return nil, fmt.Errorf("failed to resolve app config: %v", err)
}
var clientOptions []actions.ClientOption
if proxy := obj.GitHubProxy(); proxy != nil {
config := &httpproxy.Config{
NoProxy: strings.Join(proxy.NoProxy, ","),
}
if proxy.HTTP != nil {
u, err := url.Parse(proxy.HTTP.Url)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy http url %q: %w", proxy.HTTP.Url, err)
}
if ref := proxy.HTTP.CredentialSecretRef; ref != "" {
u.User, err = resolver.proxyCredentials(ctx, ref)
if err != nil {
return nil, fmt.Errorf("failed to resolve proxy credentials: %v", err)
}
}
config.HTTPProxy = u.String()
}
if proxy.HTTPS != nil {
u, err := url.Parse(proxy.HTTPS.Url)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy https url %q: %w", proxy.HTTPS.Url, err)
}
if ref := proxy.HTTPS.CredentialSecretRef; ref != "" {
u.User, err = resolver.proxyCredentials(ctx, ref)
if err != nil {
return nil, fmt.Errorf("failed to resolve proxy credentials: %v", err)
}
}
config.HTTPSProxy = u.String()
}
proxyFunc := func(req *http.Request) (*url.URL, error) {
return config.ProxyFunc()(req.URL)
}
clientOptions = append(clientOptions, actions.WithProxy(proxyFunc))
}
tlsConfig := obj.GitHubServerTLS()
if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap
err := sr.k8sClient.Get(
ctx,
types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: name,
},
&configmap,
)
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
}
return []byte(configmap.Data[key]), nil
})
if err != nil {
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
clientOptions = append(clientOptions, actions.WithRootCAs(pool))
}
return sr.multiClient.GetClientFor(
ctx,
obj.GitHubConfigUrl(),
appConfig,
obj.GetNamespace(),
clientOptions...,
)
}
func (sr *SecretResolver) resolverForObject(ctx context.Context, obj ActionsGitHubObject) (resolver, error) {
vaultConfig := obj.VaultConfig()
if vaultConfig == nil || vaultConfig.Type == "" {
return &k8sResolver{
namespace: obj.GetNamespace(),
client: sr.k8sClient,
}, nil
}
var proxy *httpproxy.Config
if vaultProxy := obj.VaultProxy(); vaultProxy != nil {
p, err := vaultProxy.ToHTTPProxyConfig(func(s string) (*corev1.Secret, error) {
var secret corev1.Secret
err := sr.k8sClient.Get(ctx, types.NamespacedName{Name: s, Namespace: obj.GetNamespace()}, &secret)
if err != nil {
return nil, fmt.Errorf("failed to get secret %s: %w", s, err)
}
return &secret, nil
})
if err != nil {
return nil, fmt.Errorf("failed to create proxy config: %v", err)
}
proxy = p
}
switch vaultConfig.Type {
case vault.VaultTypeAzureKeyVault:
akv, err := azurekeyvault.New(azurekeyvault.Config{
TenantID: vaultConfig.AzureKeyVault.TenantID,
ClientID: vaultConfig.AzureKeyVault.ClientID,
URL: vaultConfig.AzureKeyVault.URL,
CertificatePath: vaultConfig.AzureKeyVault.CertificatePath,
Proxy: proxy,
})
if err != nil {
return nil, fmt.Errorf("failed to create Azure Key Vault client: %v", err)
}
return &vaultResolver{
vault: akv,
}, nil
default:
return nil, fmt.Errorf("unknown vault type %q", vaultConfig.Type)
}
}
type resolver interface {
appConfig(ctx context.Context, key string) (*appconfig.AppConfig, error)
proxyCredentials(ctx context.Context, key string) (*url.Userinfo, error)
}
type k8sResolver struct {
namespace string
client client.Client
}
func (r *k8sResolver) appConfig(ctx context.Context, key string) (*appconfig.AppConfig, error) {
nsName := types.NamespacedName{
Namespace: r.namespace,
Name: key,
}
secret := new(corev1.Secret)
if err := r.client.Get(
ctx,
nsName,
secret,
); err != nil {
return nil, fmt.Errorf("failed to get kubernetes secret: %q", nsName.String())
}
return appconfig.FromSecret(secret)
}
func (r *k8sResolver) proxyCredentials(ctx context.Context, key string) (*url.Userinfo, error) {
nsName := types.NamespacedName{Namespace: r.namespace, Name: key}
secret := new(corev1.Secret)
if err := r.client.Get(
ctx,
nsName,
secret,
); err != nil {
return nil, fmt.Errorf("failed to get kubernetes secret: %q", nsName.String())
}
return url.UserPassword(
string(secret.Data["username"]),
string(secret.Data["password"]),
), nil
}
type vaultResolver struct {
vault vault.Vault
}
func (r *vaultResolver) appConfig(ctx context.Context, key string) (*appconfig.AppConfig, error) {
val, err := r.vault.GetSecret(ctx, key)
if err != nil {
return nil, fmt.Errorf("failed to resolve secret: %v", err)
}
return appconfig.FromJSONString(val)
}
func (r *vaultResolver) proxyCredentials(ctx context.Context, key string) (*url.Userinfo, error) {
val, err := r.vault.GetSecret(ctx, key)
if err != nil {
return nil, fmt.Errorf("failed to resolve secret: %v", err)
}
type info struct {
Username string `json:"username"`
Password string `json:"password"`
}
var i info
if err := json.Unmarshal([]byte(val), &i); err != nil {
return nil, fmt.Errorf("failed to unmarshal info: %v", err)
}
return url.UserPassword(i.Username, i.Password), nil
}

View File

@@ -20,6 +20,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/onsi/ginkgo/config" "github.com/onsi/ginkgo/config"
@@ -79,6 +80,15 @@ var _ = BeforeSuite(func() {
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil()) Expect(k8sClient).ToNot(BeNil())
failedRunnerBackoff = []time.Duration{
20 * time.Millisecond,
20 * time.Millisecond,
20 * time.Millisecond,
20 * time.Millisecond,
20 * time.Millisecond,
20 * time.Millisecond,
}
}) })
var _ = AfterSuite(func() { var _ = AfterSuite(func() {

View File

@@ -130,7 +130,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByQueuedAndInProgr
jobs, resp, err := ghc.Actions.ListWorkflowJobs(context.TODO(), user, repoName, runID, &opt) jobs, resp, err := ghc.Actions.ListWorkflowJobs(context.TODO(), user, repoName, runID, &opt)
if err != nil { if err != nil {
r.Log.Error(err, "Error listing workflow jobs") r.Log.Error(err, "Error listing workflow jobs")
return //err return // err
} }
allJobs = append(allJobs, jobs.Jobs...) allJobs = append(allJobs, jobs.Jobs...)
if resp.NextPage == 0 { if resp.NextPage == 0 {
@@ -345,7 +345,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByPercentageRunner
} }
var runnerPodList corev1.PodList var runnerPodList corev1.PodList
if err := r.Client.List(ctx, &runnerPodList, client.InNamespace(hra.Namespace), client.MatchingLabels(map[string]string{ if err := r.List(ctx, &runnerPodList, client.InNamespace(hra.Namespace), client.MatchingLabels(map[string]string{
kindLabel: hra.Spec.ScaleTargetRef.Name, kindLabel: hra.Spec.ScaleTargetRef.Name,
})); err != nil { })); err != nil {
return nil, err return nil, err

View File

@@ -29,7 +29,7 @@ func newGithubClient(server *httptest.Server) *github.Client {
if err != nil { if err != nil {
panic(err) panic(err)
} }
client.Client.BaseURL = baseURL client.BaseURL = baseURL
return client return client
} }

View File

@@ -82,8 +82,8 @@ func (s *batchScaler) Add(st *ScaleTarget) {
break batch break batch
case st := <-s.queue: case st := <-s.queue:
nsName := types.NamespacedName{ nsName := types.NamespacedName{
Namespace: st.HorizontalRunnerAutoscaler.Namespace, Namespace: st.Namespace,
Name: st.HorizontalRunnerAutoscaler.Name, Name: st.Name,
} }
b, ok := batches[nsName] b, ok := batches[nsName]
if !ok { if !ok {
@@ -208,7 +208,7 @@ func (s *batchScaler) planBatchScale(ctx context.Context, batch batchScaleOperat
// //
// In other words, updating HRA.spec.scaleTriggers[].duration does not result in delaying capacity reservations expiration any longer // In other words, updating HRA.spec.scaleTriggers[].duration does not result in delaying capacity reservations expiration any longer
// than the "intended" duration, which is the duration of the trigger when the reservation was created. // than the "intended" duration, which is the duration of the trigger when the reservation was created.
duration := copy.Spec.CapacityReservations[i].ExpirationTime.Time.Sub(copy.Spec.CapacityReservations[i].EffectiveTime.Time) duration := copy.Spec.CapacityReservations[i].ExpirationTime.Sub(copy.Spec.CapacityReservations[i].EffectiveTime.Time)
copy.Spec.CapacityReservations[i].EffectiveTime = metav1.Time{Time: now} copy.Spec.CapacityReservations[i].EffectiveTime = metav1.Time{Time: now}
copy.Spec.CapacityReservations[i].ExpirationTime = metav1.Time{Time: now.Add(duration)} copy.Spec.CapacityReservations[i].ExpirationTime = metav1.Time{Time: now.Add(duration)}
} }

View File

@@ -503,13 +503,13 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getManagedRunnerGroup
switch kind { switch kind {
case "RunnerSet": case "RunnerSet":
var rs v1alpha1.RunnerSet var rs v1alpha1.RunnerSet
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil { if err := autoscaler.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil {
return groups, err return groups, err
} }
o, e, g = rs.Spec.Organization, rs.Spec.Enterprise, rs.Spec.Group o, e, g = rs.Spec.Organization, rs.Spec.Enterprise, rs.Spec.Group
case "RunnerDeployment", "": case "RunnerDeployment", "":
var rd v1alpha1.RunnerDeployment var rd v1alpha1.RunnerDeployment
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil { if err := autoscaler.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil {
return groups, err return groups, err
} }
o, e, g = rd.Spec.Template.Spec.Organization, rd.Spec.Template.Spec.Enterprise, rd.Spec.Template.Spec.Group o, e, g = rd.Spec.Template.Spec.Organization, rd.Spec.Template.Spec.Enterprise, rd.Spec.Template.Spec.Group
@@ -562,7 +562,7 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleTarget(ctx
HRA: HRA:
for _, hra := range hras { for _, hra := range hras {
if !hra.ObjectMeta.DeletionTimestamp.IsZero() { if !hra.DeletionTimestamp.IsZero() {
continue continue
} }
@@ -603,7 +603,7 @@ HRA:
case "RunnerSet": case "RunnerSet":
var rs v1alpha1.RunnerSet var rs v1alpha1.RunnerSet
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil { if err := autoscaler.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil {
return nil, err return nil, err
} }
@@ -634,7 +634,7 @@ HRA:
case "RunnerDeployment", "": case "RunnerDeployment", "":
var rd v1alpha1.RunnerDeployment var rd v1alpha1.RunnerDeployment
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil { if err := autoscaler.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil {
return nil, err return nil, err
} }
@@ -676,7 +676,7 @@ func getValidCapacityReservations(autoscaler *v1alpha1.HorizontalRunnerAutoscale
now := time.Now() now := time.Now()
for _, reservation := range autoscaler.Spec.CapacityReservations { for _, reservation := range autoscaler.Spec.CapacityReservations {
if reservation.ExpirationTime.Time.After(now) { if reservation.ExpirationTime.After(now) {
capacityReservations = append(capacityReservations, reservation) capacityReservations = append(capacityReservations, reservation)
} }
} }
@@ -713,7 +713,7 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) indexer(rawObj client
switch hra.Spec.ScaleTargetRef.Kind { switch hra.Spec.ScaleTargetRef.Kind {
case "", "RunnerDeployment": case "", "RunnerDeployment":
var rd v1alpha1.RunnerDeployment var rd v1alpha1.RunnerDeployment
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil { if err := autoscaler.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil {
autoscaler.Log.V(1).Info(fmt.Sprintf("RunnerDeployment not found with scale target ref name %s for hra %s", hra.Spec.ScaleTargetRef.Name, hra.Name)) autoscaler.Log.V(1).Info(fmt.Sprintf("RunnerDeployment not found with scale target ref name %s for hra %s", hra.Spec.ScaleTargetRef.Name, hra.Name))
return nil return nil
} }
@@ -740,7 +740,7 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) indexer(rawObj client
return keys return keys
case "RunnerSet": case "RunnerSet":
var rs v1alpha1.RunnerSet var rs v1alpha1.RunnerSet
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil { if err := autoscaler.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil {
autoscaler.Log.V(1).Info(fmt.Sprintf("RunnerSet not found with scale target ref name %s for hra %s", hra.Spec.ScaleTargetRef.Name, hra.Name)) autoscaler.Log.V(1).Info(fmt.Sprintf("RunnerSet not found with scale target ref name %s for hra %s", hra.Spec.ScaleTargetRef.Name, hra.Name))
return nil return nil
} }

View File

@@ -71,7 +71,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(ctx context.Context, re
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if !hra.ObjectMeta.DeletionTimestamp.IsZero() { if !hra.DeletionTimestamp.IsZero() {
r.GitHubClient.DeinitForHRA(&hra) r.GitHubClient.DeinitForHRA(&hra)
return ctrl.Result{}, nil return ctrl.Result{}, nil
@@ -91,7 +91,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(ctx context.Context, re
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if !rd.ObjectMeta.DeletionTimestamp.IsZero() { if !rd.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@@ -120,14 +120,14 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(ctx context.Context, re
copy.Spec.EffectiveTime = &metav1.Time{Time: *effectiveTime} copy.Spec.EffectiveTime = &metav1.Time{Time: *effectiveTime}
} }
if err := r.Client.Patch(ctx, copy, client.MergeFrom(&rd)); err != nil { if err := r.Patch(ctx, copy, client.MergeFrom(&rd)); err != nil {
return fmt.Errorf("patching runnerdeployment to have %d replicas: %w", newDesiredReplicas, err) return fmt.Errorf("patching runnerdeployment to have %d replicas: %w", newDesiredReplicas, err)
} }
} else if ephemeral && effectiveTime != nil { } else if ephemeral && effectiveTime != nil {
copy := rd.DeepCopy() copy := rd.DeepCopy()
copy.Spec.EffectiveTime = &metav1.Time{Time: *effectiveTime} copy.Spec.EffectiveTime = &metav1.Time{Time: *effectiveTime}
if err := r.Client.Patch(ctx, copy, client.MergeFrom(&rd)); err != nil { if err := r.Patch(ctx, copy, client.MergeFrom(&rd)); err != nil {
return fmt.Errorf("patching runnerdeployment to have %d replicas: %w", newDesiredReplicas, err) return fmt.Errorf("patching runnerdeployment to have %d replicas: %w", newDesiredReplicas, err)
} }
} }
@@ -142,7 +142,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(ctx context.Context, re
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if !rs.ObjectMeta.DeletionTimestamp.IsZero() { if !rs.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@@ -160,7 +160,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(ctx context.Context, re
org: rs.Spec.Organization, org: rs.Spec.Organization,
repo: rs.Spec.Repository, repo: rs.Spec.Repository,
replicas: replicas, replicas: replicas,
labels: rs.Spec.RunnerConfig.Labels, labels: rs.Spec.Labels,
getRunnerMap: func() (map[string]struct{}, error) { getRunnerMap: func() (map[string]struct{}, error) {
// return the list of runners in namespace. Horizontal Runner Autoscaler should only be responsible for scaling resources in its own ns. // return the list of runners in namespace. Horizontal Runner Autoscaler should only be responsible for scaling resources in its own ns.
var runnerPodList corev1.PodList var runnerPodList corev1.PodList
@@ -224,14 +224,14 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(ctx context.Context, re
copy.Spec.EffectiveTime = &metav1.Time{Time: *effectiveTime} copy.Spec.EffectiveTime = &metav1.Time{Time: *effectiveTime}
} }
if err := r.Client.Patch(ctx, copy, client.MergeFrom(&rs)); err != nil { if err := r.Patch(ctx, copy, client.MergeFrom(&rs)); err != nil {
return fmt.Errorf("patching runnerset to have %d replicas: %w", newDesiredReplicas, err) return fmt.Errorf("patching runnerset to have %d replicas: %w", newDesiredReplicas, err)
} }
} else if ephemeral && effectiveTime != nil { } else if ephemeral && effectiveTime != nil {
copy := rs.DeepCopy() copy := rs.DeepCopy()
copy.Spec.EffectiveTime = &metav1.Time{Time: *effectiveTime} copy.Spec.EffectiveTime = &metav1.Time{Time: *effectiveTime}
if err := r.Client.Patch(ctx, copy, client.MergeFrom(&rs)); err != nil { if err := r.Patch(ctx, copy, client.MergeFrom(&rs)); err != nil {
return fmt.Errorf("patching runnerset to have %d replicas: %w", newDesiredReplicas, err) return fmt.Errorf("patching runnerset to have %d replicas: %w", newDesiredReplicas, err)
} }
} }
@@ -253,7 +253,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) scaleTargetFromRD(ctx context.Con
org: rd.Spec.Template.Spec.Organization, org: rd.Spec.Template.Spec.Organization,
repo: rd.Spec.Template.Spec.Repository, repo: rd.Spec.Template.Spec.Repository,
replicas: rd.Spec.Replicas, replicas: rd.Spec.Replicas,
labels: rd.Spec.Template.Spec.RunnerConfig.Labels, labels: rd.Spec.Template.Spec.Labels,
getRunnerMap: func() (map[string]struct{}, error) { getRunnerMap: func() (map[string]struct{}, error) {
// return the list of runners in namespace. Horizontal Runner Autoscaler should only be responsible for scaling resources in its own ns. // return the list of runners in namespace. Horizontal Runner Autoscaler should only be responsible for scaling resources in its own ns.
var runnerList v1alpha1.RunnerList var runnerList v1alpha1.RunnerList
@@ -484,7 +484,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) computeReplicasWithCache(ghc *arc
var reserved int var reserved int
for _, reservation := range hra.Spec.CapacityReservations { for _, reservation := range hra.Spec.CapacityReservations {
if reservation.ExpirationTime.Time.After(now) { if reservation.ExpirationTime.After(now) {
reserved += reservation.Replicas reserved += reservation.Replicas
} }
} }

View File

@@ -20,12 +20,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"k8s.io/apimachinery/pkg/api/resource"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"k8s.io/apimachinery/pkg/api/resource"
"github.com/actions/actions-runner-controller/build" "github.com/actions/actions-runner-controller/build"
"github.com/actions/actions-runner-controller/hash" "github.com/actions/actions-runner-controller/hash"
"github.com/go-logr/logr" "github.com/go-logr/logr"
@@ -107,12 +108,12 @@ func (r *RunnerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if runner.ObjectMeta.DeletionTimestamp.IsZero() { if runner.DeletionTimestamp.IsZero() {
finalizers, added := addFinalizer(runner.ObjectMeta.Finalizers, finalizerName) finalizers, added := addFinalizer(runner.Finalizers, finalizerName)
if added { if added {
newRunner := runner.DeepCopy() newRunner := runner.DeepCopy()
newRunner.ObjectMeta.Finalizers = finalizers newRunner.Finalizers = finalizers
if err := r.Update(ctx, newRunner); err != nil { if err := r.Update(ctx, newRunner); err != nil {
log.Error(err, "Failed to update runner") log.Error(err, "Failed to update runner")
@@ -271,11 +272,11 @@ func ephemeralRunnerContainerStatus(pod *corev1.Pod) *corev1.ContainerStatus {
} }
func (r *RunnerReconciler) processRunnerDeletion(runner v1alpha1.Runner, ctx context.Context, log logr.Logger, pod *corev1.Pod) (reconcile.Result, error) { func (r *RunnerReconciler) processRunnerDeletion(runner v1alpha1.Runner, ctx context.Context, log logr.Logger, pod *corev1.Pod) (reconcile.Result, error) {
finalizers, removed := removeFinalizer(runner.ObjectMeta.Finalizers, finalizerName) finalizers, removed := removeFinalizer(runner.Finalizers, finalizerName)
if removed { if removed {
newRunner := runner.DeepCopy() newRunner := runner.DeepCopy()
newRunner.ObjectMeta.Finalizers = finalizers newRunner.Finalizers = finalizers
if err := r.Patch(ctx, newRunner, client.MergeFrom(&runner)); err != nil { if err := r.Patch(ctx, newRunner, client.MergeFrom(&runner)); err != nil {
log.Error(err, "Unable to remove finalizer") log.Error(err, "Unable to remove finalizer")
@@ -305,8 +306,8 @@ func (r *RunnerReconciler) processRunnerCreation(ctx context.Context, runner v1a
if needsServiceAccount { if needsServiceAccount {
serviceAccount := &corev1.ServiceAccount{ serviceAccount := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: runner.ObjectMeta.Name, Name: runner.Name,
Namespace: runner.ObjectMeta.Namespace, Namespace: runner.Namespace,
}, },
} }
if res := r.createObject(ctx, serviceAccount, serviceAccount.ObjectMeta, &runner, log); res != nil { if res := r.createObject(ctx, serviceAccount, serviceAccount.ObjectMeta, &runner, log); res != nil {
@@ -321,7 +322,7 @@ func (r *RunnerReconciler) processRunnerCreation(ctx context.Context, runner v1a
APIGroups: []string{"actions.summerwind.dev"}, APIGroups: []string{"actions.summerwind.dev"},
Resources: []string{"runners/status"}, Resources: []string{"runners/status"},
Verbs: []string{"get", "update", "patch"}, Verbs: []string{"get", "update", "patch"},
ResourceNames: []string{runner.ObjectMeta.Name}, ResourceNames: []string{runner.Name},
}, },
}...) }...)
} }
@@ -359,8 +360,8 @@ func (r *RunnerReconciler) processRunnerCreation(ctx context.Context, runner v1a
role := &rbacv1.Role{ role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: runner.ObjectMeta.Name, Name: runner.Name,
Namespace: runner.ObjectMeta.Namespace, Namespace: runner.Namespace,
}, },
Rules: rules, Rules: rules,
} }
@@ -370,19 +371,19 @@ func (r *RunnerReconciler) processRunnerCreation(ctx context.Context, runner v1a
roleBinding := &rbacv1.RoleBinding{ roleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: runner.ObjectMeta.Name, Name: runner.Name,
Namespace: runner.ObjectMeta.Namespace, Namespace: runner.Namespace,
}, },
RoleRef: rbacv1.RoleRef{ RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io", APIGroup: "rbac.authorization.k8s.io",
Kind: "Role", Kind: "Role",
Name: runner.ObjectMeta.Name, Name: runner.Name,
}, },
Subjects: []rbacv1.Subject{ Subjects: []rbacv1.Subject{
{ {
Kind: "ServiceAccount", Kind: "ServiceAccount",
Name: runner.ObjectMeta.Name, Name: runner.Name,
Namespace: runner.ObjectMeta.Namespace, Namespace: runner.Namespace,
}, },
}, },
} }
@@ -482,7 +483,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
labels := map[string]string{} labels := map[string]string{}
for k, v := range runner.ObjectMeta.Labels { for k, v := range runner.Labels {
labels[k] = v labels[k] = v
} }
@@ -511,8 +512,8 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
// //
// See https://github.com/actions/actions-runner-controller/issues/143 for more context. // See https://github.com/actions/actions-runner-controller/issues/143 for more context.
labels[LabelKeyPodTemplateHash] = hash.FNVHashStringObjects( labels[LabelKeyPodTemplateHash] = hash.FNVHashStringObjects(
filterLabels(runner.ObjectMeta.Labels, LabelKeyRunnerTemplateHash), filterLabels(runner.Labels, LabelKeyRunnerTemplateHash),
runner.ObjectMeta.Annotations, runner.Annotations,
runner.Spec, runner.Spec,
ghc.GithubBaseURL, ghc.GithubBaseURL,
// Token change should trigger replacement. // Token change should trigger replacement.
@@ -523,10 +524,10 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
) )
objectMeta := metav1.ObjectMeta{ objectMeta := metav1.ObjectMeta{
Name: runner.ObjectMeta.Name, Name: runner.Name,
Namespace: runner.ObjectMeta.Namespace, Namespace: runner.Namespace,
Labels: labels, Labels: labels,
Annotations: runner.ObjectMeta.Annotations, Annotations: runner.Annotations,
} }
template.ObjectMeta = objectMeta template.ObjectMeta = objectMeta
@@ -649,7 +650,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
if runnerSpec.ServiceAccountName != "" { if runnerSpec.ServiceAccountName != "" {
pod.Spec.ServiceAccountName = runnerSpec.ServiceAccountName pod.Spec.ServiceAccountName = runnerSpec.ServiceAccountName
} else if r.RunnerPodDefaults.UseRunnerStatusUpdateHook || runner.Spec.ContainerMode == "kubernetes" { } else if r.RunnerPodDefaults.UseRunnerStatusUpdateHook || runner.Spec.ContainerMode == "kubernetes" {
pod.Spec.ServiceAccountName = runner.ObjectMeta.Name pod.Spec.ServiceAccountName = runner.Name
} }
if runnerSpec.AutomountServiceAccountToken != nil { if runnerSpec.AutomountServiceAccountToken != nil {
@@ -704,7 +705,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
pod.Spec.RuntimeClassName = runnerSpec.RuntimeClassName pod.Spec.RuntimeClassName = runnerSpec.RuntimeClassName
} }
pod.ObjectMeta.Name = runner.ObjectMeta.Name pod.Name = runner.Name
// Inject the registration token and the runner name // Inject the registration token and the runner name
updated := mutatePod(&pod, runner.Status.Registration.Token) updated := mutatePod(&pod, runner.Status.Registration.Token)
@@ -720,7 +721,7 @@ func mutatePod(pod *corev1.Pod, token string) *corev1.Pod {
updated := pod.DeepCopy() updated := pod.DeepCopy()
if getRunnerEnv(pod, EnvVarRunnerName) == "" { if getRunnerEnv(pod, EnvVarRunnerName) == "" {
setRunnerEnv(updated, EnvVarRunnerName, pod.ObjectMeta.Name) setRunnerEnv(updated, EnvVarRunnerName, pod.Name)
} }
if getRunnerEnv(pod, EnvVarRunnerToken) == "" { if getRunnerEnv(pod, EnvVarRunnerToken) == "" {
@@ -770,11 +771,11 @@ func runnerHookEnvs(pod *corev1.Pod) ([]corev1.EnvVar, error) {
func newRunnerPodWithContainerMode(containerMode string, template corev1.Pod, runnerSpec v1alpha1.RunnerConfig, githubBaseURL string, d RunnerPodDefaults) (corev1.Pod, error) { func newRunnerPodWithContainerMode(containerMode string, template corev1.Pod, runnerSpec v1alpha1.RunnerConfig, githubBaseURL string, d RunnerPodDefaults) (corev1.Pod, error) {
var ( var (
privileged bool = true privileged = true
dockerdInRunner bool = runnerSpec.DockerdWithinRunnerContainer != nil && *runnerSpec.DockerdWithinRunnerContainer dockerdInRunner = runnerSpec.DockerdWithinRunnerContainer != nil && *runnerSpec.DockerdWithinRunnerContainer
dockerEnabled bool = runnerSpec.DockerEnabled == nil || *runnerSpec.DockerEnabled dockerEnabled = runnerSpec.DockerEnabled == nil || *runnerSpec.DockerEnabled
ephemeral bool = runnerSpec.Ephemeral == nil || *runnerSpec.Ephemeral ephemeral = runnerSpec.Ephemeral == nil || *runnerSpec.Ephemeral
dockerdInRunnerPrivileged bool = dockerdInRunner dockerdInRunnerPrivileged = dockerdInRunner
defaultRunnerImage = d.RunnerImage defaultRunnerImage = d.RunnerImage
defaultRunnerImagePullSecrets = d.RunnerImagePullSecrets defaultRunnerImagePullSecrets = d.RunnerImagePullSecrets
@@ -797,10 +798,10 @@ func newRunnerPodWithContainerMode(containerMode string, template corev1.Pod, ru
template = *template.DeepCopy() template = *template.DeepCopy()
// This label selector is used by default when rd.Spec.Selector is empty. // This label selector is used by default when rd.Spec.Selector is empty.
template.ObjectMeta.Labels = CloneAndAddLabel(template.ObjectMeta.Labels, LabelKeyRunner, "") template.Labels = CloneAndAddLabel(template.Labels, LabelKeyRunner, "")
template.ObjectMeta.Labels = CloneAndAddLabel(template.ObjectMeta.Labels, LabelKeyPodMutation, LabelValuePodMutation) template.Labels = CloneAndAddLabel(template.Labels, LabelKeyPodMutation, LabelValuePodMutation)
if runnerSpec.GitHubAPICredentialsFrom != nil { if runnerSpec.GitHubAPICredentialsFrom != nil {
template.ObjectMeta.Annotations = CloneAndAddLabel(template.ObjectMeta.Annotations, annotationKeyGitHubAPICredsSecret, runnerSpec.GitHubAPICredentialsFrom.SecretRef.Name) template.Annotations = CloneAndAddLabel(template.Annotations, annotationKeyGitHubAPICredsSecret, runnerSpec.GitHubAPICredentialsFrom.SecretRef.Name)
} }
workDir := runnerSpec.WorkDir workDir := runnerSpec.WorkDir
@@ -887,10 +888,11 @@ func newRunnerPodWithContainerMode(containerMode string, template corev1.Pod, ru
for i := range template.Spec.Containers { for i := range template.Spec.Containers {
c := template.Spec.Containers[i] c := template.Spec.Containers[i]
if c.Name == containerName { switch c.Name {
case containerName:
runnerContainerIndex = i runnerContainerIndex = i
runnerContainer = &c runnerContainer = &c
} else if c.Name == "docker" { case "docker":
dockerdContainerIndex = i dockerdContainerIndex = i
dockerdContainer = &c dockerdContainer = &c
} }
@@ -1364,7 +1366,7 @@ func applyWorkVolumeClaimTemplateToPod(pod *corev1.Pod, workVolumeClaimTemplate
} }
for i := range pod.Spec.Volumes { for i := range pod.Spec.Volumes {
if pod.Spec.Volumes[i].Name == "work" { if pod.Spec.Volumes[i].Name == "work" {
return fmt.Errorf("Work volume should not be specified in container mode kubernetes. workVolumeClaimTemplate field should be used instead.") return fmt.Errorf("work volume should not be specified in container mode kubernetes. workVolumeClaimTemplate field should be used instead")
} }
} }
pod.Spec.Volumes = append(pod.Spec.Volumes, workVolumeClaimTemplate.V1Volume()) pod.Spec.Volumes = append(pod.Spec.Volumes, workVolumeClaimTemplate.V1Volume())

View File

@@ -79,7 +79,7 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
} }
if len(envvars) == 0 { if len(envvars) == 0 {
return ctrl.Result{}, errors.New("Could not determine env vars for runner Pod") return ctrl.Result{}, errors.New("could not determine env vars for runner Pod")
} }
var enterprise, org, repo string var enterprise, org, repo string
@@ -103,8 +103,8 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{}, err return ctrl.Result{}, err
} }
if runnerPod.ObjectMeta.DeletionTimestamp.IsZero() { if runnerPod.DeletionTimestamp.IsZero() {
finalizers, added := addFinalizer(runnerPod.ObjectMeta.Finalizers, runnerPodFinalizerName) finalizers, added := addFinalizer(runnerPod.Finalizers, runnerPodFinalizerName)
var cleanupFinalizersAdded bool var cleanupFinalizersAdded bool
if isContainerMode { if isContainerMode {
@@ -113,7 +113,7 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
if added || cleanupFinalizersAdded { if added || cleanupFinalizersAdded {
newRunner := runnerPod.DeepCopy() newRunner := runnerPod.DeepCopy()
newRunner.ObjectMeta.Finalizers = finalizers newRunner.Finalizers = finalizers
if err := r.Patch(ctx, newRunner, client.MergeFrom(&runnerPod)); err != nil { if err := r.Patch(ctx, newRunner, client.MergeFrom(&runnerPod)); err != nil {
log.Error(err, "Failed to update runner") log.Error(err, "Failed to update runner")
@@ -142,7 +142,7 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
} }
} }
if finalizers, removed := removeFinalizer(runnerPod.ObjectMeta.Finalizers, runnerLinkedResourcesFinalizerName); removed { if finalizers, removed := removeFinalizer(runnerPod.Finalizers, runnerLinkedResourcesFinalizerName); removed {
if err := r.cleanupRunnerLinkedPods(ctx, &runnerPod, log); err != nil { if err := r.cleanupRunnerLinkedPods(ctx, &runnerPod, log); err != nil {
log.Info("Runner-linked pods clean up that has failed due to an error. If this persists, please manually remove the runner-linked pods to unblock ARC", "err", err.Error()) log.Info("Runner-linked pods clean up that has failed due to an error. If this persists, please manually remove the runner-linked pods to unblock ARC", "err", err.Error())
return ctrl.Result{Requeue: true, RequeueAfter: 30 * time.Second}, nil return ctrl.Result{Requeue: true, RequeueAfter: 30 * time.Second}, nil
@@ -152,7 +152,7 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{Requeue: true, RequeueAfter: 30 * time.Second}, nil return ctrl.Result{Requeue: true, RequeueAfter: 30 * time.Second}, nil
} }
patchedPod := runnerPod.DeepCopy() patchedPod := runnerPod.DeepCopy()
patchedPod.ObjectMeta.Finalizers = finalizers patchedPod.Finalizers = finalizers
if err := r.Patch(ctx, patchedPod, client.MergeFrom(&runnerPod)); err != nil { if err := r.Patch(ctx, patchedPod, client.MergeFrom(&runnerPod)); err != nil {
log.Error(err, "Failed to update runner for finalizer linked resources removal") log.Error(err, "Failed to update runner for finalizer linked resources removal")
@@ -163,7 +163,7 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
runnerPod = *patchedPod runnerPod = *patchedPod
} }
finalizers, removed := removeFinalizer(runnerPod.ObjectMeta.Finalizers, runnerPodFinalizerName) finalizers, removed := removeFinalizer(runnerPod.Finalizers, runnerPodFinalizerName)
if removed { if removed {
// In a standard scenario, the upstream controller, like runnerset-controller, ensures this runner to be gracefully stopped before the deletion timestamp is set. // In a standard scenario, the upstream controller, like runnerset-controller, ensures this runner to be gracefully stopped before the deletion timestamp is set.
@@ -175,7 +175,7 @@ func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
} }
patchedPod := updatedPod.DeepCopy() patchedPod := updatedPod.DeepCopy()
patchedPod.ObjectMeta.Finalizers = finalizers patchedPod.Finalizers = finalizers
// We commit the removal of the finalizer so that Kuberenetes notices it and delete the pod resource from the cluster. // We commit the removal of the finalizer so that Kuberenetes notices it and delete the pod resource from the cluster.
if err := r.Patch(ctx, patchedPod, client.MergeFrom(&runnerPod)); err != nil { if err := r.Patch(ctx, patchedPod, client.MergeFrom(&runnerPod)); err != nil {
@@ -284,7 +284,7 @@ func (r *RunnerPodReconciler) cleanupRunnerLinkedPods(ctx context.Context, pod *
var runnerLinkedPodList corev1.PodList var runnerLinkedPodList corev1.PodList
if err := r.List(ctx, &runnerLinkedPodList, client.InNamespace(pod.Namespace), client.MatchingLabels( if err := r.List(ctx, &runnerLinkedPodList, client.InNamespace(pod.Namespace), client.MatchingLabels(
map[string]string{ map[string]string{
"runner-pod": pod.ObjectMeta.Name, "runner-pod": pod.Name,
}, },
)); err != nil { )); err != nil {
return fmt.Errorf("failed to list runner-linked pods: %w", err) return fmt.Errorf("failed to list runner-linked pods: %w", err)
@@ -295,7 +295,7 @@ func (r *RunnerPodReconciler) cleanupRunnerLinkedPods(ctx context.Context, pod *
errs []error errs []error
) )
for _, p := range runnerLinkedPodList.Items { for _, p := range runnerLinkedPodList.Items {
if !p.ObjectMeta.DeletionTimestamp.IsZero() { if !p.DeletionTimestamp.IsZero() {
continue continue
} }
@@ -307,7 +307,7 @@ func (r *RunnerPodReconciler) cleanupRunnerLinkedPods(ctx context.Context, pod *
if kerrors.IsNotFound(err) || kerrors.IsGone(err) { if kerrors.IsNotFound(err) || kerrors.IsGone(err) {
return return
} }
errs = append(errs, fmt.Errorf("delete pod %q error: %v", p.ObjectMeta.Name, err)) errs = append(errs, fmt.Errorf("delete pod %q error: %v", p.Name, err))
} }
}() }()
} }
@@ -330,7 +330,7 @@ func (r *RunnerPodReconciler) cleanupRunnerLinkedSecrets(ctx context.Context, po
var runnerLinkedSecretList corev1.SecretList var runnerLinkedSecretList corev1.SecretList
if err := r.List(ctx, &runnerLinkedSecretList, client.InNamespace(pod.Namespace), client.MatchingLabels( if err := r.List(ctx, &runnerLinkedSecretList, client.InNamespace(pod.Namespace), client.MatchingLabels(
map[string]string{ map[string]string{
"runner-pod": pod.ObjectMeta.Name, "runner-pod": pod.Name,
}, },
)); err != nil { )); err != nil {
return fmt.Errorf("failed to list runner-linked secrets: %w", err) return fmt.Errorf("failed to list runner-linked secrets: %w", err)
@@ -341,7 +341,7 @@ func (r *RunnerPodReconciler) cleanupRunnerLinkedSecrets(ctx context.Context, po
errs []error errs []error
) )
for _, s := range runnerLinkedSecretList.Items { for _, s := range runnerLinkedSecretList.Items {
if !s.ObjectMeta.DeletionTimestamp.IsZero() { if !s.DeletionTimestamp.IsZero() {
continue continue
} }
@@ -353,7 +353,7 @@ func (r *RunnerPodReconciler) cleanupRunnerLinkedSecrets(ctx context.Context, po
if kerrors.IsNotFound(err) || kerrors.IsGone(err) { if kerrors.IsNotFound(err) || kerrors.IsGone(err) {
return return
} }
errs = append(errs, fmt.Errorf("delete secret %q error: %v", s.ObjectMeta.Name, err)) errs = append(errs, fmt.Errorf("delete secret %q error: %v", s.Name, err))
} }
}() }()
} }

View File

@@ -90,7 +90,7 @@ var _ owner = (*ownerStatefulSet)(nil)
func (s *ownerStatefulSet) pods(ctx context.Context, c client.Client) ([]corev1.Pod, error) { func (s *ownerStatefulSet) pods(ctx context.Context, c client.Client) ([]corev1.Pod, error) {
var podList corev1.PodList var podList corev1.PodList
if err := c.List(ctx, &podList, client.MatchingLabels(s.StatefulSet.Spec.Template.ObjectMeta.Labels)); err != nil { if err := c.List(ctx, &podList, client.MatchingLabels(s.StatefulSet.Spec.Template.Labels)); err != nil {
s.Log.Error(err, "Failed to list pods managed by statefulset") s.Log.Error(err, "Failed to list pods managed by statefulset")
return nil, err return nil, err
} }

View File

@@ -73,7 +73,7 @@ func (r *RunnerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if !rd.ObjectMeta.DeletionTimestamp.IsZero() { if !rd.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@@ -112,7 +112,7 @@ func (r *RunnerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
} }
if newestSet == nil { if newestSet == nil {
if err := r.Client.Create(ctx, desiredRS); err != nil { if err := r.Create(ctx, desiredRS); err != nil {
log.Error(err, "Failed to create runnerreplicaset resource") log.Error(err, "Failed to create runnerreplicaset resource")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -138,7 +138,7 @@ func (r *RunnerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
} }
if newestTemplateHash != desiredTemplateHash { if newestTemplateHash != desiredTemplateHash {
if err := r.Client.Create(ctx, desiredRS); err != nil { if err := r.Create(ctx, desiredRS); err != nil {
log.Error(err, "Failed to create runnerreplicaset resource") log.Error(err, "Failed to create runnerreplicaset resource")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -159,7 +159,7 @@ func (r *RunnerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
// but we still need to update the existing replicaset with it. // but we still need to update the existing replicaset with it.
// Otherwise selector-based runner query will never work on replicasets created before the controller v0.17.0 // Otherwise selector-based runner query will never work on replicasets created before the controller v0.17.0
// See https://github.com/actions/actions-runner-controller/pull/355#discussion_r585379259 // See https://github.com/actions/actions-runner-controller/pull/355#discussion_r585379259
if err := r.Client.Update(ctx, updateSet); err != nil { if err := r.Update(ctx, updateSet); err != nil {
log.Error(err, "Failed to update runnerreplicaset resource") log.Error(err, "Failed to update runnerreplicaset resource")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -195,7 +195,7 @@ func (r *RunnerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
newestSet.Spec.Replicas = &newDesiredReplicas newestSet.Spec.Replicas = &newDesiredReplicas
newestSet.Spec.EffectiveTime = rd.Spec.EffectiveTime newestSet.Spec.EffectiveTime = rd.Spec.EffectiveTime
if err := r.Client.Update(ctx, newestSet); err != nil { if err := r.Update(ctx, newestSet); err != nil {
log.Error(err, "Failed to update runnerreplicaset resource") log.Error(err, "Failed to update runnerreplicaset resource")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -257,7 +257,7 @@ func (r *RunnerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
updated := rs.DeepCopy() updated := rs.DeepCopy()
zero := 0 zero := 0
updated.Spec.Replicas = &zero updated.Spec.Replicas = &zero
if err := r.Client.Update(ctx, updated); err != nil { if err := r.Update(ctx, updated); err != nil {
rslog.Error(err, "Failed to scale runnerreplicaset to zero") rslog.Error(err, "Failed to scale runnerreplicaset to zero")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -268,7 +268,7 @@ func (r *RunnerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
continue continue
} }
if err := r.Client.Delete(ctx, &rs); err != nil { if err := r.Delete(ctx, &rs); err != nil {
rslog.Error(err, "Failed to delete runnerreplicaset resource") rslog.Error(err, "Failed to delete runnerreplicaset resource")
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -445,10 +445,10 @@ func newRunnerReplicaSet(rd *v1alpha1.RunnerDeployment, commonRunnerLabels []str
templateHash := ComputeHash(&newRSTemplate) templateHash := ComputeHash(&newRSTemplate)
// Add template hash label to selector. // Add template hash label to selector.
newRSTemplate.ObjectMeta.Labels = CloneAndAddLabel(newRSTemplate.ObjectMeta.Labels, LabelKeyRunnerTemplateHash, templateHash) newRSTemplate.Labels = CloneAndAddLabel(newRSTemplate.Labels, LabelKeyRunnerTemplateHash, templateHash)
// This label selector is used by default when rd.Spec.Selector is empty. // This label selector is used by default when rd.Spec.Selector is empty.
newRSTemplate.ObjectMeta.Labels = CloneAndAddLabel(newRSTemplate.ObjectMeta.Labels, LabelKeyRunnerDeploymentName, rd.Name) newRSTemplate.Labels = CloneAndAddLabel(newRSTemplate.Labels, LabelKeyRunnerDeploymentName, rd.Name)
selector := getSelector(rd) selector := getSelector(rd)
@@ -457,9 +457,9 @@ func newRunnerReplicaSet(rd *v1alpha1.RunnerDeployment, commonRunnerLabels []str
rs := v1alpha1.RunnerReplicaSet{ rs := v1alpha1.RunnerReplicaSet{
TypeMeta: metav1.TypeMeta{}, TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
GenerateName: rd.ObjectMeta.Name + "-", GenerateName: rd.Name + "-",
Namespace: rd.ObjectMeta.Namespace, Namespace: rd.Namespace,
Labels: newRSTemplate.ObjectMeta.Labels, Labels: newRSTemplate.Labels,
}, },
Spec: v1alpha1.RunnerReplicaSetSpec{ Spec: v1alpha1.RunnerReplicaSetSpec{
Replicas: rd.Spec.Replicas, Replicas: rd.Spec.Replicas,

View File

@@ -62,7 +62,7 @@ func (r *RunnerReplicaSetReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, client.IgnoreNotFound(err) return ctrl.Result{}, client.IgnoreNotFound(err)
} }
if !rs.ObjectMeta.DeletionTimestamp.IsZero() { if !rs.DeletionTimestamp.IsZero() {
// RunnerReplicaSet cannot be gracefuly removed. // RunnerReplicaSet cannot be gracefuly removed.
// That means any runner that is running a job can be prematurely terminated. // That means any runner that is running a job can be prematurely terminated.
// To gracefully remove a RunnerReplicaSet, scale it down to zero first, observe RunnerReplicaSet's status replicas, // To gracefully remove a RunnerReplicaSet, scale it down to zero first, observe RunnerReplicaSet's status replicas,
@@ -70,14 +70,14 @@ func (r *RunnerReplicaSetReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
if rs.ObjectMeta.Labels == nil { if rs.Labels == nil {
rs.ObjectMeta.Labels = map[string]string{} rs.Labels = map[string]string{}
} }
// Template hash is usually set by the upstream controller(RunnerDeplloyment controller) on authoring // Template hash is usually set by the upstream controller(RunnerDeplloyment controller) on authoring
// RunerReplicaset resource, but it may be missing when the user directly created RunnerReplicaSet. // RunerReplicaset resource, but it may be missing when the user directly created RunnerReplicaSet.
// As a template hash is required by by the runner replica management, we dynamically add it here without ever persisting it. // As a template hash is required by by the runner replica management, we dynamically add it here without ever persisting it.
if rs.ObjectMeta.Labels[LabelKeyRunnerTemplateHash] == "" { if rs.Labels[LabelKeyRunnerTemplateHash] == "" {
template := rs.Spec.DeepCopy() template := rs.Spec.DeepCopy()
template.Replicas = nil template.Replicas = nil
template.EffectiveTime = nil template.EffectiveTime = nil
@@ -85,8 +85,8 @@ func (r *RunnerReplicaSetReconciler) Reconcile(ctx context.Context, req ctrl.Req
log.Info("Using auto-generated template hash", "value", templateHash) log.Info("Using auto-generated template hash", "value", templateHash)
rs.ObjectMeta.Labels = CloneAndAddLabel(rs.ObjectMeta.Labels, LabelKeyRunnerTemplateHash, templateHash) rs.Labels = CloneAndAddLabel(rs.Labels, LabelKeyRunnerTemplateHash, templateHash)
rs.Spec.Template.ObjectMeta.Labels = CloneAndAddLabel(rs.Spec.Template.ObjectMeta.Labels, LabelKeyRunnerTemplateHash, templateHash) rs.Spec.Template.Labels = CloneAndAddLabel(rs.Spec.Template.Labels, LabelKeyRunnerTemplateHash, templateHash)
} }
selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector)
@@ -169,8 +169,8 @@ func (r *RunnerReplicaSetReconciler) newRunner(rs v1alpha1.RunnerReplicaSet) (v1
// the "runner template hash" label to the template.meta which is necessary to make this controller work correctly // the "runner template hash" label to the template.meta which is necessary to make this controller work correctly
objectMeta := rs.Spec.Template.ObjectMeta.DeepCopy() objectMeta := rs.Spec.Template.ObjectMeta.DeepCopy()
objectMeta.GenerateName = rs.ObjectMeta.Name + "-" objectMeta.GenerateName = rs.Name + "-"
objectMeta.Namespace = rs.ObjectMeta.Namespace objectMeta.Namespace = rs.Namespace
if objectMeta.Annotations == nil { if objectMeta.Annotations == nil {
objectMeta.Annotations = map[string]string{} objectMeta.Annotations = map[string]string{}
} }

View File

@@ -77,7 +77,7 @@ func (r *RunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{}, err return ctrl.Result{}, err
} }
if !runnerSet.ObjectMeta.DeletionTimestamp.IsZero() { if !runnerSet.DeletionTimestamp.IsZero() {
r.GitHubClient.DeinitForRunnerSet(runnerSet) r.GitHubClient.DeinitForRunnerSet(runnerSet)
return ctrl.Result{}, nil return ctrl.Result{}, nil
@@ -191,11 +191,11 @@ func (r *RunnerSetReconciler) newStatefulSet(ctx context.Context, runnerSet *v1a
runnerSetWithOverrides.Labels = append(runnerSetWithOverrides.Labels, r.CommonRunnerLabels...) runnerSetWithOverrides.Labels = append(runnerSetWithOverrides.Labels, r.CommonRunnerLabels...)
template := corev1.Pod{ template := corev1.Pod{
ObjectMeta: runnerSetWithOverrides.StatefulSetSpec.Template.ObjectMeta, ObjectMeta: runnerSetWithOverrides.Template.ObjectMeta,
Spec: runnerSetWithOverrides.StatefulSetSpec.Template.Spec, Spec: runnerSetWithOverrides.Template.Spec,
} }
if runnerSet.Spec.RunnerConfig.ContainerMode == "kubernetes" { if runnerSet.Spec.ContainerMode == "kubernetes" {
found := false found := false
for i := range template.Spec.Containers { for i := range template.Spec.Containers {
if template.Spec.Containers[i].Name == containerName { if template.Spec.Containers[i].Name == containerName {
@@ -208,7 +208,7 @@ func (r *RunnerSetReconciler) newStatefulSet(ctx context.Context, runnerSet *v1a
}) })
} }
workDir := runnerSet.Spec.RunnerConfig.WorkDir workDir := runnerSet.Spec.WorkDir
if workDir == "" { if workDir == "" {
workDir = "/runner/_work" workDir = "/runner/_work"
} }
@@ -219,7 +219,7 @@ func (r *RunnerSetReconciler) newStatefulSet(ctx context.Context, runnerSet *v1a
template.Spec.ServiceAccountName = runnerSet.Spec.ServiceAccountName template.Spec.ServiceAccountName = runnerSet.Spec.ServiceAccountName
} }
template.ObjectMeta.Labels = CloneAndAddLabel(template.ObjectMeta.Labels, LabelKeyRunnerSetName, runnerSet.Name) template.Labels = CloneAndAddLabel(template.Labels, LabelKeyRunnerSetName, runnerSet.Name)
ghc, err := r.GitHubClient.InitForRunnerSet(ctx, runnerSet) ghc, err := r.GitHubClient.InitForRunnerSet(ctx, runnerSet)
if err != nil { if err != nil {
@@ -228,38 +228,38 @@ func (r *RunnerSetReconciler) newStatefulSet(ctx context.Context, runnerSet *v1a
githubBaseURL := ghc.GithubBaseURL githubBaseURL := ghc.GithubBaseURL
pod, err := newRunnerPodWithContainerMode(runnerSet.Spec.RunnerConfig.ContainerMode, template, runnerSet.Spec.RunnerConfig, githubBaseURL, r.RunnerPodDefaults) pod, err := newRunnerPodWithContainerMode(runnerSet.Spec.ContainerMode, template, runnerSet.Spec.RunnerConfig, githubBaseURL, r.RunnerPodDefaults)
if err != nil { if err != nil {
return nil, err return nil, err
} }
runnerSetWithOverrides.StatefulSetSpec.Template.ObjectMeta = pod.ObjectMeta runnerSetWithOverrides.Template.ObjectMeta = pod.ObjectMeta
runnerSetWithOverrides.StatefulSetSpec.Template.Spec = pod.Spec runnerSetWithOverrides.Template.Spec = pod.Spec
// NOTE: Seems like the only supported restart policy for statefulset is "Always"? // NOTE: Seems like the only supported restart policy for statefulset is "Always"?
// I got errosr like the below when tried to use "OnFailure": // I got errosr like the below when tried to use "OnFailure":
// StatefulSet.apps \"example-runnersetpg9rx\" is invalid: [spec.template.metadata.labels: Invalid value: map[string]string{\"runner-template-hash\" // StatefulSet.apps \"example-runnersetpg9rx\" is invalid: [spec.template.metadata.labels: Invalid value: map[string]string{\"runner-template-hash\"
// :\"85d7578bd6\", \"runnerset-name\":\"example-runnerset\"}: `selector` does not match template `labels`, spec. // :\"85d7578bd6\", \"runnerset-name\":\"example-runnerset\"}: `selector` does not match template `labels`, spec.
// template.spec.restartPolicy: Unsupported value: \"OnFailure\": supported values: \"Always\"] // template.spec.restartPolicy: Unsupported value: \"OnFailure\": supported values: \"Always\"]
runnerSetWithOverrides.StatefulSetSpec.Template.Spec.RestartPolicy = corev1.RestartPolicyAlways runnerSetWithOverrides.Template.Spec.RestartPolicy = corev1.RestartPolicyAlways
templateHash := ComputeHash(pod.Spec) templateHash := ComputeHash(pod.Spec)
// Add template hash label to selector. // Add template hash label to selector.
runnerSetWithOverrides.Template.ObjectMeta.Labels = CloneAndAddLabel(runnerSetWithOverrides.Template.ObjectMeta.Labels, LabelKeyRunnerTemplateHash, templateHash) runnerSetWithOverrides.Template.Labels = CloneAndAddLabel(runnerSetWithOverrides.Template.Labels, LabelKeyRunnerTemplateHash, templateHash)
selector := getRunnerSetSelector(runnerSet) selector := getRunnerSetSelector(runnerSet)
selector = CloneSelectorAndAddLabel(selector, LabelKeyRunnerTemplateHash, templateHash) selector = CloneSelectorAndAddLabel(selector, LabelKeyRunnerTemplateHash, templateHash)
selector = CloneSelectorAndAddLabel(selector, LabelKeyRunnerSetName, runnerSet.Name) selector = CloneSelectorAndAddLabel(selector, LabelKeyRunnerSetName, runnerSet.Name)
selector = CloneSelectorAndAddLabel(selector, LabelKeyPodMutation, LabelValuePodMutation) selector = CloneSelectorAndAddLabel(selector, LabelKeyPodMutation, LabelValuePodMutation)
runnerSetWithOverrides.StatefulSetSpec.Selector = selector runnerSetWithOverrides.Selector = selector
rs := appsv1.StatefulSet{ rs := appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{}, TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
GenerateName: runnerSet.ObjectMeta.Name + "-", GenerateName: runnerSet.Name + "-",
Namespace: runnerSet.ObjectMeta.Namespace, Namespace: runnerSet.Namespace,
Labels: CloneAndAddLabel(runnerSet.ObjectMeta.Labels, LabelKeyRunnerTemplateHash, templateHash), Labels: CloneAndAddLabel(runnerSet.Labels, LabelKeyRunnerTemplateHash, templateHash),
Annotations: map[string]string{ Annotations: map[string]string{
SyncTimeAnnotationKey: time.Now().Format(time.RFC3339), SyncTimeAnnotationKey: time.Now().Format(time.RFC3339),
}, },

View File

@@ -23,7 +23,7 @@ const (
func syncVolumes(ctx context.Context, c client.Client, log logr.Logger, ns string, runnerSet *v1alpha1.RunnerSet, statefulsets []appsv1.StatefulSet) (*ctrl.Result, error) { func syncVolumes(ctx context.Context, c client.Client, log logr.Logger, ns string, runnerSet *v1alpha1.RunnerSet, statefulsets []appsv1.StatefulSet) (*ctrl.Result, error) {
log = log.WithValues("ns", ns) log = log.WithValues("ns", ns)
for _, t := range runnerSet.Spec.StatefulSetSpec.VolumeClaimTemplates { for _, t := range runnerSet.Spec.VolumeClaimTemplates {
for _, sts := range statefulsets { for _, sts := range statefulsets {
pvcName := fmt.Sprintf("%s-%s-0", t.Name, sts.Name) pvcName := fmt.Sprintf("%s-%s-0", t.Name, sts.Name)

View File

@@ -16,7 +16,7 @@ type testResourceReader struct {
} }
func (r *testResourceReader) Get(_ context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { func (r *testResourceReader) Get(_ context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error {
nsName := types.NamespacedName{Namespace: key.Namespace, Name: key.Name} nsName := types.NamespacedName(key)
ret, ok := r.objects[nsName] ret, ok := r.objects[nsName]
if !ok { if !ok {
return &kerrors.StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonNotFound}} return &kerrors.StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonNotFound}}

View File

@@ -64,22 +64,22 @@ func Test_workVolumeClaimTemplateVolumeV1VolumeTransformation(t *testing.T) {
t.Errorf("want name %q, got %q\n", want.Name, got.Name) t.Errorf("want name %q, got %q\n", want.Name, got.Name)
} }
if got.VolumeSource.Ephemeral == nil { if got.Ephemeral == nil {
t.Fatal("work volume claim template should transform itself into Ephemeral volume source\n") t.Fatal("work volume claim template should transform itself into Ephemeral volume source\n")
} }
if got.VolumeSource.Ephemeral.VolumeClaimTemplate == nil { if got.Ephemeral.VolumeClaimTemplate == nil {
t.Fatal("work volume claim template should have ephemeral volume claim template set\n") t.Fatal("work volume claim template should have ephemeral volume claim template set\n")
} }
gotClassName := *got.VolumeSource.Ephemeral.VolumeClaimTemplate.Spec.StorageClassName gotClassName := *got.Ephemeral.VolumeClaimTemplate.Spec.StorageClassName
wantClassName := *want.VolumeSource.Ephemeral.VolumeClaimTemplate.Spec.StorageClassName wantClassName := *want.Ephemeral.VolumeClaimTemplate.Spec.StorageClassName
if gotClassName != wantClassName { if gotClassName != wantClassName {
t.Errorf("expected storage class name %q, got %q\n", wantClassName, gotClassName) t.Errorf("expected storage class name %q, got %q\n", wantClassName, gotClassName)
} }
gotAccessModes := got.VolumeSource.Ephemeral.VolumeClaimTemplate.Spec.AccessModes gotAccessModes := got.Ephemeral.VolumeClaimTemplate.Spec.AccessModes
wantAccessModes := want.VolumeSource.Ephemeral.VolumeClaimTemplate.Spec.AccessModes wantAccessModes := want.Ephemeral.VolumeClaimTemplate.Spec.AccessModes
if len(gotAccessModes) != len(wantAccessModes) { if len(gotAccessModes) != len(wantAccessModes) {
t.Fatalf("access modes lengths missmatch: got %v, expected %v\n", gotAccessModes, wantAccessModes) t.Fatalf("access modes lengths missmatch: got %v, expected %v\n", gotAccessModes, wantAccessModes)
} }

View File

@@ -43,6 +43,37 @@ You can follow [this troubleshooting guide](https://docs.github.com/en/actions/h
## Changelog ## Changelog
### 0.12.1
1. Fix indentation of startupProbe attributes in dind sidecar [#4126](https://github.com/actions/actions-runner-controller/pull/4126)
1. Remove duplicate float64 call [#4139](https://github.com/actions/actions-runner-controller/pull/4139)
1. Fix dind sidecar template [#4128](https://github.com/actions/actions-runner-controller/pull/4128)
1. Remove check if runner exists after exit code 0 [#4142](https://github.com/actions/actions-runner-controller/pull/4142)
1. Explicitly requeue during backoff ephemeral runner [#4152](https://github.com/actions/actions-runner-controller/pull/4152)
### 0.12.0
1. Allow use of client id as an app id [#4057](https://github.com/actions/actions-runner-controller/pull/4057)
1. Relax version requirements to allow patch version mismatch [#4080](https://github.com/actions/actions-runner-controller/pull/4080)
1. Refactor resource naming removing unnecessary calculations [#4076](https://github.com/actions/actions-runner-controller/pull/4076)
1. Fix busy runners metric [#4016](https://github.com/actions/actions-runner-controller/pull/4016)
1. Include more context to errors raised by github/actions client [#4032](https://github.com/actions/actions-runner-controller/pull/4032)
1. Revised dashboard [#4022](https://github.com/actions/actions-runner-controller/pull/4022)
1. feat(helm): move dind to sidecar [#3842](https://github.com/actions/actions-runner-controller/pull/3842)
1. Pin third party actions [#3981](https://github.com/actions/actions-runner-controller/pull/3981)
1. Fix docker lint warnings [#4074](https://github.com/actions/actions-runner-controller/pull/4074)
1. Bump the gomod group across 1 directory with 7 updates [#4008](https://github.com/actions/actions-runner-controller/pull/4008)
1. Bump go version [#4075](https://github.com/actions/actions-runner-controller/pull/4075)
1. Add job_workflow_ref label to listener metrics [#4054](https://github.com/actions/actions-runner-controller/pull/4054)
1. Bump github.com/cloudflare/circl from 1.6.0 to 1.6.1 [#4118](https://github.com/actions/actions-runner-controller/pull/4118)
1. Avoid nil point when config.Metrics is nil and expose all metrics if none are configured [#4101](https://github.com/actions/actions-runner-controller/pull/4101)
1. Bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2 [#4120](https://github.com/actions/actions-runner-controller/pull/4120)
1. Add startup probe to dind side-car [#4117](https://github.com/actions/actions-runner-controller/pull/4117)
1. Delete config secret when listener pod gets deleted [#4033](https://github.com/actions/actions-runner-controller/pull/4033)
1. Add response body to error when fetching access token [#4005](https://github.com/actions/actions-runner-controller/pull/4005)
1. Azure Key Vault integration to resolve secrets [#4090](https://github.com/actions/actions-runner-controller/pull/4090)
1. Create backoff mechanism for failed runners and allow re-creation of failed ephemeral runners [#4059](https://github.com/actions/actions-runner-controller/pull/4059)
### 0.11.0 ### 0.11.0
1. Add events role permission to leader_election_role [#3988](https://github.com/actions/actions-runner-controller/pull/3988) 1. Add events role permission to leader_election_role [#3988](https://github.com/actions/actions-runner-controller/pull/3988)

View File

@@ -1,6 +1,11 @@
# Visualizing Autoscaling Runner Scale Set metrics with Grafana # Visualizing Autoscaling Runner Scale Set metrics with Grafana
With metrics introduced in [gha-runner-scale-set-0.5.0](https://github.com/actions/actions-runner-controller/releases/tag/gha-runner-scale-set-0.5.0), you can now visualize the autoscaling behavior of your runner scale set with your tool of choice. This sample shows how to visualize the metrics with [Grafana](https://grafana.com/). With the metrics support introduced in [gha-runner-scale-set-0.5.0](https://github.com/actions/actions-runner-controller/releases/tag/gha-runner-scale-set-0.5.0), you can visualize the autoscaling behavior of your runner scale set with your tool of choice.
This sample dashboard shows how to visualize the metrics with [Grafana](https://grafana.com/).
> [!NOTE]
> We do not intend to provide a supported ARC dashboard. This is simply a reference and a demonstration for how you could leverage the metrics emitted by the controller-manager and listeners to visualize the autoscaling behavior of your runner scale set. We offer no promises of future upgrades to this sample.
## Demo ## Demo
@@ -8,12 +13,43 @@ With metrics introduced in [gha-runner-scale-set-0.5.0](https://github.com/actio
## Setup ## Setup
We do not intend to provide a supported ARC dashboard. This is simply a reference and a demonstration for how you could leverage the metrics emitted by the controller-manager and listeners to visualize the autoscaling behavior of your runner scale set. We offer no promises of future upgrades to this sample.
1. Make sure to have [Grafana](https://grafana.com/docs/grafana/latest/installation/) and [Prometheus](https://prometheus.io/docs/prometheus/latest/installation/) running in your cluster. 1. Make sure to have [Grafana](https://grafana.com/docs/grafana/latest/installation/) and [Prometheus](https://prometheus.io/docs/prometheus/latest/installation/) running in your cluster.
2. Make sure that Prometheus is properly scraping the metrics endpoints of the controller-manager and listeners. 2. Make sure that Prometheus is properly scraping the metrics endpoints of the controller-manager and listeners.
3. Import the [dashboard](ARC-Autoscaling-Runner-Set-Monitoring_1692627561838.json) into Grafana. 3. Import the [dashboard](ARC-Autoscaling-Runner-Set-Monitoring_1692627561838.json) into Grafana.
## Required metrics
This sample relies on the suggestion listener metrics configuration in the scale set [values.yaml](https://github.com/actions/actions-runner-controller/blob/ea27448da51385470b1ce67150aa695cfa45fd3f/charts/gha-runner-scale-set/values.yaml#L129-L270).
The following metrics are required to be scraped by Prometheus in order to populate the dashboard:
| Metric | Required labels | Source |
| ------ | ----------- | -----|
| container_fs_writes_bytes_total | namespace | cAdvisor
| container_fs_reads_bytes_total | namespace | cAdvisor
| container_memory_working_set_bytes | namespace | cAdvisor
| controller_runtime_active_workers | controller | ARC Controller
| controller_runtime_reconcile_time_seconds_sum | namespace | ARC Controller
| controller_runtime_reconcile_errors_total | namespace | ARC Controller
| gha_assigned_jobs | actions_github_com_scale_set_name, namespace | ARC Controller
| gha_controller_failed_ephemeral_runners | name, namespace | ARC Controller
| gha_controller_pending_ephemeral_runners | name, namespace | ARC Controller
| gha_controller_running_ephemeral_runners | name, namespace | ARC Controller
| gha_controller_running_listeners | namespace | ARC Controller
| gha_desired_runners | actions_github_com_scale_set_name, namespace | ARC Listener
| gha_idle_runners | actions_github_com_scale_set_name, namespace | ARC Listener
| gha_job_execution_duration_seconds_bucket | actions_github_com_scale_set_name, actions_github_com_scale_set_namespace | ARC Listener
| gha_job_startup_duration_seconds_bucket | actions_github_com_scale_set_name, actions_github_com_scale_set_namespace | ARC Listener
| gha_registered_runners | actions_github_com_scale_set_name, namespace | ARC Listener
| gha_running_jobs | actions_github_com_scale_set_name, actions_github_com_scale_set_namespace | ARC Listener
| kube_pod_container_status_ready | namespace | kube-state-metrics
| kube_pod_container_status_terminated_reason | namespace, reason | kube-state-metrics
| kube_pod_container_status_waiting | namespace | kube-state-metrics
| rest_client_requests_total | code, method, namespace | ARC Controller
| scrape_duration_seconds | | prometheus
| workqueue_depth | name, namespace | ARC Controller
| workqueue_queue_duration_seconds_sum | namespace | ARC Controller
## Details ## Details
This dashboard demonstrates some of the metrics provided by ARC and the underlying Kubernetes runtime. It provides a sample visualization of the behavior of the runner scale set, the ARC controllers, and the listeners. This should not be considered a comprehensive dashboard; it is a starting point that can be used with other metrics and logs to understand the health of the cluster. Review the [GitHub documentation detailing the Actions Runner Controller metrics and how to enable them](https://docs.github.com/en/enterprise-server@3.10/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#enabling-metrics). This dashboard demonstrates some of the metrics provided by ARC and the underlying Kubernetes runtime. It provides a sample visualization of the behavior of the runner scale set, the ARC controllers, and the listeners. This should not be considered a comprehensive dashboard; it is a starting point that can be used with other metrics and logs to understand the health of the cluster. Review the [GitHub documentation detailing the Actions Runner Controller metrics and how to enable them](https://docs.github.com/en/enterprise-server@3.10/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#enabling-metrics).
@@ -22,16 +58,25 @@ The dashboard includes the following metrics:
| Label | Description | | Label | Description |
| -------------------------------- | ----------------------------------------------------| | -------------------------------- | ----------------------------------------------------|
| Active listeners | The number of listeners currently running and attempting to manage jobs for the scale set. This should match the number of scale sets deployed. | | Startup Duration | Heat map of the wait time before a job starts, with the colors indicating the increase in the number of jobs in that time bucket. An increasing time can indicate that the cluster is resource constrained and may need additional nodes or resources to handle the load. |
| Runner States | Displays the number of runners in a given state. The finished and deleted states are not included in this panel. | | Execution Duration | Heat map of the execution time for a job, with the colors indicating the increase in the number of jobs in that time bucket. Time can be affected by the number of steps in the job, the allocated CPU, and whether there is resource contention on the node that is impacting performance |
| Failed (total) | The total number of ephemeral runners that have failed to properly start. This may require reviewing the custom resource and logs to identify and resolve the root causes. Common causes include resource issues and failure to pull the required image. | | Assigned Jobs | The number of jobs that have been assigned to the listener. This is the number of jobs that the listener is responsible for providing a runner to process. |
| Pending (total) | The total number of ephemeral runners that ARC has requested and is waiting for Kubernetes to provide in a running state. If the Kubernetes API server is responsive, this will typically match the number of runner pods that are in a pending state. This number includes requests for runner pods that have not yet been scheduled. When this number is higher than the number of runner pods in a pending state, it can indicate performance issues with the API server and resource contention. | | Desired Runners | The number of runners that the listener is requesting from the controller. This is the number of runners required to process the assigned jobs and provide idle runners. It is limited by the configured maximum runner count for the scale set. |
| Idle (total) | The total number of ephemeral runners that are available to accept jobs across all scale sets. Keeping a pool of idle runners can enable a faster start time under load, but excessive idle runners will consume resources and can prevent nodes from scaling down. | | Idle Runners | The total number of ephemeral runners that are available to accept jobs across all selected scale sets. Keeping a pool of idle runners can enable a faster start time under load, but excessive idle runners will consume resources and can prevent nodes from scaling down. |
| Total assigned jobs per listener | The number of workflow jobs acquired and assigned to the listener. The listener must provide supporting runners to complete these jobs. Once jobs are assigned, they cannot be delegated to other listeners and must be processed by the scale set or cancelled. | | Running Jobs | The number of runners that are currently processing jobs. |
| Assigned vs running jobs | Compares the number of jobs assigned against the number of runners that are currently processing jobs. When running jobs is less than assigned jobs, it can indicate that ARC is waiting on Kubernetes to provide and start additional runners. | | Failed Runners | The total number of ephemeral runners that have failed to properly start. This may require reviewing the custom resource and logs to identify and resolve the root causes. Common causes include resource issues and failure to pull the required image. |
| Average startup duration | The average time in seconds between when jobs are assigned and when a runner accepts the job and begins processing. An increasing duration can indicate that the cluster has resource contention or a lack of available nodes for scheduling jobs | | Listeners | The number of listeners currently running and attempting to manage jobs for the scale set. This should match the number of scale sets deployed. |
| Average execution duration | The average time in seconds that runners are taking to complete a job. Changes in this value reflect the efficiency of workflow jobs and the pod configuration. If the value is decreasing without changes to the job, it can indicate resource contention or CPU throttling. | | Pending Runners | The total number of ephemeral runners that ARC has requested and is waiting for Kubernetes to provide in a running state. If the Kubernetes API server is responsive, this will typically match the number of runner pods that are in a pending state. This number includes requests for runner pods that have not yet been scheduled. When this number is higher than the number of runner pods in a pending state, it can indicate performance issues. |
| Registered Runners | The total number of ephemeral runners that have been successfully registered. |
| Active Runners | The total number of runners that are active and either available or processing jobs. |
| Out of Memory | The number of containers that have been terminated by the OOMKiller. This can indicate that the requests/ limits for one or more pods on the node were configured improperly, allowing pods to request more memory than the node had available. |
| Peak Container Memory | The maximum amount of memory used by any container in a given namespace during the selected time. This can be used for tuning the memory limits for the pods and for alerts as containers get close to their limits.
| Container I/O | Shows the number of bytes read and written to the container filesystem. This can be used to identify if the container is reading or writing a large amount of data to the filesystem, which can impact performance. |
| Container Pod Status | Shows the number of containers in each status (waiting, running, terminated, ready). This can be used to identify if there are a large number of containers that are failing to start or are in a waiting state. |
| Reconcile time | The time to perform a single reconciliation task from a controller's work queue. This metric reflects the time it takes for ARC to complete each step in the processing of creating, managing, and cleaning up runners. As this increases, it can indicate resource contention, processing delays, or delays from the API server. |
| Workqueue Queue Duration | The time items spent in the work queue for a controller before being processed. This is often related to the work queue depth; as the number of items increases, it can take an increasing amount of time for an item to be processed. |
| Reconciliation errors | Reconciliation is the process of a controller ensuring the desired state and actual state of the resources match. Each time an event occurs on a resource watched by the controller, the controller is required to indicate if the new state matches the desired state. Kubernetes adds a task to the work queue for the controller to perform this reconciliation. Errors indicate that controller has not achieved a desired state and is requesting Kubernetes to queue another request for reconciliation. Ideally, this number remains close to zero. An increasing number can indicate resource contention or delays processing API server requests. This reflects Kubernetes resources that ARC is waiting to be provided or in the necessary state. As a concrete example, ARC will request the creation of a secret prior to creating the pod. If the response indicates the secret is not immediately ready, ARC will requeue the reconciliation task with the error details, incrementing this count. | | Reconciliation errors | Reconciliation is the process of a controller ensuring the desired state and actual state of the resources match. Each time an event occurs on a resource watched by the controller, the controller is required to indicate if the new state matches the desired state. Kubernetes adds a task to the work queue for the controller to perform this reconciliation. Errors indicate that controller has not achieved a desired state and is requesting Kubernetes to queue another request for reconciliation. Ideally, this number remains close to zero. An increasing number can indicate resource contention or delays processing API server requests. This reflects Kubernetes resources that ARC is waiting to be provided or in the necessary state. As a concrete example, ARC will request the creation of a secret prior to creating the pod. If the response indicates the secret is not immediately ready, ARC will requeue the reconciliation task with the error details, incrementing this count. |
| Reconciliation time | A histogram reflecting the time in seconds to perform a single reconciliation task from the controller's work queue. A histogram counts the number of requests that are processed within a given bucket of time. This metric reflects the time it takes for ARC to complete each step in the processing of creating, managing, and cleaning up runners. As this increases, it can indicate resource contention or processing delays within Kubernetes or the API server. This displays shows an average, which may hide larger or smaller times that are occurring in the processing. | | Workqueue depth | The number of tasks that Kubernetes has queued for the ARC controllers to process. This includes reconciliation requests and tasks initiated by the controller. Managing a runner requires multiple steps to prepare, create, update, and delete the runner, its resources, and the ARC custom resources. As each step is completed (or trigger reconciliation), new tasks are queued for processing. The controller will then use one or more workers to process these tasks in the order they were queued. As the depth increases, it indicates more tasks awaiting time from the controller. Growth indicates increasing work and may reflect Kubernetes resource contention or processing latencies. Each request for a new runner will result in multiple tasks being added to the work queue to prepare and create the runner and the related ARC custom resources. |
| Workqueue depth | The number of tasks that Kubernetes queued for the ARC controllers to process. This includes reconciliation requests and tasks from ARC. ARC sequentially processes a work queue of single, small task to avoid concurrency issues. Managing a runner requires multiple steps to prepare, create, update, and delete the runner, its resources, and the ARC custom resources. As each step is completed (or trigger reconciliation), new tasks are queued for processing. As the depth increases, it indicates more tasks awaiting time from the controller. Growth indicates increasing work and may indicate Kubernetes resource contention or processing latencies. Each request for a new runner will result in multiple tasks being added to the work queue to prepare and create the runner and the related ARC custom resources. | | Active Workers | The number of workers that are actively processing tasks in the work queue. If the queue is empty, then there may be no workers required to process the tasks. The number of workers for the ephemeral runner is configurable in the scale set values file. |
| API Calls | Shows the number of calls to the API server by status code and HTTP method. The method indicates the type of activity being performed, while the status code indicates the result of the activity. Error codes of 500 and above often indicate a Kubernetes issue. |
| Scrape Duration (seconds) | The amount of time required for Prometheus to read the configured metrics from components in the cluster. An increasing number may indicate a lack of resources for Prometheus and a risk of the process exceeding the configured timeout, leading to lost metrics data. | | Scrape Duration (seconds) | The amount of time required for Prometheus to read the configured metrics from components in the cluster. An increasing number may indicate a lack of resources for Prometheus and a risk of the process exceeding the configured timeout, leading to lost metrics data. |

View File

@@ -10,6 +10,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"maps"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url" "net/url"
@@ -273,16 +274,16 @@ func (c *Client) Identifier() string {
func (c *Client) Do(req *http.Request) (*http.Response, error) { func (c *Client) Do(req *http.Request) (*http.Response, error) {
resp, err := c.Client.Do(req) resp, err := c.Client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("client request failed: %w", err)
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to read the response body: %w", err)
} }
err = resp.Body.Close() err = resp.Body.Close()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to close the response body: %w", err)
} }
body = trimByteOrderMark(body) body = trimByteOrderMark(body)
@@ -294,7 +295,7 @@ func (c *Client) NewGitHubAPIRequest(ctx context.Context, method, path string, b
u := c.config.GitHubAPIURL(path) u := c.config.GitHubAPIURL(path)
req, err := http.NewRequestWithContext(ctx, method, u.String(), body) req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new GitHub API request: %w", err)
} }
req.Header.Set("User-Agent", c.userAgent.String()) req.Header.Set("User-Agent", c.userAgent.String())
@@ -305,28 +306,27 @@ func (c *Client) NewGitHubAPIRequest(ctx context.Context, method, path string, b
func (c *Client) NewActionsServiceRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { func (c *Client) NewActionsServiceRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
err := c.updateTokenIfNeeded(ctx) err := c.updateTokenIfNeeded(ctx)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue update token if needed: %w", err)
} }
parsedPath, err := url.Parse(path) parsedPath, err := url.Parse(path)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to parse path %q: %w", path, err)
} }
urlString, err := url.JoinPath(c.ActionsServiceURL, parsedPath.Path) urlString, err := url.JoinPath(c.ActionsServiceURL, parsedPath.Path)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to join path (actions_service_url=%q, parsedPath=%q): %w", c.ActionsServiceURL, parsedPath.Path, err)
} }
u, err := url.Parse(urlString) u, err := url.Parse(urlString)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to parse url string %q: %w", urlString, err)
} }
q := u.Query() q := u.Query()
for k, v := range parsedPath.Query() { maps.Copy(q, parsedPath.Query())
q[k] = v
}
if q.Get("api-version") == "" { if q.Get("api-version") == "" {
q.Set("api-version", "6.0-preview") q.Set("api-version", "6.0-preview")
} }
@@ -334,7 +334,7 @@ func (c *Client) NewActionsServiceRequest(ctx context.Context, method, path stri
req, err := http.NewRequestWithContext(ctx, method, u.String(), body) req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new request with context: %w", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
@@ -348,12 +348,12 @@ func (c *Client) GetRunnerScaleSet(ctx context.Context, runnerGroupId int, runne
path := fmt.Sprintf("/%s?runnerGroupId=%d&name=%s", scaleSetEndpoint, runnerGroupId, runnerScaleSetName) path := fmt.Sprintf("/%s?runnerGroupId=%d&name=%s", scaleSetEndpoint, runnerGroupId, runnerScaleSetName)
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil) req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -386,12 +386,12 @@ func (c *Client) GetRunnerScaleSetById(ctx context.Context, runnerScaleSetId int
path := fmt.Sprintf("/%s/%d", scaleSetEndpoint, runnerScaleSetId) path := fmt.Sprintf("/%s/%d", scaleSetEndpoint, runnerScaleSetId)
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil) req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -413,12 +413,12 @@ func (c *Client) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (
path := fmt.Sprintf("/_apis/runtime/runnergroups/?groupName=%s", runnerGroup) path := fmt.Sprintf("/_apis/runtime/runnergroups/?groupName=%s", runnerGroup)
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil) req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -469,17 +469,17 @@ func (c *Client) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (
func (c *Client) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error) { func (c *Client) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error) {
body, err := json.Marshal(runnerScaleSet) body, err := json.Marshal(runnerScaleSet)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to marshal runner scale set: %w", err)
} }
req, err := c.NewActionsServiceRequest(ctx, http.MethodPost, scaleSetEndpoint, bytes.NewReader(body)) req, err := c.NewActionsServiceRequest(ctx, http.MethodPost, scaleSetEndpoint, bytes.NewReader(body))
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -501,17 +501,17 @@ func (c *Client) UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetId int,
body, err := json.Marshal(runnerScaleSet) body, err := json.Marshal(runnerScaleSet)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to marshal runner scale set: %w", err)
} }
req, err := c.NewActionsServiceRequest(ctx, http.MethodPatch, path, bytes.NewReader(body)) req, err := c.NewActionsServiceRequest(ctx, http.MethodPatch, path, bytes.NewReader(body))
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -533,12 +533,12 @@ func (c *Client) DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetId int)
path := fmt.Sprintf("/%s/%d", scaleSetEndpoint, runnerScaleSetId) path := fmt.Sprintf("/%s/%d", scaleSetEndpoint, runnerScaleSetId)
req, err := c.NewActionsServiceRequest(ctx, http.MethodDelete, path, nil) req, err := c.NewActionsServiceRequest(ctx, http.MethodDelete, path, nil)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusNoContent { if resp.StatusCode != http.StatusNoContent {
@@ -552,7 +552,7 @@ func (c *Client) DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetId int)
func (c *Client) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, lastMessageId int64, maxCapacity int) (*RunnerScaleSetMessage, error) { func (c *Client) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, lastMessageId int64, maxCapacity int) (*RunnerScaleSetMessage, error) {
u, err := url.Parse(messageQueueUrl) u, err := url.Parse(messageQueueUrl)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to parse message queue url: %w", err)
} }
if lastMessageId > 0 { if lastMessageId > 0 {
@@ -567,7 +567,7 @@ func (c *Client) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAc
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new request with context: %w", err)
} }
req.Header.Set("Accept", "application/json; api-version=6.0-preview") req.Header.Set("Accept", "application/json; api-version=6.0-preview")
@@ -577,7 +577,7 @@ func (c *Client) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAc
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode == http.StatusAccepted { if resp.StatusCode == http.StatusAccepted {
@@ -621,14 +621,14 @@ func (c *Client) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAc
func (c *Client) DeleteMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, messageId int64) error { func (c *Client) DeleteMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, messageId int64) error {
u, err := url.Parse(messageQueueUrl) u, err := url.Parse(messageQueueUrl)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to parse message queue url: %w", err)
} }
u.Path = fmt.Sprintf("%s/%d", u.Path, messageId) u.Path = fmt.Sprintf("%s/%d", u.Path, messageId)
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u.String(), nil) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u.String(), nil)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to create new request with context: %w", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
@@ -637,7 +637,7 @@ func (c *Client) DeleteMessage(ctx context.Context, messageQueueUrl, messageQueu
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusNoContent { if resp.StatusCode != http.StatusNoContent {
@@ -673,14 +673,16 @@ func (c *Client) CreateMessageSession(ctx context.Context, runnerScaleSetId int,
requestData, err := json.Marshal(newSession) requestData, err := json.Marshal(newSession)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to marshal new session: %w", err)
} }
createdSession := &RunnerScaleSetSession{} createdSession := &RunnerScaleSetSession{}
err = c.doSessionRequest(ctx, http.MethodPost, path, bytes.NewBuffer(requestData), http.StatusOK, createdSession) if err = c.doSessionRequest(ctx, http.MethodPost, path, bytes.NewBuffer(requestData), http.StatusOK, createdSession); err != nil {
return nil, fmt.Errorf("failed to do the session request: %w", err)
}
return createdSession, err return createdSession, nil
} }
func (c *Client) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error { func (c *Client) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error {
@@ -691,19 +693,21 @@ func (c *Client) DeleteMessageSession(ctx context.Context, runnerScaleSetId int,
func (c *Client) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*RunnerScaleSetSession, error) { func (c *Client) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*RunnerScaleSetSession, error) {
path := fmt.Sprintf("/%s/%d/sessions/%s", scaleSetEndpoint, runnerScaleSetId, sessionId.String()) path := fmt.Sprintf("/%s/%d/sessions/%s", scaleSetEndpoint, runnerScaleSetId, sessionId.String())
refreshedSession := &RunnerScaleSetSession{} refreshedSession := &RunnerScaleSetSession{}
err := c.doSessionRequest(ctx, http.MethodPatch, path, nil, http.StatusOK, refreshedSession) if err := c.doSessionRequest(ctx, http.MethodPatch, path, nil, http.StatusOK, refreshedSession); err != nil {
return refreshedSession, err return nil, fmt.Errorf("failed to do the session request: %w", err)
}
return refreshedSession, nil
} }
func (c *Client) doSessionRequest(ctx context.Context, method, path string, requestData io.Reader, expectedResponseStatusCode int, responseUnmarshalTarget any) error { func (c *Client) doSessionRequest(ctx context.Context, method, path string, requestData io.Reader, expectedResponseStatusCode int, responseUnmarshalTarget any) error {
req, err := c.NewActionsServiceRequest(ctx, method, path, requestData) req, err := c.NewActionsServiceRequest(ctx, method, path, requestData)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode == expectedResponseStatusCode { if resp.StatusCode == expectedResponseStatusCode {
@@ -749,12 +753,12 @@ func (c *Client) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQ
body, err := json.Marshal(requestIds) body, err := json.Marshal(requestIds)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to marshal request ids: %w", err)
} }
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewBuffer(body)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewBuffer(body))
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new request with context: %w", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
@@ -763,7 +767,7 @@ func (c *Client) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQ
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -807,12 +811,12 @@ func (c *Client) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil) req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode == http.StatusNoContent { if resp.StatusCode == http.StatusNoContent {
@@ -842,17 +846,17 @@ func (c *Client) GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *
body, err := json.Marshal(jitRunnerSetting) body, err := json.Marshal(jitRunnerSetting)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to marshal runner settings: %w", err)
} }
req, err := c.NewActionsServiceRequest(ctx, http.MethodPost, path, bytes.NewBuffer(body)) req, err := c.NewActionsServiceRequest(ctx, http.MethodPost, path, bytes.NewBuffer(body))
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -875,12 +879,12 @@ func (c *Client) GetRunner(ctx context.Context, runnerId int64) (*RunnerReferenc
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil) req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -904,12 +908,12 @@ func (c *Client) GetRunnerByName(ctx context.Context, runnerName string) (*Runne
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil) req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@@ -945,12 +949,12 @@ func (c *Client) RemoveRunner(ctx context.Context, runnerId int64) error {
req, err := c.NewActionsServiceRequest(ctx, http.MethodDelete, path, nil) req, err := c.NewActionsServiceRequest(ctx, http.MethodDelete, path, nil)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to create new actions service request: %w", err)
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to issue the request: %w", err)
} }
if resp.StatusCode != http.StatusNoContent { if resp.StatusCode != http.StatusNoContent {
@@ -969,13 +973,13 @@ type registrationToken struct {
func (c *Client) getRunnerRegistrationToken(ctx context.Context) (*registrationToken, error) { func (c *Client) getRunnerRegistrationToken(ctx context.Context) (*registrationToken, error) {
path, err := createRegistrationTokenPath(c.config) path, err := createRegistrationTokenPath(c.config)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create registration token path: %w", err)
} }
var buf bytes.Buffer var buf bytes.Buffer
req, err := c.NewGitHubAPIRequest(ctx, http.MethodPost, path, &buf) req, err := c.NewGitHubAPIRequest(ctx, http.MethodPost, path, &buf)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new GitHub API request: %w", err)
} }
bearerToken := "" bearerToken := ""
@@ -985,7 +989,7 @@ func (c *Client) getRunnerRegistrationToken(ctx context.Context) (*registrationT
} else { } else {
accessToken, err := c.fetchAccessToken(ctx, c.config.ConfigURL.String(), c.creds.AppCreds) accessToken, err := c.fetchAccessToken(ctx, c.config.ConfigURL.String(), c.creds.AppCreds)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to fetch access token: %w", err)
} }
bearerToken = fmt.Sprintf("Bearer %v", accessToken.Token) bearerToken = fmt.Sprintf("Bearer %v", accessToken.Token)
@@ -998,14 +1002,14 @@ func (c *Client) getRunnerRegistrationToken(ctx context.Context) (*registrationT
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated { if resp.StatusCode != http.StatusCreated {
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to read the body: %w", err)
} }
return nil, &GitHubAPIError{ return nil, &GitHubAPIError{
StatusCode: resp.StatusCode, StatusCode: resp.StatusCode,
@@ -1035,13 +1039,13 @@ type accessToken struct {
func (c *Client) fetchAccessToken(ctx context.Context, gitHubConfigURL string, creds *GitHubAppAuth) (*accessToken, error) { func (c *Client) fetchAccessToken(ctx context.Context, gitHubConfigURL string, creds *GitHubAppAuth) (*accessToken, error) {
accessTokenJWT, err := createJWTForGitHubApp(creds) accessTokenJWT, err := createJWTForGitHubApp(creds)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create JWT for GitHub app: %w", err)
} }
path := fmt.Sprintf("/app/installations/%v/access_tokens", creds.AppInstallationID) path := fmt.Sprintf("/app/installations/%v/access_tokens", creds.AppInstallationID)
req, err := c.NewGitHubAPIRequest(ctx, http.MethodPost, path, nil) req, err := c.NewGitHubAPIRequest(ctx, http.MethodPost, path, nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new GitHub API request: %w", err)
} }
req.Header.Set("Content-Type", "application/vnd.github+json") req.Header.Set("Content-Type", "application/vnd.github+json")
@@ -1051,15 +1055,20 @@ func (c *Client) fetchAccessToken(ctx context.Context, gitHubConfigURL string, c
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated { if resp.StatusCode != http.StatusCreated {
errMsg := fmt.Sprintf("failed to get access token for GitHub App auth (%v)", resp.Status)
if body, err := io.ReadAll(resp.Body); err == nil {
errMsg = fmt.Sprintf("%s: %s", errMsg, string(body))
}
return nil, &GitHubAPIError{ return nil, &GitHubAPIError{
StatusCode: resp.StatusCode, StatusCode: resp.StatusCode,
RequestID: resp.Header.Get(HeaderGitHubRequestID), RequestID: resp.Header.Get(HeaderGitHubRequestID),
Err: fmt.Errorf("failed to get access token for GitHub App auth: %v", resp.Status), Err: errors.New(errMsg),
} }
} }
@@ -1096,12 +1105,12 @@ func (c *Client) getActionsServiceAdminConnection(ctx context.Context, rt *regis
enc.SetEscapeHTML(false) enc.SetEscapeHTML(false)
if err := enc.Encode(body); err != nil { if err := enc.Encode(body); err != nil {
return nil, err return nil, fmt.Errorf("failed to encode body: %w", err)
} }
req, err := c.NewGitHubAPIRequest(ctx, http.MethodPost, path, buf) req, err := c.NewGitHubAPIRequest(ctx, http.MethodPost, path, buf)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to create new GitHub API request: %w", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
@@ -1115,7 +1124,7 @@ func (c *Client) getActionsServiceAdminConnection(ctx context.Context, rt *regis
var err error var err error
resp, err = c.Do(req) resp, err = c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to issue the request: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -1208,14 +1217,14 @@ func createJWTForGitHubApp(appAuth *GitHubAppAuth) (string, error) {
claims := &jwt.RegisteredClaims{ claims := &jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(issuedAt), IssuedAt: jwt.NewNumericDate(issuedAt),
ExpiresAt: jwt.NewNumericDate(expiresAt), ExpiresAt: jwt.NewNumericDate(expiresAt),
Issuer: strconv.FormatInt(appAuth.AppID, 10), Issuer: appAuth.AppID,
} }
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(appAuth.AppPrivateKey)) privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(appAuth.AppPrivateKey))
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("failed to parse RSA private key from PEM: %w", err)
} }
return token.SignedString(privateKey) return token.SignedString(privateKey)

View File

@@ -54,7 +54,7 @@ func TestAcquireJobs(t *testing.T) {
RunnerScaleSet: &actions.RunnerScaleSet{Id: 1}, RunnerScaleSet: &actions.RunnerScaleSet{Id: 1},
MessageQueueAccessToken: "abc", MessageQueueAccessToken: "abc",
} }
var requestIDs []int64 = []int64{1} var requestIDs = []int64{1}
retryMax := 1 retryMax := 1
actualRetry := 0 actualRetry := 0

View File

@@ -101,8 +101,7 @@ func TestCreateMessageSession(t *testing.T) {
err, err,
) )
gotErr := err.(*actions.ActionsError) assert.Equal(t, want, errorTypeForComparison)
assert.Equal(t, want, gotErr)
}) })
t.Run("CreateMessageSession call is retried the correct amount of times", func(t *testing.T) { t.Run("CreateMessageSession call is retried the correct amount of times", func(t *testing.T) {

View File

@@ -67,7 +67,7 @@ func TestGetRunnerByName(t *testing.T) {
t.Run("Get Runner by Name", func(t *testing.T) { t.Run("Get Runner by Name", func(t *testing.T) {
var runnerID int64 = 1 var runnerID int64 = 1
var runnerName string = "self-hosted-ubuntu" var runnerName = "self-hosted-ubuntu"
want := &actions.RunnerReference{ want := &actions.RunnerReference{
Id: int(runnerID), Id: int(runnerID),
Name: runnerName, Name: runnerName,
@@ -87,7 +87,7 @@ func TestGetRunnerByName(t *testing.T) {
}) })
t.Run("Get Runner by name with not exist runner", func(t *testing.T) { t.Run("Get Runner by name with not exist runner", func(t *testing.T) {
var runnerName string = "self-hosted-ubuntu" var runnerName = "self-hosted-ubuntu"
response := []byte(`{"count": 0, "value": []}`) response := []byte(`{"count": 0, "value": []}`)
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -103,7 +103,7 @@ func TestGetRunnerByName(t *testing.T) {
}) })
t.Run("Default retries on server error", func(t *testing.T) { t.Run("Default retries on server error", func(t *testing.T) {
var runnerName string = "self-hosted-ubuntu" var runnerName = "self-hosted-ubuntu"
retryWaitMax := 1 * time.Millisecond retryWaitMax := 1 * time.Millisecond
retryMax := 1 retryMax := 1
@@ -181,7 +181,7 @@ func TestGetRunnerGroupByName(t *testing.T) {
t.Run("Get RunnerGroup by Name", func(t *testing.T) { t.Run("Get RunnerGroup by Name", func(t *testing.T) {
var runnerGroupID int64 = 1 var runnerGroupID int64 = 1
var runnerGroupName string = "test-runner-group" var runnerGroupName = "test-runner-group"
want := &actions.RunnerGroup{ want := &actions.RunnerGroup{
ID: runnerGroupID, ID: runnerGroupID,
Name: runnerGroupName, Name: runnerGroupName,
@@ -201,7 +201,7 @@ func TestGetRunnerGroupByName(t *testing.T) {
}) })
t.Run("Get RunnerGroup by name with not exist runner group", func(t *testing.T) { t.Run("Get RunnerGroup by name with not exist runner group", func(t *testing.T) {
var runnerGroupName string = "test-runner-group" var runnerGroupName = "test-runner-group"
response := []byte(`{"count": 0, "value": []}`) response := []byte(`{"count": 0, "value": []}`)
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@@ -3,6 +3,7 @@ package fake
import ( import (
"context" "context"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
) )
@@ -34,10 +35,6 @@ func NewMultiClient(opts ...MultiClientOption) actions.MultiClient {
return f return f
} }
func (f *fakeMultiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds actions.ActionsAuth, namespace string, options ...actions.ClientOption) (actions.ActionsService, error) { func (f *fakeMultiClient) GetClientFor(ctx context.Context, githubConfigURL string, appConfig *appconfig.AppConfig, namespace string, options ...actions.ClientOption) (actions.ActionsService, error) {
return f.defaultClient, f.defaultErr
}
func (f *fakeMultiClient) GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData actions.KubernetesSecretData, options ...actions.ClientOption) (actions.ActionsService, error) {
return f.defaultClient, f.defaultErr return f.defaultClient, f.defaultErr
} }

View File

@@ -57,7 +57,7 @@ func TestClient_Identifier(t *testing.T) {
} }
defaultAppCreds := &actions.ActionsAuth{ defaultAppCreds := &actions.ActionsAuth{
AppCreds: &actions.GitHubAppAuth{ AppCreds: &actions.GitHubAppAuth{
AppID: 123, AppID: "123",
AppInstallationID: 123, AppInstallationID: 123,
AppPrivateKey: "private key", AppPrivateKey: "private key",
}, },
@@ -90,7 +90,7 @@ func TestClient_Identifier(t *testing.T) {
old: defaultAppCreds, old: defaultAppCreds,
new: &actions.ActionsAuth{ new: &actions.ActionsAuth{
AppCreds: &actions.GitHubAppAuth{ AppCreds: &actions.GitHubAppAuth{
AppID: 456, AppID: "456",
AppInstallationID: 456, AppInstallationID: 456,
AppPrivateKey: "new private key", AppPrivateKey: "new private key",
}, },

View File

@@ -3,15 +3,14 @@ package actions
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"sync" "sync"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/go-logr/logr" "github.com/go-logr/logr"
) )
type MultiClient interface { type MultiClient interface {
GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string, options ...ClientOption) (ActionsService, error) GetClientFor(ctx context.Context, githubConfigURL string, appConfig *appconfig.AppConfig, namespace string, options ...ClientOption) (ActionsService, error)
GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData, options ...ClientOption) (ActionsService, error)
} }
type multiClient struct { type multiClient struct {
@@ -23,7 +22,8 @@ type multiClient struct {
} }
type GitHubAppAuth struct { type GitHubAppAuth struct {
AppID int64 // AppID is the ID or the Client ID of the application
AppID string
AppInstallationID int64 AppInstallationID int64
AppPrivateKey string AppPrivateKey string
} }
@@ -49,15 +49,22 @@ func NewMultiClient(logger logr.Logger) MultiClient {
} }
} }
func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string, options ...ClientOption) (ActionsService, error) { func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string, appConfig *appconfig.AppConfig, namespace string, options ...ClientOption) (ActionsService, error) {
m.logger.Info("retrieve actions client", "githubConfigURL", githubConfigURL, "namespace", namespace) m.logger.Info("retrieve actions client", "githubConfigURL", githubConfigURL, "namespace", namespace)
if creds.Token == "" && creds.AppCreds == nil { if err := appConfig.Validate(); err != nil {
return nil, fmt.Errorf("no credentials provided. either a PAT or GitHub App credentials should be provided") return nil, fmt.Errorf("failed to validate app config: %w", err)
} }
if creds.Token != "" && creds.AppCreds != nil { var creds ActionsAuth
return nil, fmt.Errorf("both PAT and GitHub App credentials provided. should only provide one") if len(appConfig.Token) > 0 {
creds.Token = appConfig.Token
} else {
creds.AppCreds = &GitHubAppAuth{
AppID: appConfig.AppID,
AppInstallationID: appConfig.AppInstallationID,
AppPrivateKey: appConfig.AppPrivateKey,
}
} }
client, err := NewClient( client, err := NewClient(
@@ -68,7 +75,7 @@ func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string,
}, options...)..., }, options...)...,
) )
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to instantiate new client: %w", err)
} }
m.mu.Lock() m.mu.Lock()
@@ -93,47 +100,3 @@ func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string,
return client, nil return client, nil
} }
type KubernetesSecretData map[string][]byte
func (m *multiClient) GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData, options ...ClientOption) (ActionsService, error) {
if len(secretData) == 0 {
return nil, fmt.Errorf("must provide secret data with either PAT or GitHub App Auth")
}
token := string(secretData["github_token"])
hasToken := len(token) > 0
appID := string(secretData["github_app_id"])
appInstallationID := string(secretData["github_app_installation_id"])
appPrivateKey := string(secretData["github_app_private_key"])
hasGitHubAppAuth := len(appID) > 0 && len(appInstallationID) > 0 && len(appPrivateKey) > 0
if hasToken && hasGitHubAppAuth {
return nil, fmt.Errorf("must provide secret with only PAT or GitHub App Auth to avoid ambiguity in client behavior")
}
if !hasToken && !hasGitHubAppAuth {
return nil, fmt.Errorf("neither PAT nor GitHub App Auth credentials provided in secret")
}
auth := ActionsAuth{}
if hasToken {
auth.Token = token
return m.GetClientFor(ctx, githubConfigURL, auth, namespace, options...)
}
parsedAppID, err := strconv.ParseInt(appID, 10, 64)
if err != nil {
return nil, err
}
parsedAppInstallationID, err := strconv.ParseInt(appInstallationID, 10, 64)
if err != nil {
return nil, err
}
auth.AppCreds = &GitHubAppAuth{AppID: parsedAppID, AppInstallationID: parsedAppInstallationID, AppPrivateKey: appPrivateKey}
return m.GetClientFor(ctx, githubConfigURL, auth, namespace, options...)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -23,10 +24,13 @@ func TestMultiClientCaching(t *testing.T) {
defaultNamespace := "default" defaultNamespace := "default"
defaultConfigURL := "https://github.com/org/repo" defaultConfigURL := "https://github.com/org/repo"
defaultCreds := &ActionsAuth{ defaultCreds := &appconfig.AppConfig{
Token: "token", Token: "token",
} }
client, err := NewClient(defaultConfigURL, defaultCreds) defaultAuth := ActionsAuth{
Token: defaultCreds.Token,
}
client, err := NewClient(defaultConfigURL, &defaultAuth)
require.NoError(t, err) require.NoError(t, err)
multiClient.clients[ActionsClientKey{client.Identifier(), defaultNamespace}] = client multiClient.clients[ActionsClientKey{client.Identifier(), defaultNamespace}] = client
@@ -35,7 +39,7 @@ func TestMultiClientCaching(t *testing.T) {
cachedClient, err := multiClient.GetClientFor( cachedClient, err := multiClient.GetClientFor(
ctx, ctx,
defaultConfigURL, defaultConfigURL,
*defaultCreds, defaultCreds,
defaultNamespace, defaultNamespace,
) )
require.NoError(t, err) require.NoError(t, err)
@@ -47,7 +51,7 @@ func TestMultiClientCaching(t *testing.T) {
newClient, err := multiClient.GetClientFor( newClient, err := multiClient.GetClientFor(
ctx, ctx,
defaultConfigURL, defaultConfigURL,
*defaultCreds, defaultCreds,
otherNamespace, otherNamespace,
) )
require.NoError(t, err) require.NoError(t, err)
@@ -63,7 +67,7 @@ func TestMultiClientOptions(t *testing.T) {
defaultConfigURL := "https://github.com/org/repo" defaultConfigURL := "https://github.com/org/repo"
t.Run("GetClientFor", func(t *testing.T) { t.Run("GetClientFor", func(t *testing.T) {
defaultCreds := &ActionsAuth{ defaultCreds := &appconfig.AppConfig{
Token: "token", Token: "token",
} }
@@ -71,7 +75,7 @@ func TestMultiClientOptions(t *testing.T) {
service, err := multiClient.GetClientFor( service, err := multiClient.GetClientFor(
ctx, ctx,
defaultConfigURL, defaultConfigURL,
*defaultCreds, defaultCreds,
defaultNamespace, defaultNamespace,
) )
service.SetUserAgent(testUserAgent) service.SetUserAgent(testUserAgent)
@@ -83,27 +87,6 @@ func TestMultiClientOptions(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent")) assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent"))
}) })
t.Run("GetClientFromSecret", func(t *testing.T) {
secret := map[string][]byte{
"github_token": []byte("token"),
}
multiClient := NewMultiClient(logger)
service, err := multiClient.GetClientFromSecret(
ctx,
defaultConfigURL,
defaultNamespace,
secret,
)
service.SetUserAgent(testUserAgent)
require.NoError(t, err)
client := service.(*Client)
req, err := client.NewGitHubAPIRequest(ctx, "GET", "/test", nil)
require.NoError(t, err)
assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent"))
})
} }
func TestCreateJWT(t *testing.T) { func TestCreateJWT(t *testing.T) {
@@ -137,7 +120,7 @@ etFcaQuTHEZyRhhJ4BU=
-----END PRIVATE KEY-----` -----END PRIVATE KEY-----`
auth := &GitHubAppAuth{ auth := &GitHubAppAuth{
AppID: 123, AppID: "123",
AppPrivateKey: key, AppPrivateKey: key,
} }
jwt, err := createJWTForGitHubApp(auth) jwt, err := createJWTForGitHubApp(auth)

View File

@@ -127,7 +127,7 @@ func NewServer(opts ...Option) *httptest.Server {
}, },
// For ListRunners // For ListRunners
"/repos/test/valid/actions/runners": config.FixedResponses.ListRunners, "/repos/test/valid/actions/runners": config.ListRunners,
"/repos/test/invalid/actions/runners": &Handler{ "/repos/test/invalid/actions/runners": &Handler{
Status: http.StatusNoContent, Status: http.StatusNoContent,
Body: "", Body: "",
@@ -204,10 +204,10 @@ func NewServer(opts ...Option) *httptest.Server {
}, },
// For auto-scaling based on the number of queued(pending) workflow runs // For auto-scaling based on the number of queued(pending) workflow runs
"/repos/test/valid/actions/runs": config.FixedResponses.ListRepositoryWorkflowRuns, "/repos/test/valid/actions/runs": config.ListRepositoryWorkflowRuns,
// For auto-scaling based on the number of queued(pending) workflow jobs // For auto-scaling based on the number of queued(pending) workflow jobs
"/repos/test/valid/actions/runs/": config.FixedResponses.ListWorkflowJobs, "/repos/test/valid/actions/runs/": config.ListWorkflowJobs,
} }
mux := http.NewServeMux() mux := http.NewServeMux()

View File

@@ -12,7 +12,7 @@ type Option func(*ServerConfig)
func WithListRepositoryWorkflowRunsResponse(status int, body, queued, in_progress string) Option { func WithListRepositoryWorkflowRunsResponse(status int, body, queued, in_progress string) Option {
return func(c *ServerConfig) { return func(c *ServerConfig) {
c.FixedResponses.ListRepositoryWorkflowRuns = &Handler{ c.ListRepositoryWorkflowRuns = &Handler{
Status: status, Status: status,
Body: body, Body: body,
Statuses: map[string]string{ Statuses: map[string]string{
@@ -25,7 +25,7 @@ func WithListRepositoryWorkflowRunsResponse(status int, body, queued, in_progres
func WithListWorkflowJobsResponse(status int, bodies map[int]string) Option { func WithListWorkflowJobsResponse(status int, bodies map[int]string) Option {
return func(c *ServerConfig) { return func(c *ServerConfig) {
c.FixedResponses.ListWorkflowJobs = &MapHandler{ c.ListWorkflowJobs = &MapHandler{
Status: status, Status: status,
Bodies: bodies, Bodies: bodies,
} }
@@ -34,7 +34,7 @@ func WithListWorkflowJobsResponse(status int, bodies map[int]string) Option {
func WithListRunnersResponse(status int, body string) Option { func WithListRunnersResponse(status int, body string) Option {
return func(c *ServerConfig) { return func(c *ServerConfig) {
c.FixedResponses.ListRunners = &ListRunnersHandler{ c.ListRunners = &ListRunnersHandler{
Status: status, Status: status,
Body: body, Body: body,
} }

View File

@@ -290,7 +290,7 @@ func (c *Client) ListRunnerGroupRepositoryAccesses(ctx context.Context, org stri
opts := github.ListOptions{PerPage: 100} opts := github.ListOptions{PerPage: 100}
for { for {
list, res, err := c.Client.Actions.ListRepositoryAccessRunnerGroup(ctx, org, runnerGroupId, &opts) list, res, err := c.Actions.ListRepositoryAccessRunnerGroup(ctx, org, runnerGroupId, &opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list repository access for runner group: %w", err) return nil, fmt.Errorf("failed to list repository access for runner group: %w", err)
} }
@@ -323,32 +323,32 @@ func (c *Client) cleanup() {
func (c *Client) createRegistrationToken(ctx context.Context, enterprise, org, repo string) (*github.RegistrationToken, *github.Response, error) { func (c *Client) createRegistrationToken(ctx context.Context, enterprise, org, repo string) (*github.RegistrationToken, *github.Response, error) {
if len(repo) > 0 { if len(repo) > 0 {
return c.Client.Actions.CreateRegistrationToken(ctx, org, repo) return c.Actions.CreateRegistrationToken(ctx, org, repo)
} }
if len(org) > 0 { if len(org) > 0 {
return c.Client.Actions.CreateOrganizationRegistrationToken(ctx, org) return c.Actions.CreateOrganizationRegistrationToken(ctx, org)
} }
return c.Client.Enterprise.CreateRegistrationToken(ctx, enterprise) return c.Enterprise.CreateRegistrationToken(ctx, enterprise)
} }
func (c *Client) removeRunner(ctx context.Context, enterprise, org, repo string, runnerID int64) (*github.Response, error) { func (c *Client) removeRunner(ctx context.Context, enterprise, org, repo string, runnerID int64) (*github.Response, error) {
if len(repo) > 0 { if len(repo) > 0 {
return c.Client.Actions.RemoveRunner(ctx, org, repo, runnerID) return c.Actions.RemoveRunner(ctx, org, repo, runnerID)
} }
if len(org) > 0 { if len(org) > 0 {
return c.Client.Actions.RemoveOrganizationRunner(ctx, org, runnerID) return c.Actions.RemoveOrganizationRunner(ctx, org, runnerID)
} }
return c.Client.Enterprise.RemoveRunner(ctx, enterprise, runnerID) return c.Enterprise.RemoveRunner(ctx, enterprise, runnerID)
} }
func (c *Client) listRunners(ctx context.Context, enterprise, org, repo string, opts *github.ListOptions) (*github.Runners, *github.Response, error) { func (c *Client) listRunners(ctx context.Context, enterprise, org, repo string, opts *github.ListOptions) (*github.Runners, *github.Response, error) {
if len(repo) > 0 { if len(repo) > 0 {
return c.Client.Actions.ListRunners(ctx, org, repo, opts) return c.Actions.ListRunners(ctx, org, repo, opts)
} }
if len(org) > 0 { if len(org) > 0 {
return c.Client.Actions.ListOrganizationRunners(ctx, org, opts) return c.Actions.ListOrganizationRunners(ctx, org, opts)
} }
return c.Client.Enterprise.ListRunners(ctx, enterprise, opts) return c.Enterprise.ListRunners(ctx, enterprise, opts)
} }
func (c *Client) ListRepositoryWorkflowRuns(ctx context.Context, user string, repoName string) ([]*github.WorkflowRun, error) { func (c *Client) ListRepositoryWorkflowRuns(ctx context.Context, user string, repoName string) ([]*github.WorkflowRun, error) {
@@ -381,7 +381,7 @@ func (c *Client) listRepositoryWorkflowRuns(ctx context.Context, user string, re
} }
for { for {
list, res, err := c.Client.Actions.ListRepositoryWorkflowRuns(ctx, user, repoName, &opts) list, res, err := c.Actions.ListRepositoryWorkflowRuns(ctx, user, repoName, &opts)
if err != nil { if err != nil {
return workflowRuns, fmt.Errorf("failed to list workflow runs: %v", err) return workflowRuns, fmt.Errorf("failed to list workflow runs: %v", err)

View File

@@ -26,7 +26,7 @@ func newTestClient() *Client {
if err != nil { if err != nil {
panic(err) panic(err)
} }
client.Client.BaseURL = baseURL client.BaseURL = baseURL
return client return client
} }

27
go.mod
View File

@@ -1,8 +1,11 @@
module github.com/actions/actions-runner-controller module github.com/actions/actions-runner-controller
go 1.24.0 go 1.24.3
require ( require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0
github.com/bradleyfalzon/ghinstallation/v2 v2.14.0 github.com/bradleyfalzon/ghinstallation/v2 v2.14.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/evanphx/json-patch v5.9.11+incompatible github.com/evanphx/json-patch v5.9.11+incompatible
@@ -17,28 +20,31 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/go-retryablehttp v0.7.7
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.23.0 github.com/onsi/ginkgo/v2 v2.23.3
github.com/onsi/gomega v1.36.2 github.com/onsi/gomega v1.36.3
github.com/prometheus/client_golang v1.21.1 github.com/prometheus/client_golang v1.21.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/teambition/rrule-go v1.8.2 github.com/teambition/rrule-go v1.8.2
go.uber.org/multierr v1.11.0 go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/net v0.37.0 golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.28.0 golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0 golang.org/x/sync v0.12.0
gomodules.xyz/jsonpatch/v2 v2.5.0 gomodules.xyz/jsonpatch/v2 v2.5.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.32.2 k8s.io/api v0.32.3
k8s.io/apimachinery v0.32.2 k8s.io/apimachinery v0.32.3
k8s.io/client-go v0.32.2 k8s.io/client-go v0.32.3
k8s.io/utils v0.0.0-20241210054802-24370beab758 k8s.io/utils v0.0.0-20241210054802-24370beab758
sigs.k8s.io/controller-runtime v0.20.3 sigs.k8s.io/controller-runtime v0.20.4
sigs.k8s.io/yaml v1.4.0 sigs.k8s.io/yaml v1.4.0
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
@@ -80,7 +86,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.2 // indirect github.com/boombuler/barcode v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect
@@ -95,6 +101,7 @@ require (
github.com/go-sql-driver/mysql v1.9.0 // indirect github.com/go-sql-driver/mysql v1.9.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/gonvenience/bunt v1.4.0 // indirect github.com/gonvenience/bunt v1.4.0 // indirect
github.com/gonvenience/idem v0.0.1 // indirect github.com/gonvenience/idem v0.0.1 // indirect
@@ -121,6 +128,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect
@@ -134,6 +142,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.4.0 // indirect github.com/pquerna/otp v1.4.0 // indirect

60
go.sum
View File

@@ -1,5 +1,22 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go v51.0.0+incompatible h1:p7blnyJSjJqf5jflHbSGhIhEpXIgIFmYZNg5uwqweso=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0/go.mod h1:hd8hTTIY3VmUVPRHNH7GVCHO3SHgXkJKZHReby/bnUQ=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
@@ -89,14 +106,16 @@ github.com/bradleyfalzon/ghinstallation/v2 v2.14.0 h1:0D4vKCHOvYrDU8u61TnE2JfNT4
github.com/bradleyfalzon/ghinstallation/v2 v2.14.0/go.mod h1:LOVmdZYVZ8jqdr4n9wWm1ocDiMz9IfMGfRkaYC1a52A= github.com/bradleyfalzon/ghinstallation/v2 v2.14.0/go.mod h1:LOVmdZYVZ8jqdr4n9wWm1ocDiMz9IfMGfRkaYC1a52A=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
@@ -134,6 +153,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -215,6 +236,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -263,12 +286,14 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.23.0 h1:FA1xjp8ieYDzlgS5ABTpdUDB7wtngggONc8a7ku2NqQ= github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
github.com/onsi/ginkgo/v2 v2.23.0/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -284,6 +309,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -336,8 +363,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -355,6 +382,7 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
@@ -405,22 +433,22 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4=
k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA=
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250304201544-e5f78fe3ede9 h1:t0huyHnz6HsokckRxAF1bY0cqPFwzINKCL7yltEjZQc= k8s.io/kube-openapi v0.0.0-20250304201544-e5f78fe3ede9 h1:t0huyHnz6HsokckRxAF1bY0cqPFwzINKCL7yltEjZQc=
k8s.io/kube-openapi v0.0.0-20250304201544-e5f78fe3ede9/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/kube-openapi v0.0.0-20250304201544-e5f78fe3ede9/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.20.3 h1:I6Ln8JfQjHH7JbtCD2HCYHoIzajoRxPNuvhvcDbZgkI= sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
sigs.k8s.io/controller-runtime v0.20.3/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=

10
main.go
View File

@@ -274,10 +274,18 @@ func main() {
log.WithName("actions-clients"), log.WithName("actions-clients"),
) )
secretResolver := actionsgithubcom.NewSecretResolver(
mgr.GetClient(),
actionsMultiClient,
)
rb := actionsgithubcom.ResourceBuilder{ rb := actionsgithubcom.ResourceBuilder{
ExcludeLabelPropagationPrefixes: excludeLabelPropagationPrefixes, ExcludeLabelPropagationPrefixes: excludeLabelPropagationPrefixes,
SecretResolver: secretResolver,
} }
log.Info("Resource builder initializing")
if err = (&actionsgithubcom.AutoscalingRunnerSetReconciler{ if err = (&actionsgithubcom.AutoscalingRunnerSetReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Log: log.WithName("AutoscalingRunnerSet").WithValues("version", build.Version), Log: log.WithName("AutoscalingRunnerSet").WithValues("version", build.Version),
@@ -297,7 +305,6 @@ func main() {
Client: mgr.GetClient(), Client: mgr.GetClient(),
Log: log.WithName("EphemeralRunner").WithValues("version", build.Version), Log: log.WithName("EphemeralRunner").WithValues("version", build.Version),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
ActionsClient: actionsMultiClient,
ResourceBuilder: rb, ResourceBuilder: rb,
}).SetupWithManager(mgr, actionsgithubcom.WithMaxConcurrentReconciles(opts.RunnerMaxConcurrentReconciles)); err != nil { }).SetupWithManager(mgr, actionsgithubcom.WithMaxConcurrentReconciles(opts.RunnerMaxConcurrentReconciles)); err != nil {
log.Error(err, "unable to create controller", "controller", "EphemeralRunner") log.Error(err, "unable to create controller", "controller", "EphemeralRunner")
@@ -308,7 +315,6 @@ func main() {
Client: mgr.GetClient(), Client: mgr.GetClient(),
Log: log.WithName("EphemeralRunnerSet").WithValues("version", build.Version), Log: log.WithName("EphemeralRunnerSet").WithValues("version", build.Version),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
ActionsClient: actionsMultiClient,
PublishMetrics: metricsAddr != "0", PublishMetrics: metricsAddr != "0",
ResourceBuilder: rb, ResourceBuilder: rb,
}).SetupWithManager(mgr); err != nil { }).SetupWithManager(mgr); err != nil {

Some files were not shown because too many files have changed in this diff Show More