mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 03:13:15 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ type RunnerSpec struct {
|
||||
// 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 +46,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 {
|
||||
|
||||
@@ -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
|
||||
@@ -48,11 +55,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 +76,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,15 +260,78 @@ 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
|
||||
}
|
||||
|
||||
pod := corev1.Pod{
|
||||
@@ -211,7 +344,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: containerName,
|
||||
Image: image,
|
||||
Image: runnerImage,
|
||||
ImagePullPolicy: "Always",
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
@@ -239,7 +372,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 +403,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 {
|
||||
|
||||
Reference in New Issue
Block a user