mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 03:13:15 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2d3ca672f | ||
|
|
829a167303 | ||
|
|
c66916a4ee | ||
|
|
f5c8a0e655 | ||
|
|
a436216d5e | ||
|
|
497ddba82d | ||
|
|
10f6cb5e90 | ||
|
|
13ef78ce20 | ||
|
|
0061979e3e | ||
|
|
e6952f5ca1 | ||
|
|
ffdbe5cee9 | ||
|
|
4970814b6c |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'runner/**'
|
||||
- '.github/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
1
Makefile
1
Makefile
@@ -64,6 +64,7 @@ docker-push:
|
||||
|
||||
# Generate the release manifest file
|
||||
release: manifests
|
||||
cd config/manager && kustomize edit set image controller=${NAME}:${VERSION}
|
||||
mkdir -p release
|
||||
kustomize build config/default > release/actions-runner-controller.yaml
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -13,7 +13,7 @@ This controller operates self-hosted runners for GitHub Actions on your Kubernet
|
||||
First, install *actions-runner-controller* with a manifest file. This will create a *actions-runner-system* namespace in your Kubernetes and deploy the required resources.
|
||||
|
||||
```
|
||||
$ kubectl -f https://github.com/summerwind/actions-runner-controller/releases/download/latest/actions-runner-controller.yaml
|
||||
$ kubectl apply -f https://github.com/summerwind/actions-runner-controller/releases/latest/download/actions-runner-controller.yaml
|
||||
```
|
||||
|
||||
Set your access token of GitHub to the secret. `${GITHUB_TOKEN}` is the value you must replace with your access token. This token is used to register Self-hosted runner by *actions-runner-controller*.
|
||||
@@ -27,9 +27,7 @@ $ kubectl create secret generic controller-manager --from-literal=github_token=$
|
||||
To launch 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.
|
||||
|
||||
```
|
||||
$ vim runner.yaml
|
||||
```
|
||||
```
|
||||
# runner.yaml
|
||||
apiVersion: actions.summerwind.dev/v1alpha1
|
||||
kind: Runner
|
||||
metadata:
|
||||
@@ -42,14 +40,15 @@ Apply the created manifest file to your Kubernetes.
|
||||
|
||||
```
|
||||
$ kubectl apply -f runner.yaml
|
||||
runner.actions.summerwind.dev/example-runner created
|
||||
```
|
||||
|
||||
You can see that the Runner resource has been created.
|
||||
|
||||
```
|
||||
$ kubectl get runners
|
||||
NAME AGE
|
||||
example-runner 1m
|
||||
NAME REPOSITORY STATUS
|
||||
example-runner summerwind/actions-runner-controller Running
|
||||
```
|
||||
|
||||
You can also see that the runner pod has been running.
|
||||
@@ -64,4 +63,4 @@ The runner you created has been registerd to your repository.
|
||||
|
||||
<img width="756" alt="Actions tab in your repository settings" src="https://user-images.githubusercontent.com/230145/73618667-8cbf9700-466c-11ea-80b6-c67e6d3f70e7.png">
|
||||
|
||||
Now your can use your self-hosted runner. See the [documentation](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/using-self-hosted-runners-in-a-workflow) on how to run a job with it.
|
||||
Now your can use your self-hosted runner. See the [official documentation](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/using-self-hosted-runners-in-a-workflow) on how to run a job with it.
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -28,14 +29,17 @@ type RunnerSpec struct {
|
||||
|
||||
// +optional
|
||||
Image string `json:"image"`
|
||||
|
||||
// +optional
|
||||
Env []corev1.EnvVar `json:"env"`
|
||||
}
|
||||
|
||||
// RunnerStatus defines the observed state of Runner
|
||||
type RunnerStatus struct {
|
||||
Registration RunnerStatusRegistration `json:"registration"`
|
||||
Phase string `json:"Phase"`
|
||||
Reason string `json:"Reason"`
|
||||
Message string `json:"Message"`
|
||||
Phase string `json:"phase"`
|
||||
Reason string `json:"reason"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type RunnerStatusRegistration struct {
|
||||
@@ -46,6 +50,8 @@ type RunnerStatusRegistration struct {
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:JSONPath=".spec.repository",name=Repository,type=string
|
||||
// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Status,type=string
|
||||
|
||||
// Runner is the Schema for the runners API
|
||||
type Runner struct {
|
||||
|
||||
@@ -21,6 +21,7 @@ limitations under the License.
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/api/core/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -29,7 +30,7 @@ func (in *Runner) DeepCopyInto(out *Runner) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
@@ -86,6 +87,13 @@ func (in *RunnerList) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RunnerSpec) DeepCopyInto(out *RunnerSpec) {
|
||||
*out = *in
|
||||
if in.Env != nil {
|
||||
in, out := &in.Env, &out.Env
|
||||
*out = make([]v1.EnvVar, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerSpec.
|
||||
|
||||
@@ -8,6 +8,13 @@ metadata:
|
||||
creationTimestamp: null
|
||||
name: runners.actions.summerwind.dev
|
||||
spec:
|
||||
additionalPrinterColumns:
|
||||
- JSONPath: .spec.repository
|
||||
name: Repository
|
||||
type: string
|
||||
- JSONPath: .status.phase
|
||||
name: Status
|
||||
type: string
|
||||
group: actions.summerwind.dev
|
||||
names:
|
||||
kind: Runner
|
||||
@@ -36,6 +43,103 @@ spec:
|
||||
spec:
|
||||
description: RunnerSpec defines the desired state of Runner
|
||||
properties:
|
||||
env:
|
||||
items:
|
||||
description: EnvVar represents an environment variable present in
|
||||
a Container.
|
||||
properties:
|
||||
name:
|
||||
description: Name of the environment variable. Must be a C_IDENTIFIER.
|
||||
type: string
|
||||
value:
|
||||
description: 'Variable references $(VAR_NAME) are expanded using
|
||||
the previous defined environment variables in the container
|
||||
and any service environment variables. If a variable cannot
|
||||
be resolved, the reference in the input string will be unchanged.
|
||||
The $(VAR_NAME) syntax can be escaped with a double $$, ie:
|
||||
$$(VAR_NAME). Escaped references will never be expanded, regardless
|
||||
of whether the variable exists or not. Defaults to "".'
|
||||
type: string
|
||||
valueFrom:
|
||||
description: Source for the environment variable's value. Cannot
|
||||
be used if value is not empty.
|
||||
properties:
|
||||
configMapKeyRef:
|
||||
description: Selects a key of a ConfigMap.
|
||||
properties:
|
||||
key:
|
||||
description: The key to select.
|
||||
type: string
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
optional:
|
||||
description: Specify whether the ConfigMap or its key
|
||||
must be defined
|
||||
type: boolean
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
fieldRef:
|
||||
description: 'Selects a field of the pod: supports metadata.name,
|
||||
metadata.namespace, metadata.labels, metadata.annotations,
|
||||
spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP.'
|
||||
properties:
|
||||
apiVersion:
|
||||
description: Version of the schema the FieldPath is written
|
||||
in terms of, defaults to "v1".
|
||||
type: string
|
||||
fieldPath:
|
||||
description: Path of the field to select in the specified
|
||||
API version.
|
||||
type: string
|
||||
required:
|
||||
- fieldPath
|
||||
type: object
|
||||
resourceFieldRef:
|
||||
description: 'Selects a resource of the container: only resources
|
||||
limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage,
|
||||
requests.cpu, requests.memory and requests.ephemeral-storage)
|
||||
are currently supported.'
|
||||
properties:
|
||||
containerName:
|
||||
description: 'Container name: required for volumes, optional
|
||||
for env vars'
|
||||
type: string
|
||||
divisor:
|
||||
description: Specifies the output format of the exposed
|
||||
resources, defaults to "1"
|
||||
type: string
|
||||
resource:
|
||||
description: 'Required: resource to select'
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
type: object
|
||||
secretKeyRef:
|
||||
description: Selects a key of a secret in the pod's namespace
|
||||
properties:
|
||||
key:
|
||||
description: The key of the secret to select from. Must
|
||||
be a valid secret key.
|
||||
type: string
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
optional:
|
||||
description: Specify whether the Secret or its key must
|
||||
be defined
|
||||
type: boolean
|
||||
required:
|
||||
- key
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
image:
|
||||
type: string
|
||||
repository:
|
||||
@@ -48,11 +152,11 @@ spec:
|
||||
status:
|
||||
description: RunnerStatus defines the observed state of Runner
|
||||
properties:
|
||||
Message:
|
||||
message:
|
||||
type: string
|
||||
Phase:
|
||||
phase:
|
||||
type: string
|
||||
Reason:
|
||||
reason:
|
||||
type: string
|
||||
registration:
|
||||
properties:
|
||||
@@ -69,9 +173,9 @@ spec:
|
||||
- token
|
||||
type: object
|
||||
required:
|
||||
- Message
|
||||
- Phase
|
||||
- Reason
|
||||
- message
|
||||
- phase
|
||||
- reason
|
||||
- registration
|
||||
type: object
|
||||
type: object
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/google/go-github/v29/github"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
@@ -36,11 +37,18 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultImage = "summerwind/actions-runner:latest"
|
||||
containerName = "runner"
|
||||
finalizerName = "runner.actions.summerwind.dev"
|
||||
)
|
||||
|
||||
type RegistrationToken struct {
|
||||
type GitHubRunner struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OS string `json:"os"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type GitHubRegistrationToken struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
@@ -49,8 +57,11 @@ type RegistrationToken struct {
|
||||
type RunnerReconciler struct {
|
||||
client.Client
|
||||
Log logr.Logger
|
||||
Recorder record.EventRecorder
|
||||
Scheme *runtime.Scheme
|
||||
GitHubClient *github.Client
|
||||
RunnerImage string
|
||||
DockerImage string
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runners,verbs=get;list;watch;create;update;patch;delete
|
||||
@@ -63,14 +74,56 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
|
||||
var runner v1alpha1.Runner
|
||||
if err := r.Get(ctx, req.NamespacedName, &runner); err != nil {
|
||||
log.Error(err, "Unable to fetch Runner")
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
if runner.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
finalizers, added := addFinalizer(runner.ObjectMeta.Finalizers)
|
||||
|
||||
if added {
|
||||
newRunner := runner.DeepCopy()
|
||||
newRunner.ObjectMeta.Finalizers = finalizers
|
||||
|
||||
if err := r.Update(ctx, newRunner); err != nil {
|
||||
log.Error(err, "Failed to update runner")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
} else {
|
||||
finalizers, removed := removeFinalizer(runner.ObjectMeta.Finalizers)
|
||||
|
||||
if removed {
|
||||
ok, err := r.unregisterRunner(ctx, runner.Spec.Repository, runner.Name)
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to unregister runner")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.V(1).Info("Runner no longer exists on GitHub")
|
||||
}
|
||||
|
||||
newRunner := runner.DeepCopy()
|
||||
newRunner.ObjectMeta.Finalizers = finalizers
|
||||
|
||||
if err := r.Update(ctx, newRunner); err != nil {
|
||||
log.Error(err, "Failed to update runner")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
log.Info("Removed runner from GitHub", "repository", runner.Spec.Repository)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if !runner.IsRegisterable() {
|
||||
reg, err := r.newRegistration(ctx, runner.Spec.Repository)
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to get new registration")
|
||||
r.Recorder.Event(&runner, corev1.EventTypeWarning, "FailedUpdateRegistrationToken", "Updating registration token failed")
|
||||
log.Error(err, "Failed to get new registration token")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
@@ -78,10 +131,11 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
updated.Status.Registration = reg
|
||||
|
||||
if err := r.Status().Update(ctx, updated); err != nil {
|
||||
log.Error(err, "Unable to update Runner status")
|
||||
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
|
||||
}
|
||||
@@ -94,17 +148,32 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
|
||||
newPod, err := r.newPod(runner)
|
||||
if err != nil {
|
||||
log.Error(err, "could not create pod")
|
||||
log.Error(err, "Could not create pod")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, &newPod); err != nil {
|
||||
log.Error(err, "failed to create pod resource")
|
||||
log.Error(err, "Failed to create pod resource")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
log.Info("Created a runner pod", "repository", runner.Spec.Repository)
|
||||
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) {
|
||||
updated := runner.DeepCopy()
|
||||
updated.Status.Phase = string(pod.Status.Phase)
|
||||
updated.Status.Reason = pod.Status.Reason
|
||||
updated.Status.Message = pod.Status.Message
|
||||
|
||||
if err := r.Status().Update(ctx, updated); err != nil {
|
||||
log.Error(err, "Failed to update runner status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if !pod.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
@@ -125,7 +194,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
|
||||
newPod, err := r.newPod(runner)
|
||||
if err != nil {
|
||||
log.Error(err, "could not create pod")
|
||||
log.Error(err, "Could not create pod")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
@@ -140,11 +209,12 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
}
|
||||
|
||||
if err := r.Delete(ctx, &pod); err != nil {
|
||||
log.Error(err, "failed to delete pod resource")
|
||||
log.Error(err, "Failed to delete pod resource")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
log.Info("Restarted a runner pod", "repository", runner.Spec.Repository)
|
||||
r.Recorder.Event(&runner, corev1.EventTypeNormal, "PodDeleted", fmt.Sprintf("Deleted pod '%s'", newPod.Name))
|
||||
log.Info("Deleted runner pod", "repository", runner.Spec.Repository)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
@@ -170,8 +240,8 @@ func (r *RunnerReconciler) newRegistration(ctx context.Context, repo string) (v1
|
||||
return reg, err
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) getRegistrationToken(ctx context.Context, repo string) (RegistrationToken, error) {
|
||||
var regToken RegistrationToken
|
||||
func (r *RunnerReconciler) getRegistrationToken(ctx context.Context, repo string) (GitHubRegistrationToken, error) {
|
||||
var regToken GitHubRegistrationToken
|
||||
|
||||
req, err := r.GitHubClient.NewRequest("POST", fmt.Sprintf("/repos/%s/actions/runners/registration-token", repo), nil)
|
||||
if err != nil {
|
||||
@@ -190,17 +260,96 @@ func (r *RunnerReconciler) getRegistrationToken(ctx context.Context, repo string
|
||||
return regToken, nil
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) unregisterRunner(ctx context.Context, repo, name string) (bool, error) {
|
||||
runners, err := r.listRunners(ctx, repo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
id := 0
|
||||
for _, runner := range runners {
|
||||
if runner.Name == name {
|
||||
id = runner.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if id == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := r.removeRunner(ctx, repo, id); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) listRunners(ctx context.Context, repo string) ([]GitHubRunner, error) {
|
||||
runners := []GitHubRunner{}
|
||||
|
||||
req, err := r.GitHubClient.NewRequest("GET", fmt.Sprintf("/repos/%s/actions/runners", repo), nil)
|
||||
if err != nil {
|
||||
return runners, err
|
||||
}
|
||||
|
||||
res, err := r.GitHubClient.Do(ctx, req, &runners)
|
||||
if err != nil {
|
||||
return runners, err
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return runners, fmt.Errorf("unexpected status: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
return runners, nil
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) removeRunner(ctx context.Context, repo string, id int) error {
|
||||
req, err := r.GitHubClient.NewRequest("DELETE", fmt.Sprintf("/repos/%s/actions/runners/%d", repo, id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := r.GitHubClient.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != 204 {
|
||||
return fmt.Errorf("unexpected status: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
var (
|
||||
privileged bool = true
|
||||
group int64 = 0
|
||||
)
|
||||
|
||||
image := runner.Spec.Image
|
||||
if image == "" {
|
||||
image = defaultImage
|
||||
runnerImage := runner.Spec.Image
|
||||
if runnerImage == "" {
|
||||
runnerImage = r.RunnerImage
|
||||
}
|
||||
|
||||
env := []corev1.EnvVar{
|
||||
{
|
||||
Name: "RUNNER_NAME",
|
||||
Value: runner.Name,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_REPO",
|
||||
Value: runner.Spec.Repository,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_TOKEN",
|
||||
Value: runner.Status.Registration.Token,
|
||||
},
|
||||
}
|
||||
env = append(env, runner.Spec.Env...)
|
||||
|
||||
pod := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: runner.Name,
|
||||
@@ -211,22 +360,9 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: containerName,
|
||||
Image: image,
|
||||
Image: runnerImage,
|
||||
ImagePullPolicy: "Always",
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: "RUNNER_NAME",
|
||||
Value: runner.Name,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_REPO",
|
||||
Value: runner.Spec.Repository,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_TOKEN",
|
||||
Value: runner.Status.Registration.Token,
|
||||
},
|
||||
},
|
||||
Env: env,
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "docker",
|
||||
@@ -239,7 +375,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
Image: "docker:19.03.5-dind",
|
||||
Image: r.DockerImage,
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "docker",
|
||||
@@ -270,8 +406,40 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
}
|
||||
|
||||
func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
r.Recorder = mgr.GetEventRecorderFor("runner-controller")
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&v1alpha1.Runner{}).
|
||||
Owns(&corev1.Pod{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func addFinalizer(finalizers []string) ([]string, bool) {
|
||||
exists := false
|
||||
for _, name := range finalizers {
|
||||
if name == finalizerName {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
|
||||
if exists {
|
||||
return finalizers, false
|
||||
}
|
||||
|
||||
return append(finalizers, finalizerName), true
|
||||
}
|
||||
|
||||
func removeFinalizer(finalizers []string) ([]string, bool) {
|
||||
removed := false
|
||||
result := []string{}
|
||||
|
||||
for _, name := range finalizers {
|
||||
if name == finalizerName {
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
result = append(result, name)
|
||||
}
|
||||
|
||||
return result, removed
|
||||
}
|
||||
|
||||
27
main.go
27
main.go
@@ -34,6 +34,11 @@ import (
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRunnerImage = "summerwind/actions-runner:v2.165.1"
|
||||
defaultDockerImage = "docker:19.03.5-dind"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
@@ -47,16 +52,28 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var (
|
||||
metricsAddr string
|
||||
enableLeaderElection bool
|
||||
|
||||
runnerImage string
|
||||
dockerImage string
|
||||
ghToken string
|
||||
)
|
||||
|
||||
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 access token of GitHub.")
|
||||
flag.Parse()
|
||||
|
||||
ghToken := os.Getenv("GITHUB_TOKEN")
|
||||
if ghToken == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: access token is not specified in the environment variable 'GITHUB_TOKEN'")
|
||||
ghToken = os.Getenv("GITHUB_TOKEN")
|
||||
}
|
||||
if ghToken == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: GitHub access token must be specified.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -85,6 +102,8 @@ func main() {
|
||||
Log: ctrl.Log.WithName("controllers").WithName("Runner"),
|
||||
Scheme: mgr.GetScheme(),
|
||||
GitHubClient: ghClient,
|
||||
RunnerImage: runnerImage,
|
||||
DockerImage: dockerImage,
|
||||
}
|
||||
|
||||
if err = runnerReconciler.SetupWithManager(mgr); err != nil {
|
||||
|
||||
@@ -9,6 +9,8 @@ RUN apt update \
|
||||
&& tar zxvf docker.tgz \
|
||||
&& install -o root -g root -m 755 docker/docker /usr/local/bin/docker \
|
||||
&& rm -rf docker docker.tgz \
|
||||
&& curl -L -o /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64 \
|
||||
&& chmod +x /usr/local/bin/dumb-init \
|
||||
&& adduser --disabled-password --gecos "" --uid 1000 runner
|
||||
|
||||
RUN mkdir -p /runner \
|
||||
@@ -21,4 +23,5 @@ RUN mkdir -p /runner \
|
||||
COPY entrypoint.sh /runner
|
||||
|
||||
USER runner:runner
|
||||
ENTRYPOINT ["/runner/entrypoint.sh"]
|
||||
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
||||
CMD ["/runner/entrypoint.sh"]
|
||||
|
||||
@@ -17,4 +17,6 @@ fi
|
||||
|
||||
cd /runner
|
||||
./config.sh --unattended --replace --name "${RUNNER_NAME}" --url "https://github.com/${RUNNER_REPO}" --token "${RUNNER_TOKEN}"
|
||||
./run.sh --once
|
||||
|
||||
unset RUNNER_NAME RUNNER_REPO RUNNER_TOKEN
|
||||
exec ./run.sh --once
|
||||
|
||||
Reference in New Issue
Block a user