mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 11:41:27 +00:00
Compare commits
95 Commits
v0.19.0
...
actions-ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58d2591f09 | ||
|
|
1a75b4558b | ||
|
|
40c88eb490 | ||
|
|
fe64850d3d | ||
|
|
4320e0e5e1 | ||
|
|
4a61c2f3aa | ||
|
|
1eb135cace | ||
|
|
d918c91bea | ||
|
|
bf35c51440 | ||
|
|
b679a54196 | ||
|
|
5da808af96 | ||
|
|
e5b5ee6f1d | ||
|
|
cf3abcc7d6 | ||
|
|
cffc2585f9 | ||
|
|
01928863b9 | ||
|
|
a98729b08b | ||
|
|
ec0915ce7c | ||
|
|
d355f05ac0 | ||
|
|
6f27b4920e | ||
|
|
f8959f973f | ||
|
|
37955fa267 | ||
|
|
63fe89b7aa | ||
|
|
3f801af72a | ||
|
|
7008b0c257 | ||
|
|
d9df455781 | ||
|
|
7e42d3fa7c | ||
|
|
0593125d96 | ||
|
|
a815c37614 | ||
|
|
3539569fed | ||
|
|
fc131870aa | ||
|
|
382afa4450 | ||
|
|
5125dd7e77 | ||
|
|
2c711506ea | ||
|
|
dfa0f2eef4 | ||
|
|
180db37a9a | ||
|
|
424c33b11f | ||
|
|
34d9c6d4db | ||
|
|
167c5b4dc9 | ||
|
|
91c22ef964 | ||
|
|
5d292ee5ff | ||
|
|
5b4b65664c | ||
|
|
b6465c5d09 | ||
|
|
dc9f9b0bfb | ||
|
|
02e05bdafb | ||
|
|
a9421edd46 | ||
|
|
fb66b28569 | ||
|
|
fabead8c8e | ||
|
|
d528d18211 | ||
|
|
7e593a80ff | ||
|
|
27bdc780a3 | ||
|
|
3948406374 | ||
|
|
743e6d6202 | ||
|
|
29260549fa | ||
|
|
f17edd500b | ||
|
|
14564c7b8e | ||
|
|
7f2795b5d6 | ||
|
|
b27b6ea2a8 | ||
|
|
f858e2e432 | ||
|
|
6f130c2db5 | ||
|
|
dcea0f7f79 | ||
|
|
f19e7ea8a8 | ||
|
|
9437e164b4 | ||
|
|
82d1be7791 | ||
|
|
dbab1a5e92 | ||
|
|
e5a9d50cb6 | ||
|
|
67031acdc4 | ||
|
|
b1bfa8787f | ||
|
|
c78116b0f9 | ||
|
|
4ec57d3e39 | ||
|
|
79543add3f | ||
|
|
7722730dc0 | ||
|
|
044f4ad4ea | ||
|
|
20394be04d | ||
|
|
7a305d2892 | ||
|
|
927d6f03ce | ||
|
|
127a9aa7c4 | ||
|
|
2703fa75d6 | ||
|
|
605ec158f4 | ||
|
|
3b45d1b334 | ||
|
|
acb906164b | ||
|
|
98da4c2adb | ||
|
|
9e1c28fcff | ||
|
|
774db3fef4 | ||
|
|
8b90b0f0e3 | ||
|
|
a277489003 | ||
|
|
1084a37174 | ||
|
|
9e4dbf497c | ||
|
|
af0ca03752 | ||
|
|
37d9599dca | ||
|
|
08a676cfd4 | ||
|
|
f2e2060ff8 | ||
|
|
dc5f90025c | ||
|
|
8566a4f453 | ||
|
|
3366dc9a63 | ||
|
|
fa94799ec8 |
@@ -7,6 +7,7 @@ config
|
||||
charts
|
||||
.github
|
||||
.envrc
|
||||
.env
|
||||
*.md
|
||||
*.txt
|
||||
*.sh
|
||||
|
||||
25
.github/lock.yml
vendored
Normal file
25
.github/lock.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Configuration for Lock Threads
|
||||
# Repo: https://github.com/dessant/lock-threads-app
|
||||
# App: https://github.com/apps/lock
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 7
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: false
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: >
|
||||
This thread has been automatically locked since there has not been
|
||||
any recent activity after it was closed. Please open a new issue for
|
||||
related bugs.
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: true
|
||||
20
.github/renovate.json5
vendored
Normal file
20
.github/renovate.json5
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": ["config:base"],
|
||||
"packageRules": [
|
||||
{
|
||||
// automatically merge an update of runner
|
||||
"matchPackageNames": ["actions/runner"],
|
||||
"extractVersion": "^v(?<version>.*)$",
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"regexManagers": [
|
||||
{
|
||||
// use https://github.com/actions/runner/releases
|
||||
"fileMatch": [".github/workflows/build-and-release-runners.yml"],
|
||||
"matchStrings": ["RUNNER_VERSION: +(?<currentValue>.*?)\\n"],
|
||||
"depNameTemplate": "actions/runner",
|
||||
"datasourceTemplate": "github-releases"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
.github/workflows/build-and-release-runners.yml
vendored
16
.github/workflows/build-and-release-runners.yml
vendored
@@ -18,6 +18,11 @@ on:
|
||||
- runner/entrypoint.sh
|
||||
- .github/workflows/build-and-release-runners.yml
|
||||
|
||||
env:
|
||||
RUNNER_VERSION: 2.282.0
|
||||
DOCKER_VERSION: 20.10.8
|
||||
DOCKERHUB_USERNAME: summerwind
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -34,10 +39,7 @@ jobs:
|
||||
- name: actions-runner-dind
|
||||
os-version: 20.04
|
||||
dockerfile: Dockerfile.dindrunner
|
||||
env:
|
||||
RUNNER_VERSION: 2.278.0
|
||||
DOCKER_VERSION: 19.03.12
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USER }}
|
||||
|
||||
steps:
|
||||
- name: Set outputs
|
||||
id: vars
|
||||
@@ -86,12 +88,8 @@ jobs:
|
||||
dockerfile: Dockerfile
|
||||
- name: actions-runner-dind
|
||||
dockerfile: Dockerfile.dindrunner
|
||||
env:
|
||||
RUNNER_VERSION: 2.277.1
|
||||
DOCKER_VERSION: 19.03.12
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USER }}
|
||||
steps:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
4
.github/workflows/on-push-lint-charts.yml
vendored
4
.github/workflows/on-push-lint-charts.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@v2.0.1
|
||||
uses: helm/chart-testing-action@v2.1.0
|
||||
|
||||
- name: Run chart-testing (list-changed)
|
||||
id: list-changed
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
run: ct lint --config charts/.ci/ct-config.yaml
|
||||
|
||||
- name: Create kind cluster
|
||||
uses: helm/kind-action@v1.0.0
|
||||
uses: helm/kind-action@v1.2.0
|
||||
if: steps.list-changed.outputs.changed == 'true'
|
||||
|
||||
# We need cert-manager already installed in the cluster because we assume the CRDs exist
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@v2.0.1
|
||||
uses: helm/chart-testing-action@v2.1.0
|
||||
|
||||
- name: Run chart-testing (list-changed)
|
||||
id: list-changed
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
run: ct lint --config charts/.ci/ct-config.yaml
|
||||
|
||||
- name: Create kind cluster
|
||||
uses: helm/kind-action@v1.0.0
|
||||
uses: helm/kind-action@v1.2.0
|
||||
if: steps.list-changed.outputs.changed == 'true'
|
||||
|
||||
# We need cert-manager already installed in the cluster because we assume the CRDs exist
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Run chart-releaser
|
||||
uses: helm/chart-releaser-action@v1.1.0
|
||||
uses: helm/chart-releaser-action@v1.2.1
|
||||
env:
|
||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
|
||||
21
.github/workflows/test-entrypoint.yaml
vendored
Normal file
21
.github/workflows/test-entrypoint.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Unit tests for entrypoint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
paths:
|
||||
- 'runner/**'
|
||||
- 'test/entrypoint/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test entrypoint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Run unit tests for entrypoint.sh
|
||||
run: |
|
||||
cd test/entrypoint
|
||||
bash entrypoint_unittest.sh
|
||||
10
.github/workflows/test.yaml
vendored
10
.github/workflows/test.yaml
vendored
@@ -17,11 +17,15 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16.5'
|
||||
- run: go version
|
||||
- name: Install kubebuilder
|
||||
run: |
|
||||
curl -L -O https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.2.0/kubebuilder_2.2.0_linux_amd64.tar.gz
|
||||
tar zxvf kubebuilder_2.2.0_linux_amd64.tar.gz
|
||||
sudo mv kubebuilder_2.2.0_linux_amd64 /usr/local/kubebuilder
|
||||
curl -L -O https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.2/kubebuilder_2.3.2_linux_amd64.tar.gz
|
||||
tar zxvf kubebuilder_2.3.2_linux_amd64.tar.gz
|
||||
sudo mv kubebuilder_2.3.2_linux_amd64 /usr/local/kubebuilder
|
||||
- name: Run tests
|
||||
run: make test
|
||||
- name: Verify manifests are up-to-date
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
# Deploy Assets
|
||||
release
|
||||
|
||||
# Binaries for programs and plugins
|
||||
@@ -15,17 +16,21 @@ bin
|
||||
*.out
|
||||
|
||||
# Kubernetes Generated files - skip generated files, except for vendored files
|
||||
|
||||
!vendor/**/zz_generated.*
|
||||
|
||||
# editor and IDE paraphernalia
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
.envrc
|
||||
.env
|
||||
.test.env
|
||||
*.pem
|
||||
|
||||
# OS
|
||||
.DS_STORE
|
||||
|
||||
/test-assets
|
||||
|
||||
138
CONTRIBUTING.md
138
CONTRIBUTING.md
@@ -1,6 +1,140 @@
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
### Helm Version Bumps
|
||||
### How to Contribute a Patch
|
||||
|
||||
Depending on what you are patching depends on how you should go about it. Below are some guides on how to test patches locally as well as develop the controller and runners.
|
||||
|
||||
When sumitting a PR for a change please provide evidence that your change works as we still need to work on improving the CI of the project. Some resources are provided for helping achieve this, see this guide for details.
|
||||
|
||||
#### Running an End to End Test
|
||||
|
||||
> **Notes for Ubuntu 20.04+ users**
|
||||
>
|
||||
> If you're using Ubuntu 20.04 or greater, you might have installed `docker` with `snap`.
|
||||
>
|
||||
> If you want to stick with `snap`-provided `docker`, do not forget to set `TMPDIR` to
|
||||
> somewhere under `$HOME`.
|
||||
> Otherwise `kind load docker-image` fail while running `docker save`.
|
||||
> See https://kind.sigs.k8s.io/docs/user/known-issues/#docker-installed-with-snap for more information.
|
||||
|
||||
To test your local changes against both PAT and App based authentication please run the `acceptance` make target with the authentication configuration details provided:
|
||||
|
||||
```shell
|
||||
# This sets `VERSION` envvar to some appropriate value
|
||||
. hack/make-env.sh
|
||||
|
||||
DOCKER_USER=*** \
|
||||
GITHUB_TOKEN=*** \
|
||||
APP_ID=*** \
|
||||
PRIVATE_KEY_FILE_PATH=path/to/pem/file \
|
||||
INSTALLATION_ID=*** \
|
||||
make acceptance
|
||||
```
|
||||
|
||||
**Rerunning a failed test**
|
||||
|
||||
When one of tests run by `make acceptance` failed, you'd probably like to rerun only the failed one.
|
||||
|
||||
It can be done by `make acceptance/run` and by setting the combination of `ACCEPTANCE_TEST_DEPLOYMENT_TOOL=helm|kubectl` and `ACCEPTANCE_TEST_SECRET_TYPE=token|app` values that failed (note, you just need to set the corresponding authentication configuration in this circumstance)
|
||||
|
||||
In the example below, we rerun the test for the combination `ACCEPTANCE_TEST_DEPLOYMENT_TOOL=helm ACCEPTANCE_TEST_SECRET_TYPE=token` only:
|
||||
|
||||
```shell
|
||||
DOCKER_USER=*** \
|
||||
GITHUB_TOKEN=*** \
|
||||
ACCEPTANCE_TEST_DEPLOYMENT_TOOL=helm
|
||||
ACCEPTANCE_TEST_SECRET_TYPE=token \
|
||||
make acceptance/run
|
||||
```
|
||||
|
||||
**Testing in a non-kind cluster**
|
||||
|
||||
If you prefer to test in a non-kind cluster, you can instead run:
|
||||
|
||||
```shell
|
||||
KUBECONFIG=path/to/kubeconfig \
|
||||
DOCKER_USER=*** \
|
||||
GITHUB_TOKEN=*** \
|
||||
APP_ID=*** \
|
||||
PRIVATE_KEY_FILE_PATH=path/to/pem/file \
|
||||
INSTALLATION_ID=*** \
|
||||
ACCEPTANCE_TEST_SECRET_TYPE=token \
|
||||
make docker-build acceptance/setup \
|
||||
acceptance/deploy \
|
||||
acceptance/tests
|
||||
```
|
||||
|
||||
#### Developing the Controller
|
||||
|
||||
Rerunning the whole acceptance test suite from scratch on every little change to the controller, the runner, and the chart would be counter-productive.
|
||||
|
||||
To make your development cycle faster, use the below command to update deploy and update all the three:
|
||||
|
||||
```shell
|
||||
# Let assume we have all other envvars like DOCKER_USER, GITHUB_TOKEN already set,
|
||||
# The below command will (re)build `actions-runner-controller:controller1` and `actions-runner:runner1`,
|
||||
# load those into kind nodes, and then rerun kubectl or helm to install/upgrade the controller,
|
||||
# and finally upgrade the runner deployment to use the new runner image.
|
||||
#
|
||||
# As helm 3 and kubectl is unable to recreate a pod when no tag change,
|
||||
# you either need to bump VERSION and RUNNER_TAG on each run,
|
||||
# or manually run `kubectl delete pod $POD` on respective pods for changes to actually take effect.
|
||||
|
||||
VERSION=controller1 \
|
||||
RUNNER_TAG=runner1 \
|
||||
make acceptance/pull acceptance/kind docker-build acceptance/load acceptance/deploy
|
||||
```
|
||||
|
||||
If you've already deployed actions-runner-controller and only want to recreate pods to use the newer image, you can run:
|
||||
|
||||
```shell
|
||||
NAME=$DOCKER_USER/actions-runner-controller \
|
||||
make docker-build acceptance/load && \
|
||||
kubectl -n actions-runner-system delete po $(kubectl -n actions-runner-system get po -ojsonpath={.items[*].metadata.name})
|
||||
```
|
||||
|
||||
Similarly, if you'd like to recreate runner pods with the newer runner image,
|
||||
|
||||
```shell
|
||||
NAME=$DOCKER_USER/actions-runner make \
|
||||
-C runner docker-{build,push}-ubuntu && \
|
||||
(kubectl get po -ojsonpath={.items[*].metadata.name} | xargs -n1 kubectl delete po)
|
||||
```
|
||||
|
||||
#### Developing the Runners
|
||||
|
||||
**Tests**
|
||||
|
||||
A set of example pipelines (./acceptance/pipelines) are provided in this repository which you can use to validate your runners are working as expected. When raising a PR please run the relevant suites to prove your change hasn't broken anything.
|
||||
|
||||
**Running Ginkgo Tests**
|
||||
|
||||
You can run the integration test suite that is written in Ginkgo with:
|
||||
|
||||
```shell
|
||||
make test-with-deps
|
||||
```
|
||||
|
||||
This will firstly install a few binaries required to setup the integration test environment and then runs `go test` to start the Ginkgo test.
|
||||
|
||||
If you don't want to use `make`, like when you're running tests from your IDE, install required binaries to `/usr/local/kubebuilder/bin`. That's the directory in which controller-runtime's `envtest` framework locates the binaries.
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /usr/local/kubebuilder/bin
|
||||
make kube-apiserver etcd
|
||||
sudo mv test-assets/{etcd,kube-apiserver} /usr/local/kubebuilder/bin/
|
||||
go test -v -run TestAPIs github.com/actions-runner-controller/actions-runner-controller/controllers
|
||||
```
|
||||
|
||||
To run Ginkgo tests selectively, set the pattern of target test names to `GINKGO_FOCUS`.
|
||||
All the Ginkgo test that matches `GINKGO_FOCUS` will be run.
|
||||
|
||||
```shell
|
||||
GINKGO_FOCUS='[It] should create a new Runner resource from the specified template, add a another Runner on replicas increased, and removes all the replicas when set to 0' \
|
||||
go test -v -run TestAPIs github.com/actions-runner-controller/actions-runner-controller/controllers
|
||||
```
|
||||
|
||||
#### Helm Version Bumps
|
||||
|
||||
**Chart Version :** When bumping the chart version follow semantic versioning https://semver.org/<br />
|
||||
**App Version :** When bumping the app version you will also need to bump the chart version too. Again, follow semantic versioning when bumping the chart.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.15 as builder
|
||||
FROM golang:1.17 as builder
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
|
||||
106
Makefile
106
Makefile
@@ -10,14 +10,16 @@ RUNNER_TAG ?= ${VERSION}
|
||||
TEST_REPO ?= ${DOCKER_USER}/actions-runner-controller
|
||||
TEST_ORG ?=
|
||||
TEST_ORG_REPO ?=
|
||||
TEST_EPHEMERAL ?= false
|
||||
SYNC_PERIOD ?= 5m
|
||||
|
||||
# From https://github.com/VictoriaMetrics/operator/pull/44
|
||||
YAML_DROP=$(YQ) delete --inplace
|
||||
YAML_DROP_PREFIX=spec.validation.openAPIV3Schema.properties.spec.properties
|
||||
USE_RUNNERSET ?=
|
||||
RUNNER_FEATURE_FLAG_EPHEMERAL ?=
|
||||
KUBECONTEXT ?= kind-acceptance
|
||||
CLUSTER ?= acceptance
|
||||
CERT_MANAGER_VERSION ?= v1.1.1
|
||||
|
||||
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
|
||||
CRD_OPTIONS ?= "crd:trivialVersions=true"
|
||||
CRD_OPTIONS ?= "crd:trivialVersions=true,generateEmbeddedObjectMeta=true"
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
@@ -47,9 +49,11 @@ endif
|
||||
|
||||
all: manager
|
||||
|
||||
GO_TEST_ARGS ?= -short
|
||||
|
||||
# Run tests
|
||||
test: generate fmt vet manifests
|
||||
go test ./... -coverprofile cover.out
|
||||
go test $(GO_TEST_ARGS) ./... -coverprofile cover.out
|
||||
|
||||
test-with-deps: kube-apiserver etcd kubectl
|
||||
# See https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/envtest#pkg-constants
|
||||
@@ -80,9 +84,9 @@ deploy: manifests
|
||||
kustomize build config/default | kubectl apply -f -
|
||||
|
||||
# Generate manifests e.g. CRD, RBAC etc.
|
||||
manifests: manifests-118 fix118 chart-crds
|
||||
manifests: manifests-gen-crds chart-crds
|
||||
|
||||
manifests-118: controller-gen
|
||||
manifests-gen-crds: controller-gen
|
||||
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
|
||||
|
||||
chart-crds:
|
||||
@@ -96,30 +100,14 @@ fmt:
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
# workaround for CRD issue with k8s 1.18 & controller-gen
|
||||
# ref: https://github.com/kubernetes/kubernetes/issues/91395
|
||||
fix118: yq
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml $(YAML_DROP_PREFIX).template.properties.spec.properties.containers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml $(YAML_DROP_PREFIX).template.properties.spec.properties.initContainers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml $(YAML_DROP_PREFIX).template.properties.spec.properties.sidecarContainers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml $(YAML_DROP_PREFIX).template.properties.spec.properties.ephemeralContainers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml $(YAML_DROP_PREFIX).template.properties.spec.properties.containers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml $(YAML_DROP_PREFIX).template.properties.spec.properties.initContainers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml $(YAML_DROP_PREFIX).template.properties.spec.properties.sidecarContainers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml $(YAML_DROP_PREFIX).template.properties.spec.properties.ephemeralContainers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runners.yaml $(YAML_DROP_PREFIX).containers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runners.yaml $(YAML_DROP_PREFIX).initContainers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runners.yaml $(YAML_DROP_PREFIX).sidecarContainers.items.properties
|
||||
$(YAML_DROP) config/crd/bases/actions.summerwind.dev_runners.yaml $(YAML_DROP_PREFIX).ephemeralContainers.items.properties
|
||||
|
||||
# Generate code
|
||||
generate: controller-gen
|
||||
$(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths="./..."
|
||||
|
||||
# Build the docker image
|
||||
docker-build:
|
||||
docker build . -t ${NAME}:${VERSION}
|
||||
docker build runner -t ${RUNNER_NAME}:${RUNNER_TAG} --build-arg TARGETPLATFORM=$(shell arch)
|
||||
docker build -t ${NAME}:${VERSION} .
|
||||
docker build -t ${RUNNER_NAME}:${RUNNER_TAG} --build-arg TARGETPLATFORM=$(shell arch) runner
|
||||
|
||||
docker-buildx:
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
@@ -158,31 +146,31 @@ acceptance: release/clean acceptance/pull docker-build release
|
||||
acceptance/run: acceptance/kind acceptance/load acceptance/setup acceptance/deploy acceptance/tests acceptance/teardown
|
||||
|
||||
acceptance/kind:
|
||||
kind create cluster --name acceptance --config acceptance/kind.yaml
|
||||
kind create cluster --name ${CLUSTER} --config acceptance/kind.yaml
|
||||
|
||||
# Set TMPDIR to somewhere under $HOME when you use docker installed with Ubuntu snap
|
||||
# Otherwise `load docker-image` fail while running `docker save`.
|
||||
# See https://kind.sigs.k8s.io/docs/user/known-issues/#docker-installed-with-snap
|
||||
acceptance/load:
|
||||
kind load docker-image ${NAME}:${VERSION} --name acceptance
|
||||
kind load docker-image quay.io/brancz/kube-rbac-proxy:v0.10.0 --name acceptance
|
||||
kind load docker-image ${RUNNER_NAME}:${RUNNER_TAG} --name acceptance
|
||||
kind load docker-image docker:dind --name acceptance
|
||||
kind load docker-image quay.io/jetstack/cert-manager-controller:v1.0.4 --name acceptance
|
||||
kind load docker-image quay.io/jetstack/cert-manager-cainjector:v1.0.4 --name acceptance
|
||||
kind load docker-image quay.io/jetstack/cert-manager-webhook:v1.0.4 --name acceptance
|
||||
kubectl cluster-info --context kind-acceptance
|
||||
kind load docker-image ${NAME}:${VERSION} --name ${CLUSTER}
|
||||
kind load docker-image quay.io/brancz/kube-rbac-proxy:v0.10.0 --name ${CLUSTER}
|
||||
kind load docker-image ${RUNNER_NAME}:${RUNNER_TAG} --name ${CLUSTER}
|
||||
kind load docker-image docker:dind --name ${CLUSTER}
|
||||
kind load docker-image quay.io/jetstack/cert-manager-controller:$(CERT_MANAGER_VERSION) --name ${CLUSTER}
|
||||
kind load docker-image quay.io/jetstack/cert-manager-cainjector:$(CERT_MANAGER_VERSION) --name ${CLUSTER}
|
||||
kind load docker-image quay.io/jetstack/cert-manager-webhook:$(CERT_MANAGER_VERSION) --name ${CLUSTER}
|
||||
kubectl cluster-info --context ${KUBECONTEXT}
|
||||
|
||||
# Pull the docker images for acceptance
|
||||
acceptance/pull:
|
||||
docker pull quay.io/brancz/kube-rbac-proxy:v0.10.0
|
||||
docker pull docker:dind
|
||||
docker pull quay.io/jetstack/cert-manager-controller:v1.0.4
|
||||
docker pull quay.io/jetstack/cert-manager-cainjector:v1.0.4
|
||||
docker pull quay.io/jetstack/cert-manager-webhook:v1.0.4
|
||||
docker pull quay.io/jetstack/cert-manager-controller:$(CERT_MANAGER_VERSION)
|
||||
docker pull quay.io/jetstack/cert-manager-cainjector:$(CERT_MANAGER_VERSION)
|
||||
docker pull quay.io/jetstack/cert-manager-webhook:$(CERT_MANAGER_VERSION)
|
||||
|
||||
acceptance/setup:
|
||||
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.0.4/cert-manager.yaml #kubectl create namespace actions-runner-system
|
||||
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/$(CERT_MANAGER_VERSION)/cert-manager.yaml #kubectl create namespace actions-runner-system
|
||||
kubectl -n cert-manager wait deploy/cert-manager-cainjector --for condition=available --timeout 90s
|
||||
kubectl -n cert-manager wait deploy/cert-manager-webhook --for condition=available --timeout 60s
|
||||
kubectl -n cert-manager wait deploy/cert-manager --for condition=available --timeout 60s
|
||||
@@ -191,22 +179,37 @@ acceptance/setup:
|
||||
sleep 5
|
||||
|
||||
acceptance/teardown:
|
||||
kind delete cluster --name acceptance
|
||||
kind delete cluster --name ${CLUSTER}
|
||||
|
||||
acceptance/deploy:
|
||||
NAME=${NAME} DOCKER_USER=${DOCKER_USER} VERSION=${VERSION} RUNNER_NAME=${RUNNER_NAME} RUNNER_TAG=${RUNNER_TAG} TEST_REPO=${TEST_REPO} \
|
||||
TEST_ORG=${TEST_ORG} TEST_ORG_REPO=${TEST_ORG_REPO} SYNC_PERIOD=${SYNC_PERIOD} \
|
||||
USE_RUNNERSET=${USE_RUNNERSET} \
|
||||
TEST_EPHEMERAL=${TEST_EPHEMERAL} \
|
||||
RUNNER_FEATURE_FLAG_EPHEMERAL=${RUNNER_FEATURE_FLAG_EPHEMERAL} \
|
||||
acceptance/deploy.sh
|
||||
|
||||
acceptance/tests:
|
||||
acceptance/checks.sh
|
||||
|
||||
# We use -count=1 instead of `go clean -testcache`
|
||||
# See https://terratest.gruntwork.io/docs/testing-best-practices/avoid-test-caching/
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
go test -count=1 -v -timeout 600s -run '^TestE2E$$' ./test/e2e
|
||||
|
||||
# Upload release file to GitHub.
|
||||
github-release: release
|
||||
ghr ${VERSION} release/
|
||||
|
||||
# find or download controller-gen
|
||||
# download controller-gen if necessary
|
||||
# Find or download controller-gen
|
||||
#
|
||||
# Note that controller-gen newer than 0.4.1 is needed for https://github.com/kubernetes-sigs/controller-tools/issues/444#issuecomment-680168439
|
||||
# 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]
|
||||
#
|
||||
# Note that controller-gen newer than 0.6.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.
|
||||
controller-gen:
|
||||
ifeq (, $(shell which controller-gen))
|
||||
ifeq (, $(wildcard $(GOBIN)/controller-gen))
|
||||
@@ -215,7 +218,7 @@ ifeq (, $(wildcard $(GOBIN)/controller-gen))
|
||||
CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
|
||||
cd $$CONTROLLER_GEN_TMP_DIR ;\
|
||||
go mod init tmp ;\
|
||||
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\
|
||||
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.6.0 ;\
|
||||
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
|
||||
}
|
||||
endif
|
||||
@@ -224,23 +227,6 @@ else
|
||||
CONTROLLER_GEN=$(shell which controller-gen)
|
||||
endif
|
||||
|
||||
# find or download yq
|
||||
# download yq if necessary
|
||||
# Use always go-version to get consistent line wraps etc.
|
||||
yq:
|
||||
ifeq (, $(wildcard $(GOBIN)/yq))
|
||||
echo "Downloading yq"
|
||||
@{ \
|
||||
set -e ;\
|
||||
YQ_TMP_DIR=$$(mktemp -d) ;\
|
||||
cd $$YQ_TMP_DIR ;\
|
||||
go mod init tmp ;\
|
||||
go get github.com/mikefarah/yq/v3@3.4.0 ;\
|
||||
rm -rf $$YQ_TMP_DIR ;\
|
||||
}
|
||||
endif
|
||||
YQ=$(GOBIN)/yq
|
||||
|
||||
OS_NAME := $(shell uname -s | tr A-Z a-z)
|
||||
|
||||
# find or download etcd
|
||||
|
||||
2
PROJECT
2
PROJECT
@@ -1,5 +1,5 @@
|
||||
domain: summerwind.dev
|
||||
repo: github.com/summerwind/actions-runner-controller
|
||||
repo: github.com/actions-runner-controller/actions-runner-controller
|
||||
resources:
|
||||
- group: actions
|
||||
kind: Runner
|
||||
|
||||
495
README.md
495
README.md
@@ -12,6 +12,7 @@ ToC:
|
||||
- [Setting Up Authentication with GitHub API](#setting-up-authentication-with-github-api)
|
||||
- [Deploying Using GitHub App Authentication](#deploying-using-github-app-authentication)
|
||||
- [Deploying Using PAT Authentication](#deploying-using-pat-authentication)
|
||||
- [Deploying Multiple Controllers](#deploying-multiple-controllers)
|
||||
- [Usage](#usage)
|
||||
- [Repository Runners](#repository-runners)
|
||||
- [Organization Runners](#organization-runners)
|
||||
@@ -27,6 +28,8 @@ ToC:
|
||||
- [Runner Labels](#runner-labels)
|
||||
- [Runner Groups](#runner-groups)
|
||||
- [Using IRSA (IAM Roles for Service Accounts) in EKS](#using-irsa-iam-roles-for-service-accounts-in-eks)
|
||||
- [Stateful Runners](#stateful-runners)
|
||||
- [Ephemeral Runners](#ephemeral-runners)
|
||||
- [Software Installed in the Runner Image](#software-installed-in-the-runner-image)
|
||||
- [Common Errors](#common-errors)
|
||||
- [Contributing](#contributing)
|
||||
@@ -43,7 +46,7 @@ actions-runner-controller uses [cert-manager](https://cert-manager.io/docs/insta
|
||||
|
||||
- [Installing cert-manager on Kubernetes](https://cert-manager.io/docs/installation/kubernetes/)
|
||||
|
||||
Install the custom resource and actions-runner-controller with `kubectl` or `helm`. This will create actions-runner-system namespace in your Kubernetes and deploy the required resources.
|
||||
Subsequent to this, install the custom resource definitions and actions-runner-controller with `kubectl` or `helm`. This will create actions-runner-system namespace in your Kubernetes and deploy the required resources.
|
||||
|
||||
**Kubectl Deployment:**
|
||||
|
||||
@@ -54,7 +57,7 @@ kubectl apply -f https://github.com/actions-runner-controller/actions-runner-con
|
||||
|
||||
**Helm Deployment:**
|
||||
|
||||
__**Note: For all configuration options for the Helm chart see the chart's [README](./charts/actions-runner-controller/README.md)
|
||||
Configure your values.yaml, see the chart's [README](./charts/actions-runner-controller/README.md) for the values documentation
|
||||
|
||||
```shell
|
||||
helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
|
||||
@@ -74,7 +77,7 @@ When deploying the solution for a GitHub Enterprise Server environment you need
|
||||
kubectl set env deploy controller-manager -c manager GITHUB_ENTERPRISE_URL=<GHEC/S URL> --namespace actions-runner-system
|
||||
```
|
||||
|
||||
__**Note: The repository maintainers do not have an enterprise environment (cloud or server). Support for the enterprise specific feature set is community driven and on a best effort basis. PRs from the community are welcomed to add features and maintain support.**__
|
||||
**_Note: The repository maintainers do not have an enterprise environment (cloud or server). Support for the enterprise specific feature set is community driven and on a best effort basis. PRs from the community are welcomed to add features and maintain support._**
|
||||
|
||||
## Setting Up Authentication with GitHub API
|
||||
|
||||
@@ -85,22 +88,23 @@ There are two ways for actions-runner-controller to authenticate with the GitHub
|
||||
|
||||
Functionality wise, there isn't much of a difference between the 2 authentication methods. The primarily benefit of authenticating via a GitHub App is an [increased API quota](https://docs.github.com/en/developers/apps/rate-limits-for-github-apps).
|
||||
|
||||
If you are deploying the solution for a GitHub Enterprise Server environment you are able to [configure your rate limiting settings](https://docs.github.com/en/enterprise-server@3.0/admin/configuration/configuring-rate-limits) making the main benefit irrelevant. If you're deploying the solution for a GitHub Enterprise Cloud or regular GitHub environment and you run into rate limiting issues, consider deploying the solution using the GitHub App authentication method instead.
|
||||
If you are deploying the solution for a GitHub Enterprise Server environment you are able to [configure your rate limit settings](https://docs.github.com/en/enterprise-server@3.0/admin/configuration/configuring-rate-limits) making the main benefit irrelevant. If you're deploying the solution for a GitHub Enterprise Cloud or regular GitHub environment and you run into rate limit issues, consider deploying the solution using the GitHub App authentication method instead.
|
||||
|
||||
### Deploying Using GitHub App Authentication
|
||||
|
||||
You can create a GitHub App for either your user account or any organization, below are the app permissions required for each supported type of runner:
|
||||
|
||||
_Note: Links are provided further down to create an app for your logged in user account or an organisation with the permissions for all runner types set in each link's query string_
|
||||
_Note: Links are provided further down to create an app for your logged in user account or an organization with the permissions for all runner types set in each link's query string_
|
||||
|
||||
**Required Permissions for Repository Runners:**<br />
|
||||
**Repository Permissions**
|
||||
|
||||
* Actions (read)
|
||||
* Administration (read / write)
|
||||
* Checks (read) (if you are going to use [Faster Autoscaling with GitHub Webhook](#faster-autoscaling-with-github-webhook))
|
||||
* Metadata (read)
|
||||
|
||||
**Required Permissions for Organisation Runners:**<br />
|
||||
**Required Permissions for Organization Runners:**<br />
|
||||
**Repository Permissions**
|
||||
|
||||
* Actions (read)
|
||||
@@ -109,6 +113,9 @@ _Note: Links are provided further down to create an app for your logged in user
|
||||
**Organization Permissions**
|
||||
* Self-hosted runners (read / write)
|
||||
|
||||
**Subscribe to events**
|
||||
* Check run (if you are going to use [Faster Autoscaling with GitHub Webhook](#faster-autoscaling-with-github-webhook))
|
||||
|
||||
_Note: All API routes mapped to their permissions can be found [here](https://docs.github.com/en/rest/reference/permissions-required-for-github-apps) if you wish to review_
|
||||
|
||||
---
|
||||
@@ -121,7 +128,7 @@ If you want to create a GitHub App for your account, open the following link to
|
||||
|
||||
If you want to create a GitHub App for your organization, replace the `:org` part of the following URL with your organization name before opening it. Then enter any unique name in the "GitHub App name" field, and hit the "Create GitHub App" button at the bottom of the page to create a GitHub App.
|
||||
|
||||
- [Create GitHub Apps on your organization](https://github.com/organizations/:org/settings/apps/new?url=http://github.com/actions-runner-controller/actions-runner-controller&webhook_active=false&public=false&administration=write&organization_self_hosted_runners=write&actions=read)
|
||||
- [Create GitHub Apps on your organization](https://github.com/organizations/:org/settings/apps/new?url=http://github.com/actions-runner-controller/actions-runner-controller&webhook_active=false&public=false&administration=write&organization_self_hosted_runners=write&actions=read&checks=read)
|
||||
|
||||
You will see an *App ID* on the page of the GitHub App you created as follows, the value of this App ID will be used later.
|
||||
|
||||
@@ -179,9 +186,9 @@ Log-in to a GitHub account that has `admin` privileges for the repository, and [
|
||||
|
||||
**Required Scopes for Enterprise Runners**
|
||||
|
||||
* enterprise:admin (Full control)
|
||||
* admin:enterprise (Full control)
|
||||
|
||||
_Note: When you deploy enterprise runners they will get access to organisations, however, access to the repositories themselves is **NOT** allowed by default. Each GitHub organisation must allow enterprise runner groups to be used in repositories as an initial one time configuration step, this only needs to be done once after which it is permanent for that runner group._
|
||||
_Note: When you deploy enterprise runners they will get access to organizations, however, access to the repositories themselves is **NOT** allowed by default. Each GitHub organization must allow enterprise runner groups to be used in repositories as an initial one time configuration step, this only needs to be done once after which it is permanent for that runner group._
|
||||
|
||||
---
|
||||
|
||||
@@ -199,6 +206,25 @@ kubectl create secret generic controller-manager \
|
||||
|
||||
Configure your values.yaml, see the chart's [README](./charts/actions-runner-controller/README.md) for deploying the secret via Helm
|
||||
|
||||
### Deploying Multiple Controllers
|
||||
|
||||
> This feature requires controller version => [v0.18.0](https://github.com/actions-runner-controller/actions-runner-controller/releases/tag/v0.18.0)
|
||||
|
||||
**_Note: Be aware when using this feature that CRDs are cluster wide and so you should upgrade all of your controllers (and your CRDs) as the same time if you are doing an upgrade. Do not mix and match CRD versions with different controller versions. Doing so risks out of control scaling._**
|
||||
|
||||
By default the controller will look for runners in all namespaces, the watch namespace feature allows you to restrict the controller to monitoring a single namespace. This then lets you deploy multiple controllers in a single cluster. You may want to do this either because you wish to scale beyond the API rate limit of a single PAT / GitHub App configuration or you wish to support multiple GitHub organizations with runners installed at the organization level in a single cluster.
|
||||
|
||||
This feature is configured via the controller `--watch-namespace` flag. When a namespace is provided via this flag the controller will only monitor runners in that namespace.
|
||||
|
||||
If you plan on installing all instances of the controller stack into a single namespace you will need to make the names of the resources are unique for each stack. In the case of Helm this can be done by giving each a unique release name, or via the `fullnameOverride` properties.
|
||||
|
||||
Alternatively, you can install each controller stack into its own unique namespace (relative to other controller stacks in the cluster), avoiding the need to uniquely prefix resources.
|
||||
|
||||
When you go to the route of sharing the namespace while giving each a unique Helm release name, you must also ensure the following values are configured correctly:
|
||||
|
||||
- `authSecret.name` needs be unique per stack when each stack is tied to runners in different GitHub organizations and repositories AND you want your GitHub credentials to narrowly scoped.
|
||||
- `leaderElectionId` needs to be unique per stack. If this is not unique to the stack the controller tries to race onto the leader election lock and resulting in only one stack working concurrently.
|
||||
|
||||
## Usage
|
||||
|
||||
[GitHub self-hosted runners can be deployed at various levels in a management hierarchy](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#about-self-hosted-runners):
|
||||
@@ -222,7 +248,7 @@ kind: Runner
|
||||
metadata:
|
||||
name: example-runner
|
||||
spec:
|
||||
repository: summerwind/actions-runner-controller
|
||||
repository: actions-runner-controller/actions-runner-controller
|
||||
env: []
|
||||
```
|
||||
|
||||
@@ -238,7 +264,7 @@ You can see that the Runner resource has been created.
|
||||
```shell
|
||||
$ kubectl get runners
|
||||
NAME REPOSITORY STATUS
|
||||
example-runner summerwind/actions-runner-controller Running
|
||||
example-runner actions-runner-controller/actions-runner-controller Running
|
||||
```
|
||||
|
||||
You can also see that the runner pod has been running.
|
||||
@@ -325,10 +351,7 @@ example-runnerdeploy2475ht2qbr mumoshu/actions-runner-controller-ci Running
|
||||
|
||||
##### Note on scaling to/from 0
|
||||
|
||||
> This is a documentation about a unreleased version of actions-runner-controller.
|
||||
>
|
||||
> It would be great if you could try building the latest controller image following https://github.com/actions-runner-controller/actions-runner-controller#contributing if you are eager to test it early and help
|
||||
> developers by reporting any bugs :smile:
|
||||
> This feature requires controller version => [v0.19.0](https://github.com/actions-runner-controller/actions-runner-controller/releases/tag/v0.19.0)
|
||||
|
||||
You can either delete the runner deployment, or update it to have `replicas: 0`, so that there will be 0 runner pods in the cluster. This, in combination with e.g. `cluster-autoscaler`, enables you to save your infrastructure cost when there's no need to run Actions jobs.
|
||||
|
||||
@@ -353,6 +376,8 @@ This, in combination with a correctly configured HorizontalRunnerAutoscaler, all
|
||||
|
||||
__**IMPORTANT : Due to limitations / a bug with GitHub's [routing engine](https://docs.github.com/en/actions/hosting-your-own-runners/using-self-hosted-runners-in-a-workflow#routing-precedence-for-self-hosted-runners) autoscaling does NOT work correctly with RunnerDeployments that target the enterprise level. Scaling activity works as expected however jobs fail to get assigned to the scaled out replicas. This was explored in issue [#470](https://github.com/actions-runner-controller/actions-runner-controller/issues/470). Once GitHub resolves the issue with their backend service we expect the solution to be able to support autoscaled enterprise runnerdeploments without any additional changes.**__
|
||||
|
||||
**NOTE: Once `workflow_job` webhook events are released on GitHub, the webhook-based autoscaling is the preferred way of autoscaling, because it is easy to configure and has the ability to accurately detect which runners to scale. See [Example 3: Scale on each `workflow_job` event](#example-3-scale-on-each-workflow_job-event)**
|
||||
|
||||
A `RunnerDeployment` (excluding enterprise runners) can scale the number of runners between `minReplicas` and `maxReplicas` fields based the chosen scaling metric as defined in the `metrics` attribute
|
||||
|
||||
**Scaling Metrics**
|
||||
@@ -374,7 +399,7 @@ The scale out performance is controlled via the manager containers startup `--sy
|
||||
**Drawbacks of this metric**
|
||||
1. Repositories must be named within the scaling metric, maintaining a list of repositories may not be viable in larger environments or self-serve environments.
|
||||
2. May not scale quick enough for some users needs. This metric is pull based and so the queue depth is polled as configured by the sync period, as a result scaling performance is bound by this sync period meaning there is a lag to scaling activity.
|
||||
3. Relatively large amounts of API requests required to maintain this metric, you may run in API rate limiting issues depending on the size of your environment and how aggressive your sync period configuration is
|
||||
3. Relatively large amounts of API requests required to maintain this metric, you may run in API rate limit issues depending on the size of your environment and how aggressive your sync period configuration is
|
||||
4. The GitHub API doesn't provide a way to filter workflow jobs to just those targeting self-hosted runners. If your environment's workflows target both self-hosted and GitHub hosted runners then the queue depth this metric scales against isn't a true 1:1 mapping of queue depth to required runner count. As a result of this, this metric may scale too aggressively for your actual self-hosted runner count needs.
|
||||
|
||||
|
||||
@@ -390,7 +415,7 @@ metadata:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
repository: summerwind/actions-runner-controller
|
||||
repository: actions-runner-controller/actions-runner-controller
|
||||
---
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: HorizontalRunnerAutoscaler
|
||||
@@ -404,7 +429,7 @@ spec:
|
||||
metrics:
|
||||
- type: TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||
repositoryNames:
|
||||
- summerwind/actions-runner-controller
|
||||
- actions-runner-controller/actions-runner-controller
|
||||
```
|
||||
|
||||
Additionally, the `HorizontalRunnerAutoscaler` also has an anti-flapping option that prevents periodic loop of scaling up and down.
|
||||
@@ -471,8 +496,8 @@ spec:
|
||||
- type: PercentageRunnersBusy
|
||||
scaleUpThreshold: '0.75' # The percentage of busy runners at which the number of desired runners are re-evaluated to scale up
|
||||
scaleDownThreshold: '0.3' # The percentage of busy runners at which the number of desired runners are re-evaluated to scale down
|
||||
ScaleUpAdjustment: '2' # The scale up runner count added to desired count
|
||||
ScaleDownAdjustment: '1' # The scale down runner count subtracted from the desired count
|
||||
scaleUpAdjustment: 2 # The scale up runner count added to desired count
|
||||
scaleDownAdjustment: 1 # The scale down runner count subtracted from the desired count
|
||||
```
|
||||
|
||||
Like the previous metric, the scale down factor respects the anti-flapping configuration is applied to the `HorizontalRunnerAutoscaler` as mentioned previously:
|
||||
@@ -521,7 +546,7 @@ immediately add "resource slack" for future GitHub Actions job runs.
|
||||
|
||||
In contrast, the standard autoscaling requires you to wait next sync period to add
|
||||
insufficient runners. You can definitely shorten the sync period to make the standard autoscaling more responsive.
|
||||
But doing so eventually result in the controller not functional due to GitHub API rate limit.
|
||||
But doing so eventually results in the controller not being functional due to it being rated limited by the GitHub API.
|
||||
|
||||
> You can learn the implementation details in [#282](https://github.com/actions-runner-controller/actions-runner-controller/pull/282)
|
||||
|
||||
@@ -572,10 +597,34 @@ spec:
|
||||
duration: "5m"
|
||||
```
|
||||
|
||||
To scale up replicas of the runners for `myorg` organization by 1 for 5 minutes on each `check_run`, you write manifests like the below:
|
||||
|
||||
```yaml
|
||||
kind: RunnerDeployment
|
||||
metadata:
|
||||
name: myrunners
|
||||
spec:
|
||||
organization: myorg
|
||||
---
|
||||
kind: HorizontalRunnerAutoscaler
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
name: myrunners
|
||||
scaleUpTriggers:
|
||||
- githubEvent:
|
||||
checkRun:
|
||||
types: ["created"]
|
||||
status: "queued"
|
||||
# allow only certain repositories within your organization to trigger autoscaling
|
||||
# repositories: ["myrepo", "myanotherrepo"]
|
||||
amount: 1
|
||||
duration: "5m"
|
||||
```
|
||||
|
||||
###### Example 2: Scale on each `pull_request` event against `develop` or `main` branches
|
||||
|
||||
```yaml
|
||||
kind: RunnerDeployment:
|
||||
kind: RunnerDeployment
|
||||
metadata:
|
||||
name: myrunners
|
||||
spec:
|
||||
@@ -596,13 +645,38 @@ spec:
|
||||
|
||||
See ["activity types"](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request) for the list of valid values for `scaleUpTriggers[].githubEvent.pullRequest.types`.
|
||||
|
||||
###### Example 3: Scale on each `workflow_job` event
|
||||
|
||||
> This feature depends on an unreleased GitHub feature
|
||||
|
||||
```yaml
|
||||
kind: RunnerDeployment
|
||||
metadata:
|
||||
name: myrunners
|
||||
spec:
|
||||
repository: example/myrepo
|
||||
---
|
||||
kind: HorizontalRunnerAutoscaler
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
name: myrunners
|
||||
scaleUpTriggers:
|
||||
- githubEvent: {}
|
||||
duration: "30m"
|
||||
```
|
||||
|
||||
You can configure your GitHub webhook settings to only include `Workflows Job` events, so that it sends us three kinds of `workflow_job` events per a job run.
|
||||
|
||||
Each kind has a `status` of `queued`, `in_progress` and `completed`.
|
||||
With the above configuration, `actions-runner-controller` adds one runner for a `workflow_job` event whose `status` is `queued`. Similarly, it removes one runner for a `workflow_job` event whose `status` is `completed`.
|
||||
|
||||
Beware that a scale-down after a scale-up is deferred until `scaleDownDelaySecondsAfterScaleOut` elapses. Let's say you had configured `scaleDownDelaySecondsAfterScaleOut` of 60 seconds, 2 consequtive workflow jobs will result in immediately adding 2 runners. The 2 runners are removed only after 60 seconds. This basically gives you 60 seconds of a "grace period" that makes it possible for self-hosted runners to immediately run additional workflow jobs enqueued in that 60 seconds.
|
||||
|
||||
You must not include `spec.metrics` like `PercentageRunnersBusy` when using this feature, as it is unnecessary. That is, if you've configured the webhook for `workflow_job`, it should be enough for all the scale-out need.
|
||||
|
||||
#### Autoscaling to/from 0
|
||||
|
||||
> This is a documentation about a unreleased version of actions-runner-controller.
|
||||
>
|
||||
> It would be great if you could try building the latest controller image following https://github.com/actions-runner-controller/actions-runner-controller#contributing if you are eager to test it early and help
|
||||
> developers by reporting any bugs :smile:
|
||||
> This feature requires controller version => [v0.19.0](https://github.com/actions-runner-controller/actions-runner-controller/releases/tag/v0.19.0)
|
||||
|
||||
Previously, we've discussed about [how to scale a RunnerDeployment to/from 0](#note-on-scaling-tofrom-0)
|
||||
|
||||
@@ -624,10 +698,7 @@ Similarly, Webhook-based autoscaling works regardless of there are active runner
|
||||
|
||||
#### Scheduled Overrides
|
||||
|
||||
> This is a documentation about a unreleased version of actions-runner-controller.
|
||||
>
|
||||
> It would be great if you could try building the latest controller image following https://github.com/actions-runner-controller/actions-runner-controller#contributing if you are eager to test it early and help
|
||||
> developers by reporting any bugs :smile:
|
||||
> This feature requires controller version => [v0.19.0](https://github.com/actions-runner-controller/actions-runner-controller/releases/tag/v0.19.0)
|
||||
|
||||
`Scheduled Overrides` allows you to configure HorizontalRunnerAutoscaler so that its Spec gets updated only during a certain period of time.
|
||||
|
||||
@@ -638,7 +709,7 @@ usually, this feature is used for following scenarios:
|
||||
|
||||
For the first scenario, you might consider configuration like the below:
|
||||
|
||||
```
|
||||
```yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: HorizontalRunnerAutoscaler
|
||||
metadata:
|
||||
@@ -659,7 +730,7 @@ spec:
|
||||
|
||||
For the second scenario, you might consider something like the below:
|
||||
|
||||
```
|
||||
```yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: HorizontalRunnerAutoscaler
|
||||
metadata:
|
||||
@@ -754,6 +825,9 @@ spec:
|
||||
operator: Exists
|
||||
|
||||
repository: mumoshu/actions-runner-controller-ci
|
||||
# The default "summerwind/actions-runner" images are available at DockerHub:
|
||||
# https://hub.docker.com/r/summerwind/actions-runner
|
||||
# You can also build your own and specify it like the below:
|
||||
image: custom-image/actions-runner:latest
|
||||
imagePullPolicy: Always
|
||||
resources:
|
||||
@@ -784,7 +858,7 @@ spec:
|
||||
# - https://mlohr.com/docker-mtu/
|
||||
dockerMTU: 1500
|
||||
# Optional Docker registry mirror
|
||||
# Docker Hub has enabled rate-limiting for free plans.
|
||||
# Docker Hub has an aggressive rate-limit configuration for free plans.
|
||||
# To avoid disruptions in your CI/CD pipelines, you might want to setup an external or on-premises Docker registry mirror.
|
||||
# More information:
|
||||
# - https://docs.docker.com/docker-hub/download-rate-limit/
|
||||
@@ -822,9 +896,31 @@ spec:
|
||||
hostPath:
|
||||
path: /mnt/docker-extra
|
||||
type: DirectoryOrCreate
|
||||
- name: repo
|
||||
hostPath:
|
||||
path: /mnt/repo
|
||||
type: DirectoryOrCreate
|
||||
dockerVolumeMounts:
|
||||
- mountPath: /var/lib/docker
|
||||
name: docker-extra
|
||||
# You can mount some of the shared volumes to the runner container using volumeMounts.
|
||||
# NOTE: Do not try to mount the volume onto the runner workdir itself as it will not work. You could mount it however on a sub directory in the runner workdir
|
||||
# Please see https://github.com/actions-runner-controller/actions-runner-controller/issues/630#issuecomment-862087323 for more information.
|
||||
volumeMounts:
|
||||
- mountPath: /home/runner/work/repo
|
||||
name: repo
|
||||
# Optional storage medium type of runner volume mount.
|
||||
# More info: https://kubernetes.io/docs/concepts/storage/volumes/#emptydir
|
||||
# "" (default) = Node's default medium
|
||||
# Memory = RAM-backed filesystem (tmpfs)
|
||||
# NOTE: Using RAM-backed filesystem gives you fastest possible storage on your host nodes.
|
||||
volumeStorageMedium: ""
|
||||
# Total amount of local storage resources required for runner volume mount.
|
||||
# The default limit is undefined.
|
||||
# NOTE: You can make sure that nodes' resources are never exceeded by limiting used storage size per runner pod.
|
||||
# You can even disable the runner mount completely by setting limit to zero if dockerdWithinRunnerContainer = true.
|
||||
# Please see https://github.com/actions-runner-controller/actions-runner-controller/pull/674 for more information.
|
||||
volumeSizeLimit: 4Gi
|
||||
# Optional name of the container runtime configuration that should be used for pods.
|
||||
# This must match the name of a RuntimeClass resource available on the cluster.
|
||||
# More info: https://kubernetes.io/docs/concepts/containers/runtime-class
|
||||
@@ -853,7 +949,7 @@ spec:
|
||||
replicas: 1
|
||||
template:
|
||||
spec:
|
||||
repository: summerwind/actions-runner-controller
|
||||
repository: actions-runner-controller/actions-runner-controller
|
||||
labels:
|
||||
- custom-runner
|
||||
```
|
||||
@@ -889,7 +985,7 @@ spec:
|
||||
|
||||
### Using IRSA (IAM Roles for Service Accounts) in EKS
|
||||
|
||||
`actions-runner-controller` v0.15.0 or later has support for IRSA in EKS.
|
||||
> This feature requires controller version => [v0.15.0](https://github.com/actions-runner-controller/actions-runner-controller/releases/tag/v0.15.0)
|
||||
|
||||
As similar as for regular pods and deployments, you firstly need an existing service account with the IAM role associated.
|
||||
Create one using e.g. `eksctl`. You can refer to [the EKS documentation](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) for more details.
|
||||
@@ -912,6 +1008,159 @@ spec:
|
||||
fsGroup: 1000
|
||||
```
|
||||
|
||||
|
||||
### Use with Istio
|
||||
|
||||
Istio 1.7.0 or greater has `holdApplicationUntilProxyStarts` added in https://github.com/istio/istio/pull/24737, which enables you to delay the `runner` container startup until the injected `istio-proxy` container finish starting. Try using it if you need to use Istio. Otherwise the runner is unlikely to work, because it fails to call any GitHub API to register itself due to `istio-proxy` being not up and running yet.
|
||||
|
||||
Note that there's no official Istio integration in actions-runner-controller. It should work, but it isn't covered by our acceptance test (a contribution to resolve this is welcomed). In addition to that, none of the actions-runner-controller maintainers use Istio daily. If you need more information, or have any issues using it, refer to the following links:
|
||||
|
||||
- https://github.com/actions-runner-controller/actions-runner-controller/issues/591
|
||||
- https://github.com/actions-runner-controller/actions-runner-controller/pull/592
|
||||
- https://github.com/istio/istio/issues/11130
|
||||
|
||||
### Stateful Runners
|
||||
|
||||
> This is a documentation about a unreleased version of actions-runner-controller.
|
||||
>
|
||||
> It would be great if you could try building the latest controller image following https://github.com/actions-runner-controller/actions-runner-controller#contributing if you are eager to test it early and help
|
||||
> developers by reporting any bugs :smile:
|
||||
|
||||
`actions-runner-controller` supports `RunnerSet` API that let you deploy stateful runners. A stateful runner is designed to be able to store some data persists across GitHub Actions workflow and job runs. You might find it useful, for example, to speed up your docker builds by persisting the docker layer cache.
|
||||
|
||||
A basic `RunnerSet` would look like this:
|
||||
|
||||
```yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: RunnerSet
|
||||
metadata:
|
||||
name: example
|
||||
spec:
|
||||
ephemeral: false
|
||||
replicas: 2
|
||||
repository: mumoshu/actions-runner-controller-ci
|
||||
# Other mandatory fields from StatefulSet
|
||||
selector:
|
||||
matchLabels:
|
||||
app: example
|
||||
serviceName: example
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: example
|
||||
```
|
||||
|
||||
As it is based on `StatefulSet`, `selector` and `template.medatada.labels` needs to be defined and have the exact same set of labels. `serviceName` must be set to some non-empty string as it is also required by `StatefulSet`.
|
||||
|
||||
Runner-related fields like `ephemeral`, `repository`, `organization`, `enterprise`, and so on should be written directly under `spec`.
|
||||
|
||||
Fields like `volumeClaimTemplates` that originates from `StatefulSet` should also be written directly under `spec`.
|
||||
|
||||
Pod-related fields like security contexts and volumes are written under `spec.template.spec` like `StatefulSet`.
|
||||
|
||||
Similarly, container-related fields like resource requests and limits, container image names and tags, security context, and so on are written under `spec.template.spec.containers`. There are two reserved container `name`, `runner` and `docker`. The former is for the container that runs [actions runner](https://github.com/actions/runner) and the latter is for the container that runs a dockerd.
|
||||
|
||||
For a more complex example, see the below:
|
||||
|
||||
```yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: RunnerSet
|
||||
metadata:
|
||||
name: example
|
||||
spec:
|
||||
# NOTE: RunnerSet supports non-ephemeral runners only today
|
||||
ephemeral: false
|
||||
replicas: 2
|
||||
repository: mumoshu/actions-runner-controller-ci
|
||||
dockerdWithinRunnerContainer: true
|
||||
template:
|
||||
spec:
|
||||
securityContext:
|
||||
#All level/role/type/user values will vary based on your SELinux policies.
|
||||
#See https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux_atomic_host/7/html/container_security_guide/docker_selinux_security_policy for information about SELinux with containers
|
||||
seLinuxOptions:
|
||||
level: "s0"
|
||||
role: "system_r"
|
||||
type: "super_t"
|
||||
user: "system_u"
|
||||
containers:
|
||||
- name: runner
|
||||
env: []
|
||||
resources:
|
||||
limits:
|
||||
cpu: "4.0"
|
||||
memory: "8Gi"
|
||||
requests:
|
||||
cpu: "2.0"
|
||||
memory: "4Gi"
|
||||
- name: docker
|
||||
resources:
|
||||
limits:
|
||||
cpu: "4.0"
|
||||
memory: "8Gi"
|
||||
requests:
|
||||
cpu: "2.0"
|
||||
memory: "4Gi"
|
||||
```
|
||||
|
||||
You can also read the design and usage documentation written in the original pull request that introduced `RunnerSet` for more information.
|
||||
|
||||
https://github.com/actions-runner-controller/actions-runner-controller/pull/629
|
||||
|
||||
Under the hood, `RunnerSet` relies on Kubernetes's `StatefulSet` and Mutating Webhook. A statefulset is used to create a number of pods that has stable names and dynamically provisioned persistent volumes, so that each statefulset-managed pod gets the same persistent volume even after restarting. A mutating webhook is used to dynamically inject a runner's "registration token" which is used to call GitHub's "Create Runner" API.
|
||||
|
||||
We envision that `RunnerSet` will eventually replace `RunnerDeployment`, as `RunnerSet` provides a more standard API that is easy to learn and use because it is based on `StatefulSet`, and it has a support for `volumeClaimTemplates` which is crucial to manage dynamically provisioned persistent volumes.
|
||||
|
||||
**Limitations**
|
||||
|
||||
A known down-side of `RunnerSet` compared to `RunnerDeployment` is that it is uanble to create [a registration-only pod on scaling-down to zero](https://github.com/actions-runner-controller/actions-runner-controller#note-on-scaling-tofrom-0). To workaround that, you need to create a `RunnerDeployment` with `spec.repliacs` set to `0` with `spec.repository`, `spec.organization`, or `spec.enterprise`, and `spec.labels` and `spec.groups` as same values as your `RunnerSet`, so that you can keep the registration-only runner regardless of the number of `RunnerSet`-managed runners.
|
||||
|
||||
A known down-side of relying on `StatefulSet` is that it misses a support for `maxUnavailable`.
|
||||
A `StatefulSet` basically works like `maxUnavailable: 1` in `Deployment`, which means that it can take down only one pod concurrently while doing a rolling-update of pods. Kubernetes 1.22 doesn't support customizing it yet so probably it takes more releases to arrive. See https://github.com/kubernetes/kubernetes/issues/68397 for more information.
|
||||
|
||||
### Ephemeral Runners
|
||||
|
||||
Both `RunnerDeployment` and `RunnerSet` has ability to configure `ephemeral: true` in the spec.
|
||||
|
||||
When it is configured, it passes a `--once` flag to every runner.
|
||||
|
||||
`--once` is an experimental `actions/runner` feature that instructs the runner to stop after the first job run. But it is a known race issue that may fetch a job even when it's being terminated. If a runner fetched a job while terminating, the job is very likely to fail because the terminating runner doesn't wait for the job to complete. This is tracked in #466.
|
||||
|
||||
> The below feature depends on an unreleased GitHub feature
|
||||
|
||||
GitHub seems to be adding an another flag called `--ephemeral` that is race-free. The pull request to add it to `actions/runner` can be found at https://github.com/actions/runner/pull/660.
|
||||
|
||||
`actions-runner-controller` has a feature flag backend by an environment variable to enable using `--ephemeral` instead of `--once`. The environment variable is `RUNNER_FEATURE_FLAG_EPHEMERAL`. You can se it to `true` on runner containers in your runner pods to enable the feature.
|
||||
|
||||
> At the time of writing this, you need to wait until GitHub rolls out the server-side feature for `--ephemeral`, AND you need to include your own `actions/runner` binary built from https://github.com/actions/runner/pull/660 into the runner container image to test this feature.
|
||||
>
|
||||
> Please see comments in [`runner/Dockerfile`](/runner/Dockerfile) for more information about how to build a custom image using your own `actions/runner` binary.
|
||||
|
||||
For example, a `RunnerSet` config with the flag enabled looks like:
|
||||
|
||||
```yaml
|
||||
kind: RunnerSet
|
||||
metadata:
|
||||
name: example-runnerset
|
||||
spec:
|
||||
# ...
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: example-runnerset
|
||||
spec:
|
||||
containers:
|
||||
- name: runner
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: RUNNER_FEATURE_FLAG_EPHEMERAL
|
||||
value: "true"
|
||||
```
|
||||
|
||||
Note that once https://github.com/actions/runner/pull/660 becomes generally available on GitHub, you no longer need to build a custom runner image to use this feature. Just set `RUNNER_FEATURE_FLAG_EPHEMERAL` and it should use `--ephemeral`.
|
||||
|
||||
In the future, `--once` might get removed in `actions/runner`. `actions-runner-controller` will make `--ephemeral` the default option for `ephemeral: true` runners until the legacy flag is removed.
|
||||
|
||||
### Software Installed in the Runner Image
|
||||
|
||||
**Cloud Tooling**<br />
|
||||
@@ -947,7 +1196,7 @@ kind: Runner
|
||||
metadata:
|
||||
name: custom-runner
|
||||
spec:
|
||||
repository: summerwind/actions-runner-controller
|
||||
repository: actions-runner-controller/actions-runner-controller
|
||||
image: YOUR_CUSTOM_DOCKER_IMAGE
|
||||
```
|
||||
|
||||
@@ -956,11 +1205,17 @@ spec:
|
||||
#### invalid header field value
|
||||
|
||||
```json
|
||||
2020-11-12T22:17:30.693Z ERROR controller-runtime.controller Reconciler error {"controller": "runner", "request": "actions-runner-system/runner-deployment-dk7q8-dk5c9", "error": "failed to create registration token: Post \"https://api.github.com/orgs/$YOUR_ORG_HERE/actions/runners/registration-token\": net/http: invalid header field value \"Bearer $YOUR_TOKEN_HERE\\n\" for key Authorization"}
|
||||
2020-11-12T22:17:30.693Z ERROR controller-runtime.controller Reconciler error
|
||||
{
|
||||
"controller": "runner",
|
||||
"request": "actions-runner-system/runner-deployment-dk7q8-dk5c9",
|
||||
"error": "failed to create registration token: Post \"https://api.github.com/orgs/$YOUR_ORG_HERE/actions/runners/registration-token\": net/http: invalid header field value \"Bearer $YOUR_TOKEN_HERE\\n\" for key Authorization"
|
||||
}
|
||||
```
|
||||
|
||||
**Solutions**<br />
|
||||
Your base64'ed PAT token has a new line at the end, it needs to be created without a `\n` added
|
||||
**Solution**
|
||||
|
||||
Your base64'ed PAT token has a new line at the end, it needs to be created without a `\n` added, either:
|
||||
* `echo -n $TOKEN | base64`
|
||||
* Create the secret as described in the docs using the shell and documented flags
|
||||
|
||||
@@ -984,25 +1239,14 @@ configuration script tries to communicate with the network.
|
||||
|
||||
**Solution**<br />
|
||||
|
||||
> This feature is experimental and will be dropped once maintainers think that
|
||||
> everyone has already migrated to use Istio's `holdApplicationUntilProxyStarts` ([istio/istio#11130](https://github.com/istio/istio/issues/11130)).
|
||||
>
|
||||
> Please read the discussion in #592 for more information.
|
||||
> Added originally to help users with older istio instances.
|
||||
> Newer Istio instances can use Istio's `holdApplicationUntilProxyStarts` attribute ([istio/istio#11130](https://github.com/istio/istio/issues/11130)) to avoid having to delay starting up the runner.
|
||||
> Please read the discussion in [#592](https://github.com/actions-runner-controller/actions-runner-controller/pull/592) for more information.
|
||||
|
||||
You can add a delay to the entrypoint script by setting the `STARTUP_DELAY` environment
|
||||
variable. This will cause the script to sleep `STARTUP_DELAY` seconds.
|
||||
_Note: Prior to the runner version v2.279.0, the environment variable referenced below was called `STARTUP_DELAY`._
|
||||
|
||||
*Example `Runner` with a 2 second startup delay:*
|
||||
```yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: Runner
|
||||
metadata:
|
||||
name: example-runner-with-sleep
|
||||
spec:
|
||||
env:
|
||||
- name: STARTUP_DELAY
|
||||
value: "2" # Remember! env var values must be strings.
|
||||
```
|
||||
You can add a delay to the runner's entrypoint script by setting the `STARTUP_DELAY_IN_SECONDS` environment
|
||||
variable for the runner pod. This will cause the script to sleep X seconds, this works with any runner kind.
|
||||
|
||||
*Example `RunnerDeployment` with a 2 second startup delay:*
|
||||
```yaml
|
||||
@@ -1014,145 +1258,10 @@ spec:
|
||||
template:
|
||||
spec:
|
||||
env:
|
||||
- name: STARTUP_DELAY
|
||||
- name: STARTUP_DELAY_IN_SECONDS
|
||||
value: "2" # Remember! env var values must be strings.
|
||||
```
|
||||
|
||||
|
||||
# Contributing
|
||||
|
||||
For more details about any requirements or process, please check out [Getting Started with Contributing](CONTRIBUTING.md).
|
||||
|
||||
**The Controller**<br />
|
||||
If you'd like to modify the controller to fork or contribute, I'd suggest using the following snippet for running
|
||||
the acceptance test:
|
||||
|
||||
```shell
|
||||
# This sets `VERSION` envvar to some appropriate value
|
||||
. hack/make-env.sh
|
||||
|
||||
DOCKER_USER=*** \
|
||||
GITHUB_TOKEN=*** \
|
||||
APP_ID=*** \
|
||||
PRIVATE_KEY_FILE_PATH=path/to/pem/file \
|
||||
INSTALLATION_ID=*** \
|
||||
make acceptance
|
||||
```
|
||||
|
||||
> **Notes for Ubuntu 20.04+ users**
|
||||
>
|
||||
> If you're using Ubuntu 20.04 or greater, you might have installed `docker` with `snap`.
|
||||
>
|
||||
> If you want to stick with `snap`-provided `docker`, do not forget to set `TMPDIR` to
|
||||
> somewhere under `$HOME`.
|
||||
> Otherwise `kind load docker-image` fail while running `docker save`.
|
||||
> See https://kind.sigs.k8s.io/docs/user/known-issues/#docker-installed-with-snap for more information.
|
||||
|
||||
Please follow the instructions explained in [Using Personal Access Token](#using-personal-access-token) to obtain
|
||||
`GITHUB_TOKEN`, and those in [Using GitHub App](#using-github-app) to obtain `APP_ID`, `INSTALLATION_ID`, and
|
||||
`PRIAVTE_KEY_FILE_PATH`.
|
||||
|
||||
The test creates a one-off `kind` cluster, deploys `cert-manager` and `actions-runner-controller`,
|
||||
creates a `RunnerDeployment` custom resource for a public Git repository to confirm that the
|
||||
controller is able to bring up a runner pod with the actions runner registration token installed.
|
||||
|
||||
**Rerunning a failed test**
|
||||
|
||||
When one of tests run by `make acceptance` failed, you'd probably like to rerun only the failed one.
|
||||
|
||||
It can be done by `make acceptance/run` and by setting the combination of `ACCEPTANCE_TEST_DEPLOYMENT_TOOL` and `ACCEPTANCE_TEST_SECRET_TYPE` values that failed.
|
||||
|
||||
In the example below, we rerun the test for the combination `ACCEPTANCE_TEST_DEPLOYMENT_TOOL=helm ACCEPTANCE_TEST_SECRET_TYPE=token` only:
|
||||
|
||||
```
|
||||
DOCKER_USER=*** \
|
||||
GITHUB_TOKEN=*** \
|
||||
APP_ID=*** \
|
||||
PRIVATE_KEY_FILE_PATH=path/to/pem/file \
|
||||
INSTALLATION_ID=*** \
|
||||
ACCEPTANCE_TEST_DEPLOYMENT_TOOL=helm ACCEPTANCE_TEST_SECRET_TYPE=token \
|
||||
make acceptance/run
|
||||
```
|
||||
|
||||
**Testing in a non-kind cluster**
|
||||
|
||||
If you prefer to test in a non-kind cluster, you can instead run:
|
||||
|
||||
```shell script
|
||||
KUBECONFIG=path/to/kubeconfig \
|
||||
DOCKER_USER=*** \
|
||||
GITHUB_TOKEN=*** \
|
||||
APP_ID=*** \
|
||||
PRIVATE_KEY_FILE_PATH=path/to/pem/file \
|
||||
INSTALLATION_ID=*** \
|
||||
ACCEPTANCE_TEST_SECRET_TYPE=token \
|
||||
make docker-build acceptance/setup \
|
||||
acceptance/deploy \
|
||||
acceptance/tests
|
||||
```
|
||||
|
||||
**Development Tips**
|
||||
|
||||
Rerunning the whole acceptance test suite from scratch on every little change to the controller, the runner, and the chart would be counter-productive.
|
||||
|
||||
To make your development cycle faster, use the below command to update deploy and update all the three:
|
||||
|
||||
```
|
||||
# Let assume we have all other envvars like DOCKER_USER, GITHUB_TOKEN already set,
|
||||
# The below command will (re)build `actions-runner-controller:controller1` and `actions-runner:runner1`,
|
||||
# load those into kind nodes, and then rerun kubectl or helm to install/upgrade the controller,
|
||||
# and finally upgrade the runner deployment to use the new runner image.
|
||||
#
|
||||
# As helm 3 and kubectl is unable to recreate a pod when no tag change,
|
||||
# you either need to bump VERSION and RUNNER_TAG on each run,
|
||||
# or manually run `kubectl delete pod $POD` on respective pods for changes to actually take effect.
|
||||
VERSION=controller1 \
|
||||
RUNNER_TAG=runner1 \
|
||||
make docker-build acceptance/load acceptance/deploy
|
||||
```
|
||||
|
||||
If you've already deployed actions-runner-controller and only want to recreate pods to use the newer image, you can run:
|
||||
|
||||
```
|
||||
NAME=$DOCKER_USER/actions-runner-controller \
|
||||
make docker-build acceptance/load && \
|
||||
kubectl -n actions-runner-system delete po $(kubectl -n actions-runner-system get po -ojsonpath={.items[*].metadata.name})
|
||||
```
|
||||
|
||||
Similarly, if you'd like to recreate runner pods with the newer runner image,
|
||||
|
||||
```
|
||||
NAME=$DOCKER_USER/actions-runner make \
|
||||
-C runner docker-{build,push}-ubuntu && \
|
||||
(kubectl get po -ojsonpath={.items[*].metadata.name} | xargs -n1 kubectl delete po)
|
||||
```
|
||||
|
||||
**Runner Tests**<br />
|
||||
A set of example pipelines (./acceptance/pipelines) are provided in this repository which you can use to validate your runners are working as expected. When raising a PR please run the relevant suites to prove your change hasn't broken anything.
|
||||
|
||||
**Running Ginkgo Tests**
|
||||
|
||||
You can run the integration test suite that is written in Ginkgo with:
|
||||
|
||||
```bash
|
||||
make test-with-deps
|
||||
```
|
||||
|
||||
This will firstly install a few binaries required to setup the integration test environment and then runs `go test` to start the Ginkgo test.
|
||||
|
||||
If you don't want to use `make`, like when you're running tests from your IDE, install required binaries to `/usr/local/kubebuilder/bin`. That's the directory in which controller-runtime's `envtest` framework locates the binaries.
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /usr/local/kubebuilder/bin
|
||||
make kube-apiserver etcd
|
||||
sudo mv test-assets/{etcd,kube-apiserver} /usr/local/kubebuilder/bin/
|
||||
go test -v -run TestAPIs github.com/summerwind/actions-runner-controller/controllers
|
||||
```
|
||||
|
||||
To run Ginkgo tests selectively, set the pattern of target test names to `GINKGO_FOCUS`.
|
||||
All the Ginkgo test that matches `GINKGO_FOCUS` will be run.
|
||||
|
||||
```bash
|
||||
GINKGO_FOCUS='[It] should create a new Runner resource from the specified template, add a another Runner on replicas increased, and removes all the replicas when set to 0' \
|
||||
go test -v -run TestAPIs github.com/summerwind/actions-runner-controller/controllers
|
||||
```
|
||||
For more details on contributing to the project (including requirements) please check out [Getting Started with Contributing](CONTRIBUTING.md).
|
||||
|
||||
@@ -1,32 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set +e
|
||||
|
||||
runner_name=
|
||||
repo_runnerdeployment_passed="skipped"
|
||||
repo_runnerset_passed="skipped"
|
||||
|
||||
while [ -z "${runner_name}" ]; do
|
||||
echo Finding the runner... 1>&2
|
||||
sleep 1
|
||||
runner_name=$(kubectl get runner --output=jsonpath="{.items[*].metadata.name}")
|
||||
done
|
||||
echo "Checking if RunnerDeployment repo test is set"
|
||||
if [ "${TEST_REPO}" ] && [ ! "${USE_RUNNERSET}" ]; then
|
||||
runner_name=
|
||||
count=0
|
||||
while [ $count -le 30 ]; do
|
||||
echo "Finding Runner ..."
|
||||
runner_name=$(kubectl get runner --output=jsonpath="{.items[*].metadata.name}")
|
||||
if [ "${runner_name}" ]; then
|
||||
while [ $count -le 30 ]; do
|
||||
runner_pod_name=
|
||||
echo "Found Runner \""${runner_name}"\""
|
||||
echo "Finding underlying pod ..."
|
||||
runner_pod_name=$(kubectl get pod --output=jsonpath="{.items[*].metadata.name}" | grep ${runner_name})
|
||||
if [ "${runner_pod_name}" ]; then
|
||||
echo "Found underlying pod \""${runner_pod_name}"\""
|
||||
echo "Waiting for pod \""${runner_pod_name}"\" to become ready..."
|
||||
kubectl wait pod/${runner_pod_name} --for condition=ready --timeout 270s
|
||||
break 2
|
||||
fi
|
||||
sleep 1
|
||||
let "count=count+1"
|
||||
done
|
||||
fi
|
||||
sleep 1
|
||||
let "count=count+1"
|
||||
done
|
||||
if [ $count -ge 30 ]; then
|
||||
repo_runnerdeployment_passed=false
|
||||
else
|
||||
repo_runnerdeployment_passed=true
|
||||
fi
|
||||
echo "Checking if RunnerSet repo test is set"
|
||||
elif [ "${TEST_REPO}" ] && [ "${USE_RUNNERSET}" ]; then
|
||||
runnerset_name=
|
||||
count=0
|
||||
while [ $count -le 30 ]; do
|
||||
echo "Finding RunnerSet ..."
|
||||
runnerset_name=$(kubectl get runnerset --output=jsonpath="{.items[*].metadata.name}")
|
||||
if [ "${runnerset_name}" ]; then
|
||||
while [ $count -le 30 ]; do
|
||||
runnerset_pod_name=
|
||||
echo "Found RunnerSet \""${runnerset_name}"\""
|
||||
echo "Finding underlying pod ..."
|
||||
runnerset_pod_name=$(kubectl get pod --output=jsonpath="{.items[*].metadata.name}" | grep ${runnerset_name})
|
||||
echo "BEFORE IF"
|
||||
if [ "${runnerset_pod_name}" ]; then
|
||||
echo "AFTER IF"
|
||||
echo "Found underlying pod \""${runnerset_pod_name}"\""
|
||||
echo "Waiting for pod \""${runnerset_pod_name}"\" to become ready..."
|
||||
kubectl wait pod/${runnerset_pod_name} --for condition=ready --timeout 270s
|
||||
break 2
|
||||
fi
|
||||
sleep 1
|
||||
let "count=count+1"
|
||||
done
|
||||
fi
|
||||
sleep 1
|
||||
let "count=count+1"
|
||||
done
|
||||
if [ $count -ge 30 ]; then
|
||||
repo_runnerset_passed=false
|
||||
else
|
||||
repo_runnerset_passed=true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo Found runner ${runner_name}.
|
||||
|
||||
# Wait a bit to make sure the runner pod is created before looking for it.
|
||||
sleep 2
|
||||
|
||||
pod_name=
|
||||
|
||||
while [ -z "${pod_name}" ]; do
|
||||
echo Finding the runner pod... 1>&2
|
||||
sleep 1
|
||||
pod_name=$(kubectl get pod --output=jsonpath="{.items[*].metadata.name}" | grep ${runner_name})
|
||||
done
|
||||
|
||||
echo Found pod ${pod_name}.
|
||||
|
||||
echo Waiting for pod ${runner_name} to become ready... 1>&2
|
||||
|
||||
kubectl wait pod/${runner_name} --for condition=ready --timeout 270s
|
||||
|
||||
echo All tests passed. 1>&2
|
||||
if [ ${repo_runnerset_passed} == true ] || [ ${repo_runnerset_passed} == "skipped" ] && \
|
||||
[ ${repo_runnerdeployment_passed} == true ] || [ ${repo_runnerdeployment_passed} == "skipped" ]; then
|
||||
echo "INFO : All tests passed or skipped"
|
||||
echo "RunnerSet Repo Test Status : ${repo_runnerset_passed}"
|
||||
echo "RunnerDeployment Repo Test Status : ${repo_runnerdeployment_passed}"
|
||||
else
|
||||
echo "ERROR : Some tests failed"
|
||||
echo "RunnerSet Repo Test Status : ${repo_runnerset_passed}"
|
||||
echo "RunnerDeployment Repo Test Status : ${repo_runnerdeployment_passed}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -35,6 +35,7 @@ if [ "${tool}" == "helm" ]; then
|
||||
--set image.repository=${NAME} \
|
||||
--set image.tag=${VERSION} \
|
||||
-f ${VALUES_FILE}
|
||||
kubectl apply -f charts/actions-runner-controller/crds
|
||||
kubectl -n actions-runner-system wait deploy/actions-runner-controller --for condition=available --timeout 60s
|
||||
else
|
||||
kubectl apply \
|
||||
@@ -46,9 +47,17 @@ fi
|
||||
# Adhocly wait for some time until actions-runner-controller's admission webhook gets ready
|
||||
sleep 20
|
||||
|
||||
RUNNER_LABEL=${RUNNER_LABEL:-self-hosted}
|
||||
|
||||
if [ -n "${TEST_REPO}" ]; then
|
||||
cat acceptance/testdata/runnerdeploy.yaml | envsubst | kubectl apply -f -
|
||||
cat acceptance/testdata/hra.yaml | envsubst | kubectl apply -f -
|
||||
if [ -n "USE_RUNNERSET" ]; then
|
||||
cat acceptance/testdata/repo.runnerset.yaml | envsubst | kubectl apply -f -
|
||||
cat acceptance/testdata/repo.runnerset.hra.yaml | envsubst | kubectl apply -f -
|
||||
else
|
||||
echo 'Deploying runnerdeployment and hra. Set USE_RUNNERSET if you want to deploy runnerset instead.'
|
||||
cat acceptance/testdata/repo.runnerdeploy.yaml | envsubst | kubectl apply -f -
|
||||
cat acceptance/testdata/repo.hra.yaml | envsubst | kubectl apply -f -
|
||||
fi
|
||||
else
|
||||
echo 'Skipped deploying runnerdeployment and hra. Set TEST_REPO to "yourorg/yourrepo" to deploy.'
|
||||
fi
|
||||
|
||||
1
acceptance/testdata/org.hra.yaml
vendored
1
acceptance/testdata/org.hra.yaml
vendored
@@ -24,6 +24,7 @@ spec:
|
||||
minReplicas: 0
|
||||
minReplicas: 0
|
||||
maxReplicas: 5
|
||||
# Used to test that HRA is working for org runners
|
||||
metrics:
|
||||
- type: PercentageRunnersBusy
|
||||
scaleUpThreshold: '0.75'
|
||||
|
||||
29
acceptance/testdata/repo.runnerset.hra.yaml
vendored
Normal file
29
acceptance/testdata/repo.runnerset.hra.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: HorizontalRunnerAutoscaler
|
||||
metadata:
|
||||
name: example-runnerset
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
kind: RunnerSet
|
||||
name: example-runnerset
|
||||
scaleUpTriggers:
|
||||
- githubEvent:
|
||||
checkRun:
|
||||
types: ["created"]
|
||||
status: "queued"
|
||||
amount: 1
|
||||
duration: "1m"
|
||||
# RunnerSet doesn't support scale from/to zero yet
|
||||
minReplicas: 1
|
||||
maxReplicas: 5
|
||||
# This should be less than 600(seconds, the default) for faster testing
|
||||
scaleDownDelaySecondsAfterScaleOut: 60
|
||||
metrics:
|
||||
- type: PercentageRunnersBusy
|
||||
scaleUpThreshold: '0.75'
|
||||
scaleDownThreshold: '0.3'
|
||||
scaleUpFactor: '2'
|
||||
scaleDownFactor: '0.5'
|
||||
- type: TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||
repositoryNames:
|
||||
- ${TEST_REPO}
|
||||
59
acceptance/testdata/repo.runnerset.yaml
vendored
Normal file
59
acceptance/testdata/repo.runnerset.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: RunnerSet
|
||||
metadata:
|
||||
name: example-runnerset
|
||||
spec:
|
||||
# MANDATORY because it is based on StatefulSet: Results in a below error when omitted:
|
||||
# missing required field "selector" in dev.summerwind.actions.v1alpha1.RunnerSet.spec
|
||||
selector:
|
||||
matchLabels:
|
||||
app: example-runnerset
|
||||
|
||||
# MANDATORY because it is based on StatefulSet: Results in a below error when omitted:
|
||||
# missing required field "serviceName" in dev.summerwind.actions.v1alpha1.RunnerSet.spec]
|
||||
serviceName: example-runnerset
|
||||
|
||||
#replicas: 1
|
||||
|
||||
# From my limited testing, `ephemeral: true` is more reliable.
|
||||
# Seomtimes, updating already deployed runners from `ephemeral: false` to `ephemeral: true` seems to
|
||||
# result in queued jobs hanging forever.
|
||||
ephemeral: ${TEST_EPHEMERAL}
|
||||
|
||||
repository: ${TEST_REPO}
|
||||
#
|
||||
# Custom runner image
|
||||
#
|
||||
image: ${RUNNER_NAME}:${RUNNER_TAG}
|
||||
#
|
||||
# dockerd within runner container
|
||||
#
|
||||
## Replace `mumoshu/actions-runner-dind:dev` with your dind image
|
||||
#dockerdWithinRunnerContainer: true
|
||||
#
|
||||
# Set the MTU used by dockerd-managed network interfaces (including docker-build-ubuntu)
|
||||
#
|
||||
#dockerMTU: 1450
|
||||
#Runner group
|
||||
# labels:
|
||||
# - "mylabel 1"
|
||||
# - "mylabel 2"
|
||||
labels:
|
||||
- "${RUNNER_LABEL}"
|
||||
#
|
||||
# Non-standard working directory
|
||||
#
|
||||
# workDir: "/"
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: example-runnerset
|
||||
spec:
|
||||
containers:
|
||||
- name: runner
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: RUNNER_FEATURE_FLAG_EPHEMERAL
|
||||
value: "${RUNNER_FEATURE_FLAG_EPHEMERAL}"
|
||||
#- name: docker
|
||||
# #image: mumoshu/actions-runner-dind:dev
|
||||
@@ -84,6 +84,10 @@ type CheckRunSpec struct {
|
||||
// Note that check_run name seem to equal to the job name you've defined in your actions workflow yaml file.
|
||||
// So it is very likely that you can utilize this to trigger depending on the job.
|
||||
Names []string `json:"names,omitempty"`
|
||||
|
||||
// Repositories is a list of GitHub repositories.
|
||||
// Any check_run event whose repository matches one of repositories in the list can trigger autoscaling.
|
||||
Repositories []string `json:"repositories,omitempty"`
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request
|
||||
@@ -106,6 +110,12 @@ type CapacityReservation struct {
|
||||
}
|
||||
|
||||
type ScaleTargetRef struct {
|
||||
// Kind is the type of resource being referenced
|
||||
// +optional
|
||||
// +kubebuilder:validation:Enum=RunnerDeployment;RunnerSet
|
||||
Kind string `json:"kind,omitempty"`
|
||||
|
||||
// Name is the name of resource being referenced
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
@@ -217,6 +227,7 @@ type CacheEntry struct {
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:shortName=hra
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:JSONPath=".spec.minReplicas",name=Min,type=number
|
||||
// +kubebuilder:printcolumn:JSONPath=".spec.maxReplicas",name=Max,type=number
|
||||
|
||||
@@ -27,6 +27,11 @@ import (
|
||||
|
||||
// RunnerSpec defines the desired state of Runner
|
||||
type RunnerSpec struct {
|
||||
RunnerConfig `json:",inline"`
|
||||
RunnerPodSpec `json:",inline"`
|
||||
}
|
||||
|
||||
type RunnerConfig struct {
|
||||
// +optional
|
||||
// +kubebuilder:validation:Pattern=`^[^/]+$`
|
||||
Enterprise string `json:"enterprise,omitempty"`
|
||||
@@ -48,53 +53,12 @@ type RunnerSpec struct {
|
||||
// +optional
|
||||
Ephemeral *bool `json:"ephemeral,omitempty"`
|
||||
|
||||
// +optional
|
||||
Containers []corev1.Container `json:"containers,omitempty"`
|
||||
// +optional
|
||||
DockerdContainerResources corev1.ResourceRequirements `json:"dockerdContainerResources,omitempty"`
|
||||
// +optional
|
||||
DockerVolumeMounts []corev1.VolumeMount `json:"dockerVolumeMounts,omitempty"`
|
||||
// +optional
|
||||
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
|
||||
// +optional
|
||||
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
|
||||
// +optional
|
||||
EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"`
|
||||
|
||||
// +optional
|
||||
Image string `json:"image"`
|
||||
// +optional
|
||||
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
|
||||
// +optional
|
||||
Env []corev1.EnvVar `json:"env,omitempty"`
|
||||
|
||||
// +optional
|
||||
Volumes []corev1.Volume `json:"volumes,omitempty"`
|
||||
// +optional
|
||||
WorkDir string `json:"workDir,omitempty"`
|
||||
|
||||
// +optional
|
||||
InitContainers []corev1.Container `json:"initContainers,omitempty"`
|
||||
// +optional
|
||||
SidecarContainers []corev1.Container `json:"sidecarContainers,omitempty"`
|
||||
// +optional
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
|
||||
// +optional
|
||||
ServiceAccountName string `json:"serviceAccountName,omitempty"`
|
||||
// +optional
|
||||
AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty"`
|
||||
// +optional
|
||||
SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"`
|
||||
// +optional
|
||||
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||
// +optional
|
||||
Affinity *corev1.Affinity `json:"affinity,omitempty"`
|
||||
// +optional
|
||||
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
|
||||
// +optional
|
||||
EphemeralContainers []corev1.EphemeralContainer `json:"ephemeralContainers,omitempty"`
|
||||
// +optional
|
||||
TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"`
|
||||
// +optional
|
||||
DockerdWithinRunnerContainer *bool `json:"dockerdWithinRunnerContainer,omitempty"`
|
||||
// +optional
|
||||
@@ -104,14 +68,86 @@ type RunnerSpec struct {
|
||||
// +optional
|
||||
DockerRegistryMirror *string `json:"dockerRegistryMirror,omitempty"`
|
||||
// +optional
|
||||
HostAliases []corev1.HostAlias `json:"hostAliases,omitempty"`
|
||||
// +optional
|
||||
VolumeSizeLimit *resource.Quantity `json:"volumeSizeLimit,omitempty"`
|
||||
// +optional
|
||||
VolumeStorageMedium *string `json:"volumeStorageMedium,omitempty"`
|
||||
}
|
||||
|
||||
// RunnerPodSpec defines the desired pod spec fields of the runner pod
|
||||
type RunnerPodSpec struct {
|
||||
// +optional
|
||||
DockerdContainerResources corev1.ResourceRequirements `json:"dockerdContainerResources,omitempty"`
|
||||
|
||||
// +optional
|
||||
DockerVolumeMounts []corev1.VolumeMount `json:"dockerVolumeMounts,omitempty"`
|
||||
|
||||
// +optional
|
||||
Containers []corev1.Container `json:"containers,omitempty"`
|
||||
|
||||
// +optional
|
||||
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
|
||||
|
||||
// +optional
|
||||
Env []corev1.EnvVar `json:"env,omitempty"`
|
||||
|
||||
// +optional
|
||||
EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"`
|
||||
|
||||
// +optional
|
||||
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
|
||||
|
||||
// +optional
|
||||
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
|
||||
|
||||
// +optional
|
||||
Volumes []corev1.Volume `json:"volumes,omitempty"`
|
||||
|
||||
// +optional
|
||||
EnableServiceLinks *bool `json:"enableServiceLinks,omitempty"`
|
||||
|
||||
// +optional
|
||||
InitContainers []corev1.Container `json:"initContainers,omitempty"`
|
||||
|
||||
// +optional
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
|
||||
|
||||
// +optional
|
||||
ServiceAccountName string `json:"serviceAccountName,omitempty"`
|
||||
|
||||
// +optional
|
||||
AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty"`
|
||||
|
||||
// +optional
|
||||
SidecarContainers []corev1.Container `json:"sidecarContainers,omitempty"`
|
||||
|
||||
// +optional
|
||||
SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"`
|
||||
|
||||
// +optional
|
||||
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||
|
||||
// +optional
|
||||
Affinity *corev1.Affinity `json:"affinity,omitempty"`
|
||||
|
||||
// +optional
|
||||
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
|
||||
|
||||
// +optional
|
||||
TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"`
|
||||
|
||||
// +optional
|
||||
EphemeralContainers []corev1.EphemeralContainer `json:"ephemeralContainers,omitempty"`
|
||||
|
||||
// +optional
|
||||
HostAliases []corev1.HostAlias `json:"hostAliases,omitempty"`
|
||||
|
||||
// RuntimeClassName is the container runtime configuration that containers should run under.
|
||||
// More info: https://kubernetes.io/docs/concepts/containers/runtime-class
|
||||
// +optional
|
||||
RuntimeClassName *string `json:"runtimeClassName,omitempty"`
|
||||
|
||||
// +optional
|
||||
DnsConfig []corev1.PodDNSConfig `json:"dnsConfig,omitempty"`
|
||||
}
|
||||
|
||||
// ValidateRepository validates repository field.
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ func (r *Runner) SetupWebhookWithManager(mgr ctrl.Manager) error {
|
||||
Complete()
|
||||
}
|
||||
|
||||
// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=mutate.runner.actions.summerwind.dev,sideEffects=None
|
||||
// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=mutate.runner.actions.summerwind.dev,sideEffects=None,admissionReviewVersions=v1beta1
|
||||
|
||||
var _ webhook.Defaulter = &Runner{}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (r *Runner) Default() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=validate.runner.actions.summerwind.dev,sideEffects=None
|
||||
// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=validate.runner.actions.summerwind.dev,sideEffects=None,admissionReviewVersions=v1beta1
|
||||
|
||||
var _ webhook.Validator = &Runner{}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ type RunnerDeploymentStatus struct {
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:shortName=rdeploy
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:JSONPath=".spec.replicas",name=Desired,type=number
|
||||
// +kubebuilder:printcolumn:JSONPath=".status.replicas",name=Current,type=number
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ func (r *RunnerDeployment) SetupWebhookWithManager(mgr ctrl.Manager) error {
|
||||
Complete()
|
||||
}
|
||||
|
||||
// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=mutate.runnerdeployment.actions.summerwind.dev
|
||||
// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=mutate.runnerdeployment.actions.summerwind.dev,sideEffects=None,admissionReviewVersions=v1beta1
|
||||
|
||||
var _ webhook.Defaulter = &RunnerDeployment{}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (r *RunnerDeployment) Default() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=validate.runnerdeployment.actions.summerwind.dev
|
||||
// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=validate.runnerdeployment.actions.summerwind.dev,sideEffects=None,admissionReviewVersions=v1beta1
|
||||
|
||||
var _ webhook.Validator = &RunnerDeployment{}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ type RunnerTemplate struct {
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:shortName=rrs
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:JSONPath=".spec.replicas",name=Desired,type=number
|
||||
// +kubebuilder:printcolumn:JSONPath=".status.replicas",name=Current,type=number
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ func (r *RunnerReplicaSet) SetupWebhookWithManager(mgr ctrl.Manager) error {
|
||||
Complete()
|
||||
}
|
||||
|
||||
// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=mutate.runnerreplicaset.actions.summerwind.dev,sideEffects=None
|
||||
// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=mutate.runnerreplicaset.actions.summerwind.dev,sideEffects=None,admissionReviewVersions=v1beta1
|
||||
|
||||
var _ webhook.Defaulter = &RunnerReplicaSet{}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (r *RunnerReplicaSet) Default() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=validate.runnerreplicaset.actions.summerwind.dev,sideEffects=None
|
||||
// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=validate.runnerreplicaset.actions.summerwind.dev,sideEffects=None,admissionReviewVersions=v1beta1
|
||||
|
||||
var _ webhook.Validator = &RunnerReplicaSet{}
|
||||
|
||||
|
||||
88
api/v1alpha1/runnerset_types.go
Normal file
88
api/v1alpha1/runnerset_types.go
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright 2021 The actions-runner-controller authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// RunnerSetSpec defines the desired state of RunnerSet
|
||||
type RunnerSetSpec struct {
|
||||
RunnerConfig `json:",inline"`
|
||||
|
||||
appsv1.StatefulSetSpec `json:",inline"`
|
||||
}
|
||||
|
||||
type RunnerSetStatus struct {
|
||||
// See K8s deployment controller code for reference
|
||||
// https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/pkg/controller/deployment/sync.go#L487-L505
|
||||
|
||||
// AvailableReplicas is the total number of available runners which have been successfully registered to GitHub and still running.
|
||||
// This corresponds to the sum of status.availableReplicas of all the runner replica sets.
|
||||
// +optional
|
||||
CurrentReplicas *int `json:"availableReplicas"`
|
||||
|
||||
// ReadyReplicas is the total number of available runners which have been successfully registered to GitHub and still running.
|
||||
// This corresponds to the sum of status.readyReplicas of all the runner replica sets.
|
||||
// +optional
|
||||
ReadyReplicas *int `json:"readyReplicas"`
|
||||
|
||||
// ReadyReplicas is the total number of available runners which have been successfully registered to GitHub and still running.
|
||||
// This corresponds to status.replicas of the runner replica set that has the desired template hash.
|
||||
// +optional
|
||||
UpdatedReplicas *int `json:"updatedReplicas"`
|
||||
|
||||
// DesiredReplicas is the total number of desired, non-terminated and latest pods to be set for the primary RunnerSet
|
||||
// This doesn't include outdated pods while upgrading the deployment and replacing the runnerset.
|
||||
// +optional
|
||||
DesiredReplicas *int `json:"desiredReplicas"`
|
||||
|
||||
// Replicas is the total number of replicas
|
||||
// +optional
|
||||
Replicas *int `json:"replicas"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:JSONPath=".spec.replicas",name=Desired,type=number
|
||||
// +kubebuilder:printcolumn:JSONPath=".status.replicas",name=Current,type=number
|
||||
// +kubebuilder:printcolumn:JSONPath=".status.updatedReplicas",name=Up-To-Date,type=number
|
||||
// +kubebuilder:printcolumn:JSONPath=".status.availableReplicas",name=Available,type=number
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
|
||||
// RunnerSet is the Schema for the runnersets API
|
||||
type RunnerSet struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec RunnerSetSpec `json:"spec,omitempty"`
|
||||
Status RunnerSetStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// RunnerList contains a list of Runner
|
||||
type RunnerSetList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []RunnerSet `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&RunnerSet{}, &RunnerSetList{})
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
@@ -71,6 +72,11 @@ func (in *CheckRunSpec) DeepCopyInto(out *CheckRunSpec) {
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Repositories != nil {
|
||||
in, out := &in.Repositories, &out.Repositories
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheckRunSpec.
|
||||
@@ -370,6 +376,61 @@ func (in *Runner) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerConfig) DeepCopyInto(out *RunnerConfig) {
|
||||
*out = *in
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Ephemeral != nil {
|
||||
in, out := &in.Ephemeral, &out.Ephemeral
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.DockerdWithinRunnerContainer != nil {
|
||||
in, out := &in.DockerdWithinRunnerContainer, &out.DockerdWithinRunnerContainer
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.DockerEnabled != nil {
|
||||
in, out := &in.DockerEnabled, &out.DockerEnabled
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.DockerMTU != nil {
|
||||
in, out := &in.DockerMTU, &out.DockerMTU
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
if in.DockerRegistryMirror != nil {
|
||||
in, out := &in.DockerRegistryMirror, &out.DockerRegistryMirror
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.VolumeSizeLimit != nil {
|
||||
in, out := &in.VolumeSizeLimit, &out.VolumeSizeLimit
|
||||
x := (*in).DeepCopy()
|
||||
*out = &x
|
||||
}
|
||||
if in.VolumeStorageMedium != nil {
|
||||
in, out := &in.VolumeStorageMedium, &out.VolumeStorageMedium
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerConfig.
|
||||
func (in *RunnerConfig) DeepCopy() *RunnerConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RunnerConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerDeployment) DeepCopyInto(out *RunnerDeployment) {
|
||||
*out = *in
|
||||
@@ -527,6 +588,149 @@ func (in *RunnerList) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerPodSpec) DeepCopyInto(out *RunnerPodSpec) {
|
||||
*out = *in
|
||||
in.DockerdContainerResources.DeepCopyInto(&out.DockerdContainerResources)
|
||||
if in.DockerVolumeMounts != nil {
|
||||
in, out := &in.DockerVolumeMounts, &out.DockerVolumeMounts
|
||||
*out = make([]v1.VolumeMount, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Containers != nil {
|
||||
in, out := &in.Containers, &out.Containers
|
||||
*out = make([]v1.Container, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Env != nil {
|
||||
in, out := &in.Env, &out.Env
|
||||
*out = make([]v1.EnvVar, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.EnvFrom != nil {
|
||||
in, out := &in.EnvFrom, &out.EnvFrom
|
||||
*out = make([]v1.EnvFromSource, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
in.Resources.DeepCopyInto(&out.Resources)
|
||||
if in.VolumeMounts != nil {
|
||||
in, out := &in.VolumeMounts, &out.VolumeMounts
|
||||
*out = make([]v1.VolumeMount, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Volumes != nil {
|
||||
in, out := &in.Volumes, &out.Volumes
|
||||
*out = make([]v1.Volume, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.EnableServiceLinks != nil {
|
||||
in, out := &in.EnableServiceLinks, &out.EnableServiceLinks
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.InitContainers != nil {
|
||||
in, out := &in.InitContainers, &out.InitContainers
|
||||
*out = make([]v1.Container, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.NodeSelector != nil {
|
||||
in, out := &in.NodeSelector, &out.NodeSelector
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.AutomountServiceAccountToken != nil {
|
||||
in, out := &in.AutomountServiceAccountToken, &out.AutomountServiceAccountToken
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.SidecarContainers != nil {
|
||||
in, out := &in.SidecarContainers, &out.SidecarContainers
|
||||
*out = make([]v1.Container, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.SecurityContext != nil {
|
||||
in, out := &in.SecurityContext, &out.SecurityContext
|
||||
*out = new(v1.PodSecurityContext)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ImagePullSecrets != nil {
|
||||
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
|
||||
*out = make([]v1.LocalObjectReference, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Affinity != nil {
|
||||
in, out := &in.Affinity, &out.Affinity
|
||||
*out = new(v1.Affinity)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Tolerations != nil {
|
||||
in, out := &in.Tolerations, &out.Tolerations
|
||||
*out = make([]v1.Toleration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.TerminationGracePeriodSeconds != nil {
|
||||
in, out := &in.TerminationGracePeriodSeconds, &out.TerminationGracePeriodSeconds
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
if in.EphemeralContainers != nil {
|
||||
in, out := &in.EphemeralContainers, &out.EphemeralContainers
|
||||
*out = make([]v1.EphemeralContainer, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.HostAliases != nil {
|
||||
in, out := &in.HostAliases, &out.HostAliases
|
||||
*out = make([]v1.HostAlias, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.RuntimeClassName != nil {
|
||||
in, out := &in.RuntimeClassName, &out.RuntimeClassName
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.DnsConfig != nil {
|
||||
in, out := &in.DnsConfig, &out.DnsConfig
|
||||
*out = make([]v1.PodDNSConfig, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerPodSpec.
|
||||
func (in *RunnerPodSpec) DeepCopy() *RunnerPodSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RunnerPodSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerReplicaSet) DeepCopyInto(out *RunnerReplicaSet) {
|
||||
*out = *in
|
||||
@@ -642,160 +846,127 @@ func (in *RunnerReplicaSetStatus) DeepCopy() *RunnerReplicaSetStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerSet) DeepCopyInto(out *RunnerSet) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerSet.
|
||||
func (in *RunnerSet) DeepCopy() *RunnerSet {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RunnerSet)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *RunnerSet) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerSetList) DeepCopyInto(out *RunnerSetList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]RunnerSet, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerSetList.
|
||||
func (in *RunnerSetList) DeepCopy() *RunnerSetList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RunnerSetList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *RunnerSetList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerSetSpec) DeepCopyInto(out *RunnerSetSpec) {
|
||||
*out = *in
|
||||
in.RunnerConfig.DeepCopyInto(&out.RunnerConfig)
|
||||
in.StatefulSetSpec.DeepCopyInto(&out.StatefulSetSpec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerSetSpec.
|
||||
func (in *RunnerSetSpec) DeepCopy() *RunnerSetSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RunnerSetSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerSetStatus) DeepCopyInto(out *RunnerSetStatus) {
|
||||
*out = *in
|
||||
if in.CurrentReplicas != nil {
|
||||
in, out := &in.CurrentReplicas, &out.CurrentReplicas
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
if in.ReadyReplicas != nil {
|
||||
in, out := &in.ReadyReplicas, &out.ReadyReplicas
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
if in.UpdatedReplicas != nil {
|
||||
in, out := &in.UpdatedReplicas, &out.UpdatedReplicas
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
if in.DesiredReplicas != nil {
|
||||
in, out := &in.DesiredReplicas, &out.DesiredReplicas
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
if in.Replicas != nil {
|
||||
in, out := &in.Replicas, &out.Replicas
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerSetStatus.
|
||||
func (in *RunnerSetStatus) DeepCopy() *RunnerSetStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RunnerSetStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerSpec) DeepCopyInto(out *RunnerSpec) {
|
||||
*out = *in
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Ephemeral != nil {
|
||||
in, out := &in.Ephemeral, &out.Ephemeral
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.Containers != nil {
|
||||
in, out := &in.Containers, &out.Containers
|
||||
*out = make([]v1.Container, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
in.DockerdContainerResources.DeepCopyInto(&out.DockerdContainerResources)
|
||||
if in.DockerVolumeMounts != nil {
|
||||
in, out := &in.DockerVolumeMounts, &out.DockerVolumeMounts
|
||||
*out = make([]v1.VolumeMount, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
in.Resources.DeepCopyInto(&out.Resources)
|
||||
if in.VolumeMounts != nil {
|
||||
in, out := &in.VolumeMounts, &out.VolumeMounts
|
||||
*out = make([]v1.VolumeMount, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.EnvFrom != nil {
|
||||
in, out := &in.EnvFrom, &out.EnvFrom
|
||||
*out = make([]v1.EnvFromSource, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Env != nil {
|
||||
in, out := &in.Env, &out.Env
|
||||
*out = make([]v1.EnvVar, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Volumes != nil {
|
||||
in, out := &in.Volumes, &out.Volumes
|
||||
*out = make([]v1.Volume, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.InitContainers != nil {
|
||||
in, out := &in.InitContainers, &out.InitContainers
|
||||
*out = make([]v1.Container, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.SidecarContainers != nil {
|
||||
in, out := &in.SidecarContainers, &out.SidecarContainers
|
||||
*out = make([]v1.Container, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.NodeSelector != nil {
|
||||
in, out := &in.NodeSelector, &out.NodeSelector
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.AutomountServiceAccountToken != nil {
|
||||
in, out := &in.AutomountServiceAccountToken, &out.AutomountServiceAccountToken
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.SecurityContext != nil {
|
||||
in, out := &in.SecurityContext, &out.SecurityContext
|
||||
*out = new(v1.PodSecurityContext)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ImagePullSecrets != nil {
|
||||
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
|
||||
*out = make([]v1.LocalObjectReference, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Affinity != nil {
|
||||
in, out := &in.Affinity, &out.Affinity
|
||||
*out = new(v1.Affinity)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Tolerations != nil {
|
||||
in, out := &in.Tolerations, &out.Tolerations
|
||||
*out = make([]v1.Toleration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.EphemeralContainers != nil {
|
||||
in, out := &in.EphemeralContainers, &out.EphemeralContainers
|
||||
*out = make([]v1.EphemeralContainer, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.TerminationGracePeriodSeconds != nil {
|
||||
in, out := &in.TerminationGracePeriodSeconds, &out.TerminationGracePeriodSeconds
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
if in.DockerdWithinRunnerContainer != nil {
|
||||
in, out := &in.DockerdWithinRunnerContainer, &out.DockerdWithinRunnerContainer
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.DockerEnabled != nil {
|
||||
in, out := &in.DockerEnabled, &out.DockerEnabled
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.DockerMTU != nil {
|
||||
in, out := &in.DockerMTU, &out.DockerMTU
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
if in.DockerRegistryMirror != nil {
|
||||
in, out := &in.DockerRegistryMirror, &out.DockerRegistryMirror
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.HostAliases != nil {
|
||||
in, out := &in.HostAliases, &out.HostAliases
|
||||
*out = make([]v1.HostAlias, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.VolumeSizeLimit != nil {
|
||||
in, out := &in.VolumeSizeLimit, &out.VolumeSizeLimit
|
||||
x := (*in).DeepCopy()
|
||||
*out = &x
|
||||
}
|
||||
if in.RuntimeClassName != nil {
|
||||
in, out := &in.RuntimeClassName, &out.RuntimeClassName
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
in.RunnerConfig.DeepCopyInto(&out.RunnerConfig)
|
||||
in.RunnerPodSpec.DeepCopyInto(&out.RunnerPodSpec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerSpec.
|
||||
|
||||
@@ -15,20 +15,16 @@ type: application
|
||||
# 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.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.11.0
|
||||
version: 0.13.0
|
||||
|
||||
# Used as the default manager tag value when no tag property is provided in the values.yaml
|
||||
appVersion: 0.18.2
|
||||
appVersion: 0.20.0
|
||||
|
||||
home: https://github.com/summerwind/actions-runner-controller
|
||||
home: https://github.com/actions-runner-controller/actions-runner-controller
|
||||
|
||||
sources:
|
||||
- https://github.com/summerwind/actions-runner-controller
|
||||
- https://github.com/actions-runner-controller/actions-runner-controller
|
||||
|
||||
maintainers:
|
||||
- name: summerwind
|
||||
email: contact@summerwind.jp
|
||||
url: https://github.com/summerwind
|
||||
- name: funkypenguin
|
||||
email: davidy@funkypenguin.co.nz
|
||||
url: https://www.funkypenguin.co.nz
|
||||
- name: actions-runner-controller
|
||||
url: https://github.com/actions-runner-controller
|
||||
|
||||
@@ -14,15 +14,18 @@ _Default values are the defaults set in the charts values.yaml, some properties
|
||||
| `replicaCount` | Set the number of controller pods | 1 |
|
||||
| `syncPeriod` | Set the period in which the controler reconciles the desired runners count | 10m |
|
||||
| `githubAPICacheDuration` | Set the cache period for API calls | |
|
||||
| `githubEnterpriseServerURL` | Set the URL for a self-hosted GitHub Enterprise Server | |
|
||||
| `logLevel` | Set the log level of the controller container | |
|
||||
| `authSecret.create` | Deploy the controller auth secret | true |
|
||||
| `authSecret.create` | Deploy the controller auth secret | false |
|
||||
| `authSecret.name` | Set the name of the auth secret | controller-manager |
|
||||
| `authSecret.github_app_id` | The ID of your GitHub App. **This can't be set at the same time as `authSecret.github_token`** | |
|
||||
| `authSecret.github_app_installation_id` | The ID of your GitHub App installation. **This can't be set at the same time as `authSecret.github_token`** | |
|
||||
| `authSecret.github_app_private_key` | The multiline string of your GitHub App's private key. **This can't be set at the same time as `authSecret.github_token`** | |
|
||||
| `authSecret.github_token` | Your chosen GitHub PAT token. **This can't be set at the same time as the `authSecret.github_app_*`** | |
|
||||
| `dockerRegistryMirror` | The default Docker Registry Mirror used by runners. |
|
||||
| `image.repository` | The "repository/image" of the controller container | summerwind/actions-runner-controller |
|
||||
| `image.tag` | The tag of the controller container | |
|
||||
| `image.actionsRunnerRepositoryAndTag` | The "repository/image" of the actions runner container | summerwind/actions-runner:latest |
|
||||
| `image.dindSidecarRepositoryAndTag` | The "repository/image" of the dind sidecar container | docker:dind |
|
||||
| `image.pullPolicy` | The pull policy of the controller image | IfNotPresent |
|
||||
| `metrics.serviceMonitor` | Deploy serviceMonitor kind for for use with prometheus-operator CRDs | false |
|
||||
@@ -30,6 +33,7 @@ _Default values are the defaults set in the charts values.yaml, some properties
|
||||
| `metrics.proxy.enabled` | Deploy kube-rbac-proxy container in controller pod | true |
|
||||
| `metrics.proxy.image.repository` | The "repository/image" of the kube-proxy container | quay.io/brancz/kube-rbac-proxy |
|
||||
| `metrics.proxy.image.tag` | The tag of the kube-proxy image to use when pulling the container | v0.10.0 |
|
||||
| `metrics.serviceMonitorLabels` | Set labels to apply to ServiceMonitor resources | |
|
||||
| `imagePullSecrets` | Specifies the secret to be used when pulling the controller pod containers | |
|
||||
| `fullNameOverride` | Override the full resource names | |
|
||||
| `nameOverride` | Override the resource name prefix | |
|
||||
@@ -53,8 +57,9 @@ _Default values are the defaults set in the charts values.yaml, some properties
|
||||
| `scope.singleNamespace` | Limit the controller to watch a single namespace | false |
|
||||
| `githubWebhookServer.logLevel` | Set the log level of the githubWebhookServer container | |
|
||||
| `githubWebhookServer.replicaCount` | Set the number of webhook server pods | 1 |
|
||||
| `githubWebhookServer.syncPeriod` | Set the period in which the controller reconciles the resources | 10m |
|
||||
| `githubWebhookServer.enabled` | Deploy the webhook server pod | false |
|
||||
| `githubWebhookServer.secret.create` | Deploy the webhook hook secret | true |
|
||||
| `githubWebhookServer.secret.create` | Deploy the webhook hook secret | false |
|
||||
| `githubWebhookServer.secret.name` | Set the name of the webhook hook secret | github-webhook-server |
|
||||
| `githubWebhookServer.secret.github_webhook_secret_token` | Set the webhook secret token value | |
|
||||
| `githubWebhookServer.imagePullSecrets` | Specifies the secret to be used when pulling the githubWebhookServer pod containers | |
|
||||
|
||||
@@ -1,285 +1,304 @@
|
||||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.3.0
|
||||
controller-gen.kubebuilder.io/version: v0.6.0
|
||||
creationTimestamp: null
|
||||
name: horizontalrunnerautoscalers.actions.summerwind.dev
|
||||
spec:
|
||||
additionalPrinterColumns:
|
||||
- JSONPath: .spec.minReplicas
|
||||
name: Min
|
||||
type: number
|
||||
- JSONPath: .spec.maxReplicas
|
||||
name: Max
|
||||
type: number
|
||||
- JSONPath: .status.desiredReplicas
|
||||
name: Desired
|
||||
type: number
|
||||
- JSONPath: .status.scheduledOverridesSummary
|
||||
name: Schedule
|
||||
type: string
|
||||
group: actions.summerwind.dev
|
||||
names:
|
||||
kind: HorizontalRunnerAutoscaler
|
||||
listKind: HorizontalRunnerAutoscalerList
|
||||
plural: horizontalrunnerautoscalers
|
||||
shortNames:
|
||||
- hra
|
||||
singular: horizontalrunnerautoscaler
|
||||
scope: Namespaced
|
||||
subresources:
|
||||
status: {}
|
||||
validation:
|
||||
openAPIV3Schema:
|
||||
description: HorizontalRunnerAutoscaler is the Schema for the horizontalrunnerautoscaler
|
||||
API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: HorizontalRunnerAutoscalerSpec defines the desired state of
|
||||
HorizontalRunnerAutoscaler
|
||||
properties:
|
||||
capacityReservations:
|
||||
items:
|
||||
description: CapacityReservation specifies the number of replicas
|
||||
temporarily added to the scale target until ExpirationTime.
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.minReplicas
|
||||
name: Min
|
||||
type: number
|
||||
- jsonPath: .spec.maxReplicas
|
||||
name: Max
|
||||
type: number
|
||||
- jsonPath: .status.desiredReplicas
|
||||
name: Desired
|
||||
type: number
|
||||
- jsonPath: .status.scheduledOverridesSummary
|
||||
name: Schedule
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: HorizontalRunnerAutoscaler is the Schema for the horizontalrunnerautoscaler
|
||||
API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: HorizontalRunnerAutoscalerSpec defines the desired state
|
||||
of HorizontalRunnerAutoscaler
|
||||
properties:
|
||||
capacityReservations:
|
||||
items:
|
||||
description: CapacityReservation specifies the number of replicas
|
||||
temporarily added to the scale target until ExpirationTime.
|
||||
properties:
|
||||
expirationTime:
|
||||
format: date-time
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
replicas:
|
||||
type: integer
|
||||
type: object
|
||||
type: array
|
||||
maxReplicas:
|
||||
description: MinReplicas is the maximum number of replicas the deployment
|
||||
is allowed to scale
|
||||
type: integer
|
||||
metrics:
|
||||
description: Metrics is the collection of various metric targets to
|
||||
calculate desired number of runners
|
||||
items:
|
||||
properties:
|
||||
repositoryNames:
|
||||
description: RepositoryNames is the list of repository names
|
||||
to be used for calculating the metric. For example, a repository
|
||||
name is the REPO part of `github.com/USER/REPO`.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
scaleDownAdjustment:
|
||||
description: ScaleDownAdjustment is the number of runners removed
|
||||
on scale-down. You can only specify either ScaleDownFactor
|
||||
or ScaleDownAdjustment.
|
||||
type: integer
|
||||
scaleDownFactor:
|
||||
description: ScaleDownFactor is the multiplicative factor applied
|
||||
to the current number of runners used to determine how many
|
||||
pods should be removed.
|
||||
type: string
|
||||
scaleDownThreshold:
|
||||
description: ScaleDownThreshold is the percentage of busy runners
|
||||
less than which will trigger the hpa to scale the runners
|
||||
down.
|
||||
type: string
|
||||
scaleUpAdjustment:
|
||||
description: ScaleUpAdjustment is the number of runners added
|
||||
on scale-up. You can only specify either ScaleUpFactor or
|
||||
ScaleUpAdjustment.
|
||||
type: integer
|
||||
scaleUpFactor:
|
||||
description: ScaleUpFactor is the multiplicative factor applied
|
||||
to the current number of runners used to determine how many
|
||||
pods should be added.
|
||||
type: string
|
||||
scaleUpThreshold:
|
||||
description: ScaleUpThreshold is the percentage of busy runners
|
||||
greater than which will trigger the hpa to scale runners up.
|
||||
type: string
|
||||
type:
|
||||
description: Type is the type of metric to be used for autoscaling.
|
||||
The only supported Type is TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
minReplicas:
|
||||
description: MinReplicas is the minimum number of replicas the deployment
|
||||
is allowed to scale
|
||||
type: integer
|
||||
scaleDownDelaySecondsAfterScaleOut:
|
||||
description: ScaleDownDelaySecondsAfterScaleUp is the approximate
|
||||
delay for a scale down followed by a scale up Used to prevent flapping
|
||||
(down->up->down->... loop)
|
||||
type: integer
|
||||
scaleTargetRef:
|
||||
description: ScaleTargetRef sis the reference to scaled resource like
|
||||
RunnerDeployment
|
||||
properties:
|
||||
expirationTime:
|
||||
format: date-time
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
enum:
|
||||
- RunnerDeployment
|
||||
- RunnerSet
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
replicas:
|
||||
type: integer
|
||||
type: object
|
||||
type: array
|
||||
maxReplicas:
|
||||
description: MinReplicas is the maximum number of replicas the deployment
|
||||
is allowed to scale
|
||||
type: integer
|
||||
metrics:
|
||||
description: Metrics is the collection of various metric targets to
|
||||
calculate desired number of runners
|
||||
items:
|
||||
properties:
|
||||
repositoryNames:
|
||||
description: RepositoryNames is the list of repository names to
|
||||
be used for calculating the metric. For example, a repository
|
||||
name is the REPO part of `github.com/USER/REPO`.
|
||||
items:
|
||||
scaleUpTriggers:
|
||||
description: "ScaleUpTriggers is an experimental feature to increase
|
||||
the desired replicas by 1 on each webhook requested received by
|
||||
the webhookBasedAutoscaler. \n This feature requires you to also
|
||||
enable and deploy the webhookBasedAutoscaler onto your cluster.
|
||||
\n Note that the added runners remain until the next sync period
|
||||
at least, and they may or may not be used by GitHub Actions depending
|
||||
on the timing. They are intended to be used to gain \"resource slack\"
|
||||
immediately after you receive a webhook from GitHub, so that you
|
||||
can loosely expect MinReplicas runners to be always available."
|
||||
items:
|
||||
properties:
|
||||
amount:
|
||||
type: integer
|
||||
duration:
|
||||
type: string
|
||||
type: array
|
||||
scaleDownAdjustment:
|
||||
description: ScaleDownAdjustment is the number of runners removed
|
||||
on scale-down. You can only specify either ScaleDownFactor or
|
||||
ScaleDownAdjustment.
|
||||
type: integer
|
||||
scaleDownFactor:
|
||||
description: ScaleDownFactor is the multiplicative factor applied
|
||||
to the current number of runners used to determine how many
|
||||
pods should be removed.
|
||||
type: string
|
||||
scaleDownThreshold:
|
||||
description: ScaleDownThreshold is the percentage of busy runners
|
||||
less than which will trigger the hpa to scale the runners down.
|
||||
type: string
|
||||
scaleUpAdjustment:
|
||||
description: ScaleUpAdjustment is the number of runners added
|
||||
on scale-up. You can only specify either ScaleUpFactor or ScaleUpAdjustment.
|
||||
type: integer
|
||||
scaleUpFactor:
|
||||
description: ScaleUpFactor is the multiplicative factor applied
|
||||
to the current number of runners used to determine how many
|
||||
pods should be added.
|
||||
type: string
|
||||
scaleUpThreshold:
|
||||
description: ScaleUpThreshold is the percentage of busy runners
|
||||
greater than which will trigger the hpa to scale runners up.
|
||||
type: string
|
||||
type:
|
||||
description: Type is the type of metric to be used for autoscaling.
|
||||
The only supported Type is TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
minReplicas:
|
||||
description: MinReplicas is the minimum number of replicas the deployment
|
||||
is allowed to scale
|
||||
type: integer
|
||||
scaleDownDelaySecondsAfterScaleOut:
|
||||
description: ScaleDownDelaySecondsAfterScaleUp is the approximate delay
|
||||
for a scale down followed by a scale up Used to prevent flapping (down->up->down->...
|
||||
loop)
|
||||
type: integer
|
||||
scaleTargetRef:
|
||||
description: ScaleTargetRef sis the reference to scaled resource like
|
||||
RunnerDeployment
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
scaleUpTriggers:
|
||||
description: "ScaleUpTriggers is an experimental feature to increase
|
||||
the desired replicas by 1 on each webhook requested received by the
|
||||
webhookBasedAutoscaler. \n This feature requires you to also enable
|
||||
and deploy the webhookBasedAutoscaler onto your cluster. \n Note that
|
||||
the added runners remain until the next sync period at least, and
|
||||
they may or may not be used by GitHub Actions depending on the timing.
|
||||
They are intended to be used to gain \"resource slack\" immediately
|
||||
after you receive a webhook from GitHub, so that you can loosely expect
|
||||
MinReplicas runners to be always available."
|
||||
items:
|
||||
properties:
|
||||
amount:
|
||||
type: integer
|
||||
duration:
|
||||
type: string
|
||||
githubEvent:
|
||||
properties:
|
||||
checkRun:
|
||||
description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#check_run
|
||||
properties:
|
||||
names:
|
||||
description: Names is a list of GitHub Actions glob patterns.
|
||||
Any check_run event whose name matches one of patterns
|
||||
in the list can trigger autoscaling. Note that check_run
|
||||
name seem to equal to the job name you've defined in
|
||||
your actions workflow yaml file. So it is very likely
|
||||
that you can utilize this to trigger depending on the
|
||||
job.
|
||||
items:
|
||||
githubEvent:
|
||||
properties:
|
||||
checkRun:
|
||||
description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#check_run
|
||||
properties:
|
||||
names:
|
||||
description: Names is a list of GitHub Actions glob
|
||||
patterns. Any check_run event whose name matches one
|
||||
of patterns in the list can trigger autoscaling. Note
|
||||
that check_run name seem to equal to the job name
|
||||
you've defined in your actions workflow yaml file.
|
||||
So it is very likely that you can utilize this to
|
||||
trigger depending on the job.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
repositories:
|
||||
description: Repositories is a list of GitHub repositories.
|
||||
Any check_run event whose repository matches one of
|
||||
repositories in the list can trigger autoscaling.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
types:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
pullRequest:
|
||||
description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request
|
||||
properties:
|
||||
branches:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
types:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
push:
|
||||
description: PushSpec is the condition for triggering scale-up
|
||||
on push event Also see https://docs.github.com/en/actions/reference/events-that-trigger-workflows#push
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
scheduledOverrides:
|
||||
description: ScheduledOverrides is the list of ScheduledOverride. It
|
||||
can be used to override a few fields of HorizontalRunnerAutoscalerSpec
|
||||
on schedule. The earlier a scheduled override is, the higher it is
|
||||
prioritized.
|
||||
items:
|
||||
description: ScheduledOverride can be used to override a few fields
|
||||
of HorizontalRunnerAutoscalerSpec on schedule. A schedule can optionally
|
||||
be recurring, so that the correspoding override happens every day,
|
||||
week, month, or year.
|
||||
properties:
|
||||
endTime:
|
||||
description: EndTime is the time at which the first override ends.
|
||||
format: date-time
|
||||
type: string
|
||||
minReplicas:
|
||||
description: MinReplicas is the number of runners while overriding.
|
||||
If omitted, it doesn't override minReplicas.
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
recurrenceRule:
|
||||
properties:
|
||||
frequency:
|
||||
description: Frequency is the name of a predefined interval
|
||||
of each recurrence. The valid values are "Daily", "Weekly",
|
||||
"Monthly", and "Yearly". If empty, the corresponding override
|
||||
happens only once.
|
||||
enum:
|
||||
- Daily
|
||||
- Weekly
|
||||
- Monthly
|
||||
- Yearly
|
||||
type: string
|
||||
untilTime:
|
||||
description: UntilTime is the time of the final recurrence.
|
||||
If empty, the schedule recurs forever.
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
startTime:
|
||||
description: StartTime is the time at which the first override
|
||||
starts.
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- endTime
|
||||
- startTime
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
cacheEntries:
|
||||
items:
|
||||
properties:
|
||||
expirationTime:
|
||||
format: date-time
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: integer
|
||||
type: object
|
||||
type: array
|
||||
desiredReplicas:
|
||||
description: DesiredReplicas is the total number of desired, non-terminated
|
||||
and latest pods to be set for the primary RunnerSet This doesn't include
|
||||
outdated pods while upgrading the deployment and replacing the runnerset.
|
||||
type: integer
|
||||
lastSuccessfulScaleOutTime:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the most recent generation observed
|
||||
for the target. It corresponds to e.g. RunnerDeployment's generation,
|
||||
which is updated on mutation by the API Server.
|
||||
format: int64
|
||||
type: integer
|
||||
scheduledOverridesSummary:
|
||||
description: ScheduledOverridesSummary is the summary of active and
|
||||
upcoming scheduled overrides to be shown in e.g. a column of a `kubectl
|
||||
get hra` output for observability.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
version: v1alpha1
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
types:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
pullRequest:
|
||||
description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request
|
||||
properties:
|
||||
branches:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
types:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
push:
|
||||
description: PushSpec is the condition for triggering scale-up
|
||||
on push event Also see https://docs.github.com/en/actions/reference/events-that-trigger-workflows#push
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
scheduledOverrides:
|
||||
description: ScheduledOverrides is the list of ScheduledOverride.
|
||||
It can be used to override a few fields of HorizontalRunnerAutoscalerSpec
|
||||
on schedule. The earlier a scheduled override is, the higher it
|
||||
is prioritized.
|
||||
items:
|
||||
description: ScheduledOverride can be used to override a few fields
|
||||
of HorizontalRunnerAutoscalerSpec on schedule. A schedule can
|
||||
optionally be recurring, so that the correspoding override happens
|
||||
every day, week, month, or year.
|
||||
properties:
|
||||
endTime:
|
||||
description: EndTime is the time at which the first override
|
||||
ends.
|
||||
format: date-time
|
||||
type: string
|
||||
minReplicas:
|
||||
description: MinReplicas is the number of runners while overriding.
|
||||
If omitted, it doesn't override minReplicas.
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
recurrenceRule:
|
||||
properties:
|
||||
frequency:
|
||||
description: Frequency is the name of a predefined interval
|
||||
of each recurrence. The valid values are "Daily", "Weekly",
|
||||
"Monthly", and "Yearly". If empty, the corresponding override
|
||||
happens only once.
|
||||
enum:
|
||||
- Daily
|
||||
- Weekly
|
||||
- Monthly
|
||||
- Yearly
|
||||
type: string
|
||||
untilTime:
|
||||
description: UntilTime is the time of the final recurrence.
|
||||
If empty, the schedule recurs forever.
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
startTime:
|
||||
description: StartTime is the time at which the first override
|
||||
starts.
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- endTime
|
||||
- startTime
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
cacheEntries:
|
||||
items:
|
||||
properties:
|
||||
expirationTime:
|
||||
format: date-time
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: integer
|
||||
type: object
|
||||
type: array
|
||||
desiredReplicas:
|
||||
description: DesiredReplicas is the total number of desired, non-terminated
|
||||
and latest pods to be set for the primary RunnerSet This doesn't
|
||||
include outdated pods while upgrading the deployment and replacing
|
||||
the runnerset.
|
||||
type: integer
|
||||
lastSuccessfulScaleOutTime:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the most recent generation observed
|
||||
for the target. It corresponds to e.g. RunnerDeployment's generation,
|
||||
which is updated on mutation by the API Server.
|
||||
format: int64
|
||||
type: integer
|
||||
scheduledOverridesSummary:
|
||||
description: ScheduledOverridesSummary is the summary of active and
|
||||
upcoming scheduled overrides to be shown in e.g. a column of a `kubectl
|
||||
get hra` output for observability.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ Due to the above you can't just do a `helm upgrade` to release the latest versio
|
||||
1. Upgrade CRDs
|
||||
|
||||
```shell
|
||||
# REMEMBER TO UPDATE THE CHART_VERSION TO RELEVANT CHART VERISON!!!!
|
||||
CHART_VERSION=0.11.0
|
||||
|
||||
curl -L https://github.com/actions-runner-controller/actions-runner-controller/releases/download/actions-runner-controller-${CHART_VERSION}/actions-runner-controller-${CHART_VERSION}.tgz | tar zxv --strip 1 actions-runner-controller/crds
|
||||
|
||||
@@ -21,4 +21,4 @@ spec:
|
||||
issuerRef:
|
||||
kind: Issuer
|
||||
name: {{ include "actions-runner-controller.selfsignedIssuerName" . }}
|
||||
secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize
|
||||
secretName: {{ include "actions-runner-controller.servingCertName" . }}
|
||||
|
||||
@@ -4,11 +4,20 @@ kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
||||
{{- with .Values.metrics.serviceMonitorLabels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
name: {{ include "actions-runner-controller.serviceMonitorName" . }}
|
||||
spec:
|
||||
endpoints:
|
||||
- path: /metrics
|
||||
port: metrics-port
|
||||
{{- if .Values.metrics.proxy.enabled }}
|
||||
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
scheme: https
|
||||
tlsConfig:
|
||||
insecureSkipVerify: true
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "actions-runner-controller.selectorLabels" . | nindent 6 }}
|
||||
|
||||
@@ -37,9 +37,18 @@ spec:
|
||||
{{- $metricsHost := .Values.metrics.proxy.enabled | ternary "127.0.0.1" "0.0.0.0" }}
|
||||
{{- $metricsPort := .Values.metrics.proxy.enabled | ternary "8080" .Values.metrics.port }}
|
||||
- "--metrics-addr={{ $metricsHost }}:{{ $metricsPort }}"
|
||||
{{- if .Values.enableLeaderElection }}
|
||||
- "--enable-leader-election"
|
||||
{{- end }}
|
||||
{{- if .Values.leaderElectionId }}
|
||||
- "--leader-election-id={{ .Values.leaderElectionId }}"
|
||||
{{- end }}
|
||||
- "--sync-period={{ .Values.syncPeriod }}"
|
||||
- "--docker-image={{ .Values.image.dindSidecarRepositoryAndTag }}"
|
||||
- "--runner-image={{ .Values.image.actionsRunnerRepositoryAndTag }}"
|
||||
{{- if .Values.dockerRegistryMirror }}
|
||||
- "--docker-registry-mirror={{ .Values.dockerRegistryMirror }}"
|
||||
{{- end }}
|
||||
{{- if .Values.scope.singleNamespace }}
|
||||
- "--watch-namespace={{ default .Release.Namespace .Values.scope.watchNamespace }}"
|
||||
{{- end }}
|
||||
@@ -52,6 +61,10 @@ spec:
|
||||
command:
|
||||
- "/manager"
|
||||
env:
|
||||
{{- if .Values.githubEnterpriseServerURL }}
|
||||
- name: GITHUB_ENTERPRISE_URL
|
||||
value: {{ .Values.githubEnterpriseServerURL }}
|
||||
{{- end }}
|
||||
- name: GITHUB_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -126,7 +139,7 @@ spec:
|
||||
- name: cert
|
||||
secret:
|
||||
defaultMode: 420
|
||||
secretName: webhook-server-cert
|
||||
secretName: {{ include "actions-runner-controller.servingCertName" . }}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
{{- with .Values.nodeSelector }}
|
||||
|
||||
@@ -35,6 +35,14 @@ rules:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- actions.summerwind.dev
|
||||
resources:
|
||||
- runnersets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- actions.summerwind.dev
|
||||
resources:
|
||||
@@ -67,4 +75,16 @@ rules:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- create
|
||||
{{- end }}
|
||||
|
||||
@@ -6,6 +6,10 @@ metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
||||
{{- if .Values.githubWebhookServer.service.annotations }}
|
||||
annotations:
|
||||
{{ toYaml .Values.githubWebhookServer.service.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.githubWebhookServer.service.type }}
|
||||
ports:
|
||||
|
||||
@@ -4,11 +4,20 @@ kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
||||
{{- with .Values.metrics.serviceMonitorLabels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
name: {{ include "actions-runner-controller-github-webhook-server.serviceMonitorName" . }}
|
||||
spec:
|
||||
endpoints:
|
||||
- path: /metrics
|
||||
port: metrics-port
|
||||
{{- if .Values.metrics.proxy.enabled }}
|
||||
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
scheme: https
|
||||
tlsConfig:
|
||||
insecureSkipVerify: true
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "actions-runner-controller-github-webhook-server.selectorLabels" . | nindent 6 }}
|
||||
|
||||
@@ -132,6 +132,62 @@ rules:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- actions.summerwind.dev
|
||||
resources:
|
||||
- runnersets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- actions.summerwind.dev
|
||||
resources:
|
||||
- runnersets/finalizers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- actions.summerwind.dev
|
||||
resources:
|
||||
- runnersets/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- "apps"
|
||||
resources:
|
||||
- statefulsets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- "apps"
|
||||
resources:
|
||||
- statefulsets/finalizers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
@@ -139,6 +195,15 @@ rules:
|
||||
verbs:
|
||||
- create
|
||||
- patch
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: MutatingWebhookConfiguration
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
@@ -8,8 +8,9 @@ metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "actions-runner-controller.servingCertName" . }}
|
||||
webhooks:
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: {{ include "actions-runner-controller.webhookServiceName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -27,8 +28,9 @@ webhooks:
|
||||
resources:
|
||||
- runners
|
||||
sideEffects: None
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: {{ include "actions-runner-controller.webhookServiceName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -46,8 +48,9 @@ webhooks:
|
||||
resources:
|
||||
- runnerdeployments
|
||||
sideEffects: None
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: {{ include "actions-runner-controller.webhookServiceName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -65,8 +68,30 @@ webhooks:
|
||||
resources:
|
||||
- runnerreplicasets
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: {{ include "actions-runner-controller.webhookServiceName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
path: /mutate-runner-set-pod
|
||||
failurePolicy: Fail
|
||||
name: mutate-runner-pod.webhook.actions.summerwind.dev
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
apiVersions:
|
||||
- v1
|
||||
operations:
|
||||
- CREATE
|
||||
resources:
|
||||
- pods
|
||||
sideEffects: None
|
||||
objectSelector:
|
||||
matchLabels:
|
||||
"actions-runner-controller/inject-registration-token": "true"
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
@@ -74,8 +99,9 @@ metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "actions-runner-controller.servingCertName" . }}
|
||||
webhooks:
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: {{ include "actions-runner-controller.webhookServiceName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -93,8 +119,9 @@ webhooks:
|
||||
resources:
|
||||
- runners
|
||||
sideEffects: None
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: {{ include "actions-runner-controller.webhookServiceName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -112,8 +139,9 @@ webhooks:
|
||||
resources:
|
||||
- runnerdeployments
|
||||
sideEffects: None
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: {{ include "actions-runner-controller.webhookServiceName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
|
||||
@@ -8,15 +8,23 @@ replicaCount: 1
|
||||
|
||||
syncPeriod: 10m
|
||||
|
||||
enableLeaderElection: true
|
||||
# Specifies the controller id for leader election.
|
||||
# Must be unique if more than one controller installed onto the same namespace.
|
||||
#leaderElectionId: "actions-runner-controller"
|
||||
|
||||
# The controller tries its best not to repeat the duplicate GitHub API call
|
||||
# within this duration.
|
||||
# Defaults to syncPeriod - 10s.
|
||||
#githubAPICacheDuration: 30s
|
||||
|
||||
# The URL of your GitHub Enterprise server, if you're using one.
|
||||
#githubEnterpriseServerURL: https://github.example.com
|
||||
|
||||
# Only 1 authentication method can be deployed at a time
|
||||
# Uncomment the configuration you are applying and fill in the details
|
||||
authSecret:
|
||||
create: true
|
||||
create: false
|
||||
name: "controller-manager"
|
||||
### GitHub Apps Configuration
|
||||
#github_app_id: ""
|
||||
@@ -25,8 +33,10 @@ authSecret:
|
||||
### GitHub PAT Configuration
|
||||
#github_token: ""
|
||||
|
||||
dockerRegistryMirror: ""
|
||||
image:
|
||||
repository: summerwind/actions-runner-controller
|
||||
repository: "summerwind/actions-runner-controller"
|
||||
actionsRunnerRepositoryAndTag: "summerwind/actions-runner:latest"
|
||||
dindSidecarRepositoryAndTag: "docker:dind"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
@@ -66,6 +76,7 @@ service:
|
||||
|
||||
metrics:
|
||||
serviceMonitor: false
|
||||
serviceMonitorLabels: {}
|
||||
port: 8443
|
||||
proxy:
|
||||
enabled: true
|
||||
@@ -113,11 +124,12 @@ scope:
|
||||
githubWebhookServer:
|
||||
enabled: false
|
||||
replicaCount: 1
|
||||
syncPeriod: 10m
|
||||
secret:
|
||||
create: true
|
||||
create: false
|
||||
name: "github-webhook-server"
|
||||
### GitHub Webhook Configuration
|
||||
#github_webhook_secret_token: ""
|
||||
github_webhook_secret_token: ""
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
@@ -141,6 +153,7 @@ githubWebhookServer:
|
||||
priorityClassName: ""
|
||||
service:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/summerwind/actions-runner-controller/controllers"
|
||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/controllers"
|
||||
zaplib "go.uber.org/zap"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
@@ -150,7 +150,7 @@ func main() {
|
||||
defer wg.Done()
|
||||
|
||||
setupLog.Info("starting webhook server")
|
||||
if err := mgr.Start(ctx.Done()); err != nil {
|
||||
if err := mgr.Start(ctx); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func main() {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-ctrl.SetupSignalHandler()
|
||||
<-ctrl.SetupSignalHandler().Done()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
|
||||
@@ -1,285 +1,304 @@
|
||||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.3.0
|
||||
controller-gen.kubebuilder.io/version: v0.6.0
|
||||
creationTimestamp: null
|
||||
name: horizontalrunnerautoscalers.actions.summerwind.dev
|
||||
spec:
|
||||
additionalPrinterColumns:
|
||||
- JSONPath: .spec.minReplicas
|
||||
name: Min
|
||||
type: number
|
||||
- JSONPath: .spec.maxReplicas
|
||||
name: Max
|
||||
type: number
|
||||
- JSONPath: .status.desiredReplicas
|
||||
name: Desired
|
||||
type: number
|
||||
- JSONPath: .status.scheduledOverridesSummary
|
||||
name: Schedule
|
||||
type: string
|
||||
group: actions.summerwind.dev
|
||||
names:
|
||||
kind: HorizontalRunnerAutoscaler
|
||||
listKind: HorizontalRunnerAutoscalerList
|
||||
plural: horizontalrunnerautoscalers
|
||||
shortNames:
|
||||
- hra
|
||||
singular: horizontalrunnerautoscaler
|
||||
scope: Namespaced
|
||||
subresources:
|
||||
status: {}
|
||||
validation:
|
||||
openAPIV3Schema:
|
||||
description: HorizontalRunnerAutoscaler is the Schema for the horizontalrunnerautoscaler
|
||||
API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: HorizontalRunnerAutoscalerSpec defines the desired state of
|
||||
HorizontalRunnerAutoscaler
|
||||
properties:
|
||||
capacityReservations:
|
||||
items:
|
||||
description: CapacityReservation specifies the number of replicas
|
||||
temporarily added to the scale target until ExpirationTime.
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.minReplicas
|
||||
name: Min
|
||||
type: number
|
||||
- jsonPath: .spec.maxReplicas
|
||||
name: Max
|
||||
type: number
|
||||
- jsonPath: .status.desiredReplicas
|
||||
name: Desired
|
||||
type: number
|
||||
- jsonPath: .status.scheduledOverridesSummary
|
||||
name: Schedule
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: HorizontalRunnerAutoscaler is the Schema for the horizontalrunnerautoscaler
|
||||
API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: HorizontalRunnerAutoscalerSpec defines the desired state
|
||||
of HorizontalRunnerAutoscaler
|
||||
properties:
|
||||
capacityReservations:
|
||||
items:
|
||||
description: CapacityReservation specifies the number of replicas
|
||||
temporarily added to the scale target until ExpirationTime.
|
||||
properties:
|
||||
expirationTime:
|
||||
format: date-time
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
replicas:
|
||||
type: integer
|
||||
type: object
|
||||
type: array
|
||||
maxReplicas:
|
||||
description: MinReplicas is the maximum number of replicas the deployment
|
||||
is allowed to scale
|
||||
type: integer
|
||||
metrics:
|
||||
description: Metrics is the collection of various metric targets to
|
||||
calculate desired number of runners
|
||||
items:
|
||||
properties:
|
||||
repositoryNames:
|
||||
description: RepositoryNames is the list of repository names
|
||||
to be used for calculating the metric. For example, a repository
|
||||
name is the REPO part of `github.com/USER/REPO`.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
scaleDownAdjustment:
|
||||
description: ScaleDownAdjustment is the number of runners removed
|
||||
on scale-down. You can only specify either ScaleDownFactor
|
||||
or ScaleDownAdjustment.
|
||||
type: integer
|
||||
scaleDownFactor:
|
||||
description: ScaleDownFactor is the multiplicative factor applied
|
||||
to the current number of runners used to determine how many
|
||||
pods should be removed.
|
||||
type: string
|
||||
scaleDownThreshold:
|
||||
description: ScaleDownThreshold is the percentage of busy runners
|
||||
less than which will trigger the hpa to scale the runners
|
||||
down.
|
||||
type: string
|
||||
scaleUpAdjustment:
|
||||
description: ScaleUpAdjustment is the number of runners added
|
||||
on scale-up. You can only specify either ScaleUpFactor or
|
||||
ScaleUpAdjustment.
|
||||
type: integer
|
||||
scaleUpFactor:
|
||||
description: ScaleUpFactor is the multiplicative factor applied
|
||||
to the current number of runners used to determine how many
|
||||
pods should be added.
|
||||
type: string
|
||||
scaleUpThreshold:
|
||||
description: ScaleUpThreshold is the percentage of busy runners
|
||||
greater than which will trigger the hpa to scale runners up.
|
||||
type: string
|
||||
type:
|
||||
description: Type is the type of metric to be used for autoscaling.
|
||||
The only supported Type is TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
minReplicas:
|
||||
description: MinReplicas is the minimum number of replicas the deployment
|
||||
is allowed to scale
|
||||
type: integer
|
||||
scaleDownDelaySecondsAfterScaleOut:
|
||||
description: ScaleDownDelaySecondsAfterScaleUp is the approximate
|
||||
delay for a scale down followed by a scale up Used to prevent flapping
|
||||
(down->up->down->... loop)
|
||||
type: integer
|
||||
scaleTargetRef:
|
||||
description: ScaleTargetRef sis the reference to scaled resource like
|
||||
RunnerDeployment
|
||||
properties:
|
||||
expirationTime:
|
||||
format: date-time
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
enum:
|
||||
- RunnerDeployment
|
||||
- RunnerSet
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
replicas:
|
||||
type: integer
|
||||
type: object
|
||||
type: array
|
||||
maxReplicas:
|
||||
description: MinReplicas is the maximum number of replicas the deployment
|
||||
is allowed to scale
|
||||
type: integer
|
||||
metrics:
|
||||
description: Metrics is the collection of various metric targets to
|
||||
calculate desired number of runners
|
||||
items:
|
||||
properties:
|
||||
repositoryNames:
|
||||
description: RepositoryNames is the list of repository names to
|
||||
be used for calculating the metric. For example, a repository
|
||||
name is the REPO part of `github.com/USER/REPO`.
|
||||
items:
|
||||
scaleUpTriggers:
|
||||
description: "ScaleUpTriggers is an experimental feature to increase
|
||||
the desired replicas by 1 on each webhook requested received by
|
||||
the webhookBasedAutoscaler. \n This feature requires you to also
|
||||
enable and deploy the webhookBasedAutoscaler onto your cluster.
|
||||
\n Note that the added runners remain until the next sync period
|
||||
at least, and they may or may not be used by GitHub Actions depending
|
||||
on the timing. They are intended to be used to gain \"resource slack\"
|
||||
immediately after you receive a webhook from GitHub, so that you
|
||||
can loosely expect MinReplicas runners to be always available."
|
||||
items:
|
||||
properties:
|
||||
amount:
|
||||
type: integer
|
||||
duration:
|
||||
type: string
|
||||
type: array
|
||||
scaleDownAdjustment:
|
||||
description: ScaleDownAdjustment is the number of runners removed
|
||||
on scale-down. You can only specify either ScaleDownFactor or
|
||||
ScaleDownAdjustment.
|
||||
type: integer
|
||||
scaleDownFactor:
|
||||
description: ScaleDownFactor is the multiplicative factor applied
|
||||
to the current number of runners used to determine how many
|
||||
pods should be removed.
|
||||
type: string
|
||||
scaleDownThreshold:
|
||||
description: ScaleDownThreshold is the percentage of busy runners
|
||||
less than which will trigger the hpa to scale the runners down.
|
||||
type: string
|
||||
scaleUpAdjustment:
|
||||
description: ScaleUpAdjustment is the number of runners added
|
||||
on scale-up. You can only specify either ScaleUpFactor or ScaleUpAdjustment.
|
||||
type: integer
|
||||
scaleUpFactor:
|
||||
description: ScaleUpFactor is the multiplicative factor applied
|
||||
to the current number of runners used to determine how many
|
||||
pods should be added.
|
||||
type: string
|
||||
scaleUpThreshold:
|
||||
description: ScaleUpThreshold is the percentage of busy runners
|
||||
greater than which will trigger the hpa to scale runners up.
|
||||
type: string
|
||||
type:
|
||||
description: Type is the type of metric to be used for autoscaling.
|
||||
The only supported Type is TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
minReplicas:
|
||||
description: MinReplicas is the minimum number of replicas the deployment
|
||||
is allowed to scale
|
||||
type: integer
|
||||
scaleDownDelaySecondsAfterScaleOut:
|
||||
description: ScaleDownDelaySecondsAfterScaleUp is the approximate delay
|
||||
for a scale down followed by a scale up Used to prevent flapping (down->up->down->...
|
||||
loop)
|
||||
type: integer
|
||||
scaleTargetRef:
|
||||
description: ScaleTargetRef sis the reference to scaled resource like
|
||||
RunnerDeployment
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
scaleUpTriggers:
|
||||
description: "ScaleUpTriggers is an experimental feature to increase
|
||||
the desired replicas by 1 on each webhook requested received by the
|
||||
webhookBasedAutoscaler. \n This feature requires you to also enable
|
||||
and deploy the webhookBasedAutoscaler onto your cluster. \n Note that
|
||||
the added runners remain until the next sync period at least, and
|
||||
they may or may not be used by GitHub Actions depending on the timing.
|
||||
They are intended to be used to gain \"resource slack\" immediately
|
||||
after you receive a webhook from GitHub, so that you can loosely expect
|
||||
MinReplicas runners to be always available."
|
||||
items:
|
||||
properties:
|
||||
amount:
|
||||
type: integer
|
||||
duration:
|
||||
type: string
|
||||
githubEvent:
|
||||
properties:
|
||||
checkRun:
|
||||
description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#check_run
|
||||
properties:
|
||||
names:
|
||||
description: Names is a list of GitHub Actions glob patterns.
|
||||
Any check_run event whose name matches one of patterns
|
||||
in the list can trigger autoscaling. Note that check_run
|
||||
name seem to equal to the job name you've defined in
|
||||
your actions workflow yaml file. So it is very likely
|
||||
that you can utilize this to trigger depending on the
|
||||
job.
|
||||
items:
|
||||
githubEvent:
|
||||
properties:
|
||||
checkRun:
|
||||
description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#check_run
|
||||
properties:
|
||||
names:
|
||||
description: Names is a list of GitHub Actions glob
|
||||
patterns. Any check_run event whose name matches one
|
||||
of patterns in the list can trigger autoscaling. Note
|
||||
that check_run name seem to equal to the job name
|
||||
you've defined in your actions workflow yaml file.
|
||||
So it is very likely that you can utilize this to
|
||||
trigger depending on the job.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
repositories:
|
||||
description: Repositories is a list of GitHub repositories.
|
||||
Any check_run event whose repository matches one of
|
||||
repositories in the list can trigger autoscaling.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
types:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
pullRequest:
|
||||
description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request
|
||||
properties:
|
||||
branches:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
types:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
push:
|
||||
description: PushSpec is the condition for triggering scale-up
|
||||
on push event Also see https://docs.github.com/en/actions/reference/events-that-trigger-workflows#push
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
scheduledOverrides:
|
||||
description: ScheduledOverrides is the list of ScheduledOverride. It
|
||||
can be used to override a few fields of HorizontalRunnerAutoscalerSpec
|
||||
on schedule. The earlier a scheduled override is, the higher it is
|
||||
prioritized.
|
||||
items:
|
||||
description: ScheduledOverride can be used to override a few fields
|
||||
of HorizontalRunnerAutoscalerSpec on schedule. A schedule can optionally
|
||||
be recurring, so that the correspoding override happens every day,
|
||||
week, month, or year.
|
||||
properties:
|
||||
endTime:
|
||||
description: EndTime is the time at which the first override ends.
|
||||
format: date-time
|
||||
type: string
|
||||
minReplicas:
|
||||
description: MinReplicas is the number of runners while overriding.
|
||||
If omitted, it doesn't override minReplicas.
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
recurrenceRule:
|
||||
properties:
|
||||
frequency:
|
||||
description: Frequency is the name of a predefined interval
|
||||
of each recurrence. The valid values are "Daily", "Weekly",
|
||||
"Monthly", and "Yearly". If empty, the corresponding override
|
||||
happens only once.
|
||||
enum:
|
||||
- Daily
|
||||
- Weekly
|
||||
- Monthly
|
||||
- Yearly
|
||||
type: string
|
||||
untilTime:
|
||||
description: UntilTime is the time of the final recurrence.
|
||||
If empty, the schedule recurs forever.
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
startTime:
|
||||
description: StartTime is the time at which the first override
|
||||
starts.
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- endTime
|
||||
- startTime
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
cacheEntries:
|
||||
items:
|
||||
properties:
|
||||
expirationTime:
|
||||
format: date-time
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: integer
|
||||
type: object
|
||||
type: array
|
||||
desiredReplicas:
|
||||
description: DesiredReplicas is the total number of desired, non-terminated
|
||||
and latest pods to be set for the primary RunnerSet This doesn't include
|
||||
outdated pods while upgrading the deployment and replacing the runnerset.
|
||||
type: integer
|
||||
lastSuccessfulScaleOutTime:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the most recent generation observed
|
||||
for the target. It corresponds to e.g. RunnerDeployment's generation,
|
||||
which is updated on mutation by the API Server.
|
||||
format: int64
|
||||
type: integer
|
||||
scheduledOverridesSummary:
|
||||
description: ScheduledOverridesSummary is the summary of active and
|
||||
upcoming scheduled overrides to be shown in e.g. a column of a `kubectl
|
||||
get hra` output for observability.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
version: v1alpha1
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
types:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
pullRequest:
|
||||
description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request
|
||||
properties:
|
||||
branches:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
types:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
push:
|
||||
description: PushSpec is the condition for triggering scale-up
|
||||
on push event Also see https://docs.github.com/en/actions/reference/events-that-trigger-workflows#push
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
scheduledOverrides:
|
||||
description: ScheduledOverrides is the list of ScheduledOverride.
|
||||
It can be used to override a few fields of HorizontalRunnerAutoscalerSpec
|
||||
on schedule. The earlier a scheduled override is, the higher it
|
||||
is prioritized.
|
||||
items:
|
||||
description: ScheduledOverride can be used to override a few fields
|
||||
of HorizontalRunnerAutoscalerSpec on schedule. A schedule can
|
||||
optionally be recurring, so that the correspoding override happens
|
||||
every day, week, month, or year.
|
||||
properties:
|
||||
endTime:
|
||||
description: EndTime is the time at which the first override
|
||||
ends.
|
||||
format: date-time
|
||||
type: string
|
||||
minReplicas:
|
||||
description: MinReplicas is the number of runners while overriding.
|
||||
If omitted, it doesn't override minReplicas.
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
recurrenceRule:
|
||||
properties:
|
||||
frequency:
|
||||
description: Frequency is the name of a predefined interval
|
||||
of each recurrence. The valid values are "Daily", "Weekly",
|
||||
"Monthly", and "Yearly". If empty, the corresponding override
|
||||
happens only once.
|
||||
enum:
|
||||
- Daily
|
||||
- Weekly
|
||||
- Monthly
|
||||
- Yearly
|
||||
type: string
|
||||
untilTime:
|
||||
description: UntilTime is the time of the final recurrence.
|
||||
If empty, the schedule recurs forever.
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
startTime:
|
||||
description: StartTime is the time at which the first override
|
||||
starts.
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- endTime
|
||||
- startTime
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
cacheEntries:
|
||||
items:
|
||||
properties:
|
||||
expirationTime:
|
||||
format: date-time
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: integer
|
||||
type: object
|
||||
type: array
|
||||
desiredReplicas:
|
||||
description: DesiredReplicas is the total number of desired, non-terminated
|
||||
and latest pods to be set for the primary RunnerSet This doesn't
|
||||
include outdated pods while upgrading the deployment and replacing
|
||||
the runnerset.
|
||||
type: integer
|
||||
lastSuccessfulScaleOutTime:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the most recent generation observed
|
||||
for the target. It corresponds to e.g. RunnerDeployment's generation,
|
||||
which is updated on mutation by the API Server.
|
||||
format: int64
|
||||
type: integer
|
||||
scheduledOverridesSummary:
|
||||
description: ScheduledOverridesSummary is the summary of active and
|
||||
upcoming scheduled overrides to be shown in e.g. a column of a `kubectl
|
||||
get hra` output for observability.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
7165
config/crd/bases/actions.summerwind.dev_runnersets.yaml
Normal file
7165
config/crd/bases/actions.summerwind.dev_runnersets.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ resources:
|
||||
- bases/actions.summerwind.dev_runnerreplicasets.yaml
|
||||
- bases/actions.summerwind.dev_runnerdeployments.yaml
|
||||
- bases/actions.summerwind.dev_horizontalrunnerautoscalers.yaml
|
||||
- bases/actions.summerwind.dev_runnersets.yaml
|
||||
# +kubebuilder:scaffold:crdkustomizeresource
|
||||
|
||||
patchesStrategicMerge:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The following patch adds a directive for certmanager to inject CA into the CRD
|
||||
# CRD conversion requires k8s 1.13 or later.
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
# The following patch enables conversion webhook for CRD
|
||||
# CRD conversion requires k8s 1.13 or later.
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: runners.actions.summerwind.dev
|
||||
spec:
|
||||
conversion:
|
||||
strategy: Webhook
|
||||
webhookClientConfig:
|
||||
# this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
|
||||
# but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
|
||||
caBundle: Cg==
|
||||
service:
|
||||
namespace: system
|
||||
name: webhook-service
|
||||
path: /convert
|
||||
webhook:
|
||||
clientConfig:
|
||||
# this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
|
||||
# but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
|
||||
caBundle: Cg==
|
||||
service:
|
||||
namespace: system
|
||||
name: webhook-service
|
||||
path: /convert
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# This patch add annotation to admission webhook config and
|
||||
# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize.
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: MutatingWebhookConfiguration
|
||||
metadata:
|
||||
name: mutating-webhook-configuration
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
name: validating-webhook-configuration
|
||||
|
||||
@@ -4,5 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
images:
|
||||
- name: controller
|
||||
newName: mumoshu/actions-runner-controller
|
||||
newTag: dev
|
||||
newName: summerwind/actions-runner-controller
|
||||
newTag: latest
|
||||
|
||||
@@ -134,6 +134,67 @@ rules:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- actions.summerwind.dev
|
||||
resources:
|
||||
- runnersets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- actions.summerwind.dev
|
||||
resources:
|
||||
- runnersets/finalizers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- actions.summerwind.dev
|
||||
resources:
|
||||
- runnersets/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- statefulsets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- statefulsets/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
|
||||
@@ -3,4 +3,4 @@ kind: Runner
|
||||
metadata:
|
||||
name: summerwind-actions-runner-controller
|
||||
spec:
|
||||
repository: summerwind/actions-runner-controller
|
||||
repository: actions-runner-controller/actions-runner-controller
|
||||
|
||||
@@ -6,4 +6,4 @@ spec:
|
||||
replicas: 2
|
||||
template:
|
||||
spec:
|
||||
repository: summerwind/actions-runner-controller
|
||||
repository: actions-runner-controller/actions-runner-controller
|
||||
|
||||
@@ -6,4 +6,4 @@ spec:
|
||||
replicas: 2
|
||||
template:
|
||||
spec:
|
||||
repository: summerwind/actions-runner-controller
|
||||
repository: actions-runner-controller/actions-runner-controller
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: MutatingWebhookConfiguration
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: mutating-webhook-configuration
|
||||
webhooks:
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
@@ -25,8 +26,9 @@ webhooks:
|
||||
resources:
|
||||
- runners
|
||||
sideEffects: None
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
@@ -43,8 +45,10 @@ webhooks:
|
||||
- UPDATE
|
||||
resources:
|
||||
- runnerdeployments
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
@@ -62,16 +66,36 @@ webhooks:
|
||||
resources:
|
||||
- runnerreplicasets
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
path: /mutate-runner-set-pod
|
||||
failurePolicy: Ignore
|
||||
name: mutate-runner-pod.webhook.actions.summerwind.dev
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
apiVersions:
|
||||
- v1
|
||||
operations:
|
||||
- CREATE
|
||||
resources:
|
||||
- pods
|
||||
sideEffects: None
|
||||
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: validating-webhook-configuration
|
||||
webhooks:
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
@@ -89,8 +113,9 @@ webhooks:
|
||||
resources:
|
||||
- runners
|
||||
sideEffects: None
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
@@ -107,8 +132,10 @@ webhooks:
|
||||
- UPDATE
|
||||
resources:
|
||||
- runnerdeployments
|
||||
- clientConfig:
|
||||
caBundle: Cg==
|
||||
sideEffects: None
|
||||
- admissionReviewVersions:
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook-service
|
||||
namespace: system
|
||||
|
||||
@@ -9,10 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -63,7 +60,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) fetchSuggestedReplicasFromCache(h
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) suggestDesiredReplicas(rd v1alpha1.RunnerDeployment, hra v1alpha1.HorizontalRunnerAutoscaler) (*int, error) {
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) suggestDesiredReplicas(st scaleTarget, hra v1alpha1.HorizontalRunnerAutoscaler) (*int, error) {
|
||||
if hra.Spec.MinReplicas == nil {
|
||||
return nil, fmt.Errorf("horizontalrunnerautoscaler %s/%s is missing minReplicas", hra.Namespace, hra.Name)
|
||||
} else if hra.Spec.MaxReplicas == nil {
|
||||
@@ -74,7 +71,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestDesiredReplicas(rd v1alpha
|
||||
numMetrics := len(metrics)
|
||||
if numMetrics == 0 {
|
||||
if len(hra.Spec.ScaleUpTriggers) == 0 {
|
||||
return r.suggestReplicasByQueuedAndInProgressWorkflowRuns(rd, hra, nil)
|
||||
return r.suggestReplicasByQueuedAndInProgressWorkflowRuns(st, hra, nil)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
@@ -92,9 +89,9 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestDesiredReplicas(rd v1alpha
|
||||
|
||||
switch primaryMetricType {
|
||||
case v1alpha1.AutoscalingMetricTypeTotalNumberOfQueuedAndInProgressWorkflowRuns:
|
||||
suggested, err = r.suggestReplicasByQueuedAndInProgressWorkflowRuns(rd, hra, &primaryMetric)
|
||||
suggested, err = r.suggestReplicasByQueuedAndInProgressWorkflowRuns(st, hra, &primaryMetric)
|
||||
case v1alpha1.AutoscalingMetricTypePercentageRunnersBusy:
|
||||
suggested, err = r.suggestReplicasByPercentageRunnersBusy(rd, hra, primaryMetric)
|
||||
suggested, err = r.suggestReplicasByPercentageRunnersBusy(st, hra, primaryMetric)
|
||||
default:
|
||||
return nil, fmt.Errorf("validting autoscaling metrics: unsupported metric type %q", primaryMetric)
|
||||
}
|
||||
@@ -127,22 +124,22 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestDesiredReplicas(rd v1alpha
|
||||
)
|
||||
}
|
||||
|
||||
return r.suggestReplicasByQueuedAndInProgressWorkflowRuns(rd, hra, &fallbackMetric)
|
||||
return r.suggestReplicasByQueuedAndInProgressWorkflowRuns(st, hra, &fallbackMetric)
|
||||
}
|
||||
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByQueuedAndInProgressWorkflowRuns(rd v1alpha1.RunnerDeployment, hra v1alpha1.HorizontalRunnerAutoscaler, metrics *v1alpha1.MetricSpec) (*int, error) {
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByQueuedAndInProgressWorkflowRuns(st scaleTarget, hra v1alpha1.HorizontalRunnerAutoscaler, metrics *v1alpha1.MetricSpec) (*int, error) {
|
||||
|
||||
var repos [][]string
|
||||
repoID := rd.Spec.Template.Spec.Repository
|
||||
repoID := st.repo
|
||||
if repoID == "" {
|
||||
orgName := rd.Spec.Template.Spec.Organization
|
||||
orgName := st.org
|
||||
if orgName == "" {
|
||||
return nil, fmt.Errorf("asserting runner deployment spec to detect bug: spec.template.organization should not be empty on this code path")
|
||||
}
|
||||
|
||||
// In case it's an organizational runners deployment without any scaling metrics defined,
|
||||
// we assume that the desired replicas should always be `minReplicas + capacityReservedThroughWebhook`.
|
||||
// See https://github.com/summerwind/actions-runner-controller/issues/377#issuecomment-793372693
|
||||
// See https://github.com/actions-runner-controller/actions-runner-controller/issues/377#issuecomment-793372693
|
||||
if metrics == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -230,14 +227,15 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByQueuedAndInProgr
|
||||
"workflow_runs_queued", queued,
|
||||
"workflow_runs_unknown", unknown,
|
||||
"namespace", hra.Namespace,
|
||||
"runner_deployment", rd.Name,
|
||||
"kind", st.kind,
|
||||
"name", st.st,
|
||||
"horizontal_runner_autoscaler", hra.Name,
|
||||
)
|
||||
|
||||
return &necessaryReplicas, nil
|
||||
}
|
||||
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByPercentageRunnersBusy(rd v1alpha1.RunnerDeployment, hra v1alpha1.HorizontalRunnerAutoscaler, metrics v1alpha1.MetricSpec) (*int, error) {
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByPercentageRunnersBusy(st scaleTarget, hra v1alpha1.HorizontalRunnerAutoscaler, metrics v1alpha1.MetricSpec) (*int, error) {
|
||||
ctx := context.Background()
|
||||
scaleUpThreshold := defaultScaleUpThreshold
|
||||
scaleDownThreshold := defaultScaleDownThreshold
|
||||
@@ -294,41 +292,15 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByPercentageRunner
|
||||
scaleDownFactor = sdf
|
||||
}
|
||||
|
||||
// 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 opts []client.ListOption
|
||||
|
||||
opts = append(opts, client.InNamespace(rd.Namespace))
|
||||
|
||||
selector, err := metav1.LabelSelectorAsSelector(getSelector(&rd))
|
||||
runnerMap, err := st.getRunnerMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts = append(opts, client.MatchingLabelsSelector{Selector: selector})
|
||||
|
||||
r.Log.V(2).Info("Finding runners with selector", "ns", rd.Namespace)
|
||||
|
||||
if err := r.List(
|
||||
ctx,
|
||||
&runnerList,
|
||||
opts...,
|
||||
); err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
runnerMap := make(map[string]struct{})
|
||||
for _, items := range runnerList.Items {
|
||||
runnerMap[items.Name] = struct{}{}
|
||||
}
|
||||
|
||||
var (
|
||||
enterprise = rd.Spec.Template.Spec.Enterprise
|
||||
organization = rd.Spec.Template.Spec.Organization
|
||||
repository = rd.Spec.Template.Spec.Repository
|
||||
enterprise = st.enterprise
|
||||
organization = st.org
|
||||
repository = st.repo
|
||||
)
|
||||
|
||||
// ListRunners will return all runners managed by GitHub - not restricted to ns
|
||||
@@ -343,7 +315,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByPercentageRunner
|
||||
|
||||
var desiredReplicasBefore int
|
||||
|
||||
if v := rd.Spec.Replicas; v == nil {
|
||||
if v := st.replicas; v == nil {
|
||||
desiredReplicasBefore = 1
|
||||
} else {
|
||||
desiredReplicasBefore = *v
|
||||
@@ -355,7 +327,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByPercentageRunner
|
||||
numRunnersBusy int
|
||||
)
|
||||
|
||||
numRunners = len(runnerList.Items)
|
||||
numRunners = len(runnerMap)
|
||||
|
||||
for _, runner := range runners {
|
||||
if _, ok := runnerMap[*runner.Name]; ok {
|
||||
@@ -382,7 +354,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByPercentageRunner
|
||||
desiredReplicas = int(float64(desiredReplicasBefore) * scaleDownFactor)
|
||||
}
|
||||
} else {
|
||||
desiredReplicas = *rd.Spec.Replicas
|
||||
desiredReplicas = *st.replicas
|
||||
}
|
||||
|
||||
// NOTES for operators:
|
||||
@@ -398,7 +370,8 @@ func (r *HorizontalRunnerAutoscalerReconciler) suggestReplicasByPercentageRunner
|
||||
"num_runners_registered", numRunnersRegistered,
|
||||
"num_runners_busy", numRunnersBusy,
|
||||
"namespace", hra.Namespace,
|
||||
"runner_deployment", rd.Name,
|
||||
"kind", st.kind,
|
||||
"name", st.st,
|
||||
"horizontal_runner_autoscaler", hra.Name,
|
||||
"enterprise", enterprise,
|
||||
"organization", organization,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/summerwind/actions-runner-controller/github"
|
||||
"github.com/summerwind/actions-runner-controller/github/fake"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github/fake"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
@@ -203,7 +204,9 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
||||
Spec: v1alpha1.RunnerDeploymentSpec{
|
||||
Template: v1alpha1.RunnerTemplate{
|
||||
Spec: v1alpha1.RunnerSpec{
|
||||
Repository: tc.repo,
|
||||
RunnerConfig: v1alpha1.RunnerConfig{
|
||||
Repository: tc.repo,
|
||||
},
|
||||
},
|
||||
},
|
||||
Replicas: tc.fixed,
|
||||
@@ -229,7 +232,9 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got, _, _, err := h.computeReplicasWithCache(log, metav1Now.Time, rd, hra, minReplicas)
|
||||
st := h.scaleTargetFromRD(context.Background(), rd)
|
||||
|
||||
got, _, _, err := h.computeReplicasWithCache(log, metav1Now.Time, st, hra, minReplicas)
|
||||
if err != nil {
|
||||
if tc.err == "" {
|
||||
t.Fatalf("unexpected error: expected none, got %v", err)
|
||||
@@ -458,7 +463,9 @@ func TestDetermineDesiredReplicas_OrganizationalRunner(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Spec: v1alpha1.RunnerSpec{
|
||||
Organization: tc.org,
|
||||
RunnerConfig: v1alpha1.RunnerConfig{
|
||||
Organization: tc.org,
|
||||
},
|
||||
},
|
||||
},
|
||||
Replicas: tc.fixed,
|
||||
@@ -493,7 +500,9 @@ func TestDetermineDesiredReplicas_OrganizationalRunner(t *testing.T) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got, _, _, err := h.computeReplicasWithCache(log, metav1Now.Time, rd, hra, minReplicas)
|
||||
st := h.scaleTargetFromRD(context.Background(), rd)
|
||||
|
||||
got, _, _, err := h.computeReplicasWithCache(log, metav1Now.Time, st, hra, minReplicas)
|
||||
if err != nil {
|
||||
if tc.err == "" {
|
||||
t.Fatalf("unexpected error: expected none, got %v", err)
|
||||
|
||||
@@ -29,13 +29,13 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
gogithub "github.com/google/go-github/v33/github"
|
||||
gogithub "github.com/google/go-github/v37/github"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -61,7 +61,7 @@ type HorizontalRunnerAutoscalerGitHubWebhook struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Reconcile(request reconcile.Request) (reconcile.Result, error) {
|
||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Reconcile(_ context.Context, request reconcile.Request) (reconcile.Result, error) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
@@ -183,6 +183,45 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons
|
||||
"action", e.GetAction(),
|
||||
)
|
||||
}
|
||||
case *gogithub.WorkflowJobEvent:
|
||||
if workflowJob := e.GetWorkflowJob(); workflowJob != nil {
|
||||
log = log.WithValues(
|
||||
"workflowJob.status", workflowJob.GetStatus(),
|
||||
"workflowJob.labels", workflowJob.Labels,
|
||||
"repository.name", e.Repo.GetName(),
|
||||
"repository.owner.login", e.Repo.Owner.GetLogin(),
|
||||
"repository.owner.type", e.Repo.Owner.GetType(),
|
||||
"action", e.GetAction(),
|
||||
)
|
||||
}
|
||||
|
||||
labels := e.WorkflowJob.Labels
|
||||
|
||||
switch e.GetAction() {
|
||||
case "queued", "completed":
|
||||
target, err = autoscaler.getJobScaleUpTargetForRepoOrOrg(
|
||||
context.TODO(),
|
||||
log,
|
||||
e.Repo.GetName(),
|
||||
e.Repo.Owner.GetLogin(),
|
||||
e.Repo.Owner.GetType(),
|
||||
labels,
|
||||
)
|
||||
|
||||
if target != nil {
|
||||
if e.GetAction() == "queued" {
|
||||
target.Amount = 1
|
||||
} else if e.GetAction() == "completed" {
|
||||
// A nagative amount is processed in the tryScale func as a scale-down request,
|
||||
// that erasese the oldest CapacityReservation with the same amount.
|
||||
// If the first CapacityReservation was with Replicas=1, this negative scale target erases that,
|
||||
// so that the resulting desired replicas decreases by 1.
|
||||
target.Amount = -1
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
||||
}
|
||||
case *gogithub.PingEvent:
|
||||
ok = true
|
||||
|
||||
@@ -227,7 +266,7 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons
|
||||
return
|
||||
}
|
||||
|
||||
if err := autoscaler.tryScaleUp(context.TODO(), target); err != nil {
|
||||
if err := autoscaler.tryScale(context.TODO(), target); err != nil {
|
||||
log.Error(err, "could not scale up")
|
||||
|
||||
return
|
||||
@@ -237,7 +276,7 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
msg := fmt.Sprintf("scaled %s by 1", target.Name)
|
||||
msg := fmt.Sprintf("scaled %s by %d", target.Name, target.Amount)
|
||||
|
||||
autoscaler.Log.Info(msg)
|
||||
|
||||
@@ -394,7 +433,137 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTarget(ctx
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) tryScaleUp(ctx context.Context, target *ScaleTarget) error {
|
||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleUpTargetForRepoOrOrg(ctx context.Context, log logr.Logger, repo, owner, ownerType string, labels []string) (*ScaleTarget, error) {
|
||||
repositoryRunnerKey := owner + "/" + repo
|
||||
|
||||
if target, err := autoscaler.getJobScaleTarget(ctx, repositoryRunnerKey, labels); err != nil {
|
||||
log.Info("finding repository-wide runner", "repository", repositoryRunnerKey)
|
||||
return nil, err
|
||||
} else if target != nil {
|
||||
log.Info("job scale up target is repository-wide runners", "repository", repo)
|
||||
return target, nil
|
||||
}
|
||||
|
||||
if ownerType == "User" {
|
||||
log.V(1).Info("no repository runner found", "organization", owner)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if target, err := autoscaler.getJobScaleTarget(ctx, owner, labels); err != nil {
|
||||
log.Info("finding organizational runner", "organization", owner)
|
||||
return nil, err
|
||||
} else if target != nil {
|
||||
log.Info("job scale up target is organizational runners", "organization", owner)
|
||||
return target, nil
|
||||
} else {
|
||||
log.V(1).Info("no repository runner or organizational runner found",
|
||||
"repository", repositoryRunnerKey,
|
||||
"organization", owner,
|
||||
)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleTarget(ctx context.Context, name string, labels []string) (*ScaleTarget, error) {
|
||||
hras, err := autoscaler.findHRAsByKey(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
autoscaler.Log.V(1).Info(fmt.Sprintf("Found %d HRAs by key", len(hras)), "key", name)
|
||||
|
||||
HRA:
|
||||
for _, hra := range hras {
|
||||
if !hra.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(hra.Spec.ScaleUpTriggers) > 1 {
|
||||
autoscaler.Log.V(1).Info("Skipping this HRA as it has too many ScaleUpTriggers to be used in workflow_job based scaling", "hra", hra.Name)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var duration metav1.Duration
|
||||
|
||||
if len(hra.Spec.ScaleUpTriggers) > 0 {
|
||||
duration = hra.Spec.ScaleUpTriggers[0].Duration
|
||||
}
|
||||
|
||||
if duration.Duration <= 0 {
|
||||
// Try to release the reserved capacity after at least 10 minutes by default,
|
||||
// we won't end up in the reserved capacity remained forever in case GitHub somehow stopped sending us "completed" workflow_job events.
|
||||
// GitHub usually send us those but nothing is 100% guaranteed, e.g. in case of something went wrong on GitHub :)
|
||||
// Probably we'd better make this configurable via custom resources in the future?
|
||||
duration.Duration = 10 * time.Minute
|
||||
}
|
||||
|
||||
switch hra.Spec.ScaleTargetRef.Kind {
|
||||
case "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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(labels) == 1 && labels[0] == "self-hosted" {
|
||||
return &ScaleTarget{HorizontalRunnerAutoscaler: hra, ScaleUpTrigger: v1alpha1.ScaleUpTrigger{Duration: duration}}, nil
|
||||
}
|
||||
|
||||
// Ensure that the RunnerSet-managed runners have all the labels requested by the workflow_job.
|
||||
for _, l := range labels {
|
||||
var matched bool
|
||||
for _, l2 := range rs.Spec.Labels {
|
||||
if l == l2 {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
continue HRA
|
||||
}
|
||||
}
|
||||
|
||||
return &ScaleTarget{HorizontalRunnerAutoscaler: hra, ScaleUpTrigger: v1alpha1.ScaleUpTrigger{Duration: duration}}, nil
|
||||
case "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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(labels) == 1 && labels[0] == "self-hosted" {
|
||||
return &ScaleTarget{HorizontalRunnerAutoscaler: hra, ScaleUpTrigger: v1alpha1.ScaleUpTrigger{Duration: duration}}, nil
|
||||
}
|
||||
|
||||
// Ensure that the RunnerDeployment-managed runners have all the labels requested by the workflow_job.
|
||||
for _, l := range labels {
|
||||
var matched bool
|
||||
for _, l2 := range rd.Spec.Template.Labels {
|
||||
if l == l2 {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
continue HRA
|
||||
}
|
||||
}
|
||||
|
||||
return &ScaleTarget{HorizontalRunnerAutoscaler: hra, ScaleUpTrigger: v1alpha1.ScaleUpTrigger{Duration: duration}}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported scaleTargetRef.kind: %v", hra.Spec.ScaleTargetRef.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) tryScale(ctx context.Context, target *ScaleTarget) error {
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -403,16 +572,38 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) tryScaleUp(ctx contex
|
||||
|
||||
amount := 1
|
||||
|
||||
if target.ScaleUpTrigger.Amount > 0 {
|
||||
if target.ScaleUpTrigger.Amount != 0 {
|
||||
amount = target.ScaleUpTrigger.Amount
|
||||
}
|
||||
|
||||
capacityReservations := getValidCapacityReservations(copy)
|
||||
|
||||
copy.Spec.CapacityReservations = append(capacityReservations, v1alpha1.CapacityReservation{
|
||||
ExpirationTime: metav1.Time{Time: time.Now().Add(target.ScaleUpTrigger.Duration.Duration)},
|
||||
Replicas: amount,
|
||||
})
|
||||
if amount > 0 {
|
||||
copy.Spec.CapacityReservations = append(capacityReservations, v1alpha1.CapacityReservation{
|
||||
ExpirationTime: metav1.Time{Time: time.Now().Add(target.ScaleUpTrigger.Duration.Duration)},
|
||||
Replicas: amount,
|
||||
})
|
||||
} else if amount < 0 {
|
||||
var reservations []v1alpha1.CapacityReservation
|
||||
|
||||
var found bool
|
||||
|
||||
for _, r := range capacityReservations {
|
||||
if !found && r.Replicas+amount == 0 {
|
||||
found = true
|
||||
} else {
|
||||
reservations = append(reservations, r)
|
||||
}
|
||||
}
|
||||
|
||||
copy.Spec.CapacityReservations = reservations
|
||||
}
|
||||
|
||||
autoscaler.Log.Info(
|
||||
"Patching hra for capacityReservations update",
|
||||
"before", target.HorizontalRunnerAutoscaler.Spec.CapacityReservations,
|
||||
"after", copy.Spec.CapacityReservations,
|
||||
)
|
||||
|
||||
if err := autoscaler.Client.Patch(ctx, copy, client.MergeFrom(&target.HorizontalRunnerAutoscaler)); err != nil {
|
||||
return fmt.Errorf("patching horizontalrunnerautoscaler to add capacity reservation: %w", err)
|
||||
@@ -443,20 +634,33 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) SetupWithManager(mgr
|
||||
|
||||
autoscaler.Recorder = mgr.GetEventRecorderFor(name)
|
||||
|
||||
if err := mgr.GetFieldIndexer().IndexField(&v1alpha1.HorizontalRunnerAutoscaler{}, scaleTargetKey, func(rawObj runtime.Object) []string {
|
||||
if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &v1alpha1.HorizontalRunnerAutoscaler{}, scaleTargetKey, func(rawObj client.Object) []string {
|
||||
hra := rawObj.(*v1alpha1.HorizontalRunnerAutoscaler)
|
||||
|
||||
if hra.Spec.ScaleTargetRef.Name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rd v1alpha1.RunnerDeployment
|
||||
switch hra.Spec.ScaleTargetRef.Kind {
|
||||
case "", "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 {
|
||||
return nil
|
||||
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{rd.Spec.Template.Spec.Repository, rd.Spec.Template.Spec.Organization}
|
||||
case "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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{rs.Spec.Repository, rs.Spec.Organization}
|
||||
}
|
||||
|
||||
return []string{rd.Spec.Template.Spec.Repository, rd.Spec.Template.Spec.Organization}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/google/go-github/v33/github"
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/summerwind/actions-runner-controller/pkg/actionsglob"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/pkg/actionsglob"
|
||||
"github.com/google/go-github/v37/github"
|
||||
)
|
||||
|
||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchCheckRunEvent(event *github.CheckRunEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool {
|
||||
@@ -38,6 +38,16 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchCheckRunEvent(ev
|
||||
return false
|
||||
}
|
||||
|
||||
if len(scaleUpTrigger.GitHubEvent.CheckRun.Repositories) > 0 {
|
||||
for _, repository := range scaleUpTrigger.GitHubEvent.CheckRun.Repositories {
|
||||
if repository == *event.Repo.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/google/go-github/v33/github"
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/google/go-github/v37/github"
|
||||
)
|
||||
|
||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchPullRequestEvent(event *github.PullRequestEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/google/go-github/v33/github"
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/google/go-github/v37/github"
|
||||
)
|
||||
|
||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchPushEvent(event *github.PushEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool {
|
||||
|
||||
@@ -4,21 +4,22 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/google/go-github/v33/github"
|
||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/google/go-github/v37/github"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -24,7 +24,8 @@ import (
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/github"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
@@ -35,8 +36,8 @@ import (
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/summerwind/actions-runner-controller/controllers/metrics"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/controllers/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -63,8 +64,7 @@ const defaultReplicas = 1
|
||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=horizontalrunnerautoscalers/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
|
||||
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
ctx := context.Background()
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := r.Log.WithValues("horizontalrunnerautoscaler", req.NamespacedName)
|
||||
|
||||
var hra v1alpha1.HorizontalRunnerAutoscaler
|
||||
@@ -78,18 +78,181 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(req ctrl.Request) (ctrl
|
||||
|
||||
metrics.SetHorizontalRunnerAutoscalerSpec(hra.ObjectMeta, hra.Spec)
|
||||
|
||||
var rd v1alpha1.RunnerDeployment
|
||||
if err := r.Get(ctx, types.NamespacedName{
|
||||
Namespace: req.Namespace,
|
||||
Name: hra.Spec.ScaleTargetRef.Name,
|
||||
}, &rd); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
kind := hra.Spec.ScaleTargetRef.Kind
|
||||
|
||||
switch kind {
|
||||
case "", "RunnerDeployment":
|
||||
var rd v1alpha1.RunnerDeployment
|
||||
if err := r.Get(ctx, types.NamespacedName{
|
||||
Namespace: req.Namespace,
|
||||
Name: hra.Spec.ScaleTargetRef.Name,
|
||||
}, &rd); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
if !rd.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
st := r.scaleTargetFromRD(ctx, rd)
|
||||
|
||||
return r.reconcile(ctx, req, log, hra, st, func(newDesiredReplicas int) error {
|
||||
currentDesiredReplicas := getIntOrDefault(rd.Spec.Replicas, defaultReplicas)
|
||||
|
||||
// Please add more conditions that we can in-place update the newest runnerreplicaset without disruption
|
||||
if currentDesiredReplicas != newDesiredReplicas {
|
||||
copy := rd.DeepCopy()
|
||||
copy.Spec.Replicas = &newDesiredReplicas
|
||||
|
||||
if err := r.Client.Patch(ctx, copy, client.MergeFrom(&rd)); err != nil {
|
||||
return fmt.Errorf("patching runnerdeployment to have %d replicas: %w", newDesiredReplicas, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
case "RunnerSet":
|
||||
var rs v1alpha1.RunnerSet
|
||||
if err := r.Get(ctx, types.NamespacedName{
|
||||
Namespace: req.Namespace,
|
||||
Name: hra.Spec.ScaleTargetRef.Name,
|
||||
}, &rs); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
if !rs.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
var replicas *int
|
||||
|
||||
if rs.Spec.Replicas != nil {
|
||||
v := int(*rs.Spec.Replicas)
|
||||
replicas = &v
|
||||
}
|
||||
|
||||
st := scaleTarget{
|
||||
st: rs.Name,
|
||||
kind: "runnerset",
|
||||
enterprise: rs.Spec.Enterprise,
|
||||
org: rs.Spec.Organization,
|
||||
repo: rs.Spec.Repository,
|
||||
replicas: replicas,
|
||||
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.
|
||||
var runnerPodList corev1.PodList
|
||||
|
||||
var opts []client.ListOption
|
||||
|
||||
opts = append(opts, client.InNamespace(rs.Namespace))
|
||||
|
||||
selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts = append(opts, client.MatchingLabelsSelector{Selector: selector})
|
||||
|
||||
r.Log.V(2).Info("Finding runnerset's runner pods with selector", "ns", rs.Namespace)
|
||||
|
||||
if err := r.List(
|
||||
ctx,
|
||||
&runnerPodList,
|
||||
opts...,
|
||||
); err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
runnerMap := make(map[string]struct{})
|
||||
for _, items := range runnerPodList.Items {
|
||||
runnerMap[items.Name] = struct{}{}
|
||||
}
|
||||
|
||||
return runnerMap, nil
|
||||
},
|
||||
}
|
||||
|
||||
return r.reconcile(ctx, req, log, hra, st, func(newDesiredReplicas int) error {
|
||||
var replicas *int
|
||||
if rs.Spec.Replicas != nil {
|
||||
v := int(*rs.Spec.Replicas)
|
||||
replicas = &v
|
||||
}
|
||||
currentDesiredReplicas := getIntOrDefault(replicas, defaultReplicas)
|
||||
|
||||
if currentDesiredReplicas != newDesiredReplicas {
|
||||
copy := rs.DeepCopy()
|
||||
v := int32(newDesiredReplicas)
|
||||
copy.Spec.Replicas = &v
|
||||
|
||||
if err := r.Client.Patch(ctx, copy, client.MergeFrom(&rs)); err != nil {
|
||||
return fmt.Errorf("patching runnerset to have %d replicas: %w", newDesiredReplicas, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if !rd.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
return ctrl.Result{}, nil
|
||||
log.Info(fmt.Sprintf("Unsupported scale target %s %s: kind %s is not supported. valid kinds are %s and %s", kind, hra.Spec.ScaleTargetRef.Name, kind, "RunnerDeployment", "RunnerSet"))
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) scaleTargetFromRD(ctx context.Context, rd v1alpha1.RunnerDeployment) scaleTarget {
|
||||
st := scaleTarget{
|
||||
st: rd.Name,
|
||||
kind: "runnerdeployment",
|
||||
enterprise: rd.Spec.Template.Spec.Enterprise,
|
||||
org: rd.Spec.Template.Spec.Organization,
|
||||
repo: rd.Spec.Template.Spec.Repository,
|
||||
replicas: rd.Spec.Replicas,
|
||||
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.
|
||||
var runnerList v1alpha1.RunnerList
|
||||
|
||||
var opts []client.ListOption
|
||||
|
||||
opts = append(opts, client.InNamespace(rd.Namespace))
|
||||
|
||||
selector, err := metav1.LabelSelectorAsSelector(getSelector(&rd))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts = append(opts, client.MatchingLabelsSelector{Selector: selector})
|
||||
|
||||
r.Log.V(2).Info("Finding runners with selector", "ns", rd.Namespace)
|
||||
|
||||
if err := r.List(
|
||||
ctx,
|
||||
&runnerList,
|
||||
opts...,
|
||||
); err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
runnerMap := make(map[string]struct{})
|
||||
for _, items := range runnerList.Items {
|
||||
runnerMap[items.Name] = struct{}{}
|
||||
}
|
||||
|
||||
return runnerMap, nil
|
||||
},
|
||||
}
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
type scaleTarget struct {
|
||||
st, kind string
|
||||
enterprise, repo, org string
|
||||
replicas *int
|
||||
|
||||
getRunnerMap func() (map[string]struct{}, error)
|
||||
}
|
||||
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) reconcile(ctx context.Context, req ctrl.Request, log logr.Logger, hra v1alpha1.HorizontalRunnerAutoscaler, st scaleTarget, updatedDesiredReplicas func(int) error) (ctrl.Result, error) {
|
||||
now := time.Now()
|
||||
|
||||
minReplicas, active, upcoming, err := r.getMinReplicas(log, now, hra)
|
||||
@@ -99,7 +262,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(req ctrl.Request) (ctrl
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
newDesiredReplicas, computedReplicas, computedReplicasFromCache, err := r.computeReplicasWithCache(log, now, rd, hra, minReplicas)
|
||||
newDesiredReplicas, computedReplicas, computedReplicasFromCache, err := r.computeReplicasWithCache(log, now, st, hra, minReplicas)
|
||||
if err != nil {
|
||||
r.Recorder.Event(&hra, corev1.EventTypeNormal, "RunnerAutoscalingFailure", err.Error())
|
||||
|
||||
@@ -108,16 +271,8 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(req ctrl.Request) (ctrl
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
currentDesiredReplicas := getIntOrDefault(rd.Spec.Replicas, defaultReplicas)
|
||||
|
||||
// Please add more conditions that we can in-place update the newest runnerreplicaset without disruption
|
||||
if currentDesiredReplicas != newDesiredReplicas {
|
||||
copy := rd.DeepCopy()
|
||||
copy.Spec.Replicas = &newDesiredReplicas
|
||||
|
||||
if err := r.Client.Patch(ctx, copy, client.MergeFrom(&rd)); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("patching runnerdeployment to have %d replicas: %w", newDesiredReplicas, err)
|
||||
}
|
||||
if err := updatedDesiredReplicas(newDesiredReplicas); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
updated := hra.DeepCopy()
|
||||
@@ -288,7 +443,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) getMinReplicas(log logr.Logger, n
|
||||
return minReplicas, active, upcoming, nil
|
||||
}
|
||||
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) computeReplicasWithCache(log logr.Logger, now time.Time, rd v1alpha1.RunnerDeployment, hra v1alpha1.HorizontalRunnerAutoscaler, minReplicas int) (int, int, *int, error) {
|
||||
func (r *HorizontalRunnerAutoscalerReconciler) computeReplicasWithCache(log logr.Logger, now time.Time, st scaleTarget, hra v1alpha1.HorizontalRunnerAutoscaler, minReplicas int) (int, int, *int, error) {
|
||||
var suggestedReplicas int
|
||||
|
||||
suggestedReplicasFromCache := r.fetchSuggestedReplicasFromCache(hra)
|
||||
@@ -304,7 +459,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) computeReplicasWithCache(log logr
|
||||
suggestedReplicas = *cached
|
||||
}
|
||||
} else {
|
||||
v, err := r.suggestDesiredReplicas(rd, hra)
|
||||
v, err := r.suggestDesiredReplicas(st, hra)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestGetValidCacheEntries(t *testing.T) {
|
||||
|
||||
@@ -7,11 +7,10 @@ import (
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v33/github"
|
||||
github2 "github.com/summerwind/actions-runner-controller/github"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
github2 "github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
"github.com/google/go-github/v37/github"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/github/fake"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github/fake"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -24,7 +23,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
)
|
||||
|
||||
type testEnvironment struct {
|
||||
@@ -52,8 +51,9 @@ var (
|
||||
// * starting all the reconcilers
|
||||
// * stopping all the reconcilers after the test ends
|
||||
// Call this function at the start of each of your tests.
|
||||
func SetupIntegrationTest(ctx context.Context) *testEnvironment {
|
||||
var stopCh chan struct{}
|
||||
func SetupIntegrationTest(ctx2 context.Context) *testEnvironment {
|
||||
var ctx context.Context
|
||||
var cancel func()
|
||||
ns := &corev1.Namespace{}
|
||||
|
||||
env := &testEnvironment{
|
||||
@@ -63,7 +63,7 @@ func SetupIntegrationTest(ctx context.Context) *testEnvironment {
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
stopCh = make(chan struct{})
|
||||
ctx, cancel = context.WithCancel(ctx2)
|
||||
*ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testns-" + randStringRunes(5)},
|
||||
}
|
||||
@@ -166,13 +166,13 @@ func SetupIntegrationTest(ctx context.Context) *testEnvironment {
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := mgr.Start(stopCh)
|
||||
err := mgr.Start(ctx)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to start manager")
|
||||
}()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
close(stopCh)
|
||||
defer cancel()
|
||||
|
||||
env.fakeGithubServer.Close()
|
||||
env.webhookServer.Close()
|
||||
@@ -214,11 +214,15 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Organization: "test",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Organization: "test",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -301,11 +305,15 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -432,11 +440,15 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -474,8 +486,9 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
{
|
||||
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
||||
CheckRun: &actionsv1alpha1.CheckRunSpec{
|
||||
Types: []string{"created"},
|
||||
Status: "pending",
|
||||
Types: []string{"created"},
|
||||
Status: "pending",
|
||||
Repositories: []string{"valid", "foo", "bar"},
|
||||
},
|
||||
},
|
||||
Amount: 1,
|
||||
@@ -505,12 +518,23 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
}
|
||||
|
||||
// Scale-up to 5 replicas on second check_run create webhook event
|
||||
replicasAfterSecondWebhook := 5
|
||||
{
|
||||
env.SendOrgCheckRunEvent("test", "valid", "pending", "created")
|
||||
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 5, "runners after second webhook event")
|
||||
env.ExpectRegisteredNumberCountEventuallyEquals(5, "count of fake list runners")
|
||||
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, replicasAfterSecondWebhook, "runners after second webhook event")
|
||||
env.ExpectRegisteredNumberCountEventuallyEquals(replicasAfterSecondWebhook, "count of fake list runners")
|
||||
env.SyncRunnerRegistrations()
|
||||
ExpectRunnerCountEventuallyEquals(ctx, ns.Name, 5)
|
||||
ExpectRunnerCountEventuallyEquals(ctx, ns.Name, replicasAfterSecondWebhook)
|
||||
}
|
||||
|
||||
// Do not scale-up on third check_run create webhook event
|
||||
// example repo is not in specified in actionsv1alpha1.CheckRunSpec.Repositories
|
||||
{
|
||||
env.SendOrgCheckRunEvent("test", "example", "pending", "created")
|
||||
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, replicasAfterSecondWebhook, "runners after third webhook event")
|
||||
env.ExpectRegisteredNumberCountEventuallyEquals(replicasAfterSecondWebhook, "count of fake list runners")
|
||||
env.SyncRunnerRegistrations()
|
||||
ExpectRunnerCountEventuallyEquals(ctx, ns.Name, replicasAfterSecondWebhook)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -537,11 +561,15 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -631,11 +659,15 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -764,11 +796,15 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -857,11 +893,15 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -956,11 +996,15 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1175,7 +1219,7 @@ func (env *testEnvironment) SyncRunnerRegistrations() {
|
||||
env.fakeRunnerList.Sync(runnerList.Items)
|
||||
}
|
||||
|
||||
func ExpectCreate(ctx context.Context, rd runtime.Object, s string) {
|
||||
func ExpectCreate(ctx context.Context, rd client.Object, s string) {
|
||||
err := k8sClient.Create(ctx, rd)
|
||||
|
||||
ExpectWithOffset(1, err).NotTo(HaveOccurred(), fmt.Sprintf("failed to create %s resource", s))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
37
controllers/metrics/runnerset.go
Normal file
37
controllers/metrics/runnerset.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
rsName = "runnerset"
|
||||
rsNamespace = "namespace"
|
||||
)
|
||||
|
||||
var (
|
||||
runnerSetMetrics = []prometheus.Collector{
|
||||
runnerSetReplicas,
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
runnerSetReplicas = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "runnerset_spec_replicas",
|
||||
Help: "replicas of RunnerSet",
|
||||
},
|
||||
[]string{rsName, rsNamespace},
|
||||
)
|
||||
)
|
||||
|
||||
func SetRunnerSet(rd v1alpha1.RunnerSet) {
|
||||
labels := prometheus.Labels{
|
||||
rsName: rd.Name,
|
||||
rsNamespace: rd.Namespace,
|
||||
}
|
||||
if rd.Spec.Replicas != nil {
|
||||
runnerSetReplicas.With(labels).Set(float64(*rd.Spec.Replicas))
|
||||
}
|
||||
}
|
||||
132
controllers/pod_runner_token_injector.go
Normal file
132
controllers/pod_runner_token_injector.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
"github.com/go-logr/logr"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
const (
|
||||
AnnotationKeyTokenExpirationDate = "actions-runner-controller/token-expires-at"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/mutate-runner-set-pod,mutating=true,failurePolicy=ignore,groups="",resources=pods,verbs=create,versions=v1,name=mutate-runner-pod.webhook.actions.summerwind.dev,sideEffects=None,admissionReviewVersions=v1beta1
|
||||
|
||||
type PodRunnerTokenInjector struct {
|
||||
client.Client
|
||||
|
||||
Name string
|
||||
Log logr.Logger
|
||||
Recorder record.EventRecorder
|
||||
GitHubClient *github.Client
|
||||
decoder *admission.Decoder
|
||||
}
|
||||
|
||||
func (t *PodRunnerTokenInjector) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
var pod corev1.Pod
|
||||
err := t.decoder.Decode(req, &pod)
|
||||
if err != nil {
|
||||
t.Log.Error(err, "Failed to decode request object")
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
if pod.Annotations == nil {
|
||||
pod.Annotations = map[string]string{}
|
||||
}
|
||||
|
||||
var runnerContainer *corev1.Container
|
||||
|
||||
for i := range pod.Spec.Containers {
|
||||
c := pod.Spec.Containers[i]
|
||||
|
||||
if c.Name == "runner" {
|
||||
runnerContainer = &c
|
||||
}
|
||||
}
|
||||
|
||||
if runnerContainer == nil {
|
||||
return newEmptyResponse()
|
||||
}
|
||||
|
||||
enterprise, okEnterprise := getEnv(runnerContainer, "RUNNER_ENTERPRISE")
|
||||
repo, okRepo := getEnv(runnerContainer, "RUNNER_REPO")
|
||||
org, okOrg := getEnv(runnerContainer, "RUNNER_ORG")
|
||||
if !okRepo || !okOrg || !okEnterprise {
|
||||
return newEmptyResponse()
|
||||
}
|
||||
|
||||
rt, err := t.GitHubClient.GetRegistrationToken(context.Background(), enterprise, org, repo, pod.Name)
|
||||
if err != nil {
|
||||
t.Log.Error(err, "Failed to get new registration token")
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
ts := rt.GetExpiresAt().Format(time.RFC3339)
|
||||
|
||||
updated := mutatePod(&pod, *rt.Token)
|
||||
|
||||
updated.Annotations[AnnotationKeyTokenExpirationDate] = ts
|
||||
|
||||
if pod.Spec.RestartPolicy != corev1.RestartPolicyOnFailure {
|
||||
updated.Spec.RestartPolicy = corev1.RestartPolicyOnFailure
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(updated)
|
||||
if err != nil {
|
||||
t.Log.Error(err, "Failed to encode new object")
|
||||
return admission.Errored(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
res := admission.PatchResponseFromRaw(req.Object.Raw, buf)
|
||||
return res
|
||||
}
|
||||
|
||||
func getEnv(container *corev1.Container, key string) (string, bool) {
|
||||
for _, env := range container.Env {
|
||||
if env.Name == key {
|
||||
return env.Value, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (t *PodRunnerTokenInjector) InjectDecoder(d *admission.Decoder) error {
|
||||
t.decoder = d
|
||||
return nil
|
||||
}
|
||||
|
||||
func newEmptyResponse() admission.Response {
|
||||
pt := admissionv1.PatchTypeJSONPatch
|
||||
return admission.Response{
|
||||
Patches: []jsonpatch.Operation{},
|
||||
AdmissionResponse: admissionv1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
PatchType: &pt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PodRunnerTokenInjector) SetupWithManager(mgr ctrl.Manager) error {
|
||||
name := "pod-runner-token-injector"
|
||||
if r.Name != "" {
|
||||
name = r.Name
|
||||
}
|
||||
|
||||
r.Recorder = mgr.GetEventRecorderFor(name)
|
||||
|
||||
mgr.GetWebhookServer().Register("/mutate-runner-set-pod", &admission.Webhook{Handler: r})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gogithub "github.com/google/go-github/v33/github"
|
||||
"github.com/summerwind/actions-runner-controller/hash"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/hash"
|
||||
gogithub "github.com/google/go-github/v37/github"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
@@ -37,8 +37,8 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/summerwind/actions-runner-controller/github"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -51,6 +51,10 @@ const (
|
||||
|
||||
// This is an annotation internal to actions-runner-controller and can change in backward-incompatible ways
|
||||
annotationKeyRegistrationOnly = "actions-runner-controller/registration-only"
|
||||
|
||||
EnvVarOrg = "RUNNER_ORG"
|
||||
EnvVarRepo = "RUNNER_REPO"
|
||||
EnvVarEnterprise = "RUNNER_ENTERPRISE"
|
||||
)
|
||||
|
||||
// RunnerReconciler reconciles a Runner object
|
||||
@@ -62,6 +66,7 @@ type RunnerReconciler struct {
|
||||
GitHubClient *github.Client
|
||||
RunnerImage string
|
||||
DockerImage string
|
||||
DockerRegistryMirror string
|
||||
Name string
|
||||
RegistrationRecheckInterval time.Duration
|
||||
RegistrationRecheckJitter time.Duration
|
||||
@@ -74,8 +79,7 @@ type RunnerReconciler struct {
|
||||
// +kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
|
||||
|
||||
func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
ctx := context.Background()
|
||||
func (r *RunnerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := r.Log.WithValues("runner", req.NamespacedName)
|
||||
|
||||
var runner v1alpha1.Runner
|
||||
@@ -90,7 +94,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
}
|
||||
|
||||
if runner.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
finalizers, added := addFinalizer(runner.ObjectMeta.Finalizers)
|
||||
finalizers, added := addFinalizer(runner.ObjectMeta.Finalizers, finalizerName)
|
||||
|
||||
if added {
|
||||
newRunner := runner.DeepCopy()
|
||||
@@ -104,7 +108,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
} else {
|
||||
finalizers, removed := removeFinalizer(runner.ObjectMeta.Finalizers)
|
||||
finalizers, removed := removeFinalizer(runner.ObjectMeta.Finalizers, finalizerName)
|
||||
|
||||
if removed {
|
||||
if len(runner.Status.Registration.Token) > 0 {
|
||||
@@ -396,7 +400,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
"Runner failed to register itself to GitHub in timely manner. "+
|
||||
"Recreating the pod to see if it resolves the issue. "+
|
||||
"CAUTION: If you see this a lot, you should investigate the root cause. "+
|
||||
"See https://github.com/summerwind/actions-runner-controller/issues/288",
|
||||
"See https://github.com/actions-runner-controller/actions-runner-controller/issues/288",
|
||||
"podCreationTimestamp", pod.CreationTimestamp,
|
||||
"currentTime", currentTime,
|
||||
"configuredRegistrationTimeout", registrationTimeout,
|
||||
@@ -563,89 +567,11 @@ func (r *RunnerReconciler) updateRegistrationToken(ctx context.Context, runner v
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
var (
|
||||
privileged bool = true
|
||||
dockerdInRunner bool = runner.Spec.DockerdWithinRunnerContainer != nil && *runner.Spec.DockerdWithinRunnerContainer
|
||||
dockerEnabled bool = runner.Spec.DockerEnabled == nil || *runner.Spec.DockerEnabled
|
||||
ephemeral bool = runner.Spec.Ephemeral == nil || *runner.Spec.Ephemeral
|
||||
dockerdInRunnerPrivileged bool = dockerdInRunner
|
||||
)
|
||||
|
||||
runnerImage := runner.Spec.Image
|
||||
if runnerImage == "" {
|
||||
runnerImage = r.RunnerImage
|
||||
}
|
||||
|
||||
workDir := runner.Spec.WorkDir
|
||||
if workDir == "" {
|
||||
workDir = "/runner/_work"
|
||||
}
|
||||
|
||||
runnerImagePullPolicy := runner.Spec.ImagePullPolicy
|
||||
if runnerImagePullPolicy == "" {
|
||||
runnerImagePullPolicy = corev1.PullAlways
|
||||
}
|
||||
|
||||
env := []corev1.EnvVar{
|
||||
{
|
||||
Name: "RUNNER_NAME",
|
||||
Value: runner.Name,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_ORG",
|
||||
Value: runner.Spec.Organization,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_REPO",
|
||||
Value: runner.Spec.Repository,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_ENTERPRISE",
|
||||
Value: runner.Spec.Enterprise,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_LABELS",
|
||||
Value: strings.Join(runner.Spec.Labels, ","),
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_GROUP",
|
||||
Value: runner.Spec.Group,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_TOKEN",
|
||||
Value: runner.Status.Registration.Token,
|
||||
},
|
||||
{
|
||||
Name: "DOCKERD_IN_RUNNER",
|
||||
Value: fmt.Sprintf("%v", dockerdInRunner),
|
||||
},
|
||||
{
|
||||
Name: "GITHUB_URL",
|
||||
Value: r.GitHubClient.GithubBaseURL,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_WORKDIR",
|
||||
Value: workDir,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_EPHEMERAL",
|
||||
Value: fmt.Sprintf("%v", ephemeral),
|
||||
},
|
||||
}
|
||||
|
||||
if metav1.HasAnnotation(runner.ObjectMeta, annotationKeyRegistrationOnly) {
|
||||
env = append(env, corev1.EnvVar{
|
||||
Name: "RUNNER_REGISTRATION_ONLY",
|
||||
Value: "true",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
env = append(env, runner.Spec.Env...)
|
||||
var template corev1.Pod
|
||||
|
||||
labels := map[string]string{}
|
||||
|
||||
for k, v := range runner.Labels {
|
||||
for k, v := range runner.ObjectMeta.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
@@ -667,63 +593,290 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
// A registered runner's session and the a registration token seem to have two different and independent
|
||||
// lifecycles.
|
||||
//
|
||||
// See https://github.com/summerwind/actions-runner-controller/issues/143 for more context.
|
||||
// See https://github.com/actions-runner-controller/actions-runner-controller/issues/143 for more context.
|
||||
labels[LabelKeyPodTemplateHash] = hash.FNVHashStringObjects(
|
||||
filterLabels(runner.Labels, LabelKeyRunnerTemplateHash),
|
||||
runner.Annotations,
|
||||
filterLabels(runner.ObjectMeta.Labels, LabelKeyRunnerTemplateHash),
|
||||
runner.ObjectMeta.Annotations,
|
||||
runner.Spec,
|
||||
r.GitHubClient.GithubBaseURL,
|
||||
)
|
||||
|
||||
objectMeta := metav1.ObjectMeta{
|
||||
Name: runner.ObjectMeta.Name,
|
||||
Namespace: runner.ObjectMeta.Namespace,
|
||||
Labels: labels,
|
||||
Annotations: runner.ObjectMeta.Annotations,
|
||||
}
|
||||
|
||||
template.ObjectMeta = objectMeta
|
||||
|
||||
if len(runner.Spec.Containers) == 0 {
|
||||
template.Spec.Containers = append(template.Spec.Containers, corev1.Container{
|
||||
Name: "runner",
|
||||
ImagePullPolicy: runner.Spec.ImagePullPolicy,
|
||||
EnvFrom: runner.Spec.EnvFrom,
|
||||
Env: runner.Spec.Env,
|
||||
Resources: runner.Spec.Resources,
|
||||
})
|
||||
|
||||
if runner.Spec.DockerdWithinRunnerContainer == nil || !*runner.Spec.DockerdWithinRunnerContainer {
|
||||
template.Spec.Containers = append(template.Spec.Containers, corev1.Container{
|
||||
Name: "docker",
|
||||
VolumeMounts: runner.Spec.DockerVolumeMounts,
|
||||
Resources: runner.Spec.DockerdContainerResources,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
template.Spec.Containers = runner.Spec.Containers
|
||||
}
|
||||
|
||||
template.Spec.SecurityContext = runner.Spec.SecurityContext
|
||||
template.Spec.EnableServiceLinks = runner.Spec.EnableServiceLinks
|
||||
|
||||
registrationOnly := metav1.HasAnnotation(runner.ObjectMeta, annotationKeyRegistrationOnly)
|
||||
|
||||
pod, err := newRunnerPod(template, runner.Spec.RunnerConfig, r.RunnerImage, r.DockerImage, r.DockerRegistryMirror, r.GitHubClient.GithubBaseURL, registrationOnly)
|
||||
if err != nil {
|
||||
return pod, err
|
||||
}
|
||||
|
||||
// Customize the pod spec according to the runner spec
|
||||
runnerSpec := runner.Spec
|
||||
|
||||
if len(runnerSpec.VolumeMounts) != 0 {
|
||||
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, runnerSpec.VolumeMounts...)
|
||||
}
|
||||
|
||||
if len(runnerSpec.Volumes) != 0 {
|
||||
pod.Spec.Volumes = append(pod.Spec.Volumes, runnerSpec.Volumes...)
|
||||
}
|
||||
if len(runnerSpec.InitContainers) != 0 {
|
||||
pod.Spec.InitContainers = append(pod.Spec.InitContainers, runnerSpec.InitContainers...)
|
||||
}
|
||||
|
||||
if runnerSpec.NodeSelector != nil {
|
||||
pod.Spec.NodeSelector = runnerSpec.NodeSelector
|
||||
}
|
||||
if runnerSpec.ServiceAccountName != "" {
|
||||
pod.Spec.ServiceAccountName = runnerSpec.ServiceAccountName
|
||||
}
|
||||
if runnerSpec.AutomountServiceAccountToken != nil {
|
||||
pod.Spec.AutomountServiceAccountToken = runnerSpec.AutomountServiceAccountToken
|
||||
}
|
||||
|
||||
if len(runnerSpec.SidecarContainers) != 0 {
|
||||
pod.Spec.Containers = append(pod.Spec.Containers, runnerSpec.SidecarContainers...)
|
||||
}
|
||||
|
||||
if len(runnerSpec.ImagePullSecrets) != 0 {
|
||||
pod.Spec.ImagePullSecrets = runnerSpec.ImagePullSecrets
|
||||
}
|
||||
|
||||
if runnerSpec.Affinity != nil {
|
||||
pod.Spec.Affinity = runnerSpec.Affinity
|
||||
}
|
||||
|
||||
if len(runnerSpec.Tolerations) != 0 {
|
||||
pod.Spec.Tolerations = runnerSpec.Tolerations
|
||||
}
|
||||
|
||||
if len(runnerSpec.EphemeralContainers) != 0 {
|
||||
pod.Spec.EphemeralContainers = runnerSpec.EphemeralContainers
|
||||
}
|
||||
|
||||
if runnerSpec.TerminationGracePeriodSeconds != nil {
|
||||
pod.Spec.TerminationGracePeriodSeconds = runnerSpec.TerminationGracePeriodSeconds
|
||||
}
|
||||
|
||||
if len(runnerSpec.HostAliases) != 0 {
|
||||
pod.Spec.HostAliases = runnerSpec.HostAliases
|
||||
}
|
||||
|
||||
if runnerSpec.RuntimeClassName != nil {
|
||||
pod.Spec.RuntimeClassName = runnerSpec.RuntimeClassName
|
||||
}
|
||||
|
||||
pod.ObjectMeta.Name = runner.ObjectMeta.Name
|
||||
|
||||
// Inject the registration token and the runner name
|
||||
updated := mutatePod(&pod, runner.Status.Registration.Token)
|
||||
|
||||
if err := ctrl.SetControllerReference(&runner, updated, r.Scheme); err != nil {
|
||||
return pod, err
|
||||
}
|
||||
|
||||
return *updated, nil
|
||||
}
|
||||
|
||||
func mutatePod(pod *corev1.Pod, token string) *corev1.Pod {
|
||||
updated := pod.DeepCopy()
|
||||
|
||||
for i := range pod.Spec.Containers {
|
||||
if pod.Spec.Containers[i].Name == "runner" {
|
||||
updated.Spec.Containers[i].Env = append(updated.Spec.Containers[i].Env,
|
||||
corev1.EnvVar{
|
||||
Name: "RUNNER_NAME",
|
||||
Value: pod.ObjectMeta.Name,
|
||||
},
|
||||
corev1.EnvVar{
|
||||
Name: "RUNNER_TOKEN",
|
||||
Value: token,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func newRunnerPod(template corev1.Pod, runnerSpec v1alpha1.RunnerConfig, defaultRunnerImage, defaultDockerImage, defaultDockerRegistryMirror string, githubBaseURL string, registrationOnly bool) (corev1.Pod, error) {
|
||||
var (
|
||||
privileged bool = true
|
||||
dockerdInRunner bool = runnerSpec.DockerdWithinRunnerContainer != nil && *runnerSpec.DockerdWithinRunnerContainer
|
||||
dockerEnabled bool = runnerSpec.DockerEnabled == nil || *runnerSpec.DockerEnabled
|
||||
ephemeral bool = runnerSpec.Ephemeral == nil || *runnerSpec.Ephemeral
|
||||
dockerdInRunnerPrivileged bool = dockerdInRunner
|
||||
)
|
||||
|
||||
workDir := runnerSpec.WorkDir
|
||||
if workDir == "" {
|
||||
workDir = "/runner/_work"
|
||||
}
|
||||
|
||||
var dockerRegistryMirror string
|
||||
if runnerSpec.DockerRegistryMirror == nil {
|
||||
dockerRegistryMirror = defaultDockerRegistryMirror
|
||||
} else {
|
||||
dockerRegistryMirror = *runnerSpec.DockerRegistryMirror
|
||||
}
|
||||
|
||||
env := []corev1.EnvVar{
|
||||
{
|
||||
Name: EnvVarOrg,
|
||||
Value: runnerSpec.Organization,
|
||||
},
|
||||
{
|
||||
Name: EnvVarRepo,
|
||||
Value: runnerSpec.Repository,
|
||||
},
|
||||
{
|
||||
Name: EnvVarEnterprise,
|
||||
Value: runnerSpec.Enterprise,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_LABELS",
|
||||
Value: strings.Join(runnerSpec.Labels, ","),
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_GROUP",
|
||||
Value: runnerSpec.Group,
|
||||
},
|
||||
{
|
||||
Name: "DOCKERD_IN_RUNNER",
|
||||
Value: fmt.Sprintf("%v", dockerdInRunner),
|
||||
},
|
||||
{
|
||||
Name: "GITHUB_URL",
|
||||
Value: githubBaseURL,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_WORKDIR",
|
||||
Value: workDir,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_EPHEMERAL",
|
||||
Value: fmt.Sprintf("%v", ephemeral),
|
||||
},
|
||||
}
|
||||
|
||||
if registrationOnly {
|
||||
env = append(env, corev1.EnvVar{
|
||||
Name: "RUNNER_REGISTRATION_ONLY",
|
||||
Value: "true",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var seLinuxOptions *corev1.SELinuxOptions
|
||||
if runner.Spec.SecurityContext != nil {
|
||||
seLinuxOptions = runner.Spec.SecurityContext.SELinuxOptions
|
||||
if template.Spec.SecurityContext != nil {
|
||||
seLinuxOptions = template.Spec.SecurityContext.SELinuxOptions
|
||||
if seLinuxOptions != nil {
|
||||
privileged = false
|
||||
dockerdInRunnerPrivileged = false
|
||||
}
|
||||
}
|
||||
|
||||
pod := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: runner.Name,
|
||||
Namespace: runner.Namespace,
|
||||
Labels: labels,
|
||||
Annotations: runner.Annotations,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: "OnFailure",
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: containerName,
|
||||
Image: runnerImage,
|
||||
ImagePullPolicy: runnerImagePullPolicy,
|
||||
Env: env,
|
||||
EnvFrom: runner.Spec.EnvFrom,
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
// Runner need to run privileged if it contains DinD
|
||||
Privileged: &dockerdInRunnerPrivileged,
|
||||
},
|
||||
Resources: runner.Spec.Resources,
|
||||
},
|
||||
},
|
||||
},
|
||||
var runnerContainerIndex, dockerdContainerIndex int
|
||||
var runnerContainer, dockerdContainer *corev1.Container
|
||||
|
||||
for i := range template.Spec.Containers {
|
||||
c := template.Spec.Containers[i]
|
||||
if c.Name == containerName {
|
||||
runnerContainerIndex = i
|
||||
runnerContainer = &c
|
||||
} else if c.Name == "docker" {
|
||||
dockerdContainerIndex = i
|
||||
dockerdContainer = &c
|
||||
}
|
||||
}
|
||||
|
||||
if mtu := runner.Spec.DockerMTU; mtu != nil && dockerdInRunner {
|
||||
pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, []corev1.EnvVar{
|
||||
if runnerContainer == nil {
|
||||
runnerContainerIndex = -1
|
||||
runnerContainer = &corev1.Container{
|
||||
Name: containerName,
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
// Runner need to run privileged if it contains DinD
|
||||
Privileged: &dockerdInRunnerPrivileged,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if dockerdContainer == nil {
|
||||
dockerdContainerIndex = -1
|
||||
dockerdContainer = &corev1.Container{
|
||||
Name: "docker",
|
||||
}
|
||||
}
|
||||
|
||||
if runnerSpec.Image != "" {
|
||||
runnerContainer.Image = runnerSpec.Image
|
||||
}
|
||||
if runnerContainer.Image == "" {
|
||||
runnerContainer.Image = defaultRunnerImage
|
||||
}
|
||||
|
||||
if runnerContainer.ImagePullPolicy == "" {
|
||||
runnerContainer.ImagePullPolicy = corev1.PullAlways
|
||||
}
|
||||
|
||||
runnerContainer.Env = append(runnerContainer.Env, env...)
|
||||
|
||||
if runnerContainer.SecurityContext == nil {
|
||||
runnerContainer.SecurityContext = &corev1.SecurityContext{}
|
||||
}
|
||||
// Runner need to run privileged if it contains DinD
|
||||
runnerContainer.SecurityContext.Privileged = &dockerdInRunnerPrivileged
|
||||
|
||||
pod := template.DeepCopy()
|
||||
|
||||
if pod.Spec.RestartPolicy == "" {
|
||||
pod.Spec.RestartPolicy = "OnFailure"
|
||||
}
|
||||
|
||||
if mtu := runnerSpec.DockerMTU; mtu != nil && dockerdInRunner {
|
||||
runnerContainer.Env = append(runnerContainer.Env, []corev1.EnvVar{
|
||||
{
|
||||
Name: "MTU",
|
||||
Value: fmt.Sprintf("%d", *runner.Spec.DockerMTU),
|
||||
Value: fmt.Sprintf("%d", *runnerSpec.DockerMTU),
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
if mirror := runner.Spec.DockerRegistryMirror; mirror != nil && dockerdInRunner {
|
||||
pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, []corev1.EnvVar{
|
||||
if dockerRegistryMirror != "" && dockerdInRunner {
|
||||
runnerContainer.Env = append(runnerContainer.Env, []corev1.EnvVar{
|
||||
{
|
||||
Name: "DOCKER_REGISTRY_MIRROR",
|
||||
Value: *runner.Spec.DockerRegistryMirror,
|
||||
Value: dockerRegistryMirror,
|
||||
},
|
||||
}...)
|
||||
}
|
||||
@@ -734,32 +887,49 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
// When you're NOT using dindWithinRunner=true,
|
||||
// it must also be shared with the dind container as it seems like required to run docker steps.
|
||||
//
|
||||
// Setting VolumeSizeLimit to zero will disable /runner emptydir mount
|
||||
//
|
||||
// VolumeStorageMedium defines ways that storage can be allocated to a volume: "", "Memory", "HugePages", "HugePages-<size>"
|
||||
//
|
||||
|
||||
runnerVolumeName := "runner"
|
||||
runnerVolumeMountPath := "/runner"
|
||||
runnerVolumeEmptyDir := &corev1.EmptyDirVolumeSource{}
|
||||
|
||||
if runner.Spec.VolumeSizeLimit != nil {
|
||||
runnerVolumeEmptyDir.SizeLimit = runner.Spec.VolumeSizeLimit
|
||||
if runnerSpec.VolumeStorageMedium != nil {
|
||||
runnerVolumeEmptyDir.Medium = corev1.StorageMedium(*runnerSpec.VolumeStorageMedium)
|
||||
}
|
||||
|
||||
pod.Spec.Volumes = append(pod.Spec.Volumes,
|
||||
corev1.Volume{
|
||||
Name: runnerVolumeName,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: runnerVolumeEmptyDir,
|
||||
},
|
||||
},
|
||||
)
|
||||
if runnerSpec.VolumeSizeLimit != nil {
|
||||
runnerVolumeEmptyDir.SizeLimit = runnerSpec.VolumeSizeLimit
|
||||
}
|
||||
|
||||
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts,
|
||||
corev1.VolumeMount{
|
||||
Name: runnerVolumeName,
|
||||
MountPath: runnerVolumeMountPath,
|
||||
},
|
||||
)
|
||||
if runnerSpec.VolumeSizeLimit == nil || !runnerSpec.VolumeSizeLimit.IsZero() {
|
||||
pod.Spec.Volumes = append(pod.Spec.Volumes,
|
||||
corev1.Volume{
|
||||
Name: runnerVolumeName,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: runnerVolumeEmptyDir,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
runnerContainer.VolumeMounts = append(runnerContainer.VolumeMounts,
|
||||
corev1.VolumeMount{
|
||||
Name: runnerVolumeName,
|
||||
MountPath: runnerVolumeMountPath,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if !dockerdInRunner && dockerEnabled {
|
||||
if runnerSpec.VolumeSizeLimit != nil && runnerSpec.VolumeSizeLimit.IsZero() {
|
||||
return *pod, fmt.Errorf(
|
||||
"%s volume can't be disabled because it is required to share the working directory between the runner and the dockerd containers",
|
||||
runnerVolumeName,
|
||||
)
|
||||
}
|
||||
|
||||
pod.Spec.Volumes = append(pod.Spec.Volumes,
|
||||
corev1.Volume{
|
||||
Name: "work",
|
||||
@@ -774,7 +944,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
},
|
||||
},
|
||||
)
|
||||
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts,
|
||||
runnerContainer.VolumeMounts = append(runnerContainer.VolumeMounts,
|
||||
corev1.VolumeMount{
|
||||
Name: "work",
|
||||
MountPath: workDir,
|
||||
@@ -785,7 +955,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
ReadOnly: true,
|
||||
},
|
||||
)
|
||||
pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, []corev1.EnvVar{
|
||||
runnerContainer.Env = append(runnerContainer.Env, []corev1.EnvVar{
|
||||
{
|
||||
Name: "DOCKER_HOST",
|
||||
Value: "tcp://localhost:2376",
|
||||
@@ -801,7 +971,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
}...)
|
||||
|
||||
// Determine the volume mounts assigned to the docker sidecar. In case extra mounts are included in the RunnerSpec, append them to the standard
|
||||
// set of mounts. See https://github.com/summerwind/actions-runner-controller/issues/435 for context.
|
||||
// set of mounts. See https://github.com/actions-runner-controller/actions-runner-controller/issues/435 for context.
|
||||
dockerVolumeMounts := []corev1.VolumeMount{
|
||||
{
|
||||
Name: "work",
|
||||
@@ -816,120 +986,66 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
MountPath: "/certs/client",
|
||||
},
|
||||
}
|
||||
if extraDockerVolumeMounts := runner.Spec.DockerVolumeMounts; extraDockerVolumeMounts != nil {
|
||||
dockerVolumeMounts = append(dockerVolumeMounts, extraDockerVolumeMounts...)
|
||||
|
||||
if dockerdContainer.Image == "" {
|
||||
dockerdContainer.Image = defaultDockerImage
|
||||
}
|
||||
|
||||
pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{
|
||||
Name: "docker",
|
||||
Image: r.DockerImage,
|
||||
VolumeMounts: dockerVolumeMounts,
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: "DOCKER_TLS_CERTDIR",
|
||||
Value: "/certs",
|
||||
},
|
||||
},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: &privileged,
|
||||
SELinuxOptions: seLinuxOptions,
|
||||
},
|
||||
Resources: runner.Spec.DockerdContainerResources,
|
||||
dockerdContainer.Env = append(dockerdContainer.Env, corev1.EnvVar{
|
||||
Name: "DOCKER_TLS_CERTDIR",
|
||||
Value: "/certs",
|
||||
})
|
||||
|
||||
if mtu := runner.Spec.DockerMTU; mtu != nil {
|
||||
pod.Spec.Containers[1].Env = append(pod.Spec.Containers[1].Env, []corev1.EnvVar{
|
||||
if dockerdContainer.SecurityContext == nil {
|
||||
dockerdContainer.SecurityContext = &corev1.SecurityContext{
|
||||
Privileged: &privileged,
|
||||
SELinuxOptions: seLinuxOptions,
|
||||
}
|
||||
}
|
||||
|
||||
dockerdContainer.VolumeMounts = append(dockerdContainer.VolumeMounts, dockerVolumeMounts...)
|
||||
|
||||
if mtu := runnerSpec.DockerMTU; mtu != nil {
|
||||
dockerdContainer.Env = append(dockerdContainer.Env, []corev1.EnvVar{
|
||||
// See https://docs.docker.com/engine/security/rootless/
|
||||
{
|
||||
Name: "DOCKERD_ROOTLESS_ROOTLESSKIT_MTU",
|
||||
Value: fmt.Sprintf("%d", *runner.Spec.DockerMTU),
|
||||
Value: fmt.Sprintf("%d", *runnerSpec.DockerMTU),
|
||||
},
|
||||
}...)
|
||||
|
||||
pod.Spec.Containers[1].Args = append(pod.Spec.Containers[1].Args,
|
||||
dockerdContainer.Args = append(dockerdContainer.Args,
|
||||
"--mtu",
|
||||
fmt.Sprintf("%d", *runner.Spec.DockerMTU),
|
||||
fmt.Sprintf("%d", *runnerSpec.DockerMTU),
|
||||
)
|
||||
}
|
||||
|
||||
if mirror := runner.Spec.DockerRegistryMirror; mirror != nil {
|
||||
pod.Spec.Containers[1].Args = append(pod.Spec.Containers[1].Args,
|
||||
fmt.Sprintf("--registry-mirror=%s", *runner.Spec.DockerRegistryMirror),
|
||||
if dockerRegistryMirror != "" {
|
||||
dockerdContainer.Args = append(dockerdContainer.Args,
|
||||
fmt.Sprintf("--registry-mirror=%s", dockerRegistryMirror),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(runner.Spec.Containers) != 0 {
|
||||
pod.Spec.Containers = runner.Spec.Containers
|
||||
for i := 0; i < len(pod.Spec.Containers); i++ {
|
||||
if pod.Spec.Containers[i].Name == containerName {
|
||||
pod.Spec.Containers[i].Env = append(pod.Spec.Containers[i].Env, env...)
|
||||
}
|
||||
if runnerContainerIndex == -1 {
|
||||
pod.Spec.Containers = append([]corev1.Container{*runnerContainer}, pod.Spec.Containers...)
|
||||
|
||||
if dockerdContainerIndex != -1 {
|
||||
dockerdContainerIndex++
|
||||
}
|
||||
} else {
|
||||
pod.Spec.Containers[runnerContainerIndex] = *runnerContainer
|
||||
}
|
||||
|
||||
if !dockerdInRunner && dockerEnabled {
|
||||
if dockerdContainerIndex == -1 {
|
||||
pod.Spec.Containers = append(pod.Spec.Containers, *dockerdContainer)
|
||||
} else {
|
||||
pod.Spec.Containers[dockerdContainerIndex] = *dockerdContainer
|
||||
}
|
||||
}
|
||||
|
||||
if len(runner.Spec.VolumeMounts) != 0 {
|
||||
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, runner.Spec.VolumeMounts...)
|
||||
}
|
||||
|
||||
if len(runner.Spec.Volumes) != 0 {
|
||||
pod.Spec.Volumes = append(pod.Spec.Volumes, runner.Spec.Volumes...)
|
||||
}
|
||||
if len(runner.Spec.InitContainers) != 0 {
|
||||
pod.Spec.InitContainers = append(pod.Spec.InitContainers, runner.Spec.InitContainers...)
|
||||
}
|
||||
|
||||
if runner.Spec.NodeSelector != nil {
|
||||
pod.Spec.NodeSelector = runner.Spec.NodeSelector
|
||||
}
|
||||
if runner.Spec.ServiceAccountName != "" {
|
||||
pod.Spec.ServiceAccountName = runner.Spec.ServiceAccountName
|
||||
}
|
||||
if runner.Spec.AutomountServiceAccountToken != nil {
|
||||
pod.Spec.AutomountServiceAccountToken = runner.Spec.AutomountServiceAccountToken
|
||||
}
|
||||
|
||||
if len(runner.Spec.SidecarContainers) != 0 {
|
||||
pod.Spec.Containers = append(pod.Spec.Containers, runner.Spec.SidecarContainers...)
|
||||
}
|
||||
|
||||
if runner.Spec.SecurityContext != nil {
|
||||
pod.Spec.SecurityContext = runner.Spec.SecurityContext
|
||||
}
|
||||
|
||||
if len(runner.Spec.ImagePullSecrets) != 0 {
|
||||
pod.Spec.ImagePullSecrets = runner.Spec.ImagePullSecrets
|
||||
}
|
||||
|
||||
if runner.Spec.Affinity != nil {
|
||||
pod.Spec.Affinity = runner.Spec.Affinity
|
||||
}
|
||||
|
||||
if len(runner.Spec.Tolerations) != 0 {
|
||||
pod.Spec.Tolerations = runner.Spec.Tolerations
|
||||
}
|
||||
|
||||
if len(runner.Spec.EphemeralContainers) != 0 {
|
||||
pod.Spec.EphemeralContainers = runner.Spec.EphemeralContainers
|
||||
}
|
||||
|
||||
if runner.Spec.TerminationGracePeriodSeconds != nil {
|
||||
pod.Spec.TerminationGracePeriodSeconds = runner.Spec.TerminationGracePeriodSeconds
|
||||
}
|
||||
|
||||
if len(runner.Spec.HostAliases) != 0 {
|
||||
pod.Spec.HostAliases = runner.Spec.HostAliases
|
||||
}
|
||||
|
||||
if runner.Spec.RuntimeClassName != nil {
|
||||
pod.Spec.RuntimeClassName = runner.Spec.RuntimeClassName
|
||||
}
|
||||
|
||||
if err := ctrl.SetControllerReference(&runner, &pod, r.Scheme); err != nil {
|
||||
return pod, err
|
||||
}
|
||||
|
||||
return pod, nil
|
||||
return *pod, nil
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
@@ -947,7 +1063,7 @@ func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func addFinalizer(finalizers []string) ([]string, bool) {
|
||||
func addFinalizer(finalizers []string, finalizerName string) ([]string, bool) {
|
||||
exists := false
|
||||
for _, name := range finalizers {
|
||||
if name == finalizerName {
|
||||
@@ -962,7 +1078,7 @@ func addFinalizer(finalizers []string) ([]string, bool) {
|
||||
return append(finalizers, finalizerName), true
|
||||
}
|
||||
|
||||
func removeFinalizer(finalizers []string) ([]string, bool) {
|
||||
func removeFinalizer(finalizers []string, finalizerName string) ([]string, bool) {
|
||||
removed := false
|
||||
result := []string{}
|
||||
|
||||
|
||||
431
controllers/runner_pod_controller.go
Normal file
431
controllers/runner_pod_controller.go
Normal file
@@ -0,0 +1,431 @@
|
||||
/*
|
||||
Copyright 2020 The actions-runner-controller authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
gogithub "github.com/google/go-github/v37/github"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
)
|
||||
|
||||
// RunnerPodReconciler reconciles a Runner object
|
||||
type RunnerPodReconciler struct {
|
||||
client.Client
|
||||
Log logr.Logger
|
||||
Recorder record.EventRecorder
|
||||
Scheme *runtime.Scheme
|
||||
GitHubClient *github.Client
|
||||
Name string
|
||||
RegistrationRecheckInterval time.Duration
|
||||
RegistrationRecheckJitter time.Duration
|
||||
}
|
||||
|
||||
const (
|
||||
// This names requires at leaset one slash to work.
|
||||
// See https://github.com/google/knative-gcp/issues/378
|
||||
runnerPodFinalizerName = "actions.summerwind.dev/runner-pod"
|
||||
|
||||
AnnotationKeyLastRegistrationCheckTime = "actions-runner-controller/last-registration-check-time"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
|
||||
|
||||
func (r *RunnerPodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := r.Log.WithValues("runnerpod", req.NamespacedName)
|
||||
|
||||
var runnerPod corev1.Pod
|
||||
if err := r.Get(ctx, req.NamespacedName, &runnerPod); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
_, isRunnerPod := runnerPod.Labels[LabelKeyRunnerSetName]
|
||||
if !isRunnerPod {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
var enterprise, org, repo string
|
||||
|
||||
envvars := runnerPod.Spec.Containers[0].Env
|
||||
for _, e := range envvars {
|
||||
switch e.Name {
|
||||
case EnvVarEnterprise:
|
||||
enterprise = e.Value
|
||||
case EnvVarOrg:
|
||||
org = e.Value
|
||||
case EnvVarRepo:
|
||||
repo = e.Value
|
||||
}
|
||||
}
|
||||
|
||||
if runnerPod.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
finalizers, added := addFinalizer(runnerPod.ObjectMeta.Finalizers, runnerPodFinalizerName)
|
||||
|
||||
if added {
|
||||
newRunner := runnerPod.DeepCopy()
|
||||
newRunner.ObjectMeta.Finalizers = finalizers
|
||||
|
||||
if err := r.Patch(ctx, newRunner, client.MergeFrom(&runnerPod)); err != nil {
|
||||
log.Error(err, "Failed to update runner")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
} else {
|
||||
finalizers, removed := removeFinalizer(runnerPod.ObjectMeta.Finalizers, runnerPodFinalizerName)
|
||||
|
||||
if removed {
|
||||
ok, err := r.unregisterRunner(ctx, enterprise, org, repo, runnerPod.Name)
|
||||
if err != nil {
|
||||
if errors.Is(err, &gogithub.RateLimitError{}) {
|
||||
// We log the underlying error when we failed calling GitHub API to list or unregisters,
|
||||
// or the runner is still busy.
|
||||
log.Error(
|
||||
err,
|
||||
fmt.Sprintf(
|
||||
"Failed to unregister runner due to GitHub API rate limits. Delaying retry for %s to avoid excessive GitHub API calls",
|
||||
retryDelayOnGitHubAPIRateLimitError,
|
||||
),
|
||||
)
|
||||
|
||||
return ctrl.Result{RequeueAfter: retryDelayOnGitHubAPIRateLimitError}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.V(1).Info("Runner no longer exists on GitHub")
|
||||
}
|
||||
|
||||
newRunner := runnerPod.DeepCopy()
|
||||
newRunner.ObjectMeta.Finalizers = finalizers
|
||||
|
||||
if err := r.Patch(ctx, newRunner, client.MergeFrom(&runnerPod)); err != nil {
|
||||
log.Error(err, "Failed to update runner for finalizer removal")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
log.Info("Removed runner from GitHub", "repository", repo, "organization", org)
|
||||
}
|
||||
|
||||
deletionTimeout := 1 * time.Minute
|
||||
currentTime := time.Now()
|
||||
deletionDidTimeout := currentTime.Sub(runnerPod.DeletionTimestamp.Add(deletionTimeout)) > 0
|
||||
|
||||
if deletionDidTimeout {
|
||||
log.Info(
|
||||
fmt.Sprintf("Failed to delete pod within %s. ", deletionTimeout)+
|
||||
"This is typically the case when a Kubernetes node became unreachable "+
|
||||
"and the kube controller started evicting nodes. Forcefully deleting the pod to not get stuck.",
|
||||
"podDeletionTimestamp", runnerPod.DeletionTimestamp,
|
||||
"currentTime", currentTime,
|
||||
"configuredDeletionTimeout", deletionTimeout,
|
||||
)
|
||||
|
||||
var force int64 = 0
|
||||
// forcefully delete runner as we would otherwise get stuck if the node stays unreachable
|
||||
if err := r.Delete(ctx, &runnerPod, &client.DeleteOptions{GracePeriodSeconds: &force}); err != nil {
|
||||
// probably
|
||||
if !kerrors.IsNotFound(err) {
|
||||
log.Error(err, "Failed to forcefully delete pod resource ...")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// forceful deletion finally succeeded
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
r.Recorder.Event(&runnerPod, corev1.EventTypeNormal, "PodDeleted", fmt.Sprintf("Forcefully deleted pod '%s'", runnerPod.Name))
|
||||
log.Info("Forcefully deleted runner pod", "repository", repo)
|
||||
// give kube manager a little time to forcefully delete the stuck pod
|
||||
return ctrl.Result{RequeueAfter: 3 * time.Second}, nil
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// If pod has ended up succeeded we need to restart it
|
||||
// Happens e.g. when dind is in runner and run completes
|
||||
stopped := runnerPod.Status.Phase == corev1.PodSucceeded
|
||||
|
||||
if !stopped {
|
||||
if runnerPod.Status.Phase == corev1.PodRunning {
|
||||
for _, status := range runnerPod.Status.ContainerStatuses {
|
||||
if status.Name != containerName {
|
||||
continue
|
||||
}
|
||||
|
||||
if status.State.Terminated != nil && status.State.Terminated.ExitCode == 0 {
|
||||
stopped = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
restart := stopped
|
||||
|
||||
var registrationRecheckDelay time.Duration
|
||||
|
||||
// all checks done below only decide whether a restart is needed
|
||||
// if a restart was already decided before, there is no need for the checks
|
||||
// saving API calls and scary log messages
|
||||
if !restart {
|
||||
registrationCheckInterval := time.Minute
|
||||
if r.RegistrationRecheckInterval > 0 {
|
||||
registrationCheckInterval = r.RegistrationRecheckInterval
|
||||
}
|
||||
|
||||
lastCheckTimeStr := runnerPod.Annotations[AnnotationKeyLastRegistrationCheckTime]
|
||||
|
||||
var lastCheckTime *time.Time
|
||||
|
||||
if lastCheckTimeStr != "" {
|
||||
t, err := time.Parse(time.RFC3339, lastCheckTimeStr)
|
||||
if err != nil {
|
||||
log.Error(err, "failed to parase last check time %q", lastCheckTimeStr)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
lastCheckTime = &t
|
||||
}
|
||||
|
||||
// We want to call ListRunners GitHub Actions API only once per runner per minute.
|
||||
// This if block, in conjunction with:
|
||||
// return ctrl.Result{RequeueAfter: registrationRecheckDelay}, nil
|
||||
// achieves that.
|
||||
if lastCheckTime != nil {
|
||||
nextCheckTime := lastCheckTime.Add(registrationCheckInterval)
|
||||
now := time.Now()
|
||||
|
||||
// Requeue scheduled by RequeueAfter can happen a bit earlier (like dozens of milliseconds)
|
||||
// so to avoid excessive, in-effective retry, we heuristically ignore the remaining delay in case it is
|
||||
// shorter than 1s
|
||||
requeueAfter := nextCheckTime.Sub(now) - time.Second
|
||||
if requeueAfter > 0 {
|
||||
log.Info(
|
||||
fmt.Sprintf("Skipped registration check because it's deferred until %s. Retrying in %s at latest", nextCheckTime, requeueAfter),
|
||||
"lastRegistrationCheckTime", lastCheckTime,
|
||||
"registrationCheckInterval", registrationCheckInterval,
|
||||
)
|
||||
|
||||
// Without RequeueAfter, the controller may not retry on scheduled. Instead, it must wait until the
|
||||
// next sync period passes, which can be too much later than nextCheckTime.
|
||||
//
|
||||
// We need to requeue on this reconcilation even though we have already scheduled the initial
|
||||
// requeue previously with `return ctrl.Result{RequeueAfter: registrationRecheckDelay}, nil`.
|
||||
// Apparently, the workqueue used by controller-runtime seems to deduplicate and resets the delay on
|
||||
// other requeues- so the initial scheduled requeue may have been reset due to requeue on
|
||||
// spec/status change.
|
||||
return ctrl.Result{RequeueAfter: requeueAfter}, nil
|
||||
}
|
||||
}
|
||||
|
||||
notFound := false
|
||||
offline := false
|
||||
|
||||
_, err := r.GitHubClient.IsRunnerBusy(ctx, enterprise, org, repo, runnerPod.Name)
|
||||
|
||||
currentTime := time.Now()
|
||||
|
||||
if err != nil {
|
||||
var notFoundException *github.RunnerNotFound
|
||||
var offlineException *github.RunnerOffline
|
||||
if errors.As(err, ¬FoundException) {
|
||||
notFound = true
|
||||
} else if errors.As(err, &offlineException) {
|
||||
offline = true
|
||||
} else {
|
||||
var e *gogithub.RateLimitError
|
||||
if errors.As(err, &e) {
|
||||
// We log the underlying error when we failed calling GitHub API to list or unregisters,
|
||||
// or the runner is still busy.
|
||||
log.Error(
|
||||
err,
|
||||
fmt.Sprintf(
|
||||
"Failed to check if runner is busy due to Github API rate limit. Retrying in %s to avoid excessive GitHub API calls",
|
||||
retryDelayOnGitHubAPIRateLimitError,
|
||||
),
|
||||
)
|
||||
|
||||
return ctrl.Result{RequeueAfter: retryDelayOnGitHubAPIRateLimitError}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
registrationTimeout := 10 * time.Minute
|
||||
durationAfterRegistrationTimeout := currentTime.Sub(runnerPod.CreationTimestamp.Add(registrationTimeout))
|
||||
registrationDidTimeout := durationAfterRegistrationTimeout > 0
|
||||
|
||||
if notFound {
|
||||
if registrationDidTimeout {
|
||||
log.Info(
|
||||
"Runner failed to register itself to GitHub in timely manner. "+
|
||||
"Recreating the pod to see if it resolves the issue. "+
|
||||
"CAUTION: If you see this a lot, you should investigate the root cause. "+
|
||||
"See https://github.com/actions-runner-controller/actions-runner-controller/issues/288",
|
||||
"podCreationTimestamp", runnerPod.CreationTimestamp,
|
||||
"currentTime", currentTime,
|
||||
"configuredRegistrationTimeout", registrationTimeout,
|
||||
)
|
||||
|
||||
restart = true
|
||||
} else {
|
||||
log.V(1).Info(
|
||||
"Runner pod exists but we failed to check if runner is busy. Apparently it still needs more time.",
|
||||
"runnerName", runnerPod.Name,
|
||||
)
|
||||
}
|
||||
} else if offline {
|
||||
if registrationDidTimeout {
|
||||
log.Info(
|
||||
"Already existing GitHub runner still appears offline . "+
|
||||
"Recreating the pod to see if it resolves the issue. "+
|
||||
"CAUTION: If you see this a lot, you should investigate the root cause. ",
|
||||
"podCreationTimestamp", runnerPod.CreationTimestamp,
|
||||
"currentTime", currentTime,
|
||||
"configuredRegistrationTimeout", registrationTimeout,
|
||||
)
|
||||
|
||||
restart = true
|
||||
} else {
|
||||
log.V(1).Info(
|
||||
"Runner pod exists but the GitHub runner appears to be still offline. Waiting for runner to get online ...",
|
||||
"runnerName", runnerPod.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (notFound || offline) && !registrationDidTimeout {
|
||||
registrationRecheckJitter := 10 * time.Second
|
||||
if r.RegistrationRecheckJitter > 0 {
|
||||
registrationRecheckJitter = r.RegistrationRecheckJitter
|
||||
}
|
||||
|
||||
registrationRecheckDelay = registrationCheckInterval + wait.Jitter(registrationRecheckJitter, 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't do anything if there's no need to restart the runner
|
||||
if !restart {
|
||||
// This guard enables us to update runner.Status.Phase to `Running` only after
|
||||
// the runner is registered to GitHub.
|
||||
if registrationRecheckDelay > 0 {
|
||||
log.V(1).Info(fmt.Sprintf("Rechecking the runner registration in %s", registrationRecheckDelay))
|
||||
|
||||
updated := runnerPod.DeepCopy()
|
||||
t := time.Now().Format(time.RFC3339)
|
||||
updated.Annotations[AnnotationKeyLastRegistrationCheckTime] = t
|
||||
|
||||
if err := r.Patch(ctx, updated, client.MergeFrom(&runnerPod)); err != nil {
|
||||
log.Error(err, "Failed to update runner pod annotation for LastRegistrationCheckTime")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{RequeueAfter: registrationRecheckDelay}, nil
|
||||
}
|
||||
|
||||
// Seeing this message, you can expect the runner to become `Running` soon.
|
||||
log.Info(
|
||||
"Runner appears to have registered and running.",
|
||||
"podCreationTimestamp", runnerPod.CreationTimestamp,
|
||||
)
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Delete current pod if recreation is needed
|
||||
if err := r.Delete(ctx, &runnerPod); err != nil {
|
||||
log.Error(err, "Failed to delete pod resource")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
r.Recorder.Event(&runnerPod, corev1.EventTypeNormal, "PodDeleted", fmt.Sprintf("Deleted pod '%s'", runnerPod.Name))
|
||||
log.Info("Deleted runner pod", "name", runnerPod.Name)
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *RunnerPodReconciler) unregisterRunner(ctx context.Context, enterprise, org, repo, name string) (bool, error) {
|
||||
runners, err := r.GitHubClient.ListRunners(ctx, enterprise, org, repo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var busy bool
|
||||
|
||||
id := int64(0)
|
||||
for _, runner := range runners {
|
||||
if runner.GetName() == name {
|
||||
// Sometimes a runner can stuck "busy" even though it is already "offline".
|
||||
// Thus removing the condition on status can block the runner pod from being terminated forever.
|
||||
busy = runner.GetBusy()
|
||||
if runner.GetStatus() != "offline" && busy {
|
||||
r.Log.Info("This runner will delay the runner pod deletion and the runner deregistration until it becomes either offline or non-busy", "name", runner.GetName(), "status", runner.GetStatus(), "busy", runner.GetBusy())
|
||||
return false, fmt.Errorf("runner is busy")
|
||||
}
|
||||
id = runner.GetID()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if id == int64(0) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Sometimes a runner can stuck "busy" even though it is already "offline".
|
||||
// Trying to remove the offline but busy runner can result in errors like the following:
|
||||
// failed to remove runner: DELETE https://api.github.com/repos/actions-runner-controller/mumoshu-actions-test/actions/runners/47: 422 Bad request - Runner \"example-runnerset-0\" is still running a job\" []
|
||||
if !busy {
|
||||
if err := r.GitHubClient.RemoveRunner(ctx, enterprise, org, repo, id); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *RunnerPodReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
name := "runnerpod-controller"
|
||||
if r.Name != "" {
|
||||
name = r.Name
|
||||
}
|
||||
|
||||
r.Recorder = mgr.GetEventRecorderFor(name)
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&corev1.Pod{}).
|
||||
Named(name).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -37,8 +37,8 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/summerwind/actions-runner-controller/controllers/metrics"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/controllers/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -65,8 +65,7 @@ type RunnerDeploymentReconciler struct {
|
||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerreplicasets/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
|
||||
|
||||
func (r *RunnerDeploymentReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
ctx := context.Background()
|
||||
func (r *RunnerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := r.Log.WithValues("runnerdeployment", req.NamespacedName)
|
||||
|
||||
var rd v1alpha1.RunnerDeployment
|
||||
@@ -155,7 +154,7 @@ func (r *RunnerDeploymentReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e
|
||||
// A selector update change doesn't trigger replicaset replacement,
|
||||
// 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
|
||||
// See https://github.com/summerwind/actions-runner-controller/pull/355#discussion_r585379259
|
||||
// See https://github.com/actions-runner-controller/actions-runner-controller/pull/355#discussion_r585379259
|
||||
if err := r.Client.Update(ctx, updateSet); err != nil {
|
||||
log.Error(err, "Failed to update runnerreplicaset resource")
|
||||
|
||||
@@ -439,7 +438,7 @@ func (r *RunnerDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
|
||||
r.Recorder = mgr.GetEventRecorderFor(name)
|
||||
|
||||
if err := mgr.GetFieldIndexer().IndexField(&v1alpha1.RunnerReplicaSet{}, runnerSetOwnerKey, func(rawObj runtime.Object) []string {
|
||||
if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &v1alpha1.RunnerReplicaSet{}, runnerSetOwnerKey, func(rawObj client.Object) []string {
|
||||
runnerSet := rawObj.(*v1alpha1.RunnerReplicaSet)
|
||||
owner := metav1.GetControllerOf(runnerSet)
|
||||
if owner == nil {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
)
|
||||
|
||||
func TestNewRunnerReplicaSet(t *testing.T) {
|
||||
@@ -50,7 +50,9 @@ func TestNewRunnerReplicaSet(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Labels: []string{"project1"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Labels: []string{"project1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -126,12 +128,13 @@ func TestNewRunnerReplicaSet(t *testing.T) {
|
||||
// * starting the 'RunnerDeploymentReconciler'
|
||||
// * stopping the 'RunnerDeploymentReconciler" after the test ends
|
||||
// Call this function at the start of each of your tests.
|
||||
func SetupDeploymentTest(ctx context.Context) *corev1.Namespace {
|
||||
var stopCh chan struct{}
|
||||
func SetupDeploymentTest(ctx2 context.Context) *corev1.Namespace {
|
||||
var ctx context.Context
|
||||
var cancel func()
|
||||
ns := &corev1.Namespace{}
|
||||
|
||||
BeforeEach(func() {
|
||||
stopCh = make(chan struct{})
|
||||
ctx, cancel = context.WithCancel(ctx2)
|
||||
*ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testns-" + randStringRunes(5)},
|
||||
}
|
||||
@@ -157,13 +160,13 @@ func SetupDeploymentTest(ctx context.Context) *corev1.Namespace {
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := mgr.Start(stopCh)
|
||||
err := mgr.Start(ctx)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to start manager")
|
||||
}()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
close(stopCh)
|
||||
defer cancel()
|
||||
|
||||
err := k8sClient.Delete(ctx, ns)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace")
|
||||
@@ -201,10 +204,14 @@ var _ = Context("Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -297,10 +304,14 @@ var _ = Context("Inside of a new namespace", func() {
|
||||
Replicas: intPtr(1),
|
||||
Template: actionsv1alpha1.RunnerTemplate{
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -393,10 +404,14 @@ var _ = Context("Inside of a new namespace", func() {
|
||||
Replicas: intPtr(1),
|
||||
Template: actionsv1alpha1.RunnerTemplate{
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
gogithub "github.com/google/go-github/v33/github"
|
||||
gogithub "github.com/google/go-github/v37/github"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -35,8 +35,8 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/summerwind/actions-runner-controller/github"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
)
|
||||
|
||||
// RunnerReplicaSetReconciler reconciles a Runner object
|
||||
@@ -56,8 +56,7 @@ type RunnerReplicaSetReconciler struct {
|
||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runners/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
|
||||
|
||||
func (r *RunnerReplicaSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
ctx := context.Background()
|
||||
func (r *RunnerReplicaSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := r.Log.WithValues("runnerreplicaset", req.NamespacedName)
|
||||
|
||||
var rs v1alpha1.RunnerReplicaSet
|
||||
@@ -232,7 +231,7 @@ func (r *RunnerReplicaSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e
|
||||
"Runner failed to register itself to GitHub in timely manner. "+
|
||||
"Marking the runner for scale down. "+
|
||||
"CAUTION: If you see this a lot, you should investigate the root cause. "+
|
||||
"See https://github.com/summerwind/actions-runner-controller/issues/288",
|
||||
"See https://github.com/actions-runner-controller/actions-runner-controller/issues/288",
|
||||
"runnerCreationTimestamp", runner.CreationTimestamp,
|
||||
"currentTime", currentTime,
|
||||
"configuredRegistrationTimeout", registrationTimeout,
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/summerwind/actions-runner-controller/github/fake"
|
||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github/fake"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -33,12 +33,13 @@ var (
|
||||
// * starting the 'RunnerReconciler'
|
||||
// * stopping the 'RunnerReplicaSetReconciler" after the test ends
|
||||
// Call this function at the start of each of your tests.
|
||||
func SetupTest(ctx context.Context) *corev1.Namespace {
|
||||
var stopCh chan struct{}
|
||||
func SetupTest(ctx2 context.Context) *corev1.Namespace {
|
||||
var ctx context.Context
|
||||
var cancel func()
|
||||
ns := &corev1.Namespace{}
|
||||
|
||||
BeforeEach(func() {
|
||||
stopCh = make(chan struct{})
|
||||
ctx, cancel = context.WithCancel(ctx2)
|
||||
*ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testns-" + randStringRunes(5)},
|
||||
}
|
||||
@@ -69,13 +70,13 @@ func SetupTest(ctx context.Context) *corev1.Namespace {
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := mgr.Start(stopCh)
|
||||
err := mgr.Start(ctx)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to start manager")
|
||||
}()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
close(stopCh)
|
||||
defer cancel()
|
||||
|
||||
server.Close()
|
||||
err := k8sClient.Delete(ctx, ns)
|
||||
@@ -128,10 +129,14 @@ var _ = Context("Inside of a new namespace", func() {
|
||||
},
|
||||
},
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
},
|
||||
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
318
controllers/runnerset_controller.go
Normal file
318
controllers/runnerset_controller.go
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
Copyright 2021 The actions-runner-controller authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/controllers/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
LabelKeyRunnerSetName = "runnerset-name"
|
||||
)
|
||||
|
||||
// RunnerSetReconciler reconciles a Runner object
|
||||
type RunnerSetReconciler struct {
|
||||
Name string
|
||||
|
||||
client.Client
|
||||
Log logr.Logger
|
||||
Recorder record.EventRecorder
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
CommonRunnerLabels []string
|
||||
GitHubBaseURL string
|
||||
RunnerImage string
|
||||
DockerImage string
|
||||
DockerRegistryMirror string
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnersets,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnersets/finalizers,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnersets/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=apps,resources=statefulsets/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
|
||||
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update
|
||||
|
||||
// Note that coordination.k8s.io/leases permission must be added to any of the controllers to avoid the following error:
|
||||
// E0613 07:02:08.004278 1 leaderelection.go:325] error retrieving resource lock actions-runner-system/actions-runner-controller: leases.coordination.k8s.io "actions-runner-controller" is forbidden: User "system:serviceaccount:actions-runner-system:actions-runner-controller" cannot get resource "leases" in API group "coordination.k8s.io" in the namespace "actions-runner-system"
|
||||
|
||||
func (r *RunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := r.Log.WithValues("runnerset", req.NamespacedName)
|
||||
|
||||
runnerSet := &v1alpha1.RunnerSet{}
|
||||
if err := r.Get(ctx, req.NamespacedName, runnerSet); err != nil {
|
||||
err = client.IgnoreNotFound(err)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err, "Could not get RunnerSet")
|
||||
}
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if !runnerSet.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
metrics.SetRunnerSet(*runnerSet)
|
||||
|
||||
desiredStatefulSet, err := r.newStatefulSet(runnerSet)
|
||||
if err != nil {
|
||||
r.Recorder.Event(runnerSet, corev1.EventTypeNormal, "RunnerAutoscalingFailure", err.Error())
|
||||
|
||||
log.Error(err, "Could not create statefulset")
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
liveStatefulSet := &appsv1.StatefulSet{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: runnerSet.Namespace, Name: runnerSet.Name}, liveStatefulSet); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
log.Error(err, "Failed to get live statefulset")
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if err := r.Client.Create(ctx, desiredStatefulSet); err != nil {
|
||||
log.Error(err, "Failed to create statefulset resource")
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
liveTemplateHash, ok := getStatefulSetTemplateHash(liveStatefulSet)
|
||||
if !ok {
|
||||
log.Info("Failed to get template hash of newest statefulset resource. It must be in an invalid state. Please manually delete the statefulset so that it is recreated")
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
desiredTemplateHash, ok := getStatefulSetTemplateHash(desiredStatefulSet)
|
||||
if !ok {
|
||||
log.Info("Failed to get template hash of desired statefulset. It must be in an invalid state. Please manually delete the statefulset so that it is recreated")
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if liveTemplateHash != desiredTemplateHash {
|
||||
copy := liveStatefulSet.DeepCopy()
|
||||
copy.Spec = desiredStatefulSet.Spec
|
||||
|
||||
if err := r.Client.Patch(ctx, copy, client.MergeFrom(liveStatefulSet)); err != nil {
|
||||
log.Error(err, "Failed to patch statefulset", "reason", errors.ReasonForError(err))
|
||||
|
||||
if errors.IsInvalid(err) {
|
||||
// NOTE: This might not be ideal but deal the forbidden error by recreating the statefulset
|
||||
// Probably we'd better create a registration-only runner to prevent queued jobs from immediately failing.
|
||||
//
|
||||
// 2021-06-13T07:19:52.760Z ERROR actions-runner-controller.runnerset Failed to patch statefulset
|
||||
// {"runnerset": "default/example-runnerset", "error": "StatefulSet.apps \"example-runnerset\" is invalid: s
|
||||
// pec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'template', and 'updateStrategy'
|
||||
// are forbidden"}
|
||||
//
|
||||
// Even though the error message includes "Forbidden", this error's reason is "Invalid".
|
||||
// That's why we're using errors.IsInvalid above.
|
||||
|
||||
if err := r.Client.Delete(ctx, liveStatefulSet); err != nil {
|
||||
log.Error(err, "Failed to delete statefulset for force-update")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
log.Info("Deleted statefulset for force-update")
|
||||
}
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// We requeue in order to clean up old runner replica sets later.
|
||||
// Otherwise, they aren't cleaned up until the next re-sync interval.
|
||||
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||
}
|
||||
|
||||
const defaultReplicas = 1
|
||||
|
||||
var replicasOfLiveStatefulSet *int
|
||||
if liveStatefulSet.Spec.Replicas != nil {
|
||||
v := int(*liveStatefulSet.Spec.Replicas)
|
||||
replicasOfLiveStatefulSet = &v
|
||||
}
|
||||
|
||||
var replicasOfDesiredStatefulSet *int
|
||||
if desiredStatefulSet.Spec.Replicas != nil {
|
||||
v := int(*desiredStatefulSet.Spec.Replicas)
|
||||
replicasOfDesiredStatefulSet = &v
|
||||
}
|
||||
|
||||
currentDesiredReplicas := getIntOrDefault(replicasOfLiveStatefulSet, defaultReplicas)
|
||||
newDesiredReplicas := getIntOrDefault(replicasOfDesiredStatefulSet, defaultReplicas)
|
||||
|
||||
// Please add more conditions that we can in-place update the newest runnerreplicaset without disruption
|
||||
if currentDesiredReplicas != newDesiredReplicas {
|
||||
v := int32(newDesiredReplicas)
|
||||
|
||||
updated := liveStatefulSet.DeepCopy()
|
||||
updated.Spec.Replicas = &v
|
||||
|
||||
if err := r.Client.Patch(ctx, updated, client.MergeFrom(liveStatefulSet)); err != nil {
|
||||
log.Error(err, "Failed to update statefulset")
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
statusReplicas := int(liveStatefulSet.Status.Replicas)
|
||||
statusReadyReplicas := int(liveStatefulSet.Status.ReadyReplicas)
|
||||
totalCurrentReplicas := int(liveStatefulSet.Status.CurrentReplicas)
|
||||
updatedReplicas := int(liveStatefulSet.Status.UpdatedReplicas)
|
||||
|
||||
status := runnerSet.Status.DeepCopy()
|
||||
|
||||
status.CurrentReplicas = &totalCurrentReplicas
|
||||
status.ReadyReplicas = &statusReadyReplicas
|
||||
status.DesiredReplicas = &newDesiredReplicas
|
||||
status.Replicas = &statusReplicas
|
||||
status.UpdatedReplicas = &updatedReplicas
|
||||
|
||||
if !reflect.DeepEqual(runnerSet.Status, status) {
|
||||
updated := runnerSet.DeepCopy()
|
||||
updated.Status = *status
|
||||
|
||||
if err := r.Status().Patch(ctx, updated, client.MergeFrom(runnerSet)); err != nil {
|
||||
log.Info("Failed to patch runnerset status. Retrying immediately", "error", err.Error())
|
||||
return ctrl.Result{
|
||||
Requeue: true,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func getStatefulSetTemplateHash(rs *appsv1.StatefulSet) (string, bool) {
|
||||
hash, ok := rs.Labels[LabelKeyRunnerTemplateHash]
|
||||
|
||||
return hash, ok
|
||||
}
|
||||
|
||||
func getRunnerSetSelector(runnerSet *v1alpha1.RunnerSet) *metav1.LabelSelector {
|
||||
selector := runnerSet.Spec.Selector
|
||||
if selector == nil {
|
||||
selector = &metav1.LabelSelector{MatchLabels: map[string]string{LabelKeyRunnerSetName: runnerSet.Name}}
|
||||
}
|
||||
|
||||
return selector
|
||||
}
|
||||
|
||||
var LabelKeyPodMutation = "actions-runner-controller/inject-registration-token"
|
||||
var LabelValuePodMutation = "true"
|
||||
|
||||
func (r *RunnerSetReconciler) newStatefulSet(runnerSet *v1alpha1.RunnerSet) (*appsv1.StatefulSet, error) {
|
||||
runnerSetWithOverrides := *runnerSet.Spec.DeepCopy()
|
||||
|
||||
for _, l := range r.CommonRunnerLabels {
|
||||
runnerSetWithOverrides.Labels = append(runnerSetWithOverrides.Labels, l)
|
||||
}
|
||||
|
||||
// This label selector is used by default when rd.Spec.Selector is empty.
|
||||
runnerSetWithOverrides.Template.ObjectMeta.Labels = CloneAndAddLabel(runnerSetWithOverrides.Template.ObjectMeta.Labels, LabelKeyRunnerSetName, runnerSet.Name)
|
||||
|
||||
runnerSetWithOverrides.Template.ObjectMeta.Labels = CloneAndAddLabel(runnerSetWithOverrides.Template.ObjectMeta.Labels, LabelKeyPodMutation, LabelValuePodMutation)
|
||||
|
||||
template := corev1.Pod{
|
||||
ObjectMeta: runnerSetWithOverrides.StatefulSetSpec.Template.ObjectMeta,
|
||||
Spec: runnerSetWithOverrides.StatefulSetSpec.Template.Spec,
|
||||
}
|
||||
|
||||
pod, err := newRunnerPod(template, runnerSet.Spec.RunnerConfig, r.RunnerImage, r.DockerImage, r.DockerRegistryMirror, r.GitHubBaseURL, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
runnerSetWithOverrides.StatefulSetSpec.Template.ObjectMeta = pod.ObjectMeta
|
||||
runnerSetWithOverrides.StatefulSetSpec.Template.Spec = pod.Spec
|
||||
// NOTE: Seems like the only supported restart policy for statefulset is "Always"?
|
||||
// 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\"
|
||||
// :\"85d7578bd6\", \"runnerset-name\":\"example-runnerset\"}: `selector` does not match template `labels`, spec.
|
||||
// template.spec.restartPolicy: Unsupported value: \"OnFailure\": supported values: \"Always\"]
|
||||
runnerSetWithOverrides.StatefulSetSpec.Template.Spec.RestartPolicy = corev1.RestartPolicyAlways
|
||||
|
||||
templateHash := ComputeHash(pod.Spec)
|
||||
|
||||
// Add template hash label to selector.
|
||||
runnerSetWithOverrides.Template.ObjectMeta.Labels = CloneAndAddLabel(runnerSetWithOverrides.Template.ObjectMeta.Labels, LabelKeyRunnerTemplateHash, templateHash)
|
||||
|
||||
selector := getRunnerSetSelector(runnerSet)
|
||||
selector = CloneSelectorAndAddLabel(selector, LabelKeyRunnerTemplateHash, templateHash)
|
||||
selector = CloneSelectorAndAddLabel(selector, LabelKeyRunnerSetName, runnerSet.Name)
|
||||
selector = CloneSelectorAndAddLabel(selector, LabelKeyPodMutation, LabelValuePodMutation)
|
||||
|
||||
runnerSetWithOverrides.StatefulSetSpec.Selector = selector
|
||||
|
||||
rs := appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: runnerSet.ObjectMeta.Name,
|
||||
Namespace: runnerSet.ObjectMeta.Namespace,
|
||||
Labels: CloneAndAddLabel(runnerSet.ObjectMeta.Labels, LabelKeyRunnerTemplateHash, templateHash),
|
||||
},
|
||||
Spec: runnerSetWithOverrides.StatefulSetSpec,
|
||||
}
|
||||
|
||||
if err := ctrl.SetControllerReference(runnerSet, &rs, r.Scheme); err != nil {
|
||||
return &rs, err
|
||||
}
|
||||
|
||||
return &rs, nil
|
||||
}
|
||||
|
||||
func (r *RunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
name := "runnerset-controller"
|
||||
if r.Name != "" {
|
||||
name = r.Name
|
||||
}
|
||||
|
||||
r.Recorder = mgr.GetEventRecorderFor(name)
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&v1alpha1.RunnerSet{}).
|
||||
Owns(&appsv1.StatefulSet{}).
|
||||
Named(name).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -17,19 +17,21 @@ limitations under the License.
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/onsi/ginkgo/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/ginkgo/config"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
// +kubebuilder:scaffold:imports
|
||||
@@ -45,15 +47,15 @@ var testEnv *envtest.Environment
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
config.GinkgoConfig.FocusString = os.Getenv("GINKGO_FOCUS")
|
||||
config.GinkgoConfig.FocusStrings = append(config.GinkgoConfig.FocusStrings, os.Getenv("GINKGO_FOCUS"))
|
||||
|
||||
RunSpecsWithDefaultAndCustomReporters(t,
|
||||
"Controller Suite",
|
||||
[]Reporter{envtest.NewlineReporter{}})
|
||||
[]Reporter{printer.NewlineReporter{}})
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func(done Done) {
|
||||
logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
|
||||
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
|
||||
|
||||
var apiServerFlags []string
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
|
||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
|
||||
"github.com/google/go-github/v33/github"
|
||||
"github.com/google/go-github/v37/github"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github/metrics"
|
||||
"github.com/bradleyfalzon/ghinstallation"
|
||||
"github.com/google/go-github/v33/github"
|
||||
"github.com/summerwind/actions-runner-controller/github/metrics"
|
||||
"github.com/google/go-github/v37/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v33/github"
|
||||
"github.com/summerwind/actions-runner-controller/github/fake"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github/fake"
|
||||
"github.com/google/go-github/v37/github"
|
||||
)
|
||||
|
||||
var server *httptest.Server
|
||||
|
||||
31
go.mod
31
go.mod
@@ -1,24 +1,27 @@
|
||||
module github.com/summerwind/actions-runner-controller
|
||||
module github.com/actions-runner-controller/actions-runner-controller
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/bradleyfalzon/ghinstallation v1.1.1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-logr/logr v0.1.0
|
||||
github.com/google/go-cmp v0.3.1
|
||||
github.com/google/go-github/v33 v33.0.1-0.20210204004227-319dcffb518a
|
||||
github.com/go-logr/logr v0.4.0
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/go-github/v37 v37.0.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/onsi/ginkgo v1.8.0
|
||||
github.com/onsi/gomega v1.5.0
|
||||
github.com/prometheus/client_golang v0.9.2
|
||||
github.com/stretchr/testify v1.4.0 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4
|
||||
github.com/onsi/gomega v1.13.0
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/teambition/rrule-go v1.6.2
|
||||
go.uber.org/zap v1.9.1
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
|
||||
k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655
|
||||
k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90
|
||||
sigs.k8s.io/controller-runtime v0.4.0
|
||||
go.uber.org/zap v1.19.0
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0
|
||||
k8s.io/api v0.21.1
|
||||
k8s.io/apimachinery v0.21.1
|
||||
k8s.io/client-go v0.21.1
|
||||
sigs.k8s.io/controller-runtime v0.9.0
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
||||
|
||||
replace github.com/google/go-github/v37 => github.com/mumoshu/go-github/v37 v37.0.100
|
||||
|
||||
77
main.go
77
main.go
@@ -23,10 +23,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/controllers"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||
"github.com/summerwind/actions-runner-controller/controllers"
|
||||
"github.com/summerwind/actions-runner-controller/github"
|
||||
zaplib "go.uber.org/zap"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
@@ -65,14 +65,16 @@ func main() {
|
||||
|
||||
metricsAddr string
|
||||
enableLeaderElection bool
|
||||
leaderElectionId string
|
||||
syncPeriod time.Duration
|
||||
|
||||
gitHubAPICacheDuration time.Duration
|
||||
|
||||
runnerImage string
|
||||
dockerImage string
|
||||
namespace string
|
||||
logLevel string
|
||||
runnerImage string
|
||||
dockerImage string
|
||||
dockerRegistryMirror string
|
||||
namespace string
|
||||
logLevel string
|
||||
|
||||
commonRunnerLabels commaSeparatedStringSlice
|
||||
)
|
||||
@@ -86,15 +88,17 @@ func main() {
|
||||
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
|
||||
"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
|
||||
flag.StringVar(&leaderElectionId, "leader-election-id", "actions-runner-controller", "Controller id for leader election.")
|
||||
flag.StringVar(&runnerImage, "runner-image", defaultRunnerImage, "The image name of self-hosted runner container.")
|
||||
flag.StringVar(&dockerImage, "docker-image", defaultDockerImage, "The image name of docker sidecar container.")
|
||||
flag.StringVar(&dockerRegistryMirror, "docker-registry-mirror", "", "The default Docker Registry Mirror used by runners.")
|
||||
flag.StringVar(&c.Token, "github-token", c.Token, "The personal access token of GitHub.")
|
||||
flag.Int64Var(&c.AppID, "github-app-id", c.AppID, "The application ID of GitHub App.")
|
||||
flag.Int64Var(&c.AppInstallationID, "github-app-installation-id", c.AppInstallationID, "The installation ID of GitHub App.")
|
||||
flag.StringVar(&c.AppPrivateKey, "github-app-private-key", c.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App")
|
||||
flag.DurationVar(&gitHubAPICacheDuration, "github-api-cache-duration", 0, "The duration until the GitHub API cache expires. Setting this to e.g. 10m results in the controller tries its best not to make the same API call within 10m to reduce the chance of being rate-limited. Defaults to mostly the same value as sync-period. If you're tweaking this in order to make autoscaling more responsive, you'll probably want to tweak sync-period, too")
|
||||
flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "Determines the minimum frequency at which K8s resources managed by this controller are reconciled. When you use autoscaling, set to a lower value like 10 minute, because this corresponds to the minimum time to react on demand change. . If you're tweaking this in order to make autoscaling more responsive, you'll probably want to tweak github-api-cache-duration, too")
|
||||
flag.Var(&commonRunnerLabels, "common-runner-labels", "Runner labels in the K1=V1,K2=V2,... format that are inherited all the runners created by the controller. See https://github.com/summerwind/actions-runner-controller/issues/321 for more information")
|
||||
flag.Var(&commonRunnerLabels, "common-runner-labels", "Runner labels in the K1=V1,K2=V2,... format that are inherited all the runners created by the controller. See https://github.com/actions-runner-controller/actions-runner-controller/issues/321 for more information")
|
||||
flag.StringVar(&namespace, "watch-namespace", "", "The namespace to watch for custom resources. Set to empty for letting it watch for all namespaces.")
|
||||
flag.StringVar(&logLevel, "log-level", logLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`)
|
||||
flag.Parse()
|
||||
@@ -127,6 +131,7 @@ func main() {
|
||||
Scheme: scheme,
|
||||
MetricsBindAddress: metricsAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: leaderElectionId,
|
||||
Port: 9443,
|
||||
SyncPeriod: &syncPeriod,
|
||||
Namespace: namespace,
|
||||
@@ -137,12 +142,13 @@ func main() {
|
||||
}
|
||||
|
||||
runnerReconciler := &controllers.RunnerReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Log: log.WithName("runner"),
|
||||
Scheme: mgr.GetScheme(),
|
||||
GitHubClient: ghClient,
|
||||
RunnerImage: runnerImage,
|
||||
DockerImage: dockerImage,
|
||||
Client: mgr.GetClient(),
|
||||
Log: log.WithName("runner"),
|
||||
Scheme: mgr.GetScheme(),
|
||||
GitHubClient: ghClient,
|
||||
RunnerImage: runnerImage,
|
||||
DockerImage: dockerImage,
|
||||
DockerRegistryMirror: dockerRegistryMirror,
|
||||
}
|
||||
|
||||
if err = runnerReconciler.SetupWithManager(mgr); err != nil {
|
||||
@@ -150,14 +156,14 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
runnerSetReconciler := &controllers.RunnerReplicaSetReconciler{
|
||||
runnerReplicaSetReconciler := &controllers.RunnerReplicaSetReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Log: log.WithName("runnerreplicaset"),
|
||||
Scheme: mgr.GetScheme(),
|
||||
GitHubClient: ghClient,
|
||||
}
|
||||
|
||||
if err = runnerSetReconciler.SetupWithManager(mgr); err != nil {
|
||||
if err = runnerReplicaSetReconciler.SetupWithManager(mgr); err != nil {
|
||||
log.Error(err, "unable to create controller", "controller", "RunnerReplicaSet")
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -174,6 +180,21 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
runnerSetReconciler := &controllers.RunnerSetReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Log: log.WithName("runnerset"),
|
||||
Scheme: mgr.GetScheme(),
|
||||
CommonRunnerLabels: commonRunnerLabels,
|
||||
RunnerImage: runnerImage,
|
||||
DockerImage: dockerImage,
|
||||
DockerRegistryMirror: dockerRegistryMirror,
|
||||
GitHubBaseURL: ghClient.GithubBaseURL,
|
||||
}
|
||||
|
||||
if err = runnerSetReconciler.SetupWithManager(mgr); err != nil {
|
||||
log.Error(err, "unable to create controller", "controller", "RunnerSet")
|
||||
os.Exit(1)
|
||||
}
|
||||
if gitHubAPICacheDuration == 0 {
|
||||
gitHubAPICacheDuration = syncPeriod - 10*time.Second
|
||||
}
|
||||
@@ -184,7 +205,7 @@ func main() {
|
||||
|
||||
log.Info(
|
||||
"Initializing actions-runner-controller",
|
||||
"github-api-cahce-duration", gitHubAPICacheDuration,
|
||||
"github-api-cache-duration", gitHubAPICacheDuration,
|
||||
"sync-period", syncPeriod,
|
||||
"runner-image", runnerImage,
|
||||
"docker-image", dockerImage,
|
||||
@@ -200,6 +221,18 @@ func main() {
|
||||
CacheDuration: gitHubAPICacheDuration,
|
||||
}
|
||||
|
||||
runnerPodReconciler := &controllers.RunnerPodReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Log: log.WithName("runnerpod"),
|
||||
Scheme: mgr.GetScheme(),
|
||||
GitHubClient: ghClient,
|
||||
}
|
||||
|
||||
if err = runnerPodReconciler.SetupWithManager(mgr); err != nil {
|
||||
log.Error(err, "unable to create controller", "controller", "RunnerPod")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = horizontalRunnerAutoscaler.SetupWithManager(mgr); err != nil {
|
||||
log.Error(err, "unable to create controller", "controller", "HorizontalRunnerAutoscaler")
|
||||
os.Exit(1)
|
||||
@@ -219,6 +252,16 @@ func main() {
|
||||
}
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
injector := &controllers.PodRunnerTokenInjector{
|
||||
Client: mgr.GetClient(),
|
||||
GitHubClient: ghClient,
|
||||
Log: ctrl.Log.WithName("webhook").WithName("PodRunnerTokenInjector"),
|
||||
}
|
||||
if err = injector.SetupWithManager(mgr); err != nil {
|
||||
log.Error(err, "unable to create webhook server", "webhook", "PodRunnerTokenInjector")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
log.Error(err, "problem running manager")
|
||||
|
||||
8
pkg/hookdeliveryforwarder/README.md
Normal file
8
pkg/hookdeliveryforwarder/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
`hookdeliveryforwarder` is currently in a Proof-of-Concept phase, not complete, and not supported.
|
||||
That being said, we are likely accept bug reports with concrete reproduction steps, but usage/support issues.
|
||||
|
||||
To use this, you need to write some Kubernetes manifest and a container image for deployment.
|
||||
|
||||
For other information, please see the original pull request introduced it.
|
||||
|
||||
https://github.com/actions-runner-controller/actions-runner-controller/pull/682
|
||||
30
pkg/hookdeliveryforwarder/checkpointer.go
Normal file
30
pkg/hookdeliveryforwarder/checkpointer.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package hookdeliveryforwarder
|
||||
|
||||
import "time"
|
||||
|
||||
type Checkpointer interface {
|
||||
GetOrCreate(hookID int64) (*State, error)
|
||||
Update(hookID int64, pos *State) error
|
||||
}
|
||||
|
||||
type InMemoryCheckpointer struct {
|
||||
t time.Time
|
||||
id int64
|
||||
}
|
||||
|
||||
func (p *InMemoryCheckpointer) GetOrCreate(hookID int64) (*State, error) {
|
||||
return &State{DeliveredAt: p.t}, nil
|
||||
}
|
||||
|
||||
func (p *InMemoryCheckpointer) Update(hookID int64, pos *State) error {
|
||||
p.t = pos.DeliveredAt
|
||||
p.id = pos.ID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewInMemoryLogPositionProvider() Checkpointer {
|
||||
return &InMemoryCheckpointer{
|
||||
t: time.Now(),
|
||||
}
|
||||
}
|
||||
95
pkg/hookdeliveryforwarder/cmd/main.go
Normal file
95
pkg/hookdeliveryforwarder/cmd/main.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/actions-runner-controller/actions-runner-controller/pkg/hookdeliveryforwarder"
|
||||
"github.com/actions-runner-controller/actions-runner-controller/pkg/hookdeliveryforwarder/configmap"
|
||||
"github.com/go-logr/logr"
|
||||
zaplib "go.uber.org/zap"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
logLevelDebug = "debug"
|
||||
logLevelInfo = "info"
|
||||
logLevelWarn = "warn"
|
||||
logLevelError = "error"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
)
|
||||
|
||||
func init() {
|
||||
_ = clientgoscheme.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
logLevel string
|
||||
|
||||
checkpointerConfig configmap.Config
|
||||
)
|
||||
|
||||
flag.StringVar(&logLevel, "log-level", logLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`)
|
||||
|
||||
checkpointerConfig.InitFlags(flag.CommandLine)
|
||||
|
||||
config := &hookdeliveryforwarder.Config{}
|
||||
|
||||
config.InitFlags((flag.CommandLine))
|
||||
|
||||
flag.Parse()
|
||||
|
||||
logger := newZapLogger(logLevel)
|
||||
|
||||
checkpointerConfig.Scheme = scheme
|
||||
checkpointerConfig.Logger = logger
|
||||
|
||||
p, mgr, err := configmap.New(&checkpointerConfig)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// TODO: Set to something that is backed by a CRD so that
|
||||
// restarting the forwarder doesn't result in missing deliveries.
|
||||
config.Checkpointer = p
|
||||
|
||||
ctx := hookdeliveryforwarder.SetupSignalHandler()
|
||||
|
||||
go func() {
|
||||
if err := mgr.Start(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "problem running manager: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
hookdeliveryforwarder.Run(ctx, config)
|
||||
}
|
||||
|
||||
func newZapLogger(logLevel string) logr.Logger {
|
||||
return zap.New(func(o *zap.Options) {
|
||||
switch logLevel {
|
||||
case logLevelDebug:
|
||||
o.Development = true
|
||||
case logLevelInfo:
|
||||
lvl := zaplib.NewAtomicLevelAt(zaplib.InfoLevel)
|
||||
o.Level = &lvl
|
||||
case logLevelWarn:
|
||||
lvl := zaplib.NewAtomicLevelAt(zaplib.WarnLevel)
|
||||
o.Level = &lvl
|
||||
case logLevelError:
|
||||
lvl := zaplib.NewAtomicLevelAt(zaplib.ErrorLevel)
|
||||
o.Level = &lvl
|
||||
}
|
||||
})
|
||||
}
|
||||
116
pkg/hookdeliveryforwarder/config.go
Normal file
116
pkg/hookdeliveryforwarder/config.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package hookdeliveryforwarder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Rules StringSlice
|
||||
MetricsAddr string
|
||||
GitHubConfig github.Config
|
||||
Checkpointer Checkpointer
|
||||
}
|
||||
|
||||
func (config *Config) InitFlags(fs *flag.FlagSet) {
|
||||
if err := envconfig.Process("github", &config.GitHubConfig); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error: Environment variable read failed.")
|
||||
}
|
||||
|
||||
flag.StringVar(&config.MetricsAddr, "metrics-addr", ":8000", "The address the metric endpoint binds to.")
|
||||
flag.Var(&config.Rules, "rule", "The rule denotes from where webhook deliveries forwarded and to where they are forwarded. Must be formatted REPO=TARGET where REPO can be just the organization name for a repostory hook or \"owner/repo\" for a repository hook.")
|
||||
flag.StringVar(&config.GitHubConfig.Token, "github-token", config.GitHubConfig.Token, "The personal access token of GitHub.")
|
||||
flag.Int64Var(&config.GitHubConfig.AppID, "github-app-id", config.GitHubConfig.AppID, "The application ID of GitHub App.")
|
||||
flag.Int64Var(&config.GitHubConfig.AppInstallationID, "github-app-installation-id", config.GitHubConfig.AppInstallationID, "The installation ID of GitHub App.")
|
||||
flag.StringVar(&config.GitHubConfig.AppPrivateKey, "github-app-private-key", config.GitHubConfig.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App")
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, config *Config) {
|
||||
c := config.GitHubConfig
|
||||
|
||||
ghClient, err := c.NewClient()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error: Client creation failed.", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
fwd, err := New(ghClient, []string(config.Rules))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "problem initializing forwarder: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if config.Checkpointer != nil {
|
||||
fwd.Checkpointer = config.Checkpointer
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/readyz", fwd.HandleReadyz)
|
||||
|
||||
srv := http.Server{
|
||||
Addr: config.MetricsAddr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer cancel()
|
||||
defer wg.Done()
|
||||
|
||||
if err := fwd.Run(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "problem running forwarder: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer cancel()
|
||||
defer wg.Done()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
|
||||
srv.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
fmt.Fprintf(os.Stderr, "problem running http server: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
type StringSlice []string
|
||||
|
||||
func (s *StringSlice) String() string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%+v", []string(*s))
|
||||
}
|
||||
|
||||
func (s *StringSlice) Set(value string) error {
|
||||
*s = append(*s, value)
|
||||
return nil
|
||||
}
|
||||
100
pkg/hookdeliveryforwarder/configmap/checkpointer.go
Normal file
100
pkg/hookdeliveryforwarder/configmap/checkpointer.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package configmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
||||
"github.com/actions-runner-controller/actions-runner-controller/pkg/hookdeliveryforwarder"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type ConfigMapCheckpointer struct {
|
||||
Name string
|
||||
NS string
|
||||
Client client.Client
|
||||
}
|
||||
|
||||
type state struct {
|
||||
DeliveredAt time.Time `json:"delivered_at"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (p *ConfigMapCheckpointer) GetOrCreate(hookID int64) (*hookdeliveryforwarder.State, error) {
|
||||
var cm corev1.ConfigMap
|
||||
|
||||
if err := p.Client.Get(context.Background(), types.NamespacedName{Namespace: p.NS, Name: p.Name}, &cm); err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cm.Name = p.Name
|
||||
cm.Namespace = p.NS
|
||||
|
||||
if err := p.Client.Create(context.Background(), &cm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
idStr := fmt.Sprintf("hook_%d", hookID)
|
||||
|
||||
var unmarshalled state
|
||||
|
||||
data, ok := cm.Data[idStr]
|
||||
|
||||
if ok {
|
||||
if err := json.Unmarshal([]byte(data), &unmarshalled); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pos := &hookdeliveryforwarder.State{
|
||||
DeliveredAt: unmarshalled.DeliveredAt,
|
||||
ID: unmarshalled.ID,
|
||||
}
|
||||
|
||||
if pos.DeliveredAt.IsZero() {
|
||||
pos.DeliveredAt = time.Now()
|
||||
}
|
||||
|
||||
return pos, nil
|
||||
}
|
||||
|
||||
func (p *ConfigMapCheckpointer) Update(hookID int64, pos *hookdeliveryforwarder.State) error {
|
||||
var cm corev1.ConfigMap
|
||||
|
||||
if err := p.Client.Get(context.Background(), types.NamespacedName{Namespace: p.NS, Name: p.Name}, &cm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var posData state
|
||||
|
||||
posData.DeliveredAt = pos.DeliveredAt
|
||||
posData.ID = pos.ID
|
||||
|
||||
idStr := fmt.Sprintf("hook_%d", hookID)
|
||||
|
||||
data, err := json.Marshal(posData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copy := cm.DeepCopy()
|
||||
|
||||
if copy.Data == nil {
|
||||
copy.Data = map[string]string{}
|
||||
}
|
||||
|
||||
copy.Data[idStr] = string(data)
|
||||
|
||||
if err := p.Client.Patch(context.Background(), copy, client.MergeFrom(&cm)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
42
pkg/hookdeliveryforwarder/configmap/config.go
Normal file
42
pkg/hookdeliveryforwarder/configmap/config.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package configmap
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Logger logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (c *Config) InitFlags(fs *flag.FlagSet) {
|
||||
fs.StringVar(&c.Name, "configmap-name", "gh-webhook-forwarder", `The name of the Kubernetes ConfigMap to which store state for check-pointing.`)
|
||||
fs.StringVar(&c.Namespace, "namespace", "default", `The Kubernetes namespace to store configmap for check-pointing.`)
|
||||
}
|
||||
|
||||
func New(checkpointerConfig *Config) (*ConfigMapCheckpointer, manager.Manager, error) {
|
||||
ctrl.SetLogger(checkpointerConfig.Logger)
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: checkpointerConfig.Scheme,
|
||||
LeaderElectionID: "hookdeliveryforwarder",
|
||||
Port: 9443,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to start manager: %v", err)
|
||||
}
|
||||
|
||||
return &ConfigMapCheckpointer{
|
||||
Client: mgr.GetClient(),
|
||||
Name: checkpointerConfig.Name,
|
||||
NS: checkpointerConfig.Namespace,
|
||||
}, mgr, nil
|
||||
}
|
||||
253
pkg/hookdeliveryforwarder/forwarder.go
Normal file
253
pkg/hookdeliveryforwarder/forwarder.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package hookdeliveryforwarder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||
gogithub "github.com/google/go-github/v37/github"
|
||||
)
|
||||
|
||||
type Forwarder struct {
|
||||
Repo string
|
||||
Target string
|
||||
|
||||
Hook gogithub.Hook
|
||||
|
||||
PollingDelay time.Duration
|
||||
|
||||
Client *github.Client
|
||||
|
||||
Checkpointer Checkpointer
|
||||
|
||||
logger
|
||||
}
|
||||
|
||||
type persistentError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e persistentError) Error() string {
|
||||
return fmt.Sprintf("%v", e.Err)
|
||||
}
|
||||
|
||||
func (f *Forwarder) Run(ctx context.Context) error {
|
||||
pollingDelay := 10 * time.Second
|
||||
if f.PollingDelay > 0 {
|
||||
pollingDelay = f.PollingDelay
|
||||
}
|
||||
|
||||
segments := strings.Split(f.Repo, "/")
|
||||
|
||||
owner := segments[0]
|
||||
|
||||
var repo string
|
||||
|
||||
if len(segments) > 1 {
|
||||
repo = segments[1]
|
||||
}
|
||||
|
||||
hooksAPI := newHooksAPI(f.Client.Client, owner, repo)
|
||||
|
||||
hooks, _, err := hooksAPI.ListHooks(ctx, nil)
|
||||
if err != nil {
|
||||
f.Errorf("Failed listing hooks: %v", err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var hook *gogithub.Hook
|
||||
|
||||
for i := range hooks {
|
||||
hook = hooks[i]
|
||||
break
|
||||
}
|
||||
|
||||
if hook == nil {
|
||||
hookConfig := &f.Hook
|
||||
|
||||
if _, ok := hookConfig.Config["url"]; !ok {
|
||||
return persistentError{Err: fmt.Errorf("config.url is missing in the hook config")}
|
||||
}
|
||||
|
||||
if _, ok := hookConfig.Config["content_type"]; !ok {
|
||||
hookConfig.Config["content_type"] = "json"
|
||||
}
|
||||
|
||||
if _, ok := hookConfig.Config["insecure_ssl"]; !ok {
|
||||
hookConfig.Config["insecure_ssl"] = 0
|
||||
}
|
||||
|
||||
if _, ok := hookConfig.Config["secret"]; !ok {
|
||||
hookConfig.Config["secret"] = os.Getenv("GITHUB_HOOK_SECRET")
|
||||
}
|
||||
|
||||
if len(hookConfig.Events) == 0 {
|
||||
hookConfig.Events = []string{"check_run", "push"}
|
||||
}
|
||||
|
||||
if hookConfig.Active == nil {
|
||||
hookConfig.Active = gogithub.Bool(true)
|
||||
}
|
||||
|
||||
h, _, err := hooksAPI.CreateHook(ctx, hookConfig)
|
||||
if err != nil {
|
||||
f.Errorf("Failed creating hook: %v", err)
|
||||
|
||||
return persistentError{Err: err}
|
||||
}
|
||||
|
||||
hook = h
|
||||
}
|
||||
|
||||
f.Logf("Using this hook for receiving deliveries to be forwarded: %+v", *hook)
|
||||
|
||||
hookDeliveries := newHookDeliveriesAPI(f.Client.Client, owner, repo, hook.GetID())
|
||||
|
||||
cur, err := f.Checkpointer.GetOrCreate(hook.GetID())
|
||||
if err != nil {
|
||||
f.Errorf("Failed to get or create log position: %v", err)
|
||||
|
||||
return persistentError{Err: err}
|
||||
}
|
||||
|
||||
LOOP:
|
||||
for {
|
||||
var (
|
||||
err error
|
||||
payloads [][]byte
|
||||
)
|
||||
|
||||
payloads, cur, err = f.getUnprocessedDeliveries(ctx, hookDeliveries, *cur)
|
||||
if err != nil {
|
||||
f.Errorf("failed getting unprocessed deliveries: %v", err)
|
||||
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range payloads {
|
||||
if _, err := http.Post(f.Target, "application/json", bytes.NewReader(p)); err != nil {
|
||||
f.Errorf("failed forwarding delivery: %v", err)
|
||||
|
||||
retryDelay := 5 * time.Second
|
||||
t := time.NewTimer(retryDelay)
|
||||
|
||||
select {
|
||||
case <-t.C:
|
||||
t.Stop()
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
continue LOOP
|
||||
} else {
|
||||
f.Logf("Successfully POSTed the payload to %s", f.Target)
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.Checkpointer.Update(hook.GetID(), cur); err != nil {
|
||||
return fmt.Errorf("failed updating checkpoint: %w", err)
|
||||
}
|
||||
|
||||
t := time.NewTimer(pollingDelay)
|
||||
|
||||
select {
|
||||
case <-t.C:
|
||||
t.Stop()
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type State struct {
|
||||
DeliveredAt time.Time
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (f *Forwarder) getUnprocessedDeliveries(ctx context.Context, hookDeliveries *hookDeliveriesAPI, pos State) ([][]byte, *State, error) {
|
||||
var (
|
||||
opts gogithub.ListCursorOptions
|
||||
)
|
||||
|
||||
opts.PerPage = 2
|
||||
|
||||
var deliveries []*gogithub.HookDelivery
|
||||
|
||||
OUTER:
|
||||
for {
|
||||
ds, resp, err := hookDeliveries.ListHookDeliveries(ctx, &opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
opts.Cursor = resp.Cursor
|
||||
|
||||
for _, d := range ds {
|
||||
d, _, err := hookDeliveries.GetHookDelivery(ctx, d.GetID())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
payload, err := d.ParseRequestPayload()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
id := d.GetID()
|
||||
deliveredAt := d.GetDeliveredAt()
|
||||
|
||||
if !pos.DeliveredAt.IsZero() && deliveredAt.Before(pos.DeliveredAt) {
|
||||
f.Logf("%s is before %s so skipping all the remaining deliveries", deliveredAt, pos.DeliveredAt)
|
||||
break OUTER
|
||||
}
|
||||
|
||||
if pos.ID != 0 && id <= pos.ID {
|
||||
break OUTER
|
||||
}
|
||||
|
||||
deliveries = append(deliveries, d)
|
||||
|
||||
f.Logf("Received %T at %s: %v", payload, deliveredAt, payload)
|
||||
|
||||
if deliveredAt.After(pos.DeliveredAt) {
|
||||
pos.DeliveredAt = deliveredAt.Time
|
||||
}
|
||||
|
||||
if id > pos.ID {
|
||||
pos.ID = id
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Cursor == "" {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
sort.Slice(deliveries, func(a, b int) bool {
|
||||
return deliveries[b].GetDeliveredAt().After(deliveries[a].GetDeliveredAt().Time)
|
||||
})
|
||||
|
||||
var payloads [][]byte
|
||||
|
||||
for _, d := range deliveries {
|
||||
payloads = append(payloads, *d.Request.RawPayload)
|
||||
}
|
||||
|
||||
return payloads, &pos, nil
|
||||
}
|
||||
46
pkg/hookdeliveryforwarder/hooks.go
Normal file
46
pkg/hookdeliveryforwarder/hooks.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package hookdeliveryforwarder
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gogithub "github.com/google/go-github/v37/github"
|
||||
)
|
||||
|
||||
type hooksAPI struct {
|
||||
ListHooks func(ctx context.Context, opts *gogithub.ListOptions) ([]*gogithub.Hook, *gogithub.Response, error)
|
||||
CreateHook func(ctx context.Context, hook *gogithub.Hook) (*gogithub.Hook, *gogithub.Response, error)
|
||||
}
|
||||
|
||||
func newHooksAPI(client *gogithub.Client, org, repo string) *hooksAPI {
|
||||
var hooksAPI *hooksAPI
|
||||
|
||||
if repo != "" {
|
||||
hooksAPI = repoHooksAPI(client.Repositories, org, repo)
|
||||
} else {
|
||||
hooksAPI = orgHooksAPI(client.Organizations, org)
|
||||
}
|
||||
|
||||
return hooksAPI
|
||||
}
|
||||
|
||||
func repoHooksAPI(svc *gogithub.RepositoriesService, org, repo string) *hooksAPI {
|
||||
return &hooksAPI{
|
||||
ListHooks: func(ctx context.Context, opts *gogithub.ListOptions) ([]*gogithub.Hook, *gogithub.Response, error) {
|
||||
return svc.ListHooks(ctx, org, repo, opts)
|
||||
},
|
||||
CreateHook: func(ctx context.Context, hook *gogithub.Hook) (*gogithub.Hook, *gogithub.Response, error) {
|
||||
return svc.CreateHook(ctx, org, repo, hook)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func orgHooksAPI(svc *gogithub.OrganizationsService, org string) *hooksAPI {
|
||||
return &hooksAPI{
|
||||
ListHooks: func(ctx context.Context, opts *gogithub.ListOptions) ([]*gogithub.Hook, *gogithub.Response, error) {
|
||||
return svc.ListHooks(ctx, org, opts)
|
||||
},
|
||||
CreateHook: func(ctx context.Context, hook *gogithub.Hook) (*gogithub.Hook, *gogithub.Response, error) {
|
||||
return svc.CreateHook(ctx, org, hook)
|
||||
},
|
||||
}
|
||||
}
|
||||
46
pkg/hookdeliveryforwarder/hooks_deliveries.go
Normal file
46
pkg/hookdeliveryforwarder/hooks_deliveries.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package hookdeliveryforwarder
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gogithub "github.com/google/go-github/v37/github"
|
||||
)
|
||||
|
||||
type hookDeliveriesAPI struct {
|
||||
GetHookDelivery func(ctx context.Context, id int64) (*gogithub.HookDelivery, *gogithub.Response, error)
|
||||
ListHookDeliveries func(ctx context.Context, opts *gogithub.ListCursorOptions) ([]*gogithub.HookDelivery, *gogithub.Response, error)
|
||||
}
|
||||
|
||||
func newHookDeliveriesAPI(client *gogithub.Client, org, repo string, hookID int64) *hookDeliveriesAPI {
|
||||
var hookDeliveries *hookDeliveriesAPI
|
||||
|
||||
if repo != "" {
|
||||
hookDeliveries = repoHookDeliveriesAPI(client.Repositories, org, repo, hookID)
|
||||
} else {
|
||||
hookDeliveries = orgHookDeliveriesAPI(client.Organizations, org, hookID)
|
||||
}
|
||||
|
||||
return hookDeliveries
|
||||
}
|
||||
|
||||
func repoHookDeliveriesAPI(svc *gogithub.RepositoriesService, org, repo string, hookID int64) *hookDeliveriesAPI {
|
||||
return &hookDeliveriesAPI{
|
||||
GetHookDelivery: func(ctx context.Context, id int64) (*gogithub.HookDelivery, *gogithub.Response, error) {
|
||||
return svc.GetHookDelivery(ctx, org, repo, hookID, id)
|
||||
},
|
||||
ListHookDeliveries: func(ctx context.Context, opts *gogithub.ListCursorOptions) ([]*gogithub.HookDelivery, *gogithub.Response, error) {
|
||||
return svc.ListHookDeliveries(ctx, org, repo, hookID, opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func orgHookDeliveriesAPI(svc *gogithub.OrganizationsService, org string, hookID int64) *hookDeliveriesAPI {
|
||||
return &hookDeliveriesAPI{
|
||||
GetHookDelivery: func(ctx context.Context, id int64) (*gogithub.HookDelivery, *gogithub.Response, error) {
|
||||
return svc.GetHookDelivery(ctx, org, hookID, id)
|
||||
},
|
||||
ListHookDeliveries: func(ctx context.Context, opts *gogithub.ListCursorOptions) ([]*gogithub.HookDelivery, *gogithub.Response, error) {
|
||||
return svc.ListHookDeliveries(ctx, org, hookID, opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user