From e37719f0c3b79db00b6b9be3b5a561c2f55c3c5b Mon Sep 17 00:00:00 2001 From: Jonny Rimek Date: Wed, 21 Jan 2026 11:57:38 +0100 Subject: [PATCH] Allow adding annotation to listener service account --- .../v1alpha1/autoscalinglistener_types.go | 12 +++++ .../v1alpha1/autoscalingrunnerset_types.go | 3 ++ .../v1alpha1/zz_generated.deepcopy.go | 39 ++++++++++++++ ...tions.github.com_autoscalinglisteners.yaml | 12 +++++ ...ions.github.com_autoscalingrunnersets.yaml | 12 +++++ ...tions.github.com_autoscalinglisteners.yaml | 12 +++++ ...ions.github.com_autoscalingrunnersets.yaml | 12 +++++ config/rbac/role.yaml | 2 + .../autoscalinglistener_controller.go | 53 ++++++++++++++++++- .../actions.github.com/resourcebuilder.go | 28 ++++++++-- .../resourcebuilder_test.go | 39 ++++++++++++++ 11 files changed, 218 insertions(+), 6 deletions(-) diff --git a/apis/actions.github.com/v1alpha1/autoscalinglistener_types.go b/apis/actions.github.com/v1alpha1/autoscalinglistener_types.go index 3943c6f6..0d0b2feb 100644 --- a/apis/actions.github.com/v1alpha1/autoscalinglistener_types.go +++ b/apis/actions.github.com/v1alpha1/autoscalinglistener_types.go @@ -21,6 +21,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// ListenerServiceAccount defines metadata to apply to the listener service account. +type ListenerServiceAccount struct { + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + // AutoscalingListenerSpec defines the desired state of AutoscalingListener type AutoscalingListenerSpec struct { // Required @@ -69,6 +78,9 @@ type AutoscalingListenerSpec struct { // +optional Template *corev1.PodTemplateSpec `json:"template,omitempty"` + + // +optional + ListenerServiceAccount *ListenerServiceAccount `json:"listenerServiceAccount,omitempty"` } // AutoscalingListenerStatus defines the observed state of AutoscalingListener diff --git a/apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go b/apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go index ecb01b58..6ec9c47c 100644 --- a/apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go +++ b/apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go @@ -84,6 +84,9 @@ type AutoscalingRunnerSetSpec struct { // +optional ListenerTemplate *corev1.PodTemplateSpec `json:"listenerTemplate,omitempty"` + // +optional + ListenerServiceAccount *ListenerServiceAccount `json:"listenerServiceAccount,omitempty"` + // +optional // +kubebuilder:validation:Minimum:=0 MaxRunners *int `json:"maxRunners,omitempty"` diff --git a/apis/actions.github.com/v1alpha1/zz_generated.deepcopy.go b/apis/actions.github.com/v1alpha1/zz_generated.deepcopy.go index f50acc08..c26cc6c7 100644 --- a/apis/actions.github.com/v1alpha1/zz_generated.deepcopy.go +++ b/apis/actions.github.com/v1alpha1/zz_generated.deepcopy.go @@ -118,6 +118,11 @@ func (in *AutoscalingListenerSpec) DeepCopyInto(out *AutoscalingListenerSpec) { *out = new(v1.PodTemplateSpec) (*in).DeepCopyInto(*out) } + if in.ListenerServiceAccount != nil { + in, out := &in.ListenerServiceAccount, &out.ListenerServiceAccount + *out = new(ListenerServiceAccount) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingListenerSpec. @@ -145,6 +150,35 @@ func (in *AutoscalingListenerStatus) DeepCopy() *AutoscalingListenerStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ListenerServiceAccount) DeepCopyInto(out *ListenerServiceAccount) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ListenerServiceAccount. +func (in *ListenerServiceAccount) DeepCopy() *ListenerServiceAccount { + if in == nil { + return nil + } + out := new(ListenerServiceAccount) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AutoscalingRunnerSet) DeepCopyInto(out *AutoscalingRunnerSet) { *out = *in @@ -233,6 +267,11 @@ func (in *AutoscalingRunnerSetSpec) DeepCopyInto(out *AutoscalingRunnerSetSpec) *out = new(v1.PodTemplateSpec) (*in).DeepCopyInto(*out) } + if in.ListenerServiceAccount != nil { + in, out := &in.ListenerServiceAccount, &out.ListenerServiceAccount + *out = new(ListenerServiceAccount) + (*in).DeepCopyInto(*out) + } if in.MaxRunners != nil { in, out := &in.MaxRunners, &out.MaxRunners *out = new(int) diff --git a/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalinglisteners.yaml b/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalinglisteners.yaml index 47ece783..9f25b661 100644 --- a/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalinglisteners.yaml +++ b/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalinglisteners.yaml @@ -199,6 +199,18 @@ spec: runnerScaleSetId: description: Required type: integer + listenerServiceAccount: + description: ListenerServiceAccount defines metadata to apply to the listener service account. + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object template: description: PodTemplateSpec describes the data a pod should have when created from a template diff --git a/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalingrunnersets.yaml b/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalingrunnersets.yaml index 55b44f1e..df2ecf02 100644 --- a/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalingrunnersets.yaml +++ b/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalingrunnersets.yaml @@ -8199,6 +8199,18 @@ spec: - containers type: object type: object + listenerServiceAccount: + description: ListenerServiceAccount defines metadata to apply to the listener service account. + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object maxRunners: minimum: 0 type: integer diff --git a/config/crd/bases/actions.github.com_autoscalinglisteners.yaml b/config/crd/bases/actions.github.com_autoscalinglisteners.yaml index 47ece783..9f25b661 100644 --- a/config/crd/bases/actions.github.com_autoscalinglisteners.yaml +++ b/config/crd/bases/actions.github.com_autoscalinglisteners.yaml @@ -199,6 +199,18 @@ spec: runnerScaleSetId: description: Required type: integer + listenerServiceAccount: + description: ListenerServiceAccount defines metadata to apply to the listener service account. + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object template: description: PodTemplateSpec describes the data a pod should have when created from a template diff --git a/config/crd/bases/actions.github.com_autoscalingrunnersets.yaml b/config/crd/bases/actions.github.com_autoscalingrunnersets.yaml index 55b44f1e..df2ecf02 100644 --- a/config/crd/bases/actions.github.com_autoscalingrunnersets.yaml +++ b/config/crd/bases/actions.github.com_autoscalingrunnersets.yaml @@ -8199,6 +8199,18 @@ spec: - containers type: object type: object + listenerServiceAccount: + description: ListenerServiceAccount defines metadata to apply to the listener service account. + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object maxRunners: minimum: 0 type: integer diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 2d41d854..e6f6b17a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -62,6 +62,8 @@ rules: - delete - get - list + - patch + - update - watch - apiGroups: - actions.github.com diff --git a/controllers/actions.github.com/autoscalinglistener_controller.go b/controllers/actions.github.com/autoscalinglistener_controller.go index 4c8ed34a..cec6bcf7 100644 --- a/controllers/actions.github.com/autoscalinglistener_controller.go +++ b/controllers/actions.github.com/autoscalinglistener_controller.go @@ -63,7 +63,7 @@ type AutoscalingListenerReconciler struct { // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=pods/status,verbs=get // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update -// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;watch;create +// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;delete;get;list;watch;update // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;delete;get;list;watch // +kubebuilder:rbac:groups=actions.github.com,resources=autoscalinglisteners,verbs=get;list;watch;create;update;patch;delete @@ -156,7 +156,14 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl. return r.createServiceAccountForListener(ctx, autoscalingListener, log) } - // TODO: make sure the service account is up to date + desiredServiceAccount := r.newScaleSetListenerServiceAccount(autoscalingListener) + if listenerServiceAccountNeedsUpdate(serviceAccount, desiredServiceAccount) { + log.Info("Updating listener service account", "namespace", serviceAccount.Namespace, "name", serviceAccount.Name) + if err := r.updateServiceAccountForListener(ctx, serviceAccount, desiredServiceAccount); err != nil { + log.Error(err, "Unable to update listener service account", "namespace", serviceAccount.Namespace, "name", serviceAccount.Name) + return ctrl.Result{}, err + } + } // Make sure the runner scale set listener role is created in the AutoscalingRunnerSet namespace listenerRole := new(rbacv1.Role) @@ -425,6 +432,48 @@ func (r *AutoscalingListenerReconciler) createServiceAccountForListener(ctx cont return ctrl.Result{}, nil } +func listenerServiceAccountNeedsUpdate(current *corev1.ServiceAccount, desired *corev1.ServiceAccount) bool { + if needsMetadataUpdate(current.Labels, desired.Labels) { + return true + } + + return needsMetadataUpdate(current.Annotations, desired.Annotations) +} + +func needsMetadataUpdate(current map[string]string, desired map[string]string) bool { + if len(desired) == 0 { + return false + } + + for key, value := range desired { + if current == nil || current[key] != value { + return true + } + } + + return false +} + +func (r *AutoscalingListenerReconciler) updateServiceAccountForListener(ctx context.Context, serviceAccount *corev1.ServiceAccount, desired *corev1.ServiceAccount) error { + return patch(ctx, r.Client, serviceAccount, func(obj *corev1.ServiceAccount) { + if obj.Labels == nil { + obj.Labels = map[string]string{} + } + for key, value := range desired.Labels { + obj.Labels[key] = value + } + + if len(desired.Annotations) > 0 { + if obj.Annotations == nil { + obj.Annotations = map[string]string{} + } + for key, value := range desired.Annotations { + obj.Annotations[key] = value + } + } + }) +} + func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, autoscalingListener *v1alpha1.AutoscalingListener, serviceAccount *corev1.ServiceAccount, appConfig *appconfig.AppConfig, logger logr.Logger) (ctrl.Result, error) { var envs []corev1.EnvVar if autoscalingListener.Spec.Proxy != nil { diff --git a/controllers/actions.github.com/resourcebuilder.go b/controllers/actions.github.com/resourcebuilder.go index 98b894a6..ea01df0c 100644 --- a/controllers/actions.github.com/resourcebuilder.go +++ b/controllers/actions.github.com/resourcebuilder.go @@ -136,6 +136,7 @@ func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1. GitHubServerTLS: autoscalingRunnerSet.Spec.GitHubServerTLS, Metrics: autoscalingRunnerSet.Spec.ListenerMetrics, Template: autoscalingRunnerSet.Spec.ListenerTemplate, + ListenerServiceAccount: autoscalingRunnerSet.Spec.ListenerServiceAccount, }, } @@ -425,14 +426,33 @@ func mergeListenerContainer(base, from *corev1.Container) { } func (b *ResourceBuilder) newScaleSetListenerServiceAccount(autoscalingListener *v1alpha1.AutoscalingListener) *corev1.ServiceAccount { + labels := b.mergeLabels(autoscalingListener.Labels, map[string]string{ + LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, + LabelKeyGitHubScaleSetName: autoscalingListener.Spec.AutoscalingRunnerSetName, + }) + + var annotations map[string]string + if autoscalingListener.Spec.ListenerServiceAccount != nil { + if len(autoscalingListener.Spec.ListenerServiceAccount.Labels) > 0 { + for k, v := range autoscalingListener.Spec.ListenerServiceAccount.Labels { + if _, ok := labels[k]; !ok { + labels[k] = v + } + } + } + + if len(autoscalingListener.Spec.ListenerServiceAccount.Annotations) > 0 { + annotations = make(map[string]string, len(autoscalingListener.Spec.ListenerServiceAccount.Annotations)) + maps.Copy(annotations, autoscalingListener.Spec.ListenerServiceAccount.Annotations) + } + } + return &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace, - Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{ - LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, - LabelKeyGitHubScaleSetName: autoscalingListener.Spec.AutoscalingRunnerSetName, - }), + Labels: labels, + Annotations: annotations, }, } } diff --git a/controllers/actions.github.com/resourcebuilder_test.go b/controllers/actions.github.com/resourcebuilder_test.go index b4c11466..652a88e2 100644 --- a/controllers/actions.github.com/resourcebuilder_test.go +++ b/controllers/actions.github.com/resourcebuilder_test.go @@ -109,6 +109,45 @@ func TestLabelPropagation(t *testing.T) { } } +func TestListenerServiceAccountMetadata(t *testing.T) { + autoscalingRunnerSet := v1alpha1.AutoscalingRunnerSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-scale-set", + Namespace: "test-ns", + Labels: map[string]string{ + LabelKeyKubernetesVersion: "0.2.0", + }, + Annotations: map[string]string{ + runnerScaleSetIDAnnotationKey: "1", + AnnotationKeyGitHubRunnerGroupName: "test-group", + AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set", + }, + }, + Spec: v1alpha1.AutoscalingRunnerSetSpec{ + GitHubConfigUrl: "https://github.com/org/repo", + ListenerServiceAccount: &v1alpha1.ListenerServiceAccount{ + Annotations: map[string]string{ + "example.com/annotation": "test-value", + }, + Labels: map[string]string{ + "example.com/label": "label-value", + }, + }, + }, + } + + var b ResourceBuilder + ephemeralRunnerSet, err := b.newEphemeralRunnerSet(&autoscalingRunnerSet) + require.NoError(t, err) + + listener, err := b.newAutoScalingListener(&autoscalingRunnerSet, ephemeralRunnerSet, autoscalingRunnerSet.Namespace, "test:latest", nil) + require.NoError(t, err) + + serviceAccount := b.newScaleSetListenerServiceAccount(listener) + assert.Equal(t, "test-value", serviceAccount.Annotations["example.com/annotation"]) + assert.Equal(t, "label-value", serviceAccount.Labels["example.com/label"]) +} + func TestGitHubURLTrimLabelValues(t *testing.T) { enterprise := strings.Repeat("a", 64) organization := strings.Repeat("b", 64)