Compare commits

..
Author SHA1 Message Date
Nikola Jokic c46200c06a test(gha-runner-scale-set): fix emptyDir tests to use ValuesFiles
(gha) Validate Helm Charts / Lint Chart (push) Has been cancelled
(gha) Validate Helm Charts / Test Chart (push) Has been cancelled
Replace SetValues with ValuesFiles for tests that use emptyDir: {}
to avoid Helm CLI's --set limitation that misparses object syntax.

- Create values_k8s_novolume_custom_volumes.yaml
- Create values_k8s_novolume_custom_volume_mounts.yaml
- Update WithCustomVolumes and WithCustomVolumeMounts tests

Fixes test failures introduced in previous commit.
2026-04-20 18:53:01 +02:00
Nikola Jokic f7c50cc9b2 Render empty arrays for kubernetes-novolume volumes fields
Fixes #4411
2026-04-20 18:43:41 +02:00
16 changed files with 150 additions and 120 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ endif
DOCKER_USER ?= $(shell echo ${DOCKER_IMAGE_NAME} | cut -d / -f1)
VERSION ?= dev
COMMIT_SHA = $(shell git rev-parse HEAD)
RUNNER_VERSION ?= 2.334.0
RUNNER_VERSION ?= 2.333.1
TARGETPLATFORM ?= $(shell arch)
RUNNER_NAME ?= ${DOCKER_USER}/actions-runner
RUNNER_TAG ?= ${VERSION}
@@ -84,9 +84,6 @@ spec:
- "--listener-metrics-endpoint="
- "--metrics-addr=0"
{{- end }}
{{- if .Values.pprof.addr }}
- "--pprof-addr={{ .Values.pprof.addr }}"
{{- end }}
{{- range .Values.flags.excludeLabelPropagationPrefixes }}
- "--exclude-label-propagation-prefix={{ . }}"
{{- end }}
@@ -96,26 +93,14 @@ spec:
{{- with .Values.flags.k8sClientRateLimiterBurst }}
- "--k8s-client-rate-limiter-burst={{ . }}"
{{- end }}
{{- with .Values.flags.rateLimiter }}
{{- with .name }}
- "--workqueue-rate-limiter={{ . }}"
{{- end }}
{{- end }}
command:
- "/manager"
{{- if or .Values.metrics .Values.pprof.addr }}
ports:
{{- end }}
{{- with .Values.metrics }}
- containerPort: {{ required "Values.metrics.controllerManagerAddr must end with a numeric port" (regexFind "[0-9]+$" .controllerManagerAddr) }}
ports:
- containerPort: {{regexReplaceAll ":([0-9]+)" .controllerManagerAddr "${1}"}}
protocol: TCP
name: metrics
{{- end }}
{{- if .Values.pprof.addr }}
- containerPort: {{ required "Values.pprof.addr must end with a numeric port" (regexFind "[0-9]+$" .Values.pprof.addr) }}
protocol: TCP
name: pprof
{{- end }}
env:
- name: CONTROLLER_MANAGER_CONTAINER_IMAGE
value: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
@@ -94,10 +94,6 @@ priorityClassName: ""
# listenerAddr: ":8080"
# listenerEndpoint: "/metrics"
## To enable pprof, uncomment the addr field below.
pprof: {}
# addr: ":6060"
flags:
## Log level can be set here with one of the following values: "debug", "info", "warn", "error".
## Defaults to "debug".
@@ -140,13 +136,6 @@ flags:
# excludeLabelPropagationPrefixes:
# - "argocd.argoproj.io/instance"
## Workqueue rate limiter configuration.
## By default, controller-runtime uses a combined rate limiter with both a per-item
## exponential backoff and an overall token bucket (10 QPS, 100 bucket size).
## Valid names: "bucket_rate_limiter" (default), "typed_rate_limiter" (per-item only, no global token bucket).
# rateLimiter:
# name: "bucket_rate_limiter"
# Overrides the default `.Release.Namespace` for all resources in this chart.
namespaceOverride: ""
@@ -468,6 +468,7 @@ env:
{{- if $tlsConfig.runnerMountPath }}
{{- $mountGitHubServerTLS = 1 }}
{{- end }}
{{- if or $container.volumeMounts $mountGitHubServerTLS }}
volumeMounts:
{{- with $container.volumeMounts }}
{{- range $i, $volMount := . }}
@@ -482,6 +483,9 @@ volumeMounts:
mountPath: {{ clean (print $tlsConfig.runnerMountPath "/" $tlsConfig.certificateFrom.configMapKeyRef.key) }}
subPath: {{ $tlsConfig.certificateFrom.configMapKeyRef.key }}
{{- end }}
{{- else }}
volumeMounts: []
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -269,7 +269,7 @@ spec:
{{- include "gha-runner-scale-set.default-mode-runner-containers" . | nindent 6 }}
{{- end }}
{{- $tlsConfig := (default (dict) .Values.githubServerTLS) }}
{{- if or .Values.template.spec.volumes (eq $containerMode.type "dind") (eq $containerMode.type "kubernetes") (eq $containerMode.type "kubernetes-novolume") $tlsConfig.runnerMountPath }}
{{- if or .Values.template.spec.volumes (eq $containerMode.type "dind") (eq $containerMode.type "kubernetes") $tlsConfig.runnerMountPath }}
volumes:
{{- if $tlsConfig.runnerMountPath }}
{{- include "gha-runner-scale-set.tls-volume" $tlsConfig | nindent 6 }}
@@ -286,4 +286,6 @@ spec:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- end }}
{{- else if eq $containerMode.type "kubernetes-novolume" }}
volumes: []
{{- end }}
@@ -1154,6 +1154,75 @@ func TestTemplateRenderedAutoScalingRunnerSet_EnableKubernetesModeNoVolume(t *te
assert.Equal(t, ars.Spec.Template.Spec.Containers[0].Image, ars.Spec.Template.Spec.Containers[0].Env[3].Value)
assert.Len(t, ars.Spec.Template.Spec.Volumes, 0, "Template.Spec should have 0 volumes")
assert.NotNil(t, ars.Spec.Template.Spec.Volumes, "Template.Spec.Volumes should be non-nil empty slice, not null")
// Regression check: ensure volumeMounts is also non-nil empty slice for kubernetes-novolume
runnerContainer := ars.Spec.Template.Spec.Containers[0]
assert.NotNil(t, runnerContainer.VolumeMounts, "runner container VolumeMounts should be non-nil empty slice, not null")
assert.Len(t, runnerContainer.VolumeMounts, 0, "runner container should have 0 volumeMounts in kubernetes-novolume mode")
}
func TestTemplateRenderedAutoScalingRunnerSet_EnableKubernetesModeNoVolume_WithCustomVolumes(t *testing.T) {
t.Parallel()
// Path to the helm chart we will test
helmChartPath, err := filepath.Abs("../../gha-runner-scale-set")
require.NoError(t, err)
testValuesPath, err := filepath.Abs("../tests/values_k8s_novolume_custom_volumes.yaml")
require.NoError(t, err)
releaseName := "test-runners"
namespaceName := "test-" + strings.ToLower(random.UniqueId())
// Test that user-provided volumes are preserved even in kubernetes-novolume mode
options := &helm.Options{
Logger: logger.Discard,
ValuesFiles: []string{testValuesPath},
KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
}
output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"})
var ars v1alpha1.AutoscalingRunnerSet
helm.UnmarshalK8SYaml(t, output, &ars)
// Override preservation: user-provided volume should be present, not replaced by empty array
assert.Len(t, ars.Spec.Template.Spec.Volumes, 1, "Template.Spec should have 1 volume (user-provided override)")
assert.Equal(t, "custom-volume", ars.Spec.Template.Spec.Volumes[0].Name, "Volume name should be preserved as custom-volume")
assert.NotNil(t, ars.Spec.Template.Spec.Volumes[0].EmptyDir, "Volume should have EmptyDir configured")
}
func TestTemplateRenderedAutoScalingRunnerSet_EnableKubernetesModeNoVolume_WithCustomVolumeMounts(t *testing.T) {
t.Parallel()
// Path to the helm chart we will test
helmChartPath, err := filepath.Abs("../../gha-runner-scale-set")
require.NoError(t, err)
testValuesPath, err := filepath.Abs("../tests/values_k8s_novolume_custom_volume_mounts.yaml")
require.NoError(t, err)
releaseName := "test-runners"
namespaceName := "test-" + strings.ToLower(random.UniqueId())
// Test that user-provided volumeMounts are preserved in kubernetes-novolume runner container
options := &helm.Options{
Logger: logger.Discard,
ValuesFiles: []string{testValuesPath},
KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
}
output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"})
var ars v1alpha1.AutoscalingRunnerSet
helm.UnmarshalK8SYaml(t, output, &ars)
runnerContainer := ars.Spec.Template.Spec.Containers[0]
// Override preservation: user-provided volumeMounts should be present, not replaced by empty array
assert.Len(t, runnerContainer.VolumeMounts, 1, "runner container should have 1 volumeMount (user-provided override)")
assert.Equal(t, "custom-volume", runnerContainer.VolumeMounts[0].Name, "VolumeMount name should be preserved as custom-volume")
assert.Equal(t, "/mnt/custom", runnerContainer.VolumeMounts[0].MountPath, "VolumeMount path should be preserved as /mnt/custom")
}
func TestTemplateRenderedAutoscalingRunnerSet_ListenerPodTemplate(t *testing.T) {
@@ -0,0 +1,18 @@
githubConfigUrl: https://github.com/actions
githubConfigSecret:
github_token: gh_token12345
containerMode:
type: kubernetes-novolume
controllerServiceAccount:
name: arc
namespace: arc-system
template:
spec:
volumes:
- name: custom-volume
emptyDir: {}
containers:
- name: runner
volumeMounts:
- name: custom-volume
mountPath: /mnt/custom
@@ -0,0 +1,13 @@
githubConfigUrl: https://github.com/actions
githubConfigSecret:
github_token: gh_token12345
containerMode:
type: kubernetes-novolume
controllerServiceAccount:
name: arc
namespace: arc-system
template:
spec:
volumes:
- name: custom-volume
emptyDir: {}
@@ -692,7 +692,7 @@ func (r *AutoscalingListenerReconciler) publishRunningListener(autoscalingListen
}
// SetupWithManager sets up the controller with the Manager.
func (r *AutoscalingListenerReconciler) SetupWithManager(mgr ctrl.Manager, opts ...Option) error {
func (r *AutoscalingListenerReconciler) SetupWithManager(mgr ctrl.Manager) error {
labelBasedWatchFunc := func(_ context.Context, obj client.Object) []reconcile.Request {
var requests []reconcile.Request
labels := obj.GetLabels()
@@ -716,16 +716,14 @@ func (r *AutoscalingListenerReconciler) SetupWithManager(mgr ctrl.Manager, opts
return requests
}
return builderWithOptions(
ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.AutoscalingListener{}).
Owns(&corev1.Pod{}).
Owns(&corev1.ServiceAccount{}).
Watches(&rbacv1.Role{}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
Watches(&rbacv1.RoleBinding{}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}),
opts,
).Complete(r)
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.AutoscalingListener{}).
Owns(&corev1.Pod{}).
Owns(&corev1.ServiceAccount{}).
Watches(&rbacv1.Role{}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
Watches(&rbacv1.RoleBinding{}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
Complete(r)
}
func listenerContainerStatus(pod *corev1.Pod) *corev1.ContainerStatus {
@@ -762,27 +762,25 @@ func (r *AutoscalingRunnerSetReconciler) listEphemeralRunnerSets(ctx context.Con
}
// SetupWithManager sets up the controller with the Manager.
func (r *AutoscalingRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager, opts ...Option) error {
return builderWithOptions(
ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.AutoscalingRunnerSet{}).
Owns(&v1alpha1.EphemeralRunnerSet{}).
Watches(&v1alpha1.AutoscalingListener{}, handler.EnqueueRequestsFromMapFunc(
func(_ context.Context, o client.Object) []reconcile.Request {
autoscalingListener := o.(*v1alpha1.AutoscalingListener)
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
Name: autoscalingListener.Spec.AutoscalingRunnerSetName,
},
func (r *AutoscalingRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.AutoscalingRunnerSet{}).
Owns(&v1alpha1.EphemeralRunnerSet{}).
Watches(&v1alpha1.AutoscalingListener{}, handler.EnqueueRequestsFromMapFunc(
func(_ context.Context, o client.Object) []reconcile.Request {
autoscalingListener := o.(*v1alpha1.AutoscalingListener)
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
Name: autoscalingListener.Spec.AutoscalingRunnerSetName,
},
}
},
)).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}),
opts,
).Complete(r)
},
}
},
)).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
Complete(r)
}
type autoscalingRunnerSetFinalizerDependencyCleaner struct {
@@ -522,14 +522,12 @@ func (r *EphemeralRunnerSetReconciler) deleteEphemeralRunnerWithActionsClient(ct
}
// SetupWithManager sets up the controller with the Manager.
func (r *EphemeralRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager, opts ...Option) error {
return builderWithOptions(
ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.EphemeralRunnerSet{}).
Owns(&v1alpha1.EphemeralRunner{}).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}),
opts,
).Complete(r)
func (r *EphemeralRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.EphemeralRunnerSet{}).
Owns(&v1alpha1.EphemeralRunner{}).
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
Complete(r)
}
type ephemeralRunnerStepper struct {
-21
View File
@@ -1,10 +1,8 @@
package actionsgithubcom
import (
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// Options is the optional configuration for the controllers, which can be
@@ -39,25 +37,6 @@ func WithMaxConcurrentReconciles(n int) Option {
}
}
// WithTypedRateLimiter sets the rate limiter for the controller's workqueue.
//
// By default, the controller-runtime uses
// workqueue.DefaultTypedControllerRateLimiter[reconcile.Request], which combines
// an exponential backoff per-item limiter with a token bucket overall limiter
// (10 QPS, 100 bucket size). In large-scale environments with many runner
// scale sets, the token bucket limiter can become a bottleneck for
// reconciliation throughput.
//
// Use this option to override the default rate limiter, for example, to use
// workqueue.DefaultTypedItemBasedRateLimiter[reconcile.Request], which removes
// the overall token bucket constraint while keeping the per-item exponential
// backoff.
func WithTypedRateLimiter(rateLimiter workqueue.TypedRateLimiter[reconcile.Request]) Option {
return func(b *controller.Options) {
b.RateLimiter = rateLimiter
}
}
// builderWithOptions applies the given options to the provided builder, if any.
// This is a helper function to avoid the need to import the controller-runtime package in every reconciler source file
// and the command package that creates the controller.
+4 -27
View File
@@ -39,12 +39,10 @@ import (
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/webhook"
// +kubebuilder:scaffold:imports
)
@@ -85,7 +83,6 @@ func main() {
listenerMetricsEndpoint string
metricsAddr string
pprofAddr string
autoScalingRunnerSetOnly bool
enableLeaderElection bool
disableAdmissionWebhook bool
@@ -113,8 +110,6 @@ func main() {
k8sClientRateLimiterQPS int
k8sClientRateLimiterBurst int
workqueueRateLimiter string
)
var c github.Config
err = envconfig.Process("github", &c)
@@ -126,7 +121,6 @@ func main() {
flag.StringVar(&listenerMetricsAddr, "listener-metrics-addr", ":8080", "The address applied to AutoscalingListener metrics server")
flag.StringVar(&listenerMetricsEndpoint, "listener-metrics-endpoint", "/metrics", "The AutoscalingListener metrics server endpoint from which the metrics are collected")
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&pprofAddr, "pprof-addr", "", "The address the pprof endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
flag.StringVar(&leaderElectionID, "leader-election-id", "actions-runner-controller", "Controller id for leader election.")
@@ -161,7 +155,6 @@ func main() {
flag.Var(&autoScalerImagePullSecrets, "auto-scaler-image-pull-secrets", "The default image-pull secret name for auto-scaler listener container.")
flag.IntVar(&k8sClientRateLimiterQPS, "k8s-client-rate-limiter-qps", 20, "The QPS value of the K8s client rate limiter.")
flag.IntVar(&k8sClientRateLimiterBurst, "k8s-client-rate-limiter-burst", 30, "The burst value of the K8s client rate limiter.")
flag.StringVar(&workqueueRateLimiter, "workqueue-rate-limiter", "", `The workqueue rate limiter to use. Valid values are "bucket_rate_limiter" (default) and "typed_rate_limiter" (per-item only, no global token bucket).`)
flag.Parse()
runnerPodDefaults.RunnerImagePullSecrets = runnerImagePullSecrets
@@ -246,7 +239,6 @@ func main() {
SyncPeriod: &syncPeriod,
DefaultNamespaces: defaultNamespaces,
},
PprofBindAddress: pprofAddr,
WebhookServer: webhookServer,
LeaderElection: enableLeaderElection,
LeaderElectionID: leaderElectionID,
@@ -301,20 +293,6 @@ func main() {
log.Info("Resource builder initializing")
var controllerOpts []actionsgithubcom.Option
switch workqueueRateLimiter {
case "typed_rate_limiter":
log.Info("Using typed rate limiter (per-item only, no global token bucket)")
controllerOpts = append(controllerOpts,
actionsgithubcom.WithTypedRateLimiter(workqueue.DefaultTypedItemBasedRateLimiter[reconcile.Request]()),
)
case "bucket_rate_limiter", "":
log.Info("Using default bucket rate limiter")
default:
log.Error(fmt.Errorf("unknown workqueue rate limiter: %s", workqueueRateLimiter), "invalid --workqueue-rate-limiter value")
os.Exit(1)
}
if err = (&actionsgithubcom.AutoscalingRunnerSetReconciler{
Client: mgr.GetClient(),
Log: log.WithName("AutoscalingRunnerSet").WithValues("version", build.Version),
@@ -324,18 +302,17 @@ func main() {
UpdateStrategy: actionsgithubcom.UpdateStrategy(updateStrategy),
DefaultRunnerScaleSetListenerImagePullSecrets: autoScalerImagePullSecrets,
ResourceBuilder: rb,
}).SetupWithManager(mgr, controllerOpts...); err != nil {
}).SetupWithManager(mgr); err != nil {
log.Error(err, "unable to create controller", "controller", "AutoscalingRunnerSet")
os.Exit(1)
}
runnerOpts := append(controllerOpts, actionsgithubcom.WithMaxConcurrentReconciles(opts.RunnerMaxConcurrentReconciles))
if err = (&actionsgithubcom.EphemeralRunnerReconciler{
Client: mgr.GetClient(),
Log: log.WithName("EphemeralRunner").WithValues("version", build.Version),
Scheme: mgr.GetScheme(),
ResourceBuilder: rb,
}).SetupWithManager(mgr, runnerOpts...); err != nil {
}).SetupWithManager(mgr, actionsgithubcom.WithMaxConcurrentReconciles(opts.RunnerMaxConcurrentReconciles)); err != nil {
log.Error(err, "unable to create controller", "controller", "EphemeralRunner")
os.Exit(1)
}
@@ -346,7 +323,7 @@ func main() {
Scheme: mgr.GetScheme(),
PublishMetrics: metricsAddr != "0",
ResourceBuilder: rb,
}).SetupWithManager(mgr, controllerOpts...); err != nil {
}).SetupWithManager(mgr); err != nil {
log.Error(err, "unable to create controller", "controller", "EphemeralRunnerSet")
os.Exit(1)
}
@@ -358,7 +335,7 @@ func main() {
ListenerMetricsAddr: listenerMetricsAddr,
ListenerMetricsEndpoint: listenerMetricsEndpoint,
ResourceBuilder: rb,
}).SetupWithManager(mgr, controllerOpts...); err != nil {
}).SetupWithManager(mgr); err != nil {
log.Error(err, "unable to create controller", "controller", "AutoscalingListener")
os.Exit(1)
}
+1 -1
View File
@@ -6,7 +6,7 @@ DIND_ROOTLESS_RUNNER_NAME ?= ${DOCKER_USER}/actions-runner-dind-rootless
OS_IMAGE ?= ubuntu-22.04
TARGETPLATFORM ?= $(shell arch)
RUNNER_VERSION ?= 2.334.0
RUNNER_VERSION ?= 2.333.1
RUNNER_CONTAINER_HOOKS_VERSION ?= 0.8.1
DOCKER_VERSION ?= 28.0.4
+1 -1
View File
@@ -1,2 +1,2 @@
RUNNER_VERSION=2.334.0
RUNNER_VERSION=2.333.1
RUNNER_CONTAINER_HOOKS_VERSION=0.8.1
+1 -1
View File
@@ -36,7 +36,7 @@ var (
testResultCMNamePrefix = "test-result-"
RunnerVersion = "2.334.0"
RunnerVersion = "2.333.1"
RunnerContainerHooksVersion = "0.8.1"
)