Allow adding annotation to listener service account

This commit is contained in:
Jonny Rimek 2026-01-21 11:57:38 +01:00
parent 02aa70a64a
commit e37719f0c3
11 changed files with 218 additions and 6 deletions

View File

@ -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

View File

@ -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"`

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -62,6 +62,8 @@ rules:
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- actions.github.com

View File

@ -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 {

View File

@ -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,
},
}
}

View File

@ -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)