mirror of
https://github.com/actions-runner-controller/actions-runner-controller.git
synced 2026-09-30 21:29:32 +02:00
741 lines
32 KiB
Go
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)
|
|
}
|
|
}
|