Compare commits

..

8 Commits

Author SHA1 Message Date
Moto Ishizawa
a436216d5e Implement finalizer 2020-02-03 21:35:01 +09:00
Moto Ishizawa
497ddba82d Record event of runner resource 2020-02-03 18:40:59 +09:00
Moto Ishizawa
10f6cb5e90 Add additional printer columns 2020-02-03 17:37:48 +09:00
Moto Ishizawa
13ef78ce20 Sync runner status with pod status 2020-02-03 17:25:38 +09:00
Moto Ishizawa
0061979e3e Add '-github-token' flag 2020-02-03 17:02:27 +09:00
Moto Ishizawa
e6952f5ca1 Add '-runner-image' and '-docker-image' flags 2020-02-03 16:56:52 +09:00
Moto Ishizawa
ffdbe5cee9 Set container image version properly in the release task 2020-02-03 16:55:38 +09:00
Moto Ishizawa
4970814b6c Do not run build workflow when .github changed 2020-02-03 16:54:49 +09:00
6 changed files with 226 additions and 31 deletions

View File

@@ -4,6 +4,7 @@ on:
- master - master
paths-ignore: paths-ignore:
- 'runner/**' - 'runner/**'
- '.github/**'
jobs: jobs:
build: build:

View File

@@ -64,6 +64,7 @@ docker-push:
# Generate the release manifest file # Generate the release manifest file
release: manifests release: manifests
cd config/manager && kustomize edit set image controller=${NAME}:${VERSION}
mkdir -p release mkdir -p release
kustomize build config/default > release/actions-runner-controller.yaml kustomize build config/default > release/actions-runner-controller.yaml

View File

@@ -33,9 +33,9 @@ type RunnerSpec struct {
// RunnerStatus defines the observed state of Runner // RunnerStatus defines the observed state of Runner
type RunnerStatus struct { type RunnerStatus struct {
Registration RunnerStatusRegistration `json:"registration"` Registration RunnerStatusRegistration `json:"registration"`
Phase string `json:"Phase"` Phase string `json:"phase"`
Reason string `json:"Reason"` Reason string `json:"reason"`
Message string `json:"Message"` Message string `json:"message"`
} }
type RunnerStatusRegistration struct { type RunnerStatusRegistration struct {
@@ -46,6 +46,8 @@ type RunnerStatusRegistration struct {
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
// +kubebuilder:subresource:status // +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 // Runner is the Schema for the runners API
type Runner struct { type Runner struct {

View File

@@ -8,6 +8,13 @@ metadata:
creationTimestamp: null creationTimestamp: null
name: runners.actions.summerwind.dev name: runners.actions.summerwind.dev
spec: spec:
additionalPrinterColumns:
- JSONPath: .spec.repository
name: Repository
type: string
- JSONPath: .status.phase
name: Status
type: string
group: actions.summerwind.dev group: actions.summerwind.dev
names: names:
kind: Runner kind: Runner
@@ -48,11 +55,11 @@ spec:
status: status:
description: RunnerStatus defines the observed state of Runner description: RunnerStatus defines the observed state of Runner
properties: properties:
Message: message:
type: string type: string
Phase: phase:
type: string type: string
Reason: reason:
type: string type: string
registration: registration:
properties: properties:
@@ -69,9 +76,9 @@ spec:
- token - token
type: object type: object
required: required:
- Message - message
- Phase - phase
- Reason - reason
- registration - registration
type: object type: object
type: object type: object

View File

@@ -26,6 +26,7 @@ import (
"github.com/google/go-github/v29/github" "github.com/google/go-github/v29/github"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@@ -36,11 +37,18 @@ import (
) )
const ( const (
defaultImage = "summerwind/actions-runner:latest"
containerName = "runner" 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"` Token string `json:"token"`
ExpiresAt string `json:"expires_at"` ExpiresAt string `json:"expires_at"`
} }
@@ -49,8 +57,11 @@ type RegistrationToken struct {
type RunnerReconciler struct { type RunnerReconciler struct {
client.Client client.Client
Log logr.Logger Log logr.Logger
Recorder record.EventRecorder
Scheme *runtime.Scheme Scheme *runtime.Scheme
GitHubClient *github.Client GitHubClient *github.Client
RunnerImage string
DockerImage string
} }
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runners,verbs=get;list;watch;create;update;patch;delete // +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 var runner v1alpha1.Runner
if err := r.Get(ctx, req.NamespacedName, &runner); err != nil { if err := r.Get(ctx, req.NamespacedName, &runner); err != nil {
log.Error(err, "Unable to fetch Runner")
return ctrl.Result{}, client.IgnoreNotFound(err) 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() { if !runner.IsRegisterable() {
reg, err := r.newRegistration(ctx, runner.Spec.Repository) reg, err := r.newRegistration(ctx, runner.Spec.Repository)
if err != nil { 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 return ctrl.Result{}, err
} }
@@ -78,10 +131,11 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
updated.Status.Registration = reg updated.Status.Registration = reg
if err := r.Status().Update(ctx, updated); err != nil { 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 return ctrl.Result{}, err
} }
r.Recorder.Event(&runner, corev1.EventTypeNormal, "RegistrationTokenUpdated", "Successfully update registration token")
log.Info("Updated registration token", "repository", runner.Spec.Repository) log.Info("Updated registration token", "repository", runner.Spec.Repository)
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@@ -94,17 +148,32 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
newPod, err := r.newPod(runner) newPod, err := r.newPod(runner)
if err != nil { if err != nil {
log.Error(err, "could not create pod") log.Error(err, "Could not create pod")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
if err := r.Create(ctx, &newPod); err != nil { 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 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 { } 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() { if !pod.ObjectMeta.DeletionTimestamp.IsZero() {
return ctrl.Result{}, err return ctrl.Result{}, err
} }
@@ -125,7 +194,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
newPod, err := r.newPod(runner) newPod, err := r.newPod(runner)
if err != nil { if err != nil {
log.Error(err, "could not create pod") log.Error(err, "Could not create pod")
return ctrl.Result{}, err 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 { 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 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 return ctrl.Result{}, nil
@@ -170,8 +240,8 @@ func (r *RunnerReconciler) newRegistration(ctx context.Context, repo string) (v1
return reg, err return reg, err
} }
func (r *RunnerReconciler) getRegistrationToken(ctx context.Context, repo string) (RegistrationToken, error) { func (r *RunnerReconciler) getRegistrationToken(ctx context.Context, repo string) (GitHubRegistrationToken, error) {
var regToken RegistrationToken var regToken GitHubRegistrationToken
req, err := r.GitHubClient.NewRequest("POST", fmt.Sprintf("/repos/%s/actions/runners/registration-token", repo), nil) req, err := r.GitHubClient.NewRequest("POST", fmt.Sprintf("/repos/%s/actions/runners/registration-token", repo), nil)
if err != nil { if err != nil {
@@ -190,15 +260,78 @@ func (r *RunnerReconciler) getRegistrationToken(ctx context.Context, repo string
return regToken, nil 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) { func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
var ( var (
privileged bool = true privileged bool = true
group int64 = 0 group int64 = 0
) )
image := runner.Spec.Image runnerImage := runner.Spec.Image
if image == "" { if runnerImage == "" {
image = defaultImage runnerImage = r.RunnerImage
} }
pod := corev1.Pod{ pod := corev1.Pod{
@@ -211,7 +344,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {
Name: containerName, Name: containerName,
Image: image, Image: runnerImage,
ImagePullPolicy: "Always", ImagePullPolicy: "Always",
Env: []corev1.EnvVar{ Env: []corev1.EnvVar{
{ {
@@ -239,7 +372,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
}, },
{ {
Name: "docker", Name: "docker",
Image: "docker:19.03.5-dind", Image: r.DockerImage,
VolumeMounts: []corev1.VolumeMount{ VolumeMounts: []corev1.VolumeMount{
{ {
Name: "docker", Name: "docker",
@@ -270,8 +403,40 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
} }
func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.Recorder = mgr.GetEventRecorderFor("runner-controller")
return ctrl.NewControllerManagedBy(mgr). return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.Runner{}). For(&v1alpha1.Runner{}).
Owns(&corev1.Pod{}). Owns(&corev1.Pod{}).
Complete(r) 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
View File

@@ -34,6 +34,11 @@ import (
// +kubebuilder:scaffold:imports // +kubebuilder:scaffold:imports
) )
const (
defaultRunnerImage = "summerwind/actions-runner:v2.165.1"
defaultDockerImage = "docker:19.03.5-dind"
)
var ( var (
scheme = runtime.NewScheme() scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup") setupLog = ctrl.Log.WithName("setup")
@@ -47,16 +52,28 @@ func init() {
} }
func main() { func main() {
var metricsAddr string var (
var enableLeaderElection bool metricsAddr string
enableLeaderElection bool
runnerImage string
dockerImage string
ghToken string
)
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") "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() flag.Parse()
ghToken := os.Getenv("GITHUB_TOKEN")
if ghToken == "" { 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) os.Exit(1)
} }
@@ -85,6 +102,8 @@ func main() {
Log: ctrl.Log.WithName("controllers").WithName("Runner"), Log: ctrl.Log.WithName("controllers").WithName("Runner"),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
GitHubClient: ghClient, GitHubClient: ghClient,
RunnerImage: runnerImage,
DockerImage: dockerImage,
} }
if err = runnerReconciler.SetupWithManager(mgr); err != nil { if err = runnerReconciler.SetupWithManager(mgr); err != nil {