From a67bb87c722e79563d58eb04938f15b1fc55f5e5 Mon Sep 17 00:00:00 2001 From: Kacper Duras Date: Sat, 28 Feb 2026 03:21:51 +0100 Subject: [PATCH] feat: propagate custom annotations to listener pod --- .../tests/template_test.go | 37 +++++++++++++++++++ .../tests/values_template_metadata.yaml | 15 ++++++++ charts/gha-runner-scale-set/values.yaml | 11 ++++++ .../actions.github.com/resourcebuilder.go | 20 ++++++++-- .../resourcebuilder_test.go | 12 ++++++ 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 charts/gha-runner-scale-set/tests/values_template_metadata.yaml diff --git a/charts/gha-runner-scale-set/tests/template_test.go b/charts/gha-runner-scale-set/tests/template_test.go index 7a65747572..cfe2b4ee52 100644 --- a/charts/gha-runner-scale-set/tests/template_test.go +++ b/charts/gha-runner-scale-set/tests/template_test.go @@ -2797,3 +2797,40 @@ func TestAutoscalingRunnerSetCustomAnnotationsAndLabelsApplied(t *testing.T) { assert.NotEqual(t, "not-propagated", autoscalingRunnerSet.Annotations["actions.github.com/cleanup-manager-role-name"]) assert.NotEqual(t, "not-propagated", autoscalingRunnerSet.Labels["app.kubernetes.io/component"]) } + +func TestTemplateRenderedRunnerSetWithTemplateMetadata(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) + + releaseName := "test-runners" + namespaceName := "test-" + strings.ToLower(random.UniqueId()) + + options := &helm.Options{ + Logger: logger.Discard, + SetValues: map[string]string{ + "githubConfigUrl": "https://github.com/actions", + "githubConfigSecret.github_token": "gh_token12345", + "controllerServiceAccount.name": "arc", + "controllerServiceAccount.namespace": "arc-system", + "template.metadata.labels.my-label": "my-value", + }, + // Use SetStrValues (--set-string) to ensure boolean-like values are treated as strings + SetStrValues: map[string]string{ + "template.metadata.annotations.karpenter\\.sh/do-not-disrupt": "true", + }, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"}) + + var ars v1alpha1.AutoscalingRunnerSet + helm.UnmarshalK8SYaml(t, output, &ars) + + assert.Equal(t, "true", ars.Spec.Template.ObjectMeta.Annotations["karpenter.sh/do-not-disrupt"], + "karpenter do-not-disrupt annotation should be set on runner pod template") + assert.Equal(t, "my-value", ars.Spec.Template.ObjectMeta.Labels["my-label"], + "custom label should be set on runner pod template") +} diff --git a/charts/gha-runner-scale-set/tests/values_template_metadata.yaml b/charts/gha-runner-scale-set/tests/values_template_metadata.yaml new file mode 100644 index 0000000000..0097a84899 --- /dev/null +++ b/charts/gha-runner-scale-set/tests/values_template_metadata.yaml @@ -0,0 +1,15 @@ +githubConfigUrl: "https://github.com/actions" +githubConfigSecret: + github_token: "gh_token12345" + +template: + metadata: + annotations: + karpenter.sh/do-not-disrupt: "true" + labels: + my-label: "my-value" + spec: + containers: + - name: runner + image: ghcr.io/actions/actions-runner:latest + command: ["/home/runner/run.sh"] diff --git a/charts/gha-runner-scale-set/values.yaml b/charts/gha-runner-scale-set/values.yaml index 8a9b64e997..81288b4a75 100644 --- a/charts/gha-runner-scale-set/values.yaml +++ b/charts/gha-runner-scale-set/values.yaml @@ -303,6 +303,17 @@ githubConfigSecret: ## template is the PodSpec for each runner Pod ## For reference: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec template: + ## template.metadata allows setting custom labels and annotations on runner pods. + ## Useful for integrations with tools like Karpenter, cost attribution, Dependabot, etc. + ## Labels and annotations set here will be propagated to each EphemeralRunner pod. + ## + ## Example - prevent Karpenter from evicting runner pods mid-job: + # metadata: + # annotations: + # karpenter.sh/do-not-disrupt: "true" + # labels: + # my-label: "my-value" + ## ## template.spec will be modified if you change the container mode ## with containerMode.type=dind, we will populate the template.spec with following pod spec ## template: diff --git a/controllers/actions.github.com/resourcebuilder.go b/controllers/actions.github.com/resourcebuilder.go index dee3f4837f..0642ca384c 100644 --- a/controllers/actions.github.com/resourcebuilder.go +++ b/controllers/actions.github.com/resourcebuilder.go @@ -108,6 +108,12 @@ func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1. annotationKeyRunnerSpecHash: autoscalingRunnerSet.ListenerSpecHash(), annotationKeyValuesHash: autoscalingRunnerSet.Annotations[annotationKeyValuesHash], } + // Propagate custom annotations from AutoscalingRunnerSet, skipping reserved ones + for k, v := range autoscalingRunnerSet.Annotations { + if !strings.HasPrefix(k, "actions.github.com/") { + annotations[k] = v + } + } if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil { return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err) @@ -283,15 +289,23 @@ func (b *ResourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.A labels := make(map[string]string, len(autoscalingListener.Labels)) maps.Copy(labels, autoscalingListener.Labels) + annotations := make(map[string]string) + for k, v := range autoscalingListener.Annotations { + if !strings.HasPrefix(k, "actions.github.com/") { + annotations[k] = v + } + } + newRunnerScaleSetListenerPod := &corev1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: autoscalingListener.Name, - Namespace: autoscalingListener.Namespace, - Labels: labels, + Name: autoscalingListener.Name, + Namespace: autoscalingListener.Namespace, + Labels: labels, + Annotations: annotations, OwnerReferences: []metav1.OwnerReference{ { APIVersion: autoscalingListener.GetObjectKind().GroupVersionKind().GroupVersion().String(), diff --git a/controllers/actions.github.com/resourcebuilder_test.go b/controllers/actions.github.com/resourcebuilder_test.go index 24af4ca57e..3157098904 100644 --- a/controllers/actions.github.com/resourcebuilder_test.go +++ b/controllers/actions.github.com/resourcebuilder_test.go @@ -30,6 +30,8 @@ func TestLabelPropagation(t *testing.T) { runnerScaleSetIDAnnotationKey: "1", AnnotationKeyGitHubRunnerGroupName: "test-group", AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set", + "karpenter.sh/do-not-disrupt": "true", + "my-company.io/team": "platform", }, }, Spec: v1alpha1.AutoscalingRunnerSetSpec{ @@ -76,6 +78,13 @@ func TestLabelPropagation(t *testing.T) { assert.NotContains(t, listener.Labels, "directly.excluded.org/label") assert.Equal(t, "not-excluded-value", listener.Labels["directly.excluded.org/arbitrary"]) + // Custom annotations should be propagated to the listener + assert.Equal(t, "true", listener.Annotations["karpenter.sh/do-not-disrupt"]) + assert.Equal(t, "platform", listener.Annotations["my-company.io/team"]) + // Reserved actions.github.com/ annotations must not be propagated + assert.NotContains(t, listener.Annotations, AnnotationKeyGitHubRunnerGroupName) + assert.NotContains(t, listener.Annotations, AnnotationKeyGitHubRunnerScaleSetName) + listenerServiceAccount := &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: "test", @@ -84,6 +93,9 @@ func TestLabelPropagation(t *testing.T) { listenerPod, err := b.newScaleSetListenerPod(listener, &corev1.Secret{}, listenerServiceAccount, nil) require.NoError(t, err) assert.Equal(t, listenerPod.Labels, listener.Labels) + // Custom annotations must also reach the listener pod itself + assert.Equal(t, "true", listenerPod.Annotations["karpenter.sh/do-not-disrupt"]) + assert.Equal(t, "platform", listenerPod.Annotations["my-company.io/team"]) ephemeralRunner := b.newEphemeralRunner(ephemeralRunnerSet) require.NoError(t, err)