From 11cb9b78829f8640ceb3bcb677e5d608dc3299ea Mon Sep 17 00:00:00 2001 From: Felipe Galindo Sanchez Date: Sat, 9 Jul 2022 23:11:29 -0700 Subject: [PATCH] feat: allow to discover runner statuses (#1268) * feat: allow to discover runner statuses * fix manifests * Bump runner version to 2.289.1 which includes the hooks support * Add feedback from review * Update reference to newRunnerPod * Fix TestNewRunnerPodFromRunnerController and make hooks file names job specific * Fix additional TestNewRunnerPod test * Cover additional feedback from review * fix rbac manager role * Add permissions to service account for container mode if not provided * Rename flag to runner.statusUpdateHook.enabled and fix needsServiceAccount Co-authored-by: Yusuke Kuoka --- api/v1alpha1/runner_types.go | 17 +-- charts/actions-runner-controller/README.md | 1 + .../crds/actions.summerwind.dev_runners.yaml | 3 + .../templates/deployment.yaml | 3 + .../templates/manager_role.yaml | 26 ++++ charts/actions-runner-controller/values.yaml | 4 + .../bases/actions.summerwind.dev_runners.yaml | 3 + config/rbac/role.yaml | 24 ++++ controllers/new_runner_pod_test.go | 26 +++- controllers/runner_controller.go | 133 +++++++++++++++++- controllers/runnerset_controller.go | 15 +- main.go | 30 ++-- runner/actions-runner.dockerfile | 5 +- runner/entrypoint.sh | 10 ++ runner/hooks/job-completed.d/update-status | 4 + runner/hooks/job-completed.sh | 12 ++ runner/hooks/job-started.d/update-status | 4 + runner/hooks/job-started.sh | 12 ++ runner/update-status | 31 ++++ 19 files changed, 318 insertions(+), 45 deletions(-) create mode 100755 runner/hooks/job-completed.d/update-status create mode 100755 runner/hooks/job-completed.sh create mode 100755 runner/hooks/job-started.d/update-status create mode 100644 runner/hooks/job-started.sh create mode 100755 runner/update-status diff --git a/api/v1alpha1/runner_types.go b/api/v1alpha1/runner_types.go index 23bcd74..626f2af 100644 --- a/api/v1alpha1/runner_types.go +++ b/api/v1alpha1/runner_types.go @@ -183,11 +183,6 @@ func (rs *RunnerSpec) Validate(rootPath *field.Path) field.ErrorList { errList = append(errList, field.Invalid(rootPath.Child("workVolumeClaimTemplate"), rs.WorkVolumeClaimTemplate, err.Error())) } - err = rs.validateIsServiceAccountNameSet() - if err != nil { - errList = append(errList, field.Invalid(rootPath.Child("serviceAccountName"), rs.ServiceAccountName, err.Error())) - } - return errList } @@ -226,17 +221,6 @@ func (rs *RunnerSpec) validateWorkVolumeClaimTemplate() error { return rs.WorkVolumeClaimTemplate.validate() } -func (rs *RunnerSpec) validateIsServiceAccountNameSet() error { - if rs.ContainerMode != "kubernetes" { - return nil - } - - if rs.ServiceAccountName == "" { - return errors.New("service account name is required if container mode is kubernetes") - } - return nil -} - // RunnerStatus defines the observed state of Runner type RunnerStatus struct { // Turns true only if the runner pod is ready. @@ -317,6 +301,7 @@ func (w *WorkVolumeClaimTemplate) V1VolumeMount(mountPath string) corev1.VolumeM // +kubebuilder:printcolumn:JSONPath=".spec.repository",name=Repository,type=string // +kubebuilder:printcolumn:JSONPath=".spec.labels",name=Labels,type=string // +kubebuilder:printcolumn:JSONPath=".status.phase",name=Status,type=string +// +kubebuilder:printcolumn:JSONPath=".status.message",name=Message,type=string // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // Runner is the Schema for the runners API diff --git a/charts/actions-runner-controller/README.md b/charts/actions-runner-controller/README.md index 15483f2..7d5a776 100644 --- a/charts/actions-runner-controller/README.md +++ b/charts/actions-runner-controller/README.md @@ -73,6 +73,7 @@ All additional docs are kept in the `docs/` folder, this README is solely for do | `scope.watchNamespace` | Tells the controller and the github webhook server which namespace to watch if `scope.singleNamespace` is true | `Release.Namespace` (the default namespace of the helm chart). | | `scope.singleNamespace` | Limit the controller to watch a single namespace | false | | `certManagerEnabled` | Enable cert-manager. If disabled you must set admissionWebHooks.caBundle and create TLS secrets manually | true | +| `runner.statusUpdateHook.enabled` | Use custom RBAC for runners (role, role binding and service account), this will enable reporting runner statuses | false | | `admissionWebHooks.caBundle` | Base64-encoded PEM bundle containing the CA that signed the webhook's serving certificate | | | `githubWebhookServer.logLevel` | Set the log level of the githubWebhookServer container | | | `githubWebhookServer.replicaCount` | Set the number of webhook server pods | 1 | diff --git a/charts/actions-runner-controller/crds/actions.summerwind.dev_runners.yaml b/charts/actions-runner-controller/crds/actions.summerwind.dev_runners.yaml index d657102..f2a1430 100644 --- a/charts/actions-runner-controller/crds/actions.summerwind.dev_runners.yaml +++ b/charts/actions-runner-controller/crds/actions.summerwind.dev_runners.yaml @@ -30,6 +30,9 @@ spec: - jsonPath: .status.phase name: Status type: string + - jsonPath: .status.message + name: Message + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date diff --git a/charts/actions-runner-controller/templates/deployment.yaml b/charts/actions-runner-controller/templates/deployment.yaml index f35d6ff..60e4e85 100644 --- a/charts/actions-runner-controller/templates/deployment.yaml +++ b/charts/actions-runner-controller/templates/deployment.yaml @@ -67,6 +67,9 @@ spec: {{- if .Values.runnerGithubURL }} - "--runner-github-url={{ .Values.runnerGithubURL }}" {{- end }} + {{- if .Values.runner.statusUpdateHook.enabled }} + - "--runner-status-update-hook" + {{- end }} command: - "/manager" env: diff --git a/charts/actions-runner-controller/templates/manager_role.yaml b/charts/actions-runner-controller/templates/manager_role.yaml index d5a41f2..9101adb 100644 --- a/charts/actions-runner-controller/templates/manager_role.yaml +++ b/charts/actions-runner-controller/templates/manager_role.yaml @@ -258,3 +258,29 @@ rules: - get - list - watch +{{- if .Values.runner.statusUpdateHook.enabled }} +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - create + - delete + - get +{{- end }} \ No newline at end of file diff --git a/charts/actions-runner-controller/values.yaml b/charts/actions-runner-controller/values.yaml index c805ba0..6137f62 100644 --- a/charts/actions-runner-controller/values.yaml +++ b/charts/actions-runner-controller/values.yaml @@ -67,6 +67,10 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +runner: + statusUpdateHook: + enabled: false + serviceAccount: # Specifies whether a service account should be created create: true diff --git a/config/crd/bases/actions.summerwind.dev_runners.yaml b/config/crd/bases/actions.summerwind.dev_runners.yaml index d657102..f2a1430 100644 --- a/config/crd/bases/actions.summerwind.dev_runners.yaml +++ b/config/crd/bases/actions.summerwind.dev_runners.yaml @@ -30,6 +30,9 @@ spec: - jsonPath: .status.phase name: Status type: string + - jsonPath: .status.message + name: Message + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index bea2701..37c567d 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -258,3 +258,27 @@ rules: - get - list - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - create + - delete + - get diff --git a/controllers/new_runner_pod_test.go b/controllers/new_runner_pod_test.go index b65b476..086912c 100644 --- a/controllers/new_runner_pod_test.go +++ b/controllers/new_runner_pod_test.go @@ -125,6 +125,10 @@ func TestNewRunnerPod(t *testing.T) { Name: "RUNNER_EPHEMERAL", Value: "true", }, + { + Name: "RUNNER_STATUS_UPDATE_HOOK", + Value: "false", + }, { Name: "DOCKER_HOST", Value: "tcp://localhost:2376", @@ -255,6 +259,10 @@ func TestNewRunnerPod(t *testing.T) { Name: "RUNNER_EPHEMERAL", Value: "true", }, + { + Name: "RUNNER_STATUS_UPDATE_HOOK", + Value: "false", + }, }, VolumeMounts: []corev1.VolumeMount{ { @@ -333,6 +341,10 @@ func TestNewRunnerPod(t *testing.T) { Name: "RUNNER_EPHEMERAL", Value: "true", }, + { + Name: "RUNNER_STATUS_UPDATE_HOOK", + Value: "false", + }, }, VolumeMounts: []corev1.VolumeMount{ { @@ -515,7 +527,7 @@ func TestNewRunnerPod(t *testing.T) { for i := range testcases { tc := testcases[i] t.Run(tc.description, func(t *testing.T) { - got, err := newRunnerPod(tc.template, tc.config, defaultRunnerImage, defaultRunnerImagePullSecrets, defaultDockerImage, defaultDockerRegistryMirror, githubBaseURL) + got, err := newRunnerPod(tc.template, tc.config, defaultRunnerImage, defaultRunnerImagePullSecrets, defaultDockerImage, defaultDockerRegistryMirror, githubBaseURL, false) require.NoError(t, err) require.Equal(t, tc.want, got) }) @@ -624,6 +636,10 @@ func TestNewRunnerPodFromRunnerController(t *testing.T) { Name: "RUNNER_EPHEMERAL", Value: "true", }, + { + Name: "RUNNER_STATUS_UPDATE_HOOK", + Value: "false", + }, { Name: "DOCKER_HOST", Value: "tcp://localhost:2376", @@ -769,6 +785,10 @@ func TestNewRunnerPodFromRunnerController(t *testing.T) { Name: "RUNNER_EPHEMERAL", Value: "true", }, + { + Name: "RUNNER_STATUS_UPDATE_HOOK", + Value: "false", + }, { Name: "RUNNER_NAME", Value: "runner", @@ -866,6 +886,10 @@ func TestNewRunnerPodFromRunnerController(t *testing.T) { Name: "RUNNER_EPHEMERAL", Value: "true", }, + { + Name: "RUNNER_STATUS_UPDATE_HOOK", + Value: "false", + }, { Name: "RUNNER_NAME", Value: "runner", diff --git a/controllers/runner_controller.go b/controllers/runner_controller.go index 69f0fdf..5af012d 100644 --- a/controllers/runner_controller.go +++ b/controllers/runner_controller.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "reflect" "strconv" "strings" "time" @@ -35,6 +36,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1" @@ -70,8 +72,8 @@ type RunnerReconciler struct { Name string RegistrationRecheckInterval time.Duration RegistrationRecheckJitter time.Duration - - UnregistrationRetryDelay time.Duration + UseRunnerStatusUpdateHook bool + UnregistrationRetryDelay time.Duration } // +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runners,verbs=get;list;watch;create;update;patch;delete @@ -81,6 +83,9 @@ type RunnerReconciler struct { // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;delete // +kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=create;delete;get +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;delete;get +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;delete;get func (r *RunnerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("runner", req.NamespacedName) @@ -135,7 +140,7 @@ func (r *RunnerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr ready := runnerPodReady(&pod) - if runner.Status.Phase != phase || runner.Status.Ready != ready { + if (runner.Status.Phase != phase || runner.Status.Ready != ready) && !r.UseRunnerStatusUpdateHook || runner.Status.Phase == "" && r.UseRunnerStatusUpdateHook { if pod.Status.Phase == corev1.PodRunning { // Seeing this message, you can expect the runner to become `Running` soon. log.V(1).Info( @@ -256,6 +261,91 @@ func (r *RunnerReconciler) processRunnerCreation(ctx context.Context, runner v1a return ctrl.Result{}, err } + needsServiceAccount := runner.Spec.ServiceAccountName == "" && (r.UseRunnerStatusUpdateHook || runner.Spec.ContainerMode == "kubernetes") + if needsServiceAccount { + serviceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: runner.ObjectMeta.Name, + Namespace: runner.ObjectMeta.Namespace, + }, + } + if res := r.createObject(ctx, serviceAccount, serviceAccount.ObjectMeta, &runner, log); res != nil { + return *res, nil + } + + rules := []rbacv1.PolicyRule{} + + if r.UseRunnerStatusUpdateHook { + rules = append(rules, []rbacv1.PolicyRule{ + { + APIGroups: []string{"actions.summerwind.dev"}, + Resources: []string{"runners/status"}, + Verbs: []string{"get", "update", "patch"}, + ResourceNames: []string{runner.ObjectMeta.Name}, + }, + }...) + } + + if runner.Spec.ContainerMode == "kubernetes" { + // Permissions based on https://github.com/actions/runner-container-hooks/blob/main/packages/k8s/README.md + rules = append(rules, []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "list", "create", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"pods/exec"}, + Verbs: []string{"get", "create"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"pods/log"}, + Verbs: []string{"get", "list", "watch"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"get", "list", "create", "delete"}, + }, + }...) + } + + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: runner.ObjectMeta.Name, + Namespace: runner.ObjectMeta.Namespace, + }, + Rules: rules, + } + if res := r.createObject(ctx, role, role.ObjectMeta, &runner, log); res != nil { + return *res, nil + } + + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: runner.ObjectMeta.Name, + Namespace: runner.ObjectMeta.Namespace, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: runner.ObjectMeta.Name, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: runner.ObjectMeta.Name, + Namespace: runner.ObjectMeta.Namespace, + }, + }, + } + if res := r.createObject(ctx, roleBinding, roleBinding.ObjectMeta, &runner, log); res != nil { + return *res, nil + } + } + if err := r.Create(ctx, &newPod); err != nil { if kerrors.IsAlreadyExists(err) { // Gracefully handle pod-already-exists errors due to informer cache delay. @@ -278,6 +368,27 @@ func (r *RunnerReconciler) processRunnerCreation(ctx context.Context, runner v1a return ctrl.Result{}, nil } +func (r *RunnerReconciler) createObject(ctx context.Context, obj client.Object, meta metav1.ObjectMeta, runner *v1alpha1.Runner, log logr.Logger) *ctrl.Result { + kind := strings.Split(reflect.TypeOf(obj).String(), ".")[1] + if err := ctrl.SetControllerReference(runner, obj, r.Scheme); err != nil { + log.Error(err, fmt.Sprintf("Could not add owner reference to %s %s. %s", kind, meta.Name, err.Error())) + return &ctrl.Result{Requeue: true} + } + if err := r.Create(ctx, obj); err != nil { + if kerrors.IsAlreadyExists(err) { + log.Info(fmt.Sprintf("Failed to create %s %s as it already exists. Reusing existing %s", kind, meta.Name, kind)) + r.Recorder.Event(runner, corev1.EventTypeNormal, fmt.Sprintf("%sReused", kind), fmt.Sprintf("Reused %s '%s'", kind, meta.Name)) + return nil + } + + log.Error(err, fmt.Sprintf("Retrying as failed to create %s %s resource", kind, meta.Name)) + return &ctrl.Result{Requeue: true} + } + r.Recorder.Event(runner, corev1.EventTypeNormal, fmt.Sprintf("%sCreated", kind), fmt.Sprintf("Created %s '%s'", kind, meta.Name)) + log.Info(fmt.Sprintf("Created %s", kind), "name", meta.Name) + return nil +} + func (r *RunnerReconciler) updateRegistrationToken(ctx context.Context, runner v1alpha1.Runner) (bool, error) { if runner.IsRegisterable() { return false, nil @@ -426,7 +537,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) { } } - pod, err := newRunnerPodWithContainerMode(runner.Spec.ContainerMode, template, runner.Spec.RunnerConfig, r.RunnerImage, r.RunnerImagePullSecrets, r.DockerImage, r.DockerRegistryMirror, r.GitHubClient.GithubBaseURL) + pod, err := newRunnerPodWithContainerMode(runner.Spec.ContainerMode, template, runner.Spec.RunnerConfig, r.RunnerImage, r.RunnerImagePullSecrets, r.DockerImage, r.DockerRegistryMirror, r.GitHubClient.GithubBaseURL, r.UseRunnerStatusUpdateHook) if err != nil { return pod, err } @@ -474,9 +585,13 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) { if runnerSpec.NodeSelector != nil { pod.Spec.NodeSelector = runnerSpec.NodeSelector } + if runnerSpec.ServiceAccountName != "" { pod.Spec.ServiceAccountName = runnerSpec.ServiceAccountName + } else if r.UseRunnerStatusUpdateHook || runner.Spec.ContainerMode == "kubernetes" { + pod.Spec.ServiceAccountName = runner.ObjectMeta.Name } + if runnerSpec.AutomountServiceAccountToken != nil { pod.Spec.AutomountServiceAccountToken = runnerSpec.AutomountServiceAccountToken } @@ -589,7 +704,7 @@ func runnerHookEnvs(pod *corev1.Pod) ([]corev1.EnvVar, error) { }, nil } -func newRunnerPodWithContainerMode(containerMode string, template corev1.Pod, runnerSpec v1alpha1.RunnerConfig, defaultRunnerImage string, defaultRunnerImagePullSecrets []string, defaultDockerImage, defaultDockerRegistryMirror string, githubBaseURL string) (corev1.Pod, error) { +func newRunnerPodWithContainerMode(containerMode string, template corev1.Pod, runnerSpec v1alpha1.RunnerConfig, defaultRunnerImage string, defaultRunnerImagePullSecrets []string, defaultDockerImage, defaultDockerRegistryMirror string, githubBaseURL string, useRunnerStatusUpdateHook bool) (corev1.Pod, error) { var ( privileged bool = true dockerdInRunner bool = runnerSpec.DockerdWithinRunnerContainer != nil && *runnerSpec.DockerdWithinRunnerContainer @@ -665,6 +780,10 @@ func newRunnerPodWithContainerMode(containerMode string, template corev1.Pod, ru Name: EnvVarEphemeral, Value: fmt.Sprintf("%v", ephemeral), }, + { + Name: "RUNNER_STATUS_UPDATE_HOOK", + Value: fmt.Sprintf("%v", useRunnerStatusUpdateHook), + }, } var seLinuxOptions *corev1.SELinuxOptions @@ -962,8 +1081,8 @@ func newRunnerPodWithContainerMode(containerMode string, template corev1.Pod, ru return *pod, nil } -func newRunnerPod(template corev1.Pod, runnerSpec v1alpha1.RunnerConfig, defaultRunnerImage string, defaultRunnerImagePullSecrets []string, defaultDockerImage, defaultDockerRegistryMirror string, githubBaseURL string) (corev1.Pod, error) { - return newRunnerPodWithContainerMode("", template, runnerSpec, defaultRunnerImage, defaultRunnerImagePullSecrets, defaultDockerImage, defaultDockerRegistryMirror, githubBaseURL) +func newRunnerPod(template corev1.Pod, runnerSpec v1alpha1.RunnerConfig, defaultRunnerImage string, defaultRunnerImagePullSecrets []string, defaultDockerImage, defaultDockerRegistryMirror string, githubBaseURL string, useRunnerStatusUpdateHookEphemeralRole bool) (corev1.Pod, error) { + return newRunnerPodWithContainerMode("", template, runnerSpec, defaultRunnerImage, defaultRunnerImagePullSecrets, defaultDockerImage, defaultDockerRegistryMirror, githubBaseURL, useRunnerStatusUpdateHookEphemeralRole) } func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error { diff --git a/controllers/runnerset_controller.go b/controllers/runnerset_controller.go index 85050a0..57703ac 100644 --- a/controllers/runnerset_controller.go +++ b/controllers/runnerset_controller.go @@ -45,12 +45,13 @@ type RunnerSetReconciler struct { Recorder record.EventRecorder Scheme *runtime.Scheme - CommonRunnerLabels []string - GitHubBaseURL string - RunnerImage string - RunnerImagePullSecrets []string - DockerImage string - DockerRegistryMirror string + CommonRunnerLabels []string + GitHubBaseURL string + RunnerImage string + RunnerImagePullSecrets []string + DockerImage string + DockerRegistryMirror string + UseRunnerStatusUpdateHook bool } // +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnersets,verbs=get;list;watch;create;update;patch;delete @@ -221,7 +222,7 @@ func (r *RunnerSetReconciler) newStatefulSet(runnerSet *v1alpha1.RunnerSet) (*ap template.ObjectMeta.Labels = CloneAndAddLabel(template.ObjectMeta.Labels, LabelKeyRunnerSetName, runnerSet.Name) - pod, err := newRunnerPodWithContainerMode(runnerSet.Spec.RunnerConfig.ContainerMode, template, runnerSet.Spec.RunnerConfig, r.RunnerImage, r.RunnerImagePullSecrets, r.DockerImage, r.DockerRegistryMirror, r.GitHubBaseURL) + pod, err := newRunnerPodWithContainerMode(runnerSet.Spec.RunnerConfig.ContainerMode, template, runnerSet.Spec.RunnerConfig, r.RunnerImage, r.RunnerImagePullSecrets, r.DockerImage, r.DockerRegistryMirror, r.GitHubBaseURL, r.UseRunnerStatusUpdateHook) if err != nil { return nil, err } diff --git a/main.go b/main.go index 8204824..ee95c90 100644 --- a/main.go +++ b/main.go @@ -68,11 +68,12 @@ func main() { err error ghClient *github.Client - metricsAddr string - enableLeaderElection bool - leaderElectionId string - port int - syncPeriod time.Duration + metricsAddr string + enableLeaderElection bool + runnerStatusUpdateHook bool + leaderElectionId string + port int + syncPeriod time.Duration gitHubAPICacheDuration time.Duration defaultScaleDownDelay time.Duration @@ -112,6 +113,7 @@ func main() { flag.StringVar(&c.BasicauthUsername, "github-basicauth-username", c.BasicauthUsername, "Username for GitHub basic auth to use instead of PAT or GitHub APP in case it's running behind a proxy API") flag.StringVar(&c.BasicauthPassword, "github-basicauth-password", c.BasicauthPassword, "Password for GitHub basic auth to use instead of PAT or GitHub APP in case it's running behind a proxy API") flag.StringVar(&c.RunnerGitHubURL, "runner-github-url", c.RunnerGitHubURL, "GitHub URL to be used by runners during registration") + flag.BoolVar(&runnerStatusUpdateHook, "runner-status-update-hook", false, "Use custom RBAC for runners (role, role binding and service account).") flag.DurationVar(&gitHubAPICacheDuration, "github-api-cache-duration", 0, "DEPRECATED: The duration until the GitHub API cache expires. Setting this to e.g. 10m results in the controller tries its best not to make the same API call within 10m to reduce the chance of being rate-limited. Defaults to mostly the same value as sync-period. If you're tweaking this in order to make autoscaling more responsive, you'll probably want to tweak sync-period, too") flag.DurationVar(&defaultScaleDownDelay, "default-scale-down-delay", controllers.DefaultScaleDownDelay, "The approximate delay for a scale down followed by a scale up, used to prevent flapping (down->up->down->... loop)") flag.IntVar(&port, "port", 9443, "The port to which the admission webhook endpoint should bind") @@ -148,12 +150,13 @@ func main() { } runnerReconciler := &controllers.RunnerReconciler{ - Client: mgr.GetClient(), - Log: log.WithName("runner"), - Scheme: mgr.GetScheme(), - GitHubClient: ghClient, - DockerImage: dockerImage, - DockerRegistryMirror: dockerRegistryMirror, + Client: mgr.GetClient(), + Log: log.WithName("runner"), + Scheme: mgr.GetScheme(), + GitHubClient: ghClient, + DockerImage: dockerImage, + DockerRegistryMirror: dockerRegistryMirror, + UseRunnerStatusUpdateHook: runnerStatusUpdateHook, // Defaults for self-hosted runner containers RunnerImage: runnerImage, RunnerImagePullSecrets: runnerImagePullSecrets, @@ -197,8 +200,9 @@ func main() { DockerRegistryMirror: dockerRegistryMirror, GitHubBaseURL: ghClient.GithubBaseURL, // Defaults for self-hosted runner containers - RunnerImage: runnerImage, - RunnerImagePullSecrets: runnerImagePullSecrets, + RunnerImage: runnerImage, + RunnerImagePullSecrets: runnerImagePullSecrets, + UseRunnerStatusUpdateHook: runnerStatusUpdateHook, } if err = runnerSetReconciler.SetupWithManager(mgr); err != nil { diff --git a/runner/actions-runner.dockerfile b/runner/actions-runner.dockerfile index 57699d8..71c228f 100644 --- a/runner/actions-runner.dockerfile +++ b/runner/actions-runner.dockerfile @@ -116,7 +116,10 @@ RUN mkdir /opt/hostedtoolcache \ # We place the scripts in `/usr/bin` so that users who extend this image can # override them with scripts of the same name placed in `/usr/local/bin`. -COPY entrypoint.sh logger.bash /usr/bin/ +COPY entrypoint.sh logger.bash update-status /usr/bin/ + +# Configure hooks folder structure. +COPY hooks /etc/arc/hooks/ ENV HOME=/home/runner # Add the Python "User Script Directory" to the PATH diff --git a/runner/entrypoint.sh b/runner/entrypoint.sh index 013d246..214ef23 100755 --- a/runner/entrypoint.sh +++ b/runner/entrypoint.sh @@ -4,6 +4,13 @@ source logger.bash RUNNER_ASSETS_DIR=${RUNNER_ASSETS_DIR:-/runnertmp} RUNNER_HOME=${RUNNER_HOME:-/runner} +# Let GitHub runner execute these hooks. These environment variables are used by GitHub's Runner as described here +# https://github.com/actions/runner/blob/main/docs/adrs/1751-runner-job-hooks.md +# Scripts referenced in the ACTIONS_RUNNER_HOOK_ environment variables must end in .sh or .ps1 +# for it to become a valid hook script, otherwise GitHub will fail to run the hook +export ACTIONS_RUNNER_HOOK_JOB_STARTED=/etc/arc/hooks/job-started.sh +export ACTIONS_RUNNER_HOOK_JOB_COMPLETED=/etc/arc/hooks/job-completed.sh + if [ ! -z "${STARTUP_DELAY_IN_SECONDS}" ]; then log.notice "Delaying startup by ${STARTUP_DELAY_IN_SECONDS} seconds" sleep ${STARTUP_DELAY_IN_SECONDS} @@ -77,6 +84,8 @@ if [ "${DISABLE_RUNNER_UPDATE:-}" == "true" ]; then log.debug 'Passing --disableupdate to config.sh to disable automatic runner updates.' fi +update-status "Registering" + retries_left=10 while [[ ${retries_left} -gt 0 ]]; do log.debug 'Configuring the runner.' @@ -155,4 +164,5 @@ unset RUNNER_NAME RUNNER_REPO RUNNER_TOKEN STARTUP_DELAY_IN_SECONDS DISABLE_WAIT if [ -z "${UNITTEST:-}" ]; then mapfile -t env '" + exit 64 +fi + +if [[ ${RUNNER_STATUS_UPDATE_HOOK:-false} == true ]]; then + + apiserver=https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS} + serviceaccount=/var/run/secrets/kubernetes.io/serviceaccount + namespace=$(cat ${serviceaccount}/namespace) + token=$(cat ${serviceaccount}/token) + phase=$1 + shift + + jq -n --arg phase "$phase" --arg message "${*:-}" '.status.phase = $phase | .status.message = $message' | curl \ + --cacert ${serviceaccount}/ca.crt \ + --data @- \ + --noproxy '*' \ + --header "Content-Type: application/merge-patch+json" \ + --header "Authorization: Bearer ${token}" \ + --show-error \ + --silent \ + --request PATCH \ + "${apiserver}/apis/actions.summerwind.dev/v1alpha1/namespaces/${namespace}/runners/${HOSTNAME}/status" + 1>&- +fi