mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 11:41:27 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc35bdfa85 | ||
|
|
ece8fd8fe4 | ||
|
|
dcf8524b5c | ||
|
|
4eb45d3c7f | ||
|
|
1c30bdf35b | ||
|
|
3f335ca628 | ||
|
|
f2a2ab7ede | ||
|
|
40c5050978 | ||
|
|
99a53a6e79 | ||
|
|
6d78fb07b3 | ||
|
|
faaca10fba | ||
|
|
d16dfac0f8 | ||
|
|
af483d83da | ||
|
|
92920926fe | ||
|
|
7d0bfb77e3 | ||
|
|
c4074130e8 | ||
|
|
be2e61f209 | ||
|
|
da818a898a | ||
|
|
2d250d5e06 | ||
|
|
231cde1531 |
44
.github/workflows/build-runner.yml
vendored
44
.github/workflows/build-runner.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
paths:
|
||||
- runner/patched/*
|
||||
- runner/Dockerfile
|
||||
- runner/dindrunner.Dockerfile
|
||||
- runner/entrypoint.sh
|
||||
- .github/workflows/build-runner.yml
|
||||
name: Runner
|
||||
@@ -21,6 +22,7 @@ jobs:
|
||||
env:
|
||||
RUNNER_VERSION: 2.273.5
|
||||
DOCKER_VERSION: 19.03.12
|
||||
DOCKERHUB_USERNAME: ${{ github.repository_owner }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -33,19 +35,45 @@ jobs:
|
||||
|
||||
- name: Build Container Image
|
||||
working-directory: runner
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--build-arg RUNNER_VERSION=${RUNNER_VERSION} \
|
||||
--build-arg DOCKER_VERSION=${DOCKER_VERSION} \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag summerwind/actions-runner:v${RUNNER_VERSION} \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner:v${RUNNER_VERSION} \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner:latest \
|
||||
-f Dockerfile .
|
||||
docker buildx build \
|
||||
--build-arg RUNNER_VERSION=${RUNNER_VERSION} \
|
||||
--build-arg DOCKER_VERSION=${DOCKER_VERSION} \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner-dind:v${RUNNER_VERSION} \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner-dind:latest \
|
||||
-f dindrunner.Dockerfile .
|
||||
|
||||
- name: Push Container Image
|
||||
working-directory: runner
|
||||
run: |
|
||||
docker login -u summerwind --password-stdin <<<${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
docker push summerwind/actions-runner:v${RUNNER_VERSION}
|
||||
docker tag summerwind/actions-runner:v${RUNNER_VERSION} summerwind/actions-runner:latest
|
||||
docker push summerwind/actions-runner:latest
|
||||
- name: Login to GitHub Docker Registry
|
||||
run: echo "${DOCKERHUB_PASSWORD}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ github.repository_owner }}
|
||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and Push Container Image
|
||||
working-directory: runner
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--build-arg RUNNER_VERSION=${RUNNER_VERSION} \
|
||||
--build-arg DOCKER_VERSION=${DOCKER_VERSION} \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner:v${RUNNER_VERSION} \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner:latest \
|
||||
-f Dockerfile . --push
|
||||
docker buildx build \
|
||||
--build-arg RUNNER_VERSION=${RUNNER_VERSION} \
|
||||
--build-arg DOCKER_VERSION=${DOCKER_VERSION} \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner-dind:v${RUNNER_VERSION} \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner-dind:latest \
|
||||
-f dindrunner.Dockerfile . --push
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -38,12 +38,14 @@ jobs:
|
||||
- name: Login to GitHub Docker Registry
|
||||
run: echo "${DOCKERHUB_PASSWORD}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
||||
env:
|
||||
DOCKERHUB_USERNAME: summerwind
|
||||
DOCKERHUB_USERNAME: ${{ github.repository_owner }}
|
||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build Container Image
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ github.repository_owner }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag summerwind/actions-runner-controller:${{ env.VERSION }} \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner-controller:${{ env.VERSION }} \
|
||||
-f Dockerfile . --push
|
||||
|
||||
35
.github/workflows/wip.yml
vendored
Normal file
35
.github/workflows/wip.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- "runner/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: release-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
buildx-version: latest
|
||||
|
||||
- name: Login to GitHub Docker Registry
|
||||
run: echo "${DOCKERHUB_PASSWORD}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ github.repository_owner }}
|
||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build Container Image
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ github.repository_owner }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${DOCKERHUB_USERNAME}/actions-runner-controller:latest \
|
||||
-f Dockerfile . --push
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.13 as builder
|
||||
FROM golang:1.15 as builder
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
|
||||
42
Makefile
42
Makefile
@@ -1,5 +1,8 @@
|
||||
NAME ?= summerwind/actions-runner-controller
|
||||
VERSION ?= latest
|
||||
# From https://github.com/VictoriaMetrics/operator/pull/44
|
||||
YAML_DROP=$(YQ) delete --inplace
|
||||
YAML_DROP_PREFIX=spec.validation.openAPIV3Schema.properties.spec.properties
|
||||
|
||||
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
|
||||
CRD_OPTIONS ?= "crd:trivialVersions=true"
|
||||
@@ -56,7 +59,9 @@ deploy: manifests
|
||||
kustomize build config/default | kubectl apply -f -
|
||||
|
||||
# Generate manifests e.g. CRD, RBAC etc.
|
||||
manifests: controller-gen
|
||||
manifests: manifests-118 fix118
|
||||
|
||||
manifests-118: controller-gen
|
||||
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
|
||||
|
||||
# Run go fmt against code
|
||||
@@ -67,6 +72,22 @@ 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="./..."
|
||||
@@ -105,6 +126,7 @@ github-release: release
|
||||
# download controller-gen if necessary
|
||||
controller-gen:
|
||||
ifeq (, $(shell which controller-gen))
|
||||
ifeq (, $(wildcard $(GOBIN)/controller-gen))
|
||||
@{ \
|
||||
set -e ;\
|
||||
CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
|
||||
@@ -113,7 +135,25 @@ ifeq (, $(shell which controller-gen))
|
||||
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\
|
||||
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
|
||||
}
|
||||
endif
|
||||
CONTROLLER_GEN=$(GOBIN)/controller-gen
|
||||
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
|
||||
|
||||
112
README.md
112
README.md
@@ -17,9 +17,19 @@ actions-runner-controller uses [cert-manager](https://cert-manager.io/docs/insta
|
||||
Install the custom resource and actions-runner-controller itself. This will create actions-runner-system namespace in your Kubernetes and deploy the required resources.
|
||||
|
||||
```
|
||||
$ kubectl apply -f https://github.com/summerwind/actions-runner-controller/releases/latest/download/actions-runner-controller.yaml
|
||||
kubectl apply -f https://github.com/summerwind/actions-runner-controller/releases/latest/download/actions-runner-controller.yaml
|
||||
```
|
||||
|
||||
### Github Enterprise support
|
||||
|
||||
If you use either Github Enterprise Cloud or Server (and have recent enought version supporting Actions), you can use **actions-runner-controller** with those, too. Authentication works same way as with public Github (repo and organization level).
|
||||
|
||||
```shell
|
||||
kubectl set env deploy controller-manager -c manager GITHUB_ENTERPRISE_URL=<GHEC/S URL>
|
||||
```
|
||||
|
||||
[Enterprise level](https://docs.github.com/en/enterprise-server@2.22/actions/hosting-your-own-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-an-enterprise) runners are not working yet as there's no API definition for those.
|
||||
|
||||
## Setting up authentication with GitHub API
|
||||
|
||||
There are two ways for actions-runner-controller to authenticate with the GitHub API:
|
||||
@@ -58,7 +68,7 @@ When the installation is complete, you will be taken to a URL in one of the foll
|
||||
|
||||
Finally, register the App ID (`APP_ID`), Installation ID (`INSTALLATION_ID`), and downloaded private key file (`PRIVATE_KEY_FILE_PATH`) to Kubernetes as Secret.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ kubectl create secret generic controller-manager \
|
||||
-n actions-runner-system \
|
||||
--from-literal=github_app_id=${APP_ID} \
|
||||
@@ -80,8 +90,8 @@ Open the Create Token page from the following link, grant the `repo` and/or `adm
|
||||
|
||||
Register the created token (`GITHUB_TOKEN`) as a Kubernetes secret.
|
||||
|
||||
```
|
||||
$ kubectl create secret generic controller-manager \
|
||||
```shell
|
||||
kubectl create secret generic controller-manager \
|
||||
-n actions-runner-system \
|
||||
--from-literal=github_token=${GITHUB_TOKEN}
|
||||
```
|
||||
@@ -97,7 +107,7 @@ There are two ways to use this controller:
|
||||
|
||||
To launch a single self-hosted runner, you need to create a manifest file includes *Runner* resource as follows. This example launches a self-hosted runner with name *example-runner* for the *summerwind/actions-runner-controller* repository.
|
||||
|
||||
```
|
||||
```yaml
|
||||
# runner.yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: Runner
|
||||
@@ -110,14 +120,14 @@ spec:
|
||||
|
||||
Apply the created manifest file to your Kubernetes.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ kubectl apply -f runner.yaml
|
||||
runner.actions.summerwind.dev/example-runner created
|
||||
```
|
||||
|
||||
You can see that the Runner resource has been created.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ kubectl get runners
|
||||
NAME REPOSITORY STATUS
|
||||
example-runner summerwind/actions-runner-controller Running
|
||||
@@ -125,7 +135,7 @@ example-runner summerwind/actions-runner-controller Running
|
||||
|
||||
You can also see that the runner pod has been running.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ kubectl get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
example-runner 2/2 Running 0 1m
|
||||
@@ -141,7 +151,7 @@ Now you can use your self-hosted runner. See the [official documentation](https:
|
||||
|
||||
To add the runner to an organization, you only need to replace the `repository` field with `organization`, so the runner will register itself to the organization.
|
||||
|
||||
```
|
||||
```yaml
|
||||
# runner.yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: Runner
|
||||
@@ -175,14 +185,14 @@ spec:
|
||||
|
||||
Apply the manifest file to your cluster:
|
||||
|
||||
```
|
||||
```shell
|
||||
$ kubectl apply -f runner.yaml
|
||||
runnerdeployment.actions.summerwind.dev/example-runnerdeploy created
|
||||
```
|
||||
|
||||
You can see that 2 runners have been created as specified by `replicas: 2`:
|
||||
|
||||
```
|
||||
```shell
|
||||
$ kubectl get runners
|
||||
NAME REPOSITORY STATUS
|
||||
example-runnerdeploy2475h595fr mumoshu/actions-runner-controller-ci Running
|
||||
@@ -195,7 +205,7 @@ example-runnerdeploy2475ht2qbr mumoshu/actions-runner-controller-ci Running
|
||||
|
||||
In the below example, `actions-runner` checks for pending workflow runs for each sync period, and scale to e.g. 3 if there're 3 pending jobs at sync time.
|
||||
|
||||
```
|
||||
```yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: RunnerDeployment
|
||||
metadata:
|
||||
@@ -225,7 +235,7 @@ Please also note that the sync period is set to 10 minutes by default and it's c
|
||||
Additionally, the autoscaling feature has an anti-flapping option that prevents periodic loop of scaling up and down.
|
||||
By default, it doesn't scale down until the grace period of 10 minutes passes after a scale up. The grace period can be configured by setting `scaleDownDelaySecondsAfterScaleUp`:
|
||||
|
||||
```
|
||||
```yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: RunnerDeployment
|
||||
metadata:
|
||||
@@ -251,6 +261,28 @@ spec:
|
||||
- summerwind/actions-runner-controller
|
||||
```
|
||||
|
||||
## Runner with DinD
|
||||
|
||||
When using default runner, runner pod starts up 2 containers: runner and DinD (Docker-in-Docker). This might create issues if there's `LimitRange` set to namespace.
|
||||
|
||||
```yaml
|
||||
# dindrunnerdeployment.yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: RunnerDeployment
|
||||
metadata:
|
||||
name: example-dindrunnerdeploy
|
||||
spec:
|
||||
replicas: 2
|
||||
template:
|
||||
spec:
|
||||
image: summerwind/actions-runner-dind
|
||||
dockerdWithinRunnerContainer: true
|
||||
repository: mumoshu/actions-runner-controller-ci
|
||||
env: []
|
||||
```
|
||||
|
||||
This also helps with resources, as you don't need to give resources separately to docker and runner.
|
||||
|
||||
## Additional tweaks
|
||||
|
||||
You can pass details through the spec selector. Here's an eg. of what you may like to do:
|
||||
@@ -283,6 +315,17 @@ spec:
|
||||
requests:
|
||||
cpu: "2.0"
|
||||
memory: "4Gi"
|
||||
# If set to true, runner pod container only 1 container that's expected to be able to run docker, too.
|
||||
# image summerwind/actions-runner-dind or custom one should be used with true -value
|
||||
dockerdWithinRunnerContainer: false
|
||||
# Valid if dockerdWithinRunnerContainer is not true
|
||||
dockerdContainerResources:
|
||||
limits:
|
||||
cpu: "4.0"
|
||||
memory: "8Gi"
|
||||
requests:
|
||||
cpu: "2.0"
|
||||
memory: "4Gi"
|
||||
sidecarContainers:
|
||||
- name: mysql
|
||||
image: mysql:5.7
|
||||
@@ -330,22 +373,41 @@ jobs:
|
||||
|
||||
Note that if you specify `self-hosted` in your workflow, then this will run your job on _any_ self-hosted runner, regardless of the labels that they have.
|
||||
|
||||
## Runner Groups
|
||||
|
||||
Runner groups can be used to limit which repositories are able to use the GitHub Runner at an Organisation level.
|
||||
|
||||
To add the runner to the group `NewGroup`, specify the group in your `Runner` or `RunnerDeployment` spec.
|
||||
|
||||
```yaml
|
||||
# runnerdeployment.yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: RunnerDeployment
|
||||
metadata:
|
||||
name: custom-runner
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
spec:
|
||||
group: NewGroup
|
||||
```
|
||||
|
||||
## Software installed in the runner image
|
||||
|
||||
The GitHub hosted runners include a large amount of pre-installed software packages. For Ubuntu 18.04, this list can be found at https://github.com/actions/virtual-environments/blob/master/images/linux/Ubuntu1804-README.md
|
||||
The GitHub hosted runners include a large amount of pre-installed software packages. For Ubuntu 18.04, this list can be found at <https://github.com/actions/virtual-environments/blob/master/images/linux/Ubuntu1804-README.md>
|
||||
|
||||
The container image is based on Ubuntu 18.04, but it does not contain all of the software installed on the GitHub runners. It contains the following subset of packages from the GitHub runners:
|
||||
|
||||
* Basic CLI packages
|
||||
* git (2.26)
|
||||
* docker
|
||||
* build-essentials
|
||||
- Basic CLI packages
|
||||
- git (2.26)
|
||||
- docker
|
||||
- build-essentials
|
||||
|
||||
The virtual environments from GitHub contain a lot more software packages (different versions of Java, Node.js, Golang, .NET, etc) which are not provided in the runner image. Most of these have dedicated setup actions which allow the tools to be installed on-demand in a workflow, for example: `actions/setup-java` or `actions/setup-node`
|
||||
|
||||
If there is a need to include packages in the runner image for which there is no setup action, then this can be achieved by building a custom container image for the runner. The easiest way is to start with the `summerwind/actions-runner` image and installing the extra dependencies directly in the docker image:
|
||||
|
||||
```yaml
|
||||
```shell
|
||||
FROM summerwind/actions-runner:v2.169.1
|
||||
|
||||
RUN sudo apt update -y \
|
||||
@@ -364,3 +426,15 @@ spec:
|
||||
repository: summerwind/actions-runner-controller
|
||||
image: YOUR_CUSTOM_DOCKER_IMAGE
|
||||
```
|
||||
|
||||
# Alternatives
|
||||
|
||||
The following is a list of alternative solutions that may better fit you depending on your use-case:
|
||||
|
||||
- <https://github.com/evryfs/github-actions-runner-operator/>
|
||||
|
||||
Although the situation can change over time, as of writing this sentence, the benefits of using `actions-runner-controller` over the alternatives are:
|
||||
|
||||
- `actions-runner-controller` has the ability to autoscale runners based on number of pending/progressing jobs (#99)
|
||||
- `actions-runner-controller` is able to gracefully stop runners (#103)
|
||||
- `actions-runner-controller` has ARM support
|
||||
|
||||
@@ -36,9 +36,14 @@ type RunnerSpec struct {
|
||||
// +optional
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
|
||||
// +optional
|
||||
Group string `json:"group,omitempty"`
|
||||
|
||||
// +optional
|
||||
Containers []corev1.Container `json:"containers,omitempty"`
|
||||
// +optional
|
||||
DockerdContainerResources corev1.ResourceRequirements `json:"dockerdContainerResources,omitempty"`
|
||||
// +optional
|
||||
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
|
||||
// +optional
|
||||
VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
|
||||
@@ -77,6 +82,8 @@ type RunnerSpec struct {
|
||||
EphemeralContainers []corev1.EphemeralContainer `json:"ephemeralContainers,omitempty"`
|
||||
// +optional
|
||||
TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"`
|
||||
// +optional
|
||||
DockerdWithinRunnerContainer *bool `json:"dockerdWithinRunnerContainer,omitempty"`
|
||||
}
|
||||
|
||||
// ValidateRepository validates repository field.
|
||||
|
||||
@@ -435,6 +435,7 @@ func (in *RunnerSpec) DeepCopyInto(out *RunnerSpec) {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
in.DockerdContainerResources.DeepCopyInto(&out.DockerdContainerResources)
|
||||
in.Resources.DeepCopyInto(&out.Resources)
|
||||
if in.VolumeMounts != nil {
|
||||
in, out := &in.VolumeMounts, &out.VolumeMounts
|
||||
@@ -524,6 +525,11 @@ func (in *RunnerSpec) DeepCopyInto(out *RunnerSpec) {
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
if in.DockerdWithinRunnerContainer != nil {
|
||||
in, out := &in.DockerdWithinRunnerContainer, &out.DockerdWithinRunnerContainer
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerSpec.
|
||||
|
||||
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
@@ -16,7 +16,10 @@ import (
|
||||
)
|
||||
|
||||
func newGithubClient(server *httptest.Server) *github.Client {
|
||||
client, err := github.NewClientWithAccessToken("token")
|
||||
c := github.Config{
|
||||
Token: "token",
|
||||
}
|
||||
client, err := c.NewClient()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ var _ = Context("Inside of a new namespace", func() {
|
||||
Spec: actionsv1alpha1.RunnerSpec{
|
||||
Repository: "test/valid",
|
||||
Image: "bar",
|
||||
Group: "baz",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "FOO", Value: "FOOVALUE"},
|
||||
},
|
||||
|
||||
@@ -120,40 +120,13 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if !runner.IsRegisterable() {
|
||||
rt, err := r.GitHubClient.GetRegistrationToken(ctx, runner.Spec.Organization, runner.Spec.Repository, runner.Name)
|
||||
if err != nil {
|
||||
r.Recorder.Event(&runner, corev1.EventTypeWarning, "FailedUpdateRegistrationToken", "Updating registration token failed")
|
||||
log.Error(err, "Failed to get new registration token")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
updated := runner.DeepCopy()
|
||||
updated.Status.Registration = v1alpha1.RunnerStatusRegistration{
|
||||
Organization: runner.Spec.Organization,
|
||||
Repository: runner.Spec.Repository,
|
||||
Labels: runner.Spec.Labels,
|
||||
Token: rt.GetToken(),
|
||||
ExpiresAt: metav1.NewTime(rt.GetExpiresAt().Time),
|
||||
}
|
||||
|
||||
if err := r.Status().Update(ctx, updated); err != nil {
|
||||
log.Error(err, "Failed to update runner status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
r.Recorder.Event(&runner, corev1.EventTypeNormal, "RegistrationTokenUpdated", "Successfully update registration token")
|
||||
log.Info("Updated registration token", "repository", runner.Spec.Repository)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
var pod corev1.Pod
|
||||
if err := r.Get(ctx, req.NamespacedName, &pod); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
newPod, err := r.newPod(runner)
|
||||
newPod, err := r.newPod(ctx, runner)
|
||||
if err != nil {
|
||||
log.Error(err, "Could not create pod")
|
||||
return ctrl.Result{}, err
|
||||
@@ -167,7 +140,11 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
r.Recorder.Event(&runner, corev1.EventTypeNormal, "PodCreated", fmt.Sprintf("Created pod '%s'", newPod.Name))
|
||||
log.Info("Created runner pod", "repository", runner.Spec.Repository)
|
||||
} else {
|
||||
if runner.Status.Phase != string(pod.Status.Phase) {
|
||||
// If pod has ended up succeeded we need to restart it
|
||||
// Happens e.g. when dind is in runner and run completes
|
||||
restart := pod.Status.Phase == corev1.PodSucceeded
|
||||
|
||||
if !restart && runner.Status.Phase != string(pod.Status.Phase) {
|
||||
updated := runner.DeepCopy()
|
||||
updated.Status.Phase = string(pod.Status.Phase)
|
||||
updated.Status.Reason = pod.Status.Reason
|
||||
@@ -185,8 +162,6 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
restart := false
|
||||
|
||||
if pod.Status.Phase == corev1.PodRunning {
|
||||
for _, status := range pod.Status.ContainerStatuses {
|
||||
if status.Name != containerName {
|
||||
@@ -199,7 +174,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
}
|
||||
}
|
||||
|
||||
newPod, err := r.newPod(runner)
|
||||
newPod, err := r.newPod(ctx, runner)
|
||||
if err != nil {
|
||||
log.Error(err, "Could not create pod")
|
||||
return ctrl.Result{}, err
|
||||
@@ -274,12 +249,21 @@ func (r *RunnerReconciler) unregisterRunner(ctx context.Context, org, repo, name
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
func (r *RunnerReconciler) newPod(ctx context.Context, runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
var (
|
||||
privileged bool = true
|
||||
group int64 = 0
|
||||
privileged bool = true
|
||||
dockerdInRunner bool = runner.Spec.DockerdWithinRunnerContainer != nil && *runner.Spec.DockerdWithinRunnerContainer
|
||||
err error
|
||||
)
|
||||
|
||||
token := runner.Status.Registration.Token
|
||||
if !runner.IsRegisterable() {
|
||||
token, err = r.getRegistrationToken(ctx, runner)
|
||||
if err != nil {
|
||||
return corev1.Pod{}, err
|
||||
}
|
||||
}
|
||||
|
||||
runnerImage := runner.Spec.Image
|
||||
if runnerImage == "" {
|
||||
runnerImage = r.RunnerImage
|
||||
@@ -307,9 +291,21 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
Name: "RUNNER_LABELS",
|
||||
Value: strings.Join(runner.Spec.Labels, ","),
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_GROUP",
|
||||
Value: runner.Spec.Group,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_TOKEN",
|
||||
Value: runner.Status.Registration.Token,
|
||||
Value: token,
|
||||
},
|
||||
{
|
||||
Name: "DOCKERD_IN_RUNNER",
|
||||
Value: fmt.Sprintf("%v", dockerdInRunner),
|
||||
},
|
||||
{
|
||||
Name: "GITHUB_URL",
|
||||
Value: r.GitHubClient.GithubBaseURL,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -330,58 +326,68 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
ImagePullPolicy: runnerImagePullPolicy,
|
||||
Env: env,
|
||||
EnvFrom: runner.Spec.EnvFrom,
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "work",
|
||||
MountPath: "/runner/_work",
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
MountPath: "/var/run",
|
||||
},
|
||||
},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
RunAsGroup: &group,
|
||||
// Runner need to run privileged if it contains DinD
|
||||
Privileged: runner.Spec.DockerdWithinRunnerContainer,
|
||||
},
|
||||
Resources: runner.Spec.Resources,
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
Image: r.DockerImage,
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "work",
|
||||
MountPath: "/runner/_work",
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
MountPath: "/var/run",
|
||||
},
|
||||
},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: &privileged,
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "work",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !dockerdInRunner {
|
||||
pod.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "work",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
||||
},
|
||||
},
|
||||
}
|
||||
pod.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: "work",
|
||||
MountPath: "/runner/_work",
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
MountPath: "/var/run",
|
||||
},
|
||||
}
|
||||
pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{
|
||||
Name: "docker",
|
||||
Image: r.DockerImage,
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "work",
|
||||
MountPath: "/runner/_work",
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
MountPath: "/var/run",
|
||||
},
|
||||
},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: &privileged,
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
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 len(runner.Spec.VolumeMounts) != 0 {
|
||||
@@ -478,3 +484,20 @@ func removeFinalizer(finalizers []string) ([]string, bool) {
|
||||
|
||||
return result, removed
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) getRegistrationToken(ctx context.Context, runner v1alpha1.Runner) (string, error) {
|
||||
log := r.Log.WithValues("runner", runner.Name)
|
||||
if runner.IsRegisterable() {
|
||||
return runner.Status.Registration.Token, nil
|
||||
} else {
|
||||
rt, err := r.GitHubClient.GetRegistrationToken(ctx, runner.Spec.Organization, runner.Spec.Repository, runner.Name)
|
||||
if err != nil {
|
||||
r.Recorder.Event(&runner, corev1.EventTypeWarning, "FailedUpdateRegistrationToken", "Updating registration token failed")
|
||||
log.Error(err, "Failed to get new registration token")
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Info("Updated registration token", "repository", runner.Spec.Repository)
|
||||
return rt.GetToken(), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -13,39 +14,65 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Config contains configuration for Github client
|
||||
type Config struct {
|
||||
EnterpriseURL string `split_words:"true"`
|
||||
AppID int64 `split_words:"true"`
|
||||
AppInstallationID int64 `split_words:"true"`
|
||||
AppPrivateKey string `split_words:"true"`
|
||||
Token string
|
||||
}
|
||||
|
||||
// Client wraps GitHub client with some additional
|
||||
type Client struct {
|
||||
*github.Client
|
||||
regTokens map[string]*github.RegistrationToken
|
||||
mu sync.Mutex
|
||||
// GithubBaseURL to Github without API suffix.
|
||||
GithubBaseURL string
|
||||
}
|
||||
|
||||
// NewClient returns a client authenticated as a GitHub App.
|
||||
func NewClient(appID, installationID int64, privateKeyPath string) (*Client, error) {
|
||||
tr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, appID, installationID, privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authentication failed: %v", err)
|
||||
func (c *Config) NewClient() (*Client, error) {
|
||||
var (
|
||||
httpClient *http.Client
|
||||
client *github.Client
|
||||
)
|
||||
githubBaseURL := "https://github.com/"
|
||||
if len(c.Token) > 0 {
|
||||
httpClient = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: c.Token},
|
||||
))
|
||||
} else {
|
||||
tr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, c.AppID, c.AppInstallationID, c.AppPrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authentication failed: %v", err)
|
||||
}
|
||||
if len(c.EnterpriseURL) > 0 {
|
||||
githubAPIURL, err := getEnterpriseApiUrl(c.EnterpriseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enterprise url incorrect: %v", err)
|
||||
}
|
||||
tr.BaseURL = githubAPIURL
|
||||
}
|
||||
httpClient = &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
gh := github.NewClient(&http.Client{Transport: tr})
|
||||
if len(c.EnterpriseURL) > 0 {
|
||||
var err error
|
||||
client, err = github.NewEnterpriseClient(c.EnterpriseURL, c.EnterpriseURL, httpClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enterprise client creation failed: %v", err)
|
||||
}
|
||||
githubBaseURL = fmt.Sprintf("%s://%s%s", client.BaseURL.Scheme, client.BaseURL.Host, strings.TrimSuffix(client.BaseURL.Path, "api/v3/"))
|
||||
} else {
|
||||
client = github.NewClient(httpClient)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
Client: gh,
|
||||
regTokens: map[string]*github.RegistrationToken{},
|
||||
mu: sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewClientWithAccessToken returns a client authenticated with personal access token.
|
||||
func NewClientWithAccessToken(token string) (*Client, error) {
|
||||
tc := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
))
|
||||
|
||||
return &Client{
|
||||
Client: github.NewClient(tc),
|
||||
regTokens: map[string]*github.RegistrationToken{},
|
||||
mu: sync.Mutex{},
|
||||
Client: client,
|
||||
regTokens: map[string]*github.RegistrationToken{},
|
||||
mu: sync.Mutex{},
|
||||
GithubBaseURL: githubBaseURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -199,3 +226,21 @@ func splitOwnerAndRepo(repo string) (string, string, error) {
|
||||
}
|
||||
return chunk[0], chunk[1], nil
|
||||
}
|
||||
|
||||
func getEnterpriseApiUrl(baseURL string) (string, error) {
|
||||
baseEndpoint, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !strings.HasSuffix(baseEndpoint.Path, "/") {
|
||||
baseEndpoint.Path += "/"
|
||||
}
|
||||
if !strings.HasSuffix(baseEndpoint.Path, "/api/v3/") &&
|
||||
!strings.HasPrefix(baseEndpoint.Host, "api.") &&
|
||||
!strings.Contains(baseEndpoint.Host, ".api.") {
|
||||
baseEndpoint.Path += "api/v3/"
|
||||
}
|
||||
|
||||
// Trim trailing slash, otherwise there's double slash added to token endpoint
|
||||
return fmt.Sprintf("%s://%s%s", baseEndpoint.Scheme, baseEndpoint.Host, strings.TrimSuffix(baseEndpoint.Path, "/")), nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ import (
|
||||
var server *httptest.Server
|
||||
|
||||
func newTestClient() *Client {
|
||||
client, err := NewClientWithAccessToken("token")
|
||||
c := Config{
|
||||
Token: "token",
|
||||
}
|
||||
client, err := c.NewClient()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/summerwind/actions-runner-controller
|
||||
|
||||
go 1.13
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/bradleyfalzon/ghinstallation v1.1.1
|
||||
@@ -9,6 +9,7 @@ require (
|
||||
github.com/google/go-github/v32 v32.1.1-0.20200822031813-d57a3a84ba04
|
||||
github.com/google/go-querystring v1.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/stretchr/testify v1.4.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -158,6 +158,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
|
||||
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
||||
73
main.go
73
main.go
@@ -20,9 +20,9 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
@@ -62,74 +62,37 @@ func main() {
|
||||
|
||||
runnerImage string
|
||||
dockerImage string
|
||||
|
||||
ghToken string
|
||||
ghAppID int64
|
||||
ghAppInstallationID int64
|
||||
ghAppPrivateKey string
|
||||
)
|
||||
|
||||
var c github.Config
|
||||
err = envconfig.Process("github", &c)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error: Environment variable read failed.")
|
||||
}
|
||||
|
||||
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(&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(&ghToken, "github-token", "", "The personal access token of GitHub.")
|
||||
flag.Int64Var(&ghAppID, "github-app-id", 0, "The application ID of GitHub App.")
|
||||
flag.Int64Var(&ghAppInstallationID, "github-app-installation-id", 0, "The installation ID of GitHub App.")
|
||||
flag.StringVar(&ghAppPrivateKey, "github-app-private-key", "", "The path of a private key file to authenticate as a GitHub App")
|
||||
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(&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")
|
||||
flag.Parse()
|
||||
|
||||
if ghToken == "" {
|
||||
ghToken = os.Getenv("GITHUB_TOKEN")
|
||||
}
|
||||
if ghAppID == 0 {
|
||||
appID, err := strconv.ParseInt(os.Getenv("GITHUB_APP_ID"), 10, 64)
|
||||
if err == nil {
|
||||
ghAppID = appID
|
||||
}
|
||||
}
|
||||
if ghAppInstallationID == 0 {
|
||||
appInstallationID, err := strconv.ParseInt(os.Getenv("GITHUB_APP_INSTALLATION_ID"), 10, 64)
|
||||
if err == nil {
|
||||
ghAppInstallationID = appInstallationID
|
||||
}
|
||||
}
|
||||
if ghAppPrivateKey == "" {
|
||||
ghAppPrivateKey = os.Getenv("GITHUB_APP_PRIVATE_KEY")
|
||||
}
|
||||
logger := zap.New(func(o *zap.Options) {
|
||||
o.Development = true
|
||||
})
|
||||
|
||||
if ghAppID != 0 {
|
||||
if ghAppInstallationID == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Error: The installation ID must be specified.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if ghAppPrivateKey == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: The path of a private key file must be specified.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ghClient, err = github.NewClient(ghAppID, ghAppInstallationID, ghAppPrivateKey)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: Failed to create GitHub client: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if ghToken != "" {
|
||||
ghClient, err = github.NewClientWithAccessToken(ghToken)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: Failed to create GitHub client: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "Error: GitHub App credentials or personal access token must be specified.")
|
||||
ghClient, err = c.NewClient()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error: Client creation failed.", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctrl.SetLogger(zap.New(func(o *zap.Options) {
|
||||
o.Development = true
|
||||
}))
|
||||
ctrl.SetLogger(logger)
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: scheme,
|
||||
|
||||
@@ -50,7 +50,9 @@ RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
||||
&& install -o root -g root -m 755 docker/docker /usr/local/bin/docker \
|
||||
&& rm -rf docker docker.tgz \
|
||||
&& adduser --disabled-password --gecos "" --uid 1000 runner \
|
||||
&& groupadd docker \
|
||||
&& usermod -aG sudo runner \
|
||||
&& usermod -aG docker runner \
|
||||
&& echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers
|
||||
|
||||
# Runner download supports amd64 as x64
|
||||
@@ -67,6 +69,6 @@ RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
||||
COPY entrypoint.sh /runner
|
||||
COPY patched /runner/patched
|
||||
|
||||
USER runner:runner
|
||||
USER runner
|
||||
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
||||
CMD ["/runner/entrypoint.sh"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
NAME ?= summerwind/actions-runner
|
||||
DIND_RUNNER_NAME ?= ${NAME}-dind
|
||||
TAG ?= latest
|
||||
|
||||
RUNNER_VERSION ?= 2.273.5
|
||||
@@ -23,10 +24,14 @@ endif
|
||||
|
||||
docker-build:
|
||||
docker build --build-arg RUNNER_VERSION=${RUNNER_VERSION} --build-arg DOCKER_VERSION=${DOCKER_VERSION} -t ${NAME}:${TAG} -t ${NAME}:v${RUNNER_VERSION} .
|
||||
docker build --build-arg RUNNER_VERSION=${RUNNER_VERSION} --build-arg DOCKER_VERSION=${DOCKER_VERSION} -t ${DIND_RUNNER_NAME}:${TAG} -t ${DIND_RUNNER_NAME}:v${RUNNER_VERSION} -f dindrunner.Dockerfile .
|
||||
|
||||
|
||||
docker-push:
|
||||
docker push ${NAME}:${TAG}
|
||||
docker push ${NAME}:v${RUNNER_VERSION}
|
||||
docker push ${DIND_RUNNER_NAME}:${TAG}
|
||||
docker push ${DIND_RUNNER_NAME}:v${RUNNER_VERSION}
|
||||
|
||||
docker-buildx:
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
@@ -39,3 +44,9 @@ docker-buildx:
|
||||
-t "${NAME}:latest" \
|
||||
-f Dockerfile \
|
||||
. ${PUSH_ARG}
|
||||
docker buildx build --platform ${PLATFORMS} \
|
||||
--build-arg RUNNER_VERSION=${RUNNER_VERSION} \
|
||||
--build-arg DOCKER_VERSION=${DOCKER_VERSION} \
|
||||
-t "${DIND_RUNNER_NAME}:latest" \
|
||||
-f dindrunner.Dockerfile \
|
||||
. ${PUSH_ARG}
|
||||
|
||||
100
runner/dindrunner.Dockerfile
Normal file
100
runner/dindrunner.Dockerfile
Normal file
@@ -0,0 +1,100 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
# Dev + DinD dependencies
|
||||
RUN apt update \
|
||||
&& apt install -y software-properties-common \
|
||||
&& add-apt-repository -y ppa:git-core/ppa \
|
||||
&& apt install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
ca-certificates \
|
||||
dnsutils \
|
||||
ftp \
|
||||
git \
|
||||
iproute2 \
|
||||
iptables \
|
||||
iputils-ping \
|
||||
jq \
|
||||
libunwind8 \
|
||||
locales \
|
||||
netcat \
|
||||
openssh-client \
|
||||
parallel \
|
||||
rsync \
|
||||
shellcheck \
|
||||
sudo \
|
||||
supervisor \
|
||||
telnet \
|
||||
time \
|
||||
tzdata \
|
||||
unzip \
|
||||
upx \
|
||||
wget \
|
||||
zip \
|
||||
zstd \
|
||||
&& rm -rf /var/lib/apt/list/*
|
||||
|
||||
# Runner user
|
||||
RUN adduser --disabled-password --gecos "" --uid 1000 runner \
|
||||
&& groupadd docker \
|
||||
&& usermod -aG sudo runner \
|
||||
&& usermod -aG docker runner \
|
||||
&& echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG RUNNER_VERSION=2.272.0
|
||||
ARG DOCKER_CHANNEL=stable
|
||||
ARG DOCKER_VERSION=19.03.13
|
||||
ARG DEBUG=false
|
||||
|
||||
# Docker installation
|
||||
RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
||||
&& if [ "$ARCH" = "arm64" ]; then export ARCH=aarch64 ; fi \
|
||||
&& if [ "$ARCH" = "amd64" ]; then export ARCH=x86_64 ; fi \
|
||||
&& if ! curl -L -o docker.tgz "https://download.docker.com/linux/static/${DOCKER_CHANNEL}/${ARCH}/docker-${DOCKER_VERSION}.tgz"; then \
|
||||
echo >&2 "error: failed to download 'docker-${DOCKER_VERSION}' from '${DOCKER_CHANNEL}' for '${ARCH}'"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "Downloaded Docker from https://download.docker.com/linux/static/${DOCKER_CHANNEL}/${ARCH}/docker-${DOCKER_VERSION}.tgz"; \
|
||||
tar --extract \
|
||||
--file docker.tgz \
|
||||
--strip-components 1 \
|
||||
--directory /usr/local/bin/ \
|
||||
; \
|
||||
rm docker.tgz; \
|
||||
dockerd --version; \
|
||||
docker --version
|
||||
|
||||
# Runner download supports amd64 as x64
|
||||
RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
||||
&& if [ "$ARCH" = "amd64" ]; then export ARCH=x64 ; fi \
|
||||
&& mkdir -p /runner \
|
||||
&& cd /runner \
|
||||
&& curl -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${ARCH}-${RUNNER_VERSION}.tar.gz \
|
||||
&& tar xzf ./runner.tar.gz \
|
||||
&& rm runner.tar.gz \
|
||||
&& ./bin/installdependencies.sh \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
COPY modprobe startup.sh /usr/local/bin/
|
||||
COPY supervisor/ /etc/supervisor/conf.d/
|
||||
COPY logger.sh /opt/bash-utils/logger.sh
|
||||
COPY entrypoint.sh /usr/local/bin/
|
||||
|
||||
RUN chmod +x /usr/local/bin/startup.sh /usr/local/bin/entrypoint.sh /usr/local/bin/modprobe
|
||||
|
||||
RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
||||
&& curl -L -o /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_${ARCH} \
|
||||
&& chmod +x /usr/local/bin/dumb-init
|
||||
|
||||
VOLUME /var/lib/docker
|
||||
|
||||
COPY patched /runner/patched
|
||||
|
||||
# No group definition, as that makes it harder to run docker.
|
||||
USER runner
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
||||
CMD ["startup.sh"]
|
||||
@@ -1,11 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "${GITHUB_URL}" ]; then
|
||||
echo "Working with public GitHub" 1>&2
|
||||
GITHUB_URL="https://github.com/"
|
||||
else
|
||||
length=${#GITHUB_URL}
|
||||
last_char=${GITHUB_URL:length-1:1}
|
||||
|
||||
[[ $last_char != "/" ]] && GITHUB_URL="$GITHUB_URL/"; :
|
||||
echo "Github endpoint URL ${GITHUB_URL}"
|
||||
fi
|
||||
|
||||
if [ -z "${RUNNER_NAME}" ]; then
|
||||
echo "RUNNER_NAME must be set" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${RUNNER_ORG}" -a -n "${RUNNER_REPO}" ]; then
|
||||
if [ -n "${RUNNER_ORG}" ] && [ -n "${RUNNER_REPO}" ]; then
|
||||
ATTACH="${RUNNER_ORG}/${RUNNER_REPO}"
|
||||
elif [ -n "${RUNNER_ORG}" ]; then
|
||||
ATTACH="${RUNNER_ORG}"
|
||||
@@ -25,8 +36,12 @@ if [ -z "${RUNNER_TOKEN}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${RUNNER_REPO}" ] && [ -n "${RUNNER_ORG}" ] && [ -n "${RUNNER_GROUP}" ];then
|
||||
RUNNER_GROUP_ARG="--runnergroup ${RUNNER_GROUP}"
|
||||
fi
|
||||
|
||||
cd /runner
|
||||
./config.sh --unattended --replace --name "${RUNNER_NAME}" --url "https://github.com/${ATTACH}" --token "${RUNNER_TOKEN}" ${LABEL_ARG}
|
||||
./config.sh --unattended --replace --name "${RUNNER_NAME}" --url "${GITHUB_URL}${ATTACH}" --token "${RUNNER_TOKEN}" ${RUNNER_GROUP_ARG} ${LABEL_ARG}
|
||||
|
||||
for f in runsvc.sh RunnerService.js; do
|
||||
diff {bin,patched}/${f} || :
|
||||
|
||||
24
runner/logger.sh
Normal file
24
runner/logger.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
# Logger from this post http://www.cubicrace.com/2016/03/log-tracing-mechnism-for-shell-scripts.html
|
||||
|
||||
function INFO(){
|
||||
local function_name="${FUNCNAME[1]}"
|
||||
local msg="$1"
|
||||
timeAndDate=`date`
|
||||
echo "[$timeAndDate] [INFO] [${0}] $msg"
|
||||
}
|
||||
|
||||
|
||||
function DEBUG(){
|
||||
local function_name="${FUNCNAME[1]}"
|
||||
local msg="$1"
|
||||
timeAndDate=`date`
|
||||
echo "[$timeAndDate] [DEBUG] [${0}] $msg"
|
||||
}
|
||||
|
||||
function ERROR(){
|
||||
local function_name="${FUNCNAME[1]}"
|
||||
local msg="$1"
|
||||
timeAndDate=`date`
|
||||
echo "[$timeAndDate] [ERROR] $msg"
|
||||
}
|
||||
20
runner/modprobe
Normal file
20
runner/modprobe
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# "modprobe" without modprobe
|
||||
# https://twitter.com/lucabruno/status/902934379835662336
|
||||
|
||||
# this isn't 100% fool-proof, but it'll have a much higher success rate than simply using the "real" modprobe
|
||||
|
||||
# Docker often uses "modprobe -va foo bar baz"
|
||||
# so we ignore modules that start with "-"
|
||||
for module; do
|
||||
if [ "${module#-}" = "$module" ]; then
|
||||
ip link show "$module" || true
|
||||
lsmod | grep "$module" || true
|
||||
fi
|
||||
done
|
||||
|
||||
# remove /usr/local/... from PATH so we can exec the real modprobe as a last resort
|
||||
export PATH='/usr/sbin:/usr/bin:/sbin:/bin'
|
||||
exec modprobe "$@"
|
||||
37
runner/startup.sh
Normal file
37
runner/startup.sh
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
source /opt/bash-utils/logger.sh
|
||||
|
||||
function wait_for_process () {
|
||||
local max_time_wait=30
|
||||
local process_name="$1"
|
||||
local waited_sec=0
|
||||
while ! pgrep "$process_name" >/dev/null && ((waited_sec < max_time_wait)); do
|
||||
INFO "Process $process_name is not running yet. Retrying in 1 seconds"
|
||||
INFO "Waited $waited_sec seconds of $max_time_wait seconds"
|
||||
sleep 1
|
||||
((waited_sec=waited_sec+1))
|
||||
if ((waited_sec >= max_time_wait)); then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
INFO "Starting supervisor"
|
||||
sudo /usr/bin/supervisord -n >> /dev/null 2>&1 &
|
||||
|
||||
INFO "Waiting for processes to be running"
|
||||
processes=(dockerd)
|
||||
|
||||
for process in "${processes[@]}"; do
|
||||
wait_for_process "$process"
|
||||
if [ $? -ne 0 ]; then
|
||||
ERROR "$process is not running after max time"
|
||||
exit 1
|
||||
else
|
||||
INFO "$process is running"
|
||||
fi
|
||||
done
|
||||
|
||||
# Wait processes to be running
|
||||
entrypoint.sh
|
||||
6
runner/supervisor/dockerd.conf
Normal file
6
runner/supervisor/dockerd.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
[program:dockerd]
|
||||
command=/usr/local/bin/dockerd
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/dockerd.err.log
|
||||
stdout_logfile=/var/log/dockerd.out.log
|
||||
Reference in New Issue
Block a user