mirror of
https://github.com/actions-runner-controller/actions-runner-controller.git
synced 2026-09-30 22:28:17 +02:00
When the AutoscalingRunnerSet's runner spec changes, the EphemeralRunnerSet has to delete its idle and pending runners so they are rebuilt from the new spec. That was detected by hashing Spec.EphemeralRunnerSpec into the actions.github.com/integrity-hash annotation and comparing the annotation against a freshly computed hash. Replace it with a monotonic revision counter split across spec and status. The AutoscalingRunnerSet controller bumps Spec.ActionableRevision when it patches a new runner spec across; the EphemeralRunnerSet controller advances Status.AppliedActionableRevision only after the cleanup has actually succeeded. Because the applied marker lives in status and is written last, a controller that crashes part-way through the cleanup comes back with the applied revision still behind the spec revision and redoes the work, instead of skipping runners that are still running the old spec. The drift check uses apiequality.Semantic.DeepEqual rather than cmp.Equal or reflect.DeepEqual. Most PodSpec collection fields carry omitempty, so a template containing an explicitly empty value (`env: []`) is dropped when the EphemeralRunnerSet is written and reads back as nil. A strict comparison would report drift on every reconcile, bump the revision each time, and delete every idle and pending runner forever. helpers_drift_test.go pins that behaviour with a round-trip-through-JSON test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
552 lines
24 KiB
Go
552 lines
24 KiB
Go
package actionsgithubcom
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
|
"github.com/actions/scaleset"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
func TestMetadataPropagation(t *testing.T) {
|
|
autoscalingRunnerSet := v1alpha1.AutoscalingRunnerSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-scale-set",
|
|
Namespace: "test-ns",
|
|
Labels: map[string]string{
|
|
LabelKeyKubernetesPartOf: labelValueKubernetesPartOf,
|
|
LabelKeyKubernetesVersion: "0.2.0",
|
|
"arbitrary-label": "random-value",
|
|
"example.com/label": "example-value",
|
|
"example.com/example": "example-value",
|
|
"directly.excluded.org/label": "excluded-value",
|
|
"directly.excluded.org/arbitrary": "not-excluded-value",
|
|
},
|
|
Annotations: map[string]string{
|
|
runnerScaleSetIDAnnotationKey: "1",
|
|
AnnotationKeyGitHubRunnerGroupName: "test-group",
|
|
AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set",
|
|
},
|
|
},
|
|
Spec: v1alpha1.AutoscalingRunnerSetSpec{
|
|
GitHubConfigUrl: "https://github.com/org/repo",
|
|
AutoscalingListenerMetadata: &v1alpha1.ResourceMeta{
|
|
Labels: map[string]string{
|
|
"test.com/autoscaling-listener-label": "autoscaling-listener-label",
|
|
},
|
|
Annotations: map[string]string{
|
|
"test.com/autoscaling-listener-annotation": "autoscaling-listener-annotation",
|
|
},
|
|
},
|
|
ListenerServiceAccountMetadata: &v1alpha1.ResourceMeta{
|
|
Labels: map[string]string{
|
|
"test.com/listener-service-account-label": "listener-service-account-label",
|
|
},
|
|
Annotations: map[string]string{
|
|
"test.com/listener-service-account-annotation": "listener-service-account-annotation",
|
|
},
|
|
},
|
|
ListenerRoleMetadata: &v1alpha1.ResourceMeta{
|
|
Labels: map[string]string{
|
|
"test.com/listener-role-label": "listener-role-label",
|
|
},
|
|
Annotations: map[string]string{
|
|
"test.com/listener-role-annotation": "listener-role-annotation",
|
|
},
|
|
},
|
|
ListenerRoleBindingMetadata: &v1alpha1.ResourceMeta{
|
|
Labels: map[string]string{
|
|
"test.com/listener-role-binding-label": "listener-role-binding-label",
|
|
},
|
|
Annotations: map[string]string{
|
|
"test.com/listener-role-binding-annotation": "listener-role-binding-annotation",
|
|
},
|
|
},
|
|
ListenerConfigSecretMetadata: &v1alpha1.ResourceMeta{
|
|
Labels: map[string]string{
|
|
"test.com/listener-config-secret-label": "listener-config-secret-label",
|
|
},
|
|
Annotations: map[string]string{
|
|
"test.com/listener-config-secret-annotation": "listener-config-secret-annotation",
|
|
},
|
|
},
|
|
EphemeralRunnerSetMetadata: &v1alpha1.ResourceMeta{
|
|
Labels: map[string]string{
|
|
"test.com/ephemeral-runner-set-label": "ephemeral-runner-set-label",
|
|
},
|
|
Annotations: map[string]string{
|
|
"test.com/ephemeral-runner-set-annotation": "ephemeral-runner-set-annotation",
|
|
},
|
|
},
|
|
EphemeralRunnerMetadata: &v1alpha1.ResourceMeta{
|
|
Labels: map[string]string{
|
|
"test.com/ephemeral-runner-label": "ephemeral-runner-label",
|
|
},
|
|
Annotations: map[string]string{
|
|
"test.com/ephemeral-runner-annotation": "ephemeral-runner-annotation",
|
|
},
|
|
},
|
|
EphemeralRunnerConfigSecretMetadata: &v1alpha1.ResourceMeta{
|
|
Labels: map[string]string{
|
|
"test.com/ephemeral-runner-config-secret-label": "ephemeral-runner-config-secret-label",
|
|
},
|
|
Annotations: map[string]string{
|
|
"test.com/ephemeral-runner-config-secret-annotation": "ephemeral-runner-config-secret-annotation",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
cache := NewResourceCache()
|
|
b := ResourceBuilder{
|
|
ExcludeLabelPropagationPrefixes: []string{
|
|
"example.com/",
|
|
"directly.excluded.org/label",
|
|
},
|
|
ResourceCache: &cache,
|
|
}
|
|
ephemeralRunnerSet, err := b.newEphemeralRunnerSet(&autoscalingRunnerSet)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, labelValueKubernetesPartOf, ephemeralRunnerSet.Labels[LabelKeyKubernetesPartOf])
|
|
assert.Equal(t, "runner-set", ephemeralRunnerSet.Labels[LabelKeyKubernetesComponent])
|
|
assert.Equal(t, autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion], ephemeralRunnerSet.Labels[LabelKeyKubernetesVersion])
|
|
assert.NotContains(t, ephemeralRunnerSet.Annotations, annotationKeyIntegrityHash)
|
|
assert.Equal(t, autoscalingRunnerSet.Name, ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetName])
|
|
assert.Equal(t, autoscalingRunnerSet.Namespace, ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetNamespace])
|
|
assert.Equal(t, "", ephemeralRunnerSet.Labels[LabelKeyGitHubEnterprise])
|
|
assert.Equal(t, "org", ephemeralRunnerSet.Labels[LabelKeyGitHubOrganization])
|
|
assert.Equal(t, "repo", ephemeralRunnerSet.Labels[LabelKeyGitHubRepository])
|
|
assert.Equal(t, autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerGroupName], ephemeralRunnerSet.Annotations[AnnotationKeyGitHubRunnerGroupName])
|
|
assert.Equal(t, autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName], ephemeralRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName])
|
|
assert.Equal(t, autoscalingRunnerSet.Labels["arbitrary-label"], ephemeralRunnerSet.Labels["arbitrary-label"])
|
|
assert.Equal(t, "ephemeral-runner-set-label", ephemeralRunnerSet.Labels["test.com/ephemeral-runner-set-label"])
|
|
assert.Equal(t, "ephemeral-runner-set-annotation", ephemeralRunnerSet.Annotations["test.com/ephemeral-runner-set-annotation"])
|
|
|
|
listener, err := b.newAutoscalingListener(&autoscalingRunnerSet, ephemeralRunnerSet, autoscalingRunnerSet.Namespace, "test:latest", nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, labelValueKubernetesPartOf, listener.Labels[LabelKeyKubernetesPartOf])
|
|
assert.Equal(t, "runner-scale-set-listener", listener.Labels[LabelKeyKubernetesComponent])
|
|
assert.Equal(t, autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion], listener.Labels[LabelKeyKubernetesVersion])
|
|
assert.NotEmpty(t, listener.Annotations[annotationKeyIntegrityHash])
|
|
assert.Equal(t, autoscalingRunnerSet.Name, listener.Labels[LabelKeyGitHubScaleSetName])
|
|
assert.Equal(t, autoscalingRunnerSet.Namespace, listener.Labels[LabelKeyGitHubScaleSetNamespace])
|
|
assert.Equal(t, "", listener.Labels[LabelKeyGitHubEnterprise])
|
|
assert.Equal(t, "org", listener.Labels[LabelKeyGitHubOrganization])
|
|
assert.Equal(t, "repo", listener.Labels[LabelKeyGitHubRepository])
|
|
assert.Equal(t, autoscalingRunnerSet.Labels["arbitrary-label"], listener.Labels["arbitrary-label"])
|
|
assert.Equal(t, "autoscaling-listener-label", listener.Labels["test.com/autoscaling-listener-label"])
|
|
assert.Equal(t, "autoscaling-listener-annotation", listener.Annotations["test.com/autoscaling-listener-annotation"])
|
|
|
|
assert.NotContains(t, listener.Labels, "example.com/label")
|
|
assert.NotContains(t, listener.Labels, "example.com/example")
|
|
assert.NotContains(t, listener.Labels, "directly.excluded.org/label")
|
|
assert.Equal(t, "not-excluded-value", listener.Labels["directly.excluded.org/arbitrary"])
|
|
|
|
listenerServiceAccount, err := b.newScaleSetListenerServiceAccount(listener)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "listener-service-account-label", listenerServiceAccount.Labels["test.com/listener-service-account-label"])
|
|
assert.Equal(t, "listener-service-account-annotation", listenerServiceAccount.Annotations["test.com/listener-service-account-annotation"])
|
|
|
|
listenerRole := b.newScaleSetListenerRole(listener)
|
|
assert.Equal(t, "listener-role-label", listenerRole.Labels["test.com/listener-role-label"])
|
|
assert.Equal(t, "listener-role-annotation", listenerRole.Annotations["test.com/listener-role-annotation"])
|
|
|
|
listenerRoleBinding := b.newScaleSetListenerRoleBinding(listener, listenerRole, listenerServiceAccount)
|
|
assert.Equal(t, "listener-role-binding-label", listenerRoleBinding.Labels["test.com/listener-role-binding-label"])
|
|
assert.Equal(t, "listener-role-binding-annotation", listenerRoleBinding.Annotations["test.com/listener-role-binding-annotation"])
|
|
|
|
listenerPod, err := b.newScaleSetListenerPod(
|
|
listener,
|
|
&corev1.Secret{},
|
|
listenerServiceAccount,
|
|
listenerRole,
|
|
listenerRoleBinding,
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, listenerPod.Labels, listener.Labels)
|
|
|
|
ephemeralRunner, err := b.newEphemeralRunner(ephemeralRunnerSet)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, []string{ephemeralRunnerFinalizerName, ephemeralRunnerActionsFinalizerName}, ephemeralRunner.Finalizers)
|
|
|
|
for _, key := range commonLabelKeys {
|
|
if key == LabelKeyKubernetesComponent {
|
|
continue
|
|
}
|
|
assert.Equal(t, ephemeralRunnerSet.Labels[key], ephemeralRunner.Labels[key])
|
|
}
|
|
assert.Equal(t, "runner", ephemeralRunner.Labels[LabelKeyKubernetesComponent])
|
|
assert.Equal(t, autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerGroupName], ephemeralRunner.Annotations[AnnotationKeyGitHubRunnerGroupName])
|
|
assert.Equal(t, autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName], ephemeralRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName])
|
|
assert.Equal(t, "ephemeral-runner-label", ephemeralRunner.Labels["test.com/ephemeral-runner-label"])
|
|
assert.Equal(t, "ephemeral-runner-annotation", ephemeralRunner.Annotations["test.com/ephemeral-runner-annotation"])
|
|
|
|
runnerSecret, err := b.newEphemeralRunnerJitSecret(ephemeralRunner, &scaleset.RunnerScaleSetJitRunnerConfig{
|
|
Runner: &scaleset.RunnerReference{
|
|
ID: 1,
|
|
Name: "test",
|
|
RunnerScaleSetID: 1,
|
|
},
|
|
EncodedJITConfig: "",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "ephemeral-runner-config-secret-label", runnerSecret.Labels["test.com/ephemeral-runner-config-secret-label"])
|
|
assert.Equal(t, "ephemeral-runner-config-secret-annotation", runnerSecret.Annotations["test.com/ephemeral-runner-config-secret-annotation"])
|
|
|
|
pod, err := b.newEphemeralRunnerPod(ephemeralRunner, runnerSecret)
|
|
require.NoError(t, err)
|
|
for key := range ephemeralRunner.Labels {
|
|
assert.Equal(t, ephemeralRunner.Labels[key], pod.Labels[key])
|
|
}
|
|
}
|
|
|
|
func TestEphemeralRunnerSetProxySecretZIdentityHash(t *testing.T) {
|
|
ephemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-scale-set",
|
|
Namespace: "test-ns",
|
|
Labels: map[string]string{
|
|
LabelKeyGitHubScaleSetName: "test-scale-set",
|
|
LabelKeyGitHubScaleSetNamespace: "test-ns",
|
|
},
|
|
},
|
|
}
|
|
|
|
var b ResourceBuilder
|
|
proxySecret, err := b.newEphemeralRunnerSetProxySecret(ephemeralRunnerSet, map[string][]byte{
|
|
"http_proxy": []byte("http://proxy.example.com"),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
actualHash := proxySecret.Annotations[annotationKeyIntegrityHash]
|
|
assert.NotEmpty(t, actualHash)
|
|
assert.Equal(t, ephemeralRunnerSetProxySecretZIdentityHash(proxySecret), actualHash)
|
|
|
|
changedProxySecret := proxySecret.DeepCopy()
|
|
changedProxySecret.Data["http_proxy"] = []byte("http://updated-proxy.example.com")
|
|
assert.NotEqual(t, actualHash, ephemeralRunnerSetProxySecretZIdentityHash(changedProxySecret))
|
|
}
|
|
|
|
func TestGitHubURLTrimLabelValues(t *testing.T) {
|
|
enterprise := strings.Repeat("a", 64)
|
|
organization := strings.Repeat("b", 64)
|
|
repository := strings.Repeat("c", 64)
|
|
|
|
autoscalingRunnerSet := v1alpha1.AutoscalingRunnerSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-scale-set",
|
|
Namespace: "test-ns",
|
|
Labels: map[string]string{
|
|
LabelKeyKubernetesPartOf: labelValueKubernetesPartOf,
|
|
LabelKeyKubernetesVersion: "0.2.0",
|
|
},
|
|
Annotations: map[string]string{
|
|
runnerScaleSetIDAnnotationKey: "1",
|
|
AnnotationKeyGitHubRunnerGroupName: "test-group",
|
|
AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set",
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("org/repo", func(t *testing.T) {
|
|
autoscalingRunnerSet := autoscalingRunnerSet.DeepCopy()
|
|
autoscalingRunnerSet.Spec = v1alpha1.AutoscalingRunnerSetSpec{
|
|
GitHubConfigUrl: fmt.Sprintf("https://github.com/%s/%s", organization, repository),
|
|
}
|
|
|
|
cache := NewResourceCache()
|
|
b := ResourceBuilder{ResourceCache: &cache}
|
|
ephemeralRunnerSet, err := b.newEphemeralRunnerSet(autoscalingRunnerSet)
|
|
require.NoError(t, err)
|
|
assert.Len(t, ephemeralRunnerSet.Labels[LabelKeyGitHubEnterprise], 0)
|
|
assert.Len(t, ephemeralRunnerSet.Labels[LabelKeyGitHubOrganization], 63)
|
|
assert.Len(t, ephemeralRunnerSet.Labels[LabelKeyGitHubRepository], 63)
|
|
assert.True(t, strings.HasSuffix(ephemeralRunnerSet.Labels[LabelKeyGitHubOrganization], trimLabelVauleSuffix))
|
|
assert.True(t, strings.HasSuffix(ephemeralRunnerSet.Labels[LabelKeyGitHubRepository], trimLabelVauleSuffix))
|
|
|
|
listener, err := b.newAutoscalingListener(autoscalingRunnerSet, ephemeralRunnerSet, autoscalingRunnerSet.Namespace, "test:latest", nil)
|
|
require.NoError(t, err)
|
|
assert.Len(t, listener.Labels[LabelKeyGitHubEnterprise], 0)
|
|
assert.Len(t, listener.Labels[LabelKeyGitHubOrganization], 63)
|
|
assert.Len(t, listener.Labels[LabelKeyGitHubRepository], 63)
|
|
assert.True(t, strings.HasSuffix(ephemeralRunnerSet.Labels[LabelKeyGitHubOrganization], trimLabelVauleSuffix))
|
|
assert.True(t, strings.HasSuffix(ephemeralRunnerSet.Labels[LabelKeyGitHubRepository], trimLabelVauleSuffix))
|
|
})
|
|
|
|
t.Run("enterprise", func(t *testing.T) {
|
|
autoscalingRunnerSet := autoscalingRunnerSet.DeepCopy()
|
|
autoscalingRunnerSet.Spec = v1alpha1.AutoscalingRunnerSetSpec{
|
|
GitHubConfigUrl: fmt.Sprintf("https://github.com/enterprises/%s", enterprise),
|
|
}
|
|
|
|
cache := NewResourceCache()
|
|
b := ResourceBuilder{ResourceCache: &cache}
|
|
ephemeralRunnerSet, err := b.newEphemeralRunnerSet(autoscalingRunnerSet)
|
|
require.NoError(t, err)
|
|
assert.Len(t, ephemeralRunnerSet.Labels[LabelKeyGitHubEnterprise], 63)
|
|
assert.True(t, strings.HasSuffix(ephemeralRunnerSet.Labels[LabelKeyGitHubEnterprise], trimLabelVauleSuffix))
|
|
assert.Len(t, ephemeralRunnerSet.Labels[LabelKeyGitHubOrganization], 0)
|
|
assert.Len(t, ephemeralRunnerSet.Labels[LabelKeyGitHubRepository], 0)
|
|
|
|
listener, err := b.newAutoscalingListener(autoscalingRunnerSet, ephemeralRunnerSet, autoscalingRunnerSet.Namespace, "test:latest", nil)
|
|
require.NoError(t, err)
|
|
assert.Len(t, listener.Labels[LabelKeyGitHubEnterprise], 63)
|
|
assert.True(t, strings.HasSuffix(ephemeralRunnerSet.Labels[LabelKeyGitHubEnterprise], trimLabelVauleSuffix))
|
|
assert.Len(t, listener.Labels[LabelKeyGitHubOrganization], 0)
|
|
assert.Len(t, listener.Labels[LabelKeyGitHubRepository], 0)
|
|
})
|
|
}
|
|
|
|
func TestOwnershipRelationships(t *testing.T) {
|
|
// Create an AutoscalingRunnerSet
|
|
autoscalingRunnerSet := v1alpha1.AutoscalingRunnerSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-scale-set",
|
|
Namespace: "test-ns",
|
|
UID: "test-autoscaling-runner-set-uid",
|
|
Labels: map[string]string{
|
|
LabelKeyKubernetesPartOf: labelValueKubernetesPartOf,
|
|
LabelKeyKubernetesVersion: "0.2.0",
|
|
},
|
|
Annotations: map[string]string{
|
|
runnerScaleSetIDAnnotationKey: "1",
|
|
AnnotationKeyGitHubRunnerGroupName: "test-group",
|
|
AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set",
|
|
annotationKeyIntegrityHash: "test-hash",
|
|
},
|
|
},
|
|
Spec: v1alpha1.AutoscalingRunnerSetSpec{
|
|
GitHubConfigUrl: "https://github.com/org/repo",
|
|
},
|
|
}
|
|
|
|
// Initialize ResourceBuilder
|
|
cache := NewResourceCache()
|
|
b := ResourceBuilder{ResourceCache: &cache}
|
|
|
|
// Create EphemeralRunnerSet
|
|
ephemeralRunnerSet, err := b.newEphemeralRunnerSet(&autoscalingRunnerSet)
|
|
require.NoError(t, err)
|
|
|
|
// Create and test Listener Pod ownership
|
|
listener, err := b.newAutoscalingListener(&autoscalingRunnerSet, ephemeralRunnerSet, autoscalingRunnerSet.Namespace, "test:latest", nil)
|
|
require.NoError(t, err)
|
|
listener.UID = "test-listener-uid"
|
|
|
|
listenerServiceAccount, err := b.newScaleSetListenerServiceAccount(listener)
|
|
require.NoError(t, err)
|
|
listenerRole := b.newScaleSetListenerRole(listener)
|
|
listenerRoleBinding := b.newScaleSetListenerRoleBinding(listener, listenerRole, listenerServiceAccount)
|
|
|
|
listenerPod, err := b.newScaleSetListenerPod(
|
|
listener,
|
|
&corev1.Secret{},
|
|
listenerServiceAccount,
|
|
listenerRole,
|
|
listenerRoleBinding,
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, listenerPod.OwnerReferences, 1, "Listener Pod should have exactly one owner reference")
|
|
ownerRef := listenerPod.OwnerReferences[0]
|
|
assert.Equal(t, v1alpha1.GroupVersion.String(), ownerRef.APIVersion, "Owner reference APIVersion should match GroupVersion")
|
|
assert.Equal(t, "AutoscalingListener", ownerRef.Kind, "Owner reference Kind should be AutoscalingListener")
|
|
assert.Equal(t, listener.GetName(), ownerRef.Name, "Owner reference name should match AutoscalingListener name")
|
|
assert.Equal(t, listener.GetUID(), ownerRef.UID, "Owner reference UID should match AutoscalingListener UID")
|
|
assert.Equal(t, true, *ownerRef.Controller, "Controller flag should be true")
|
|
assert.Equal(t, true, *ownerRef.BlockOwnerDeletion, "BlockOwnerDeletion flag should be true")
|
|
|
|
// Test EphemeralRunnerSet ownership
|
|
require.Len(t, ephemeralRunnerSet.OwnerReferences, 1, "EphemeralRunnerSet should have exactly one owner reference")
|
|
ownerRef = ephemeralRunnerSet.OwnerReferences[0]
|
|
assert.Equal(t, v1alpha1.GroupVersion.String(), ownerRef.APIVersion, "Owner reference APIVersion should match GroupVersion")
|
|
assert.Equal(t, "AutoscalingRunnerSet", ownerRef.Kind, "Owner reference Kind should be AutoscalingRunnerSet")
|
|
assert.Equal(t, autoscalingRunnerSet.GetName(), ownerRef.Name, "Owner reference name should match AutoscalingRunnerSet name")
|
|
assert.Equal(t, autoscalingRunnerSet.GetUID(), ownerRef.UID, "Owner reference UID should match AutoscalingRunnerSet UID")
|
|
assert.Equal(t, true, *ownerRef.Controller, "Controller flag should be true")
|
|
assert.Equal(t, true, *ownerRef.BlockOwnerDeletion, "BlockOwnerDeletion flag should be true")
|
|
|
|
// Create EphemeralRunner
|
|
ephemeralRunner, err := b.newEphemeralRunner(ephemeralRunnerSet)
|
|
require.NoError(t, err)
|
|
|
|
// Test EphemeralRunner ownership
|
|
require.Len(t, ephemeralRunner.OwnerReferences, 1, "EphemeralRunner should have exactly one owner reference")
|
|
ownerRef = ephemeralRunner.OwnerReferences[0]
|
|
assert.Equal(t, v1alpha1.GroupVersion.String(), ownerRef.APIVersion, "Owner reference APIVersion should match GroupVersion")
|
|
assert.Equal(t, "EphemeralRunnerSet", ownerRef.Kind, "Owner reference Kind should be EphemeralRunnerSet")
|
|
assert.Equal(t, ephemeralRunnerSet.GetName(), ownerRef.Name, "Owner reference name should match EphemeralRunnerSet name")
|
|
assert.Equal(t, ephemeralRunnerSet.GetUID(), ownerRef.UID, "Owner reference UID should match EphemeralRunnerSet UID")
|
|
assert.Equal(t, true, *ownerRef.Controller, "Controller flag should be true")
|
|
assert.Equal(t, true, *ownerRef.BlockOwnerDeletion, "BlockOwnerDeletion flag should be true")
|
|
|
|
// Create EphemeralRunnerPod
|
|
runnerSecret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-secret",
|
|
},
|
|
}
|
|
pod, err := b.newEphemeralRunnerPod(ephemeralRunner, runnerSecret)
|
|
require.NoError(t, err)
|
|
|
|
// Test EphemeralRunnerPod ownership
|
|
require.Len(t, pod.OwnerReferences, 1, "EphemeralRunnerPod should have exactly one owner reference")
|
|
ownerRef = pod.OwnerReferences[0]
|
|
assert.Equal(t, v1alpha1.GroupVersion.String(), ownerRef.APIVersion, "Owner reference APIVersion should match GroupVersion")
|
|
assert.Equal(t, "EphemeralRunner", ownerRef.Kind, "Owner reference Kind should be EphemeralRunner")
|
|
assert.Equal(t, ephemeralRunner.GetName(), ownerRef.Name, "Owner reference name should match EphemeralRunner name")
|
|
assert.Equal(t, ephemeralRunner.GetUID(), ownerRef.UID, "Owner reference UID should match EphemeralRunner UID")
|
|
assert.Equal(t, true, *ownerRef.Controller, "Controller flag should be true")
|
|
assert.Equal(t, true, *ownerRef.BlockOwnerDeletion, "BlockOwnerDeletion flag should be true")
|
|
}
|
|
|
|
func TestListenerPodNodeSelector(t *testing.T) {
|
|
autoscalingRunnerSet := v1alpha1.AutoscalingRunnerSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-scale-set",
|
|
Namespace: "test-ns",
|
|
Labels: map[string]string{
|
|
LabelKeyKubernetesPartOf: labelValueKubernetesPartOf,
|
|
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",
|
|
},
|
|
}
|
|
|
|
cache := NewResourceCache()
|
|
b := ResourceBuilder{ResourceCache: &cache}
|
|
ephemeralRunnerSet, err := b.newEphemeralRunnerSet(&autoscalingRunnerSet)
|
|
require.NoError(t, err)
|
|
|
|
listener, err := b.newAutoscalingListener(&autoscalingRunnerSet, ephemeralRunnerSet, autoscalingRunnerSet.Namespace, "test:latest", nil)
|
|
require.NoError(t, err)
|
|
|
|
listenerServiceAccount, err := b.newScaleSetListenerServiceAccount(listener)
|
|
require.NoError(t, err)
|
|
listenerRole := b.newScaleSetListenerRole(listener)
|
|
listenerRoleBinding := b.newScaleSetListenerRoleBinding(listener, listenerRole, listenerServiceAccount)
|
|
|
|
t.Run("default listener pod has linux nodeSelector", func(t *testing.T) {
|
|
pod, err := b.newScaleSetListenerPod(
|
|
listener,
|
|
&corev1.Secret{},
|
|
listenerServiceAccount,
|
|
listenerRole,
|
|
listenerRoleBinding,
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, pod.Spec.NodeSelector)
|
|
assert.Equal(t, "linux", pod.Spec.NodeSelector[LabelKeyKubernetesOS],
|
|
"listener pod should default to linux nodeSelector")
|
|
})
|
|
|
|
t.Run("nil listenerTemplate preserves linux nodeSelector", func(t *testing.T) {
|
|
listenerNoTemplate := listener.DeepCopy()
|
|
listenerNoTemplate.Spec.Template = nil
|
|
|
|
pod, err := b.newScaleSetListenerPod(
|
|
listenerNoTemplate,
|
|
&corev1.Secret{},
|
|
listenerServiceAccount,
|
|
listenerRole,
|
|
listenerRoleBinding,
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, pod.Spec.NodeSelector)
|
|
assert.Equal(t, "linux", pod.Spec.NodeSelector[LabelKeyKubernetesOS],
|
|
"listener pod should keep linux nodeSelector when no template is provided")
|
|
})
|
|
|
|
t.Run("listenerTemplate with nil nodeSelector preserves linux default", func(t *testing.T) {
|
|
listenerWithTemplate := listener.DeepCopy()
|
|
listenerWithTemplate.Spec.Template = &corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
// NodeSelector intentionally nil
|
|
Tolerations: []corev1.Toleration{
|
|
{Key: "example.com/test", Operator: corev1.TolerationOpExists},
|
|
},
|
|
},
|
|
}
|
|
|
|
pod, err := b.newScaleSetListenerPod(
|
|
listenerWithTemplate,
|
|
&corev1.Secret{},
|
|
listenerServiceAccount,
|
|
listenerRole,
|
|
listenerRoleBinding,
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, pod.Spec.NodeSelector,
|
|
"linux nodeSelector should not be cleared by template with nil nodeSelector")
|
|
assert.Equal(t, "linux", pod.Spec.NodeSelector[LabelKeyKubernetesOS])
|
|
assert.Len(t, pod.Spec.Tolerations, 1, "other template fields should still be applied")
|
|
})
|
|
|
|
t.Run("listenerTemplate with explicit nodeSelector overrides default", func(t *testing.T) {
|
|
listenerWithTemplate := listener.DeepCopy()
|
|
listenerWithTemplate.Spec.Template = &corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
NodeSelector: map[string]string{
|
|
LabelKeyKubernetesOS: "linux",
|
|
"custom-label/pool": "listeners",
|
|
},
|
|
},
|
|
}
|
|
|
|
pod, err := b.newScaleSetListenerPod(
|
|
listenerWithTemplate,
|
|
&corev1.Secret{},
|
|
listenerServiceAccount,
|
|
listenerRole,
|
|
listenerRoleBinding,
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, pod.Spec.NodeSelector)
|
|
assert.Equal(t, "linux", pod.Spec.NodeSelector[LabelKeyKubernetesOS])
|
|
assert.Equal(t, "listeners", pod.Spec.NodeSelector["custom-label/pool"],
|
|
"explicit template nodeSelector should be applied")
|
|
})
|
|
|
|
t.Run("listenerTemplate with empty nodeSelector overrides default", func(t *testing.T) {
|
|
listenerWithTemplate := listener.DeepCopy()
|
|
listenerWithTemplate.Spec.Template = &corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
NodeSelector: map[string]string{},
|
|
},
|
|
}
|
|
|
|
pod, err := b.newScaleSetListenerPod(
|
|
listenerWithTemplate,
|
|
&corev1.Secret{},
|
|
listenerServiceAccount,
|
|
listenerRole,
|
|
listenerRoleBinding,
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
// An explicitly set empty map is non-nil, so it overrides the default.
|
|
// This is intentional: the user explicitly opted out of nodeSelector constraints.
|
|
assert.NotNil(t, pod.Spec.NodeSelector)
|
|
assert.Empty(t, pod.Spec.NodeSelector,
|
|
"explicitly empty nodeSelector should override the linux default")
|
|
})
|
|
}
|