Compare commits

..

20 Commits

Author SHA1 Message Date
Reinier Timmer
bc35bdfa85 fixed label argument in entrypoint (#162)
* fixed label argument in entrypoint

* Removed quotes from RUNNER_GROUP_ARG
2020-11-11 08:44:51 +09:00
Yusuke Kuoka
ece8fd8fe4 Bump Go to 1.15 (#160)
Closes #104
2020-11-10 17:16:04 +09:00
Dan Webb
dcf8524b5c Adds RUNNER_GROUP argument to the runner registration (#157)
* Adds RUNNER_GROUP argument to the runner registration

Adds the ability to register a runner to a predefined runner_group

Resolves #137

* Update README with runner group example

- Updates the README with instructions of how to add the runner to a
  group
- Fix code fencing for shell and yaml blocks in the README
- Use consistent bullet points (dash not asterisk)
2020-11-10 17:15:54 +09:00
Yusuke Kuoka
4eb45d3c7f Fix build error 2020-11-10 17:09:16 +09:00
Juho Saarinen
1c30bdf35b Add GHE URL to transport (#152)
Fixes #149
2020-11-10 17:05:09 +09:00
Yusuke Kuoka
3f335ca628 Fix panic on startup when misconfigured (#154)
Fixes #153
2020-11-10 17:03:33 +09:00
Juho Saarinen
f2a2ab7ede Check token validity only when creating new pod (#159)
Fixes #143
2020-11-10 17:02:30 +09:00
Juho Saarinen
40c5050978 Added support for other than public GitHub URL (#146)
Refactoring a bit
2020-10-28 22:15:53 +09:00
Juho Saarinen
99a53a6e79 Releasing latest controller from master push (#147)
Fixes #135
2020-10-28 22:13:35 +09:00
Yusuke Kuoka
6d78fb07b3 Fix permission error with the default setup since v0.9.4 (#142)
Fixes #138
2020-10-25 11:25:48 +09:00
Yusuke Kuoka
faaca10fba Rename Runner.Spec.dockerWithinRunnerContainer to docker"d"WithinRunnerContainer (#134)
* Rename Runner.Spec.dockerWithinRunnerContainer to dockerdWithinRunnerContainer

Ref https://github.com/summerwind/actions-runner-controller/pull/126#issuecomment-712501790
2020-10-21 21:32:40 +09:00
Juho Saarinen
d16dfac0f8 Restart if pod ends up succeeded (#136)
Fixes #132
2020-10-21 21:32:26 +09:00
Juho Saarinen
af483d83da Possibility to define resources for DIND container (#125)
Ref #119
2020-10-21 10:26:19 +09:00
Juho Saarinen
92920926fe Configurable "runner and DinD in a single container" (#126) 2020-10-20 08:48:28 +09:00
Brendan Galloway
7d0bfb77e3 Inject Env Vars into Runner defined Container Spec (#127)
The runner token is now injected into the `runner` container defined within Runner.Spec.Containers[]
2020-10-20 08:43:53 +09:00
Brendan Galloway
c4074130e8 Patch additional protocol instances during manifest generation (#129)
Fixes #128
2020-10-19 09:57:53 +09:00
Yusuke Kuoka
be2e61f209 Add "alternatives" section to README (#124)
Grow together :)
2020-10-15 08:40:02 +09:00
Juho Saarinen
da818a898a Manifests valid for K8s 1.18 and 1.19 (#123)
Fixes #113
Fixes #116
2020-10-15 08:39:46 +09:00
Jesse Newland
2d250d5e06 Fix release build (#122) 2020-10-14 08:43:14 +09:00
Jesse Newland
231cde1531 Update build-runner workflow to be compatible with forks, fix image push (#117)
Partly revert and enhances #115

This is a follow-up to #115 that replaces the hardcoded `summerwind` portion of the image name with `${{ github.repository_owner }}` to enable contributors to test the image pushing behavior and fixes image building by conditionally passing `--push` to the build step based on the event that triggered the workflow.

After setting the `DOCKER_ACCESS_TOKEN` Secret on my fork of this repository, I was able to use this updated workflow to [build and push](https://github.com/urcomputeringpal/actions-runner-controller/runs/1242793758?check_suite_focus=true) a [set of images](https://hub.docker.com/r/urcomputeringpal/actions-runner/tags) and confirm their functionality. I imagine this will be useful to future contributors who wish to help with the chore of keeping up with https://github.com/actions/runner/releases.
2020-10-13 21:14:36 +09:00
27 changed files with 2039 additions and 17005 deletions

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:1.13 as builder
FROM golang:1.15 as builder
ARG TARGETPLATFORM

View File

@@ -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
View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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"},
},

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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,

View File

@@ -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"]

View File

@@ -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}

View 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"]

View File

@@ -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
View 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
View 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
View 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

View 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