diff --git a/api/v1alpha1/runner_types.go b/api/v1alpha1/runner_types.go index 23bcd74e..626f2af7 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 15483f2a..7d5a776e 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 d6571023..f2a14301 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 f35d6ff3..60e4e856 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 d5a41f2d..9101adb1 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 c805ba06..6137f62d 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 d6571023..f2a14301 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 bea27013..37c567d5 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 b65b4767..086912cb 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 69f0fdfa..5af012d1 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 85050a05..57703ac7 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 8204824b..ee95c908 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 57699d87..71c228f0 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 013d2468..214ef238 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