Files
actions-runner-controller/controllers/actions.github.com/resourcebuilder_test.go
T

741 lines
32 KiB
Go

package actionsgithubcom
import (
"fmt"
"strings"
"sync"
"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",
Generation: 7,
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, "actions.github.com/integrity-hash")
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, "7", ephemeralRunnerSet.Annotations[AnnotationKeyAutoscalingRunnerSetGeneration])
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.NotContains(t, listener.Annotations, "actions.github.com/integrity-hash")
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 TestEphemeralRunnerSetProxySecretMetadata(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)
assert.Equal(t, proxyEphemeralRunnerSetSecretName(ephemeralRunnerSet), proxySecret.Name)
assert.Equal(t, ephemeralRunnerSet.Namespace, proxySecret.Namespace)
assert.Equal(t, ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetName], proxySecret.Labels[LabelKeyGitHubScaleSetName])
assert.Equal(t, ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetNamespace], proxySecret.Labels[LabelKeyGitHubScaleSetNamespace])
assert.NotContains(t, proxySecret.Annotations, "actions.github.com/integrity-hash")
}
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",
},
},
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")
})
}
// TestNewEphemeralRunnerStampsActionableRevision pins the annotation the
// Outdated lifecycle is built on. The controller compares a runner's actionable
// revision against the set's applied revision to decide whether an Outdated
// report concerns the current runner spec or one that has since been replaced.
// A runner that lost this annotation would parse as revision 0 and be treated as
// stale, so it would be deleted and replaced instead of holding the set
// Outdated, and the set would never stop scaling.
func TestNewEphemeralRunnerStampsActionableRevision(t *testing.T) {
newSet := func(revision int64, metadata *v1alpha1.ResourceMeta) *v1alpha1.EphemeralRunnerSet {
return &v1alpha1.EphemeralRunnerSet{
ObjectMeta: metav1.ObjectMeta{Name: "test-ers", Namespace: "test-ns"},
Spec: v1alpha1.EphemeralRunnerSetSpec{
ActionableRevision: revision,
EphemeralRunnerMetadata: metadata,
},
}
}
var b ResourceBuilder
t.Run("stamps the set's actionable revision", func(t *testing.T) {
runner, err := b.newEphemeralRunner(newSet(7, nil))
require.NoError(t, err)
assert.Equal(t, "7", runner.Annotations[AnnotationKeyActionableRevision])
})
// The zero value is what an unupgraded set carries, and it has to round-trip
// as "0" rather than being omitted: the classifier parses a missing
// annotation as 0 too, so an absent stamp would be indistinguishable from a
// genuine revision 0 and upgrades would silently rely on that coincidence.
t.Run("stamps the zero revision explicitly", func(t *testing.T) {
runner, err := b.newEphemeralRunner(newSet(0, nil))
require.NoError(t, err)
assert.Equal(t, "0", runner.Annotations[AnnotationKeyActionableRevision])
})
// User-supplied runner annotations are merged underneath the controller's
// own, so they cannot forge a revision. If this inverted, a user annotation
// could make every runner look stale and the set would delete and recreate
// runners forever.
t.Run("user metadata cannot override it", func(t *testing.T) {
runner, err := b.newEphemeralRunner(newSet(7, &v1alpha1.ResourceMeta{
Annotations: map[string]string{AnnotationKeyActionableRevision: "1"},
}))
require.NoError(t, err)
assert.Equal(t, "7", runner.Annotations[AnnotationKeyActionableRevision])
})
}
// TestNewEphemeralRunnerDoesNotShareItsSpec pins that every runner built from a
// set owns its spec outright.
//
// Creating a runner hands the object to the API server and decodes the reply
// back into it, and the decoder writes into the maps and slice elements it
// already finds rather than allocating new ones. Runners are built and created
// in parallel, so a spec shared between two of them is memory two goroutines
// write at the same time, which takes the process down rather than failing a
// request. The set is read by all of them at once, so its own copy has to come
// through untouched too.
func TestNewEphemeralRunnerDoesNotShareItsSpec(t *testing.T) {
b := &ResourceBuilder{}
set := &v1alpha1.EphemeralRunnerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-set",
Namespace: "test-ns",
Labels: map[string]string{"set-label": "original"},
Annotations: map[string]string{"set-annotation": "original"},
},
Spec: v1alpha1.EphemeralRunnerSetSpec{
Replicas: 2,
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigURL: "https://github.com/org/repo",
Proxy: &v1alpha1.ProxyConfig{
HTTP: &v1alpha1.ProxyServerConfig{Url: "http://original"},
NoProxy: []string{"original"},
},
EphemeralRunnerConfigSecretMetadata: &v1alpha1.ResourceMeta{
Labels: map[string]string{"secret-label": "original"},
},
PodTemplateSpec: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"pod-label": "original"},
Annotations: map[string]string{"pod-annotation": "original"},
},
Spec: corev1.PodSpec{
NodeSelector: map[string]string{"node": "original"},
Volumes: []corev1.Volume{{Name: "original"}},
Containers: []corev1.Container{{
Name: v1alpha1.EphemeralRunnerContainerName,
Image: "original",
Env: []corev1.EnvVar{{Name: "KEY", Value: "original"}},
}},
},
},
},
},
}
unchanged := set.DeepCopy()
first, err := b.newEphemeralRunner(set)
require.NoError(t, err)
second, err := b.newEphemeralRunner(set)
require.NoError(t, err)
// Write over everything the decoder would reach on its way through the
// reply, as it would for the runner that happened to be created first.
first.Spec.Spec.Containers[0].Image = "decoded"
first.Spec.Spec.Containers[0].Env[0].Value = "decoded"
first.Spec.Spec.Volumes[0].Name = "decoded"
first.Spec.Spec.NodeSelector["node"] = "decoded"
first.Spec.Labels["pod-label"] = "decoded"
first.Spec.Annotations["pod-annotation"] = "decoded"
first.Spec.Proxy.HTTP.Url = "decoded"
first.Spec.Proxy.NoProxy[0] = "decoded"
first.Spec.EphemeralRunnerConfigSecretMetadata.Labels["secret-label"] = "decoded"
first.Labels["set-label"] = "decoded"
first.Annotations["set-annotation"] = "decoded"
assert.Equal(t, "original", second.Spec.Spec.Containers[0].Image)
assert.Equal(t, "original", second.Spec.Spec.Containers[0].Env[0].Value)
assert.Equal(t, "original", second.Spec.Spec.Volumes[0].Name)
assert.Equal(t, "original", second.Spec.Spec.NodeSelector["node"])
assert.Equal(t, "original", second.Spec.Labels["pod-label"])
assert.Equal(t, "original", second.Spec.Annotations["pod-annotation"])
assert.Equal(t, "http://original", second.Spec.Proxy.HTTP.Url)
assert.Equal(t, "original", second.Spec.Proxy.NoProxy[0])
assert.Equal(t, "original", second.Spec.EphemeralRunnerConfigSecretMetadata.Labels["secret-label"])
assert.Equal(t, "original", second.Labels["set-label"])
assert.Equal(t, "original", second.Annotations["set-annotation"])
assert.Equal(t, unchanged.Spec, set.Spec, "the set a runner was built from was written into")
assert.Equal(t, unchanged.Labels, set.Labels)
assert.Equal(t, unchanged.Annotations, set.Annotations)
}
// TestNewEphemeralRunnerIsSafeToBuildConcurrentlyWithoutAScheme pins that a
// builder that was never given a scheme can still build runners in parallel.
//
// Runners are built concurrently, and the ownership reference needs a scheme to
// resolve the owner's kind. A builder without one falls back to a scheme it
// makes itself, and doing that by assigning to the builder would be a write
// every other goroutine is reading at the same time: they would race on the
// field, and one could pick up a scheme another had allocated but not yet
// registered the types on, which fails the build with an unknown kind rather
// than racing quietly. Every runner here has to come back owned, whichever
// goroutine got there first. The race itself is only reported under -race.
func TestNewEphemeralRunnerIsSafeToBuildConcurrentlyWithoutAScheme(t *testing.T) {
b := &ResourceBuilder{}
require.Nil(t, b.Scheme, "the fallback only runs for a builder without a scheme")
set := &v1alpha1.EphemeralRunnerSet{
ObjectMeta: metav1.ObjectMeta{Name: "test-set", Namespace: "test-ns"},
Spec: v1alpha1.EphemeralRunnerSetSpec{
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigURL: "https://github.com/org/repo",
PodTemplateSpec: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: v1alpha1.EphemeralRunnerContainerName}},
},
},
},
},
}
const runners = 32
var wg sync.WaitGroup
built := make([]*v1alpha1.EphemeralRunner, runners)
errs := make([]error, runners)
start := make(chan struct{})
for i := range runners {
wg.Add(1)
go func() {
defer wg.Done()
<-start
built[i], errs[i] = b.newEphemeralRunner(set)
}()
}
close(start)
wg.Wait()
for i := range runners {
require.NoError(t, errs[i])
require.Len(t, built[i].OwnerReferences, 1, "the runner has to come back owned by the set")
assert.Equal(t, set.Name, built[i].OwnerReferences[0].Name)
assert.Equal(t, "EphemeralRunnerSet", built[i].OwnerReferences[0].Kind)
}
}