This commit is contained in:
Nikola Jokic
2026-06-23 15:53:22 +02:00
parent a3a8670adf
commit 6c37cfee11
7 changed files with 3014 additions and 4 deletions
@@ -0,0 +1,957 @@
/*
Copyright 2020 The actions-runner-controller authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package actionsgithubcom
import (
"context"
"testing"
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
logf "sigs.k8s.io/controller-runtime/pkg/log"
)
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_FinalizerAdd benchmarks adding finalizer to AutoscalingListener
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_FinalizerAdd(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create AutoscalingRunnerSet (required dependency)
ars := NewMinimalAutoscalingRunnerSet(namespace, arsName)
// Create listener without finalizer
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = nil
// Create secret for listener configuration
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ars, listener, secret).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logf.Log.WithName("benchmark"),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
for b.Loop() {
// Reset listener state (remove finalizer for each iteration)
updatedListener := &actionsv1alpha1.AutoscalingListener{}
_ = fakeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: listenerName}, updatedListener)
updatedListener.Finalizers = nil
_ = fakeClient.Update(ctx, updatedListener)
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_FinalizerRemove benchmarks removing finalizer during deletion
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_FinalizerRemove(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create AutoscalingRunnerSet
ars := NewMinimalAutoscalingRunnerSet(namespace, arsName)
// Create listener with finalizer and deletion timestamp
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = []string{autoscalingListenerFinalizerName}
now := metav1.Now()
listener.DeletionTimestamp = &now
// Create secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ars, listener, secret).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logf.Log.WithName("benchmark"),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
for b.Loop() {
// Reset listener with finalizer
updatedListener := &actionsv1alpha1.AutoscalingListener{}
if err := fakeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: listenerName}, updatedListener); err == nil {
updatedListener.Finalizers = []string{autoscalingListenerFinalizerName}
_ = fakeClient.Update(ctx, updatedListener)
}
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_MissingARSDeletion benchmarks deleting listener when AutoscalingRunnerSet is missing
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_MissingARSDeletion(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create listener with finalizer but NO AutoscalingRunnerSet
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = []string{autoscalingListenerFinalizerName}
// Create secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(listener, secret).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logf.Log.WithName("benchmark"),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
for b.Loop() {
// Recreate listener for each iteration
_ = fakeClient.Delete(ctx, listener)
newListener := NewMinimalAutoscalingListener(namespace, listenerName)
newListener.Spec.AutoscalingRunnerSetNamespace = namespace
newListener.Spec.AutoscalingRunnerSetName = arsName
newListener.Spec.GitHubConfigSecret = secret.Name
newListener.Finalizers = []string{autoscalingListenerFinalizerName}
_ = fakeClient.Create(ctx, newListener)
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_CreateServiceAccount benchmarks creating ServiceAccount
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_CreateServiceAccount(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create AutoscalingRunnerSet
ars := NewMinimalAutoscalingRunnerSet(namespace, arsName)
// Create listener with finalizer (already added)
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = []string{autoscalingListenerFinalizerName}
// Create secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ars, listener, secret).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logf.Log.WithName("benchmark"),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
b.ResetTimer()
for b.Loop() {
// Delete ServiceAccount to trigger creation path
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
},
}
_ = fakeClient.Delete(ctx, sa)
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_UpdateServiceAccount benchmarks updating ServiceAccount
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_UpdateServiceAccount(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create AutoscalingRunnerSet
ars := NewMinimalAutoscalingRunnerSet(namespace, arsName)
// Create listener with finalizer
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = []string{autoscalingListenerFinalizerName}
// Create ServiceAccount with outdated labels
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
Labels: map[string]string{
"outdated-label": "old-value",
},
},
}
// Create secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ars, listener, sa, secret).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
for b.Loop() {
// Reset ServiceAccount labels to trigger update
updatedSA := &corev1.ServiceAccount{}
_ = fakeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: listenerName}, updatedSA)
updatedSA.Labels = map[string]string{"outdated-label": "old-value"}
_ = fakeClient.Update(ctx, updatedSA)
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_CreateRole benchmarks creating Role
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_CreateRole(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create AutoscalingRunnerSet
ars := NewMinimalAutoscalingRunnerSet(namespace, arsName)
// Create listener with finalizer
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = []string{autoscalingListenerFinalizerName}
// Create ServiceAccount (prerequisite)
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
},
}
// Create secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ars, listener, sa, secret).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Delete Role to trigger creation path
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
},
}
_ = fakeClient.Delete(ctx, role)
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_UpdateRole benchmarks updating Role
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_UpdateRole(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create AutoscalingRunnerSet
ars := NewMinimalAutoscalingRunnerSet(namespace, arsName)
// Create listener with finalizer
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = []string{autoscalingListenerFinalizerName}
// Create ServiceAccount
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
},
}
// Create Role with outdated labels
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
Labels: map[string]string{
"outdated-label": "old-value",
},
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"pods"},
Verbs: []string{"get"},
},
},
}
// Create secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ars, listener, sa, role, secret).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Reset Role labels to trigger update
updatedRole := &rbacv1.Role{}
_ = fakeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: listenerName}, updatedRole)
updatedRole.Labels = map[string]string{"outdated-label": "old-value"}
_ = fakeClient.Update(ctx, updatedRole)
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_CreateRoleBinding benchmarks creating RoleBinding
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_CreateRoleBinding(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create AutoscalingRunnerSet
ars := NewMinimalAutoscalingRunnerSet(namespace, arsName)
// Create listener with finalizer
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = []string{autoscalingListenerFinalizerName}
// Create ServiceAccount
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
},
}
// Create Role
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
},
Rules: []rbacv1.PolicyRule{},
}
// Create secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ars, listener, sa, role, secret).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Delete RoleBinding to trigger creation path
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
},
}
_ = fakeClient.Delete(ctx, rb)
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_UpdateRoleBinding benchmarks updating RoleBinding
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_UpdateRoleBinding(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create AutoscalingRunnerSet
ars := NewMinimalAutoscalingRunnerSet(namespace, arsName)
// Create listener with finalizer
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = []string{autoscalingListenerFinalizerName}
// Create ServiceAccount
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
},
}
// Create Role
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
},
Rules: []rbacv1.PolicyRule{},
}
// Create RoleBinding with outdated labels
roleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
Labels: map[string]string{
"outdated-label": "old-value",
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: listenerName,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: listenerName,
Namespace: namespace,
},
},
}
// Create secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ars, listener, sa, role, roleBinding, secret).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Reset RoleBinding labels to trigger update
updatedRB := &rbacv1.RoleBinding{}
_ = fakeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: listenerName}, updatedRB)
updatedRB.Labels = map[string]string{"outdated-label": "old-value"}
_ = fakeClient.Update(ctx, updatedRB)
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingListener_SteadyState benchmarks no-op steady-state reconciliation
func BenchmarkActionsGithub_Reconcile_AutoscalingListener_SteadyState(b *testing.B) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = actionsv1alpha1.AddToScheme(scheme)
namespace := "test-ns"
listenerName := "test-listener"
arsName := "test-ars"
// Create AutoscalingRunnerSet
ars := NewMinimalAutoscalingRunnerSet(namespace, arsName)
// Create fully configured listener
listener := NewMinimalAutoscalingListener(namespace, listenerName)
listener.Spec.AutoscalingRunnerSetNamespace = namespace
listener.Spec.AutoscalingRunnerSetName = arsName
listener.Finalizers = []string{autoscalingListenerFinalizerName}
// Create all required resources (steady state)
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
Labels: map[string]string{
"actions.github.com/autoscaling-listener-name": listenerName,
"actions.github.com/autoscaling-listener-namespace": namespace,
},
},
}
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
Labels: map[string]string{
"auto-scaling-listener-name": listenerName,
"auto-scaling-listener-namespace": namespace,
},
},
Rules: rulesForListenerRole([]string{listener.Spec.EphemeralRunnerSetName}),
}
roleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
Labels: map[string]string{
"auto-scaling-listener-name": listenerName,
"auto-scaling-listener-namespace": namespace,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: listenerName,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: listenerName,
Namespace: namespace,
},
},
}
// Create config secret
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: scaleSetListenerConfigName(listener),
Namespace: namespace,
Labels: map[string]string{
"auto-scaling-listener-name": listenerName,
},
},
Data: map[string][]byte{
"listener": []byte(`{"url":"https://github.com/example/repo","token":"fake-token"}`),
},
}
// Create github config secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: namespace,
},
Data: map[string][]byte{
"github_token": []byte("fake-token"),
},
}
listener.Spec.GitHubConfigSecret = secret.Name
// Create listener pod (running state)
listenerPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: listenerName,
Namespace: namespace,
Labels: map[string]string{
"auto-scaling-listener-name": listenerName,
},
Annotations: map[string]string{
AnnotationKeyIntegrityHash: "deterministic-hash",
},
},
Spec: corev1.PodSpec{
ServiceAccountName: listenerName,
Containers: []corev1.Container{
{
Name: autoscalingListenerContainerName,
Image: listener.Spec.Image,
},
},
},
Status: corev1.PodStatus{
Phase: corev1.PodRunning,
ContainerStatuses: []corev1.ContainerStatus{
{
Name: autoscalingListenerContainerName,
State: corev1.ContainerState{
Running: &corev1.ContainerStateRunning{
StartedAt: metav1.Now(),
},
},
},
},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ars, listener, sa, role, roleBinding, configSecret, secret, listenerPod).
WithStatusSubresource(&actionsv1alpha1.AutoscalingListener{}).
Build()
secretResolver := secretresolver.New(fakeClient, NewFakeMultiClient())
rb := ResourceBuilder{SecretResolver: secretResolver}
reconciler := &AutoscalingListenerReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: rb,
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: client.ObjectKey{
Namespace: namespace,
Name: listenerName,
},
}
// Warmup
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
// No-op steady state - everything is in sync
_, _ = reconciler.Reconcile(ctx, req)
}
}
@@ -701,6 +701,9 @@ func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetName(ctx context.Co
logger.Info("Updating runner scale set name as an annotation")
original := autoscalingRunnerSet.DeepCopy()
if autoscalingRunnerSet.Annotations == nil {
autoscalingRunnerSet.Annotations = make(map[string]string, 1)
}
autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName] = updatedRunnerScaleSet.Name
if err := r.Patch(ctx, autoscalingRunnerSet, client.MergeFrom(original)); err != nil {
logger.Error(err, "Failed to update runner scale set name annotation")
@@ -0,0 +1,509 @@
/*
Copyright 2020 The actions-runner-controller authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package actionsgithubcom
import (
"context"
"testing"
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/build"
scalefake "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient/fake"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
"github.com/actions/scaleset"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
// BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_AddFinalizer benchmarks the reconcile loop
// when adding a finalizer to a new AutoscalingRunnerSet
func BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_AddFinalizer(b *testing.B) {
ctx := context.Background()
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Create fake secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "test-ns",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
// Create minimal AutoscalingRunnerSet without finalizer
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(secret, ars).
WithStatusSubresource(&actionsv1alpha1.AutoscalingRunnerSet{}).
Build()
reconciler := &AutoscalingRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ControllerNamespace: "test-ns",
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(fakeClient, NewFakeMultiClient()),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "test-ns",
Name: "test-ars",
},
}
for b.Loop() {
b.StopTimer()
// Reset the AutoscalingRunnerSet to initial state (no finalizer)
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Annotations = map[string]string{
AnnotationKeyIntegrityHash: autoscalingRunnerSetIntegrityHash(ars),
}
ResetBenchmarkAutoscalingRunnerSet(b, ctx, fakeClient, ars)
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_RemoveFinalizer benchmarks the reconcile loop
// when removing a finalizer during deletion
func BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_RemoveFinalizer(b *testing.B) {
ctx := context.Background()
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Create fake secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "test-ns",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
// Create AutoscalingRunnerSet with finalizer and deletion timestamp
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Finalizers = []string{autoscalingRunnerSetFinalizerName}
now := metav1.Now()
ars.DeletionTimestamp = &now
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(secret, ars).
WithStatusSubresource(&actionsv1alpha1.AutoscalingRunnerSet{}).
Build()
reconciler := &AutoscalingRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ControllerNamespace: "test-ns",
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(fakeClient, NewFakeMultiClient()),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "test-ns",
Name: "test-ars",
},
}
for b.Loop() {
b.StopTimer()
// Reset the AutoscalingRunnerSet with finalizer
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Finalizers = []string{autoscalingRunnerSetFinalizerName}
now := metav1.Now()
ars.DeletionTimestamp = &now
_ = fakeClient.Update(ctx, ars)
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_CreateScaleSet benchmarks the reconcile loop
// when creating a runner scale set
func BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_CreateScaleSet(b *testing.B) {
ctx := context.Background()
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Create fake secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "test-ns",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
// Create AutoscalingRunnerSet with finalizer but no scale set annotations
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Finalizers = []string{autoscalingRunnerSetFinalizerName}
ars.Spec.RunnerGroup = "default"
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(secret, ars).
WithStatusSubresource(&actionsv1alpha1.AutoscalingRunnerSet{}).
Build()
// Create fake scale client with runner group and scale set creation support
fakeScaleClient := scalefake.NewMultiClient(
scalefake.WithClient(
scalefake.NewClient(
scalefake.WithGetRunnerGroupByNameFunc(func(ctx context.Context, groupName string) (*scaleset.RunnerGroup, error) {
return &scaleset.RunnerGroup{ID: 1, Name: groupName}, nil
}),
scalefake.WithCreateRunnerScaleSetFunc(func(ctx context.Context, rs *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error) {
return &scaleset.RunnerScaleSet{
ID: 1,
Name: rs.Name,
RunnerGroupID: rs.RunnerGroupID,
RunnerGroupName: "default",
}, nil
}),
scalefake.WithGetRunnerScaleSet(nil, nil),
),
),
)
reconciler := &AutoscalingRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ControllerNamespace: "test-ns",
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(fakeClient, fakeScaleClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "test-ns",
Name: "test-ars",
},
}
for b.Loop() {
b.StopTimer()
// Reset the AutoscalingRunnerSet to state before scale set creation
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Finalizers = []string{autoscalingRunnerSetFinalizerName}
ars.Spec.RunnerGroup = "default"
// Remove scale set annotations to trigger creation
ars.Annotations = nil
ResetBenchmarkAutoscalingRunnerSet(b, ctx, fakeClient, ars)
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_OutdatedCleanup benchmarks the reconcile loop
// when cleaning up outdated resources
func BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_OutdatedCleanup(b *testing.B) {
ctx := context.Background()
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Create fake secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "test-ns",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
// Create AutoscalingRunnerSet in outdated phase
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Finalizers = []string{autoscalingRunnerSetFinalizerName}
ars.Status.Phase = actionsv1alpha1.AutoscalingRunnerSetPhaseOutdated
// Create AutoscalingListener to be cleaned up
listener := NewMinimalAutoscalingListener("test-ns", "test-ars-listener")
listener.Spec.AutoscalingRunnerSetNamespace = "test-ns"
listener.Spec.AutoscalingRunnerSetName = "test-ars"
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(secret, ars, listener).
WithStatusSubresource(&actionsv1alpha1.AutoscalingRunnerSet{}, &actionsv1alpha1.AutoscalingListener{}).
Build()
reconciler := &AutoscalingRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ControllerNamespace: "test-ns",
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(fakeClient, NewFakeMultiClient()),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "test-ns",
Name: "test-ars",
},
}
for b.Loop() {
b.StopTimer()
// Reset the AutoscalingRunnerSet to outdated state
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Finalizers = []string{autoscalingRunnerSetFinalizerName}
ars.Status.Phase = actionsv1alpha1.AutoscalingRunnerSetPhaseOutdated
ResetBenchmarkAutoscalingRunnerSet(b, ctx, fakeClient, ars)
// Recreate listener
listener := NewMinimalAutoscalingListener("test-ns", "test-ars-listener")
listener.Spec.AutoscalingRunnerSetNamespace = "test-ns"
listener.Spec.AutoscalingRunnerSetName = "test-ars"
_ = fakeClient.Create(ctx, listener)
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_CreateEphemeralRunnerSet benchmarks the reconcile loop
// when creating an EphemeralRunnerSet
func BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_CreateEphemeralRunnerSet(b *testing.B) {
ctx := context.Background()
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Create fake secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "test-ns",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
// Create AutoscalingRunnerSet with finalizer and scale set ID but no EphemeralRunnerSet
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Finalizers = []string{autoscalingRunnerSetFinalizerName}
ars.Annotations = map[string]string{
runnerScaleSetIDAnnotationKey: "1",
AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set",
AnnotationKeyGitHubRunnerGroupName: "default",
AnnotationKeyIntegrityHash: "test-hash",
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(secret, ars).
WithStatusSubresource(&actionsv1alpha1.AutoscalingRunnerSet{}, &actionsv1alpha1.EphemeralRunnerSet{}).
Build()
reconciler := &AutoscalingRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ControllerNamespace: "test-ns",
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(fakeClient, NewFakeMultiClient()),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "test-ns",
Name: "test-ars",
},
}
for b.Loop() {
b.StopTimer()
// Delete any existing EphemeralRunnerSet
ersList := &actionsv1alpha1.EphemeralRunnerSetList{}
_ = fakeClient.List(ctx, ersList, client.InNamespace("test-ns"))
for _, ers := range ersList.Items {
_ = fakeClient.Delete(ctx, &ers)
}
// Reset AutoscalingRunnerSet
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Finalizers = []string{autoscalingRunnerSetFinalizerName}
ars.Annotations = map[string]string{
runnerScaleSetIDAnnotationKey: "1",
AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set",
AnnotationKeyGitHubRunnerGroupName: "default",
AnnotationKeyIntegrityHash: "test-hash",
}
ResetBenchmarkAutoscalingRunnerSet(b, ctx, fakeClient, ars)
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
}
// BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_CreateListener benchmarks the reconcile loop
// when creating an AutoscalingListener
func BenchmarkActionsGithub_Reconcile_AutoscalingRunnerSet_CreateListener(b *testing.B) {
ctx := context.Background()
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Create fake secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "test-ns",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
// Create AutoscalingRunnerSet with finalizer and scale set ID
ars := NewMinimalAutoscalingRunnerSet("test-ns", "test-ars")
ars.Spec.GitHubConfigSecret = "test-secret"
ars.Labels = map[string]string{
LabelKeyKubernetesVersion: build.Version,
}
ars.Finalizers = []string{autoscalingRunnerSetFinalizerName}
ars.Annotations = map[string]string{
runnerScaleSetIDAnnotationKey: "1",
AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set",
AnnotationKeyGitHubRunnerGroupName: "default",
AnnotationKeyIntegrityHash: "test-hash",
}
// Create EphemeralRunnerSet
ers := NewMinimalEphemeralRunnerSet("test-ns", "test-ars")
ers.Annotations = map[string]string{
AnnotationKeyIntegrityHash: "test-hash",
}
ers.Spec.Replicas = 1
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(secret, ars, ers).
WithStatusSubresource(
&actionsv1alpha1.AutoscalingRunnerSet{},
&actionsv1alpha1.EphemeralRunnerSet{},
&actionsv1alpha1.AutoscalingListener{},
).
Build()
reconciler := &AutoscalingRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
ControllerNamespace: "test-ns",
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc:latest",
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(fakeClient, NewFakeMultiClient()),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "test-ns",
Name: "test-ars",
},
}
for b.Loop() {
b.StopTimer()
// Delete any existing AutoscalingListener
listenerList := &actionsv1alpha1.AutoscalingListenerList{}
_ = fakeClient.List(ctx, listenerList, client.InNamespace("test-ns"))
for _, listener := range listenerList.Items {
_ = fakeClient.Delete(ctx, &listener)
}
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
}
@@ -0,0 +1,221 @@
/*
Copyright 2020 The actions-runner-controller authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package actionsgithubcom
import (
"context"
"testing"
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
scalefake "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient/fake"
"github.com/actions/scaleset"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
benchmarkGitHubConfigURL = "https://github.com/actions/actions-runner-controller"
benchmarkGitHubConfigSecret = "test-secret"
benchmarkRunnerImage = "ghcr.io/actions/runner:latest"
)
func NewBenchmarkScheme(b *testing.B) *runtime.Scheme {
b.Helper()
scheme := runtime.NewScheme()
if err := clientgoscheme.AddToScheme(scheme); err != nil {
b.Fatalf("add client-go scheme: %v", err)
}
if err := actionsv1alpha1.AddToScheme(scheme); err != nil {
b.Fatalf("add actions.github.com scheme: %v", err)
}
return scheme
}
func NewBenchmarkRequest(namespace, name string) ctrl.Request {
return ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: namespace,
Name: name,
},
}
}
func ResetBenchmarkAutoscalingRunnerSet(b *testing.B, ctx context.Context, k8sClient client.Client, ars *actionsv1alpha1.AutoscalingRunnerSet) {
b.Helper()
current := &actionsv1alpha1.AutoscalingRunnerSet{}
key := client.ObjectKeyFromObject(ars)
if err := k8sClient.Get(ctx, key, current); err != nil {
b.Fatalf("get autoscaling runner set %s: %v", key, err)
}
ars.ResourceVersion = current.ResourceVersion
if err := k8sClient.Update(ctx, ars); err != nil {
b.Fatalf("reset autoscaling runner set %s: %v", key, err)
}
}
func WarmupIteration(b *testing.B, fn func()) {
b.Helper()
b.StopTimer()
fn()
b.StartTimer()
}
func RunBenchmarkIterations(b *testing.B, fn func()) {
b.Helper()
b.ResetTimer()
for i := 0; i < b.N; i++ {
fn()
}
}
func NewFakeMultiClient() *scalefake.MultiClient {
runnerGroup := &scaleset.RunnerGroup{ID: 1, Name: "default"}
runnerScaleSet := &scaleset.RunnerScaleSet{
ID: 1,
Name: "test-scale-set",
RunnerGroupID: runnerGroup.ID,
RunnerGroupName: runnerGroup.Name,
}
runner := &scaleset.RunnerReference{ID: 1, Name: "test-runner"}
return scalefake.NewMultiClient(
scalefake.WithClient(
scalefake.NewClient(
scalefake.WithGetRunnerGroupByName(runnerGroup, nil),
scalefake.WithGetRunnerScaleSet(runnerScaleSet, nil),
scalefake.WithGetRunnerScaleSetByID(runnerScaleSet, nil),
scalefake.WithGetRunner(runner, nil),
scalefake.WithGetRunnerByName(runner, nil),
scalefake.WithGenerateJitRunnerConfig(
&scaleset.RunnerScaleSetJitRunnerConfig{
Runner: runner,
EncodedJITConfig: "fake-jit-config",
},
nil,
),
),
),
)
}
func NewMinimalAutoscalingRunnerSet(namespace, name string) *actionsv1alpha1.AutoscalingRunnerSet {
minRunners := 0
maxRunners := 1
return &actionsv1alpha1.AutoscalingRunnerSet{
TypeMeta: metav1.TypeMeta{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "AutoscalingRunnerSet",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: actionsv1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: benchmarkGitHubConfigURL,
GitHubConfigSecret: benchmarkGitHubConfigSecret,
RunnerGroup: "default",
MinRunners: &minRunners,
MaxRunners: &maxRunners,
Template: benchmarkRunnerTemplate(),
},
}
}
func NewMinimalAutoscalingListener(namespace, name string) *actionsv1alpha1.AutoscalingListener {
return &actionsv1alpha1.AutoscalingListener{
TypeMeta: metav1.TypeMeta{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "AutoscalingListener",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: actionsv1alpha1.AutoscalingListenerSpec{
GitHubConfigURL: benchmarkGitHubConfigURL,
GitHubConfigSecret: benchmarkGitHubConfigSecret,
RunnerScaleSetID: 1,
MaxRunners: 1,
Image: benchmarkRunnerImage,
},
}
}
func NewMinimalEphemeralRunnerSet(namespace, name string) *actionsv1alpha1.EphemeralRunnerSet {
return &actionsv1alpha1.EphemeralRunnerSet{
TypeMeta: metav1.TypeMeta{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "EphemeralRunnerSet",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: actionsv1alpha1.EphemeralRunnerSetSpec{
Replicas: 1,
PatchID: 1,
EphemeralRunnerSpec: actionsv1alpha1.EphemeralRunnerSpec{
GitHubConfigURL: benchmarkGitHubConfigURL,
GitHubConfigSecret: benchmarkGitHubConfigSecret,
RunnerScaleSetID: 1,
PodTemplateSpec: benchmarkRunnerTemplate(),
},
},
}
}
func NewMinimalEphemeralRunner(namespace, name string) *actionsv1alpha1.EphemeralRunner {
return &actionsv1alpha1.EphemeralRunner{
TypeMeta: metav1.TypeMeta{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "EphemeralRunner",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: actionsv1alpha1.EphemeralRunnerSpec{
GitHubConfigURL: benchmarkGitHubConfigURL,
GitHubConfigSecret: benchmarkGitHubConfigSecret,
RunnerScaleSetID: 1,
PodTemplateSpec: benchmarkRunnerTemplate(),
},
}
}
func benchmarkRunnerTemplate() corev1.PodTemplateSpec {
return corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: actionsv1alpha1.EphemeralRunnerContainerName,
Image: benchmarkRunnerImage,
},
},
},
}
}
@@ -0,0 +1,896 @@
/*
Copyright 2020 The actions-runner-controller authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package actionsgithubcom
import (
"context"
"errors"
"testing"
"time"
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient/fake"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
"github.com/actions/scaleset"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
)
// BenchmarkActionsGithub_Reconcile_EphemeralRunner_NoOp benchmarks a no-op reconcile
// scenario where an existing EphemeralRunner with finalizers requires no changes
func BenchmarkActionsGithub_Reconcile_EphemeralRunner_NoOp(b *testing.B) {
ctx := context.Background()
// Setup: Create scheme and fake client
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Create minimal EphemeralRunner with finalizers already added
runner := NewMinimalEphemeralRunner("default", "test-runner")
runner.Finalizers = []string{
ephemeralRunnerFinalizerName,
ephemeralRunnerActionsFinalizerName,
}
runner.Spec.GitHubConfigSecret = "config-secret"
runner.Status.RunnerID = 12345
runner.Status.RunnerName = "test-runner"
// Create config secret
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
// Create JIT secret for the runner (already exists, so no JIT creation needed)
jitSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-runner",
Namespace: "default",
},
Data: map[string][]byte{
"jitToken": []byte("test-jit-token"),
"runnerId": []byte("12345"),
"runnerName": []byte("test-runner"),
},
}
// Create pod (already exists, so no pod creation needed)
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-runner",
Namespace: "default",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "runner",
Image: "ghcr.io/actions/runner:latest",
},
},
},
Status: corev1.PodStatus{
Phase: corev1.PodRunning,
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret, jitSecret, pod).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}, &corev1.Pod{}).
Build()
// Setup reconciler
multiClient := NewFakeMultiClient()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-runner",
},
}
// Warmup iteration
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
// Benchmark loop
for b.Loop() {
_, err := reconciler.Reconcile(ctx, req)
if err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunner_FinalizerAdd benchmarks the
// path where finalizers are added to a new EphemeralRunner
func BenchmarkActionsGithub_Reconcile_EphemeralRunner_FinalizerAdd(b *testing.B) {
ctx := context.Background()
// Setup: Create scheme
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
multiClient := fake.NewMultiClient(
fake.WithClient(
fake.NewClient(
fake.WithGetRunner(nil, scaleset.RunnerNotFoundError),
fake.WithGenerateJitRunnerConfig(
&scaleset.RunnerScaleSetJitRunnerConfig{
Runner: &scaleset.RunnerReference{ID: 1, Name: "test-runner"},
EncodedJITConfig: "fake-jit-config",
},
nil,
),
),
),
)
// Warmup iteration
{
runner := NewMinimalEphemeralRunner("default", "warmup-runner")
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "warmup-runner",
},
}
_, _ = reconciler.Reconcile(ctx, req)
}
// Benchmark loop: Each iteration needs fresh objects since finalizers are added
for b.Loop() {
b.StopTimer()
runner := NewMinimalEphemeralRunner("default", "test-runner")
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-runner",
},
}
b.StartTimer()
_, err := reconciler.Reconcile(ctx, req)
if err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunner_Deletion benchmarks the deletion
// path where finalizers are removed and resources are cleaned up
func BenchmarkActionsGithub_Reconcile_EphemeralRunner_Deletion(b *testing.B) {
ctx := context.Background()
// Setup: Create scheme
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
multiClient := NewFakeMultiClient()
// Warmup iteration
{
now := metav1.Now()
runner := NewMinimalEphemeralRunner("default", "warmup-runner")
runner.Finalizers = []string{ephemeralRunnerFinalizerName}
runner.DeletionTimestamp = &now
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "warmup-runner",
},
}
_, _ = reconciler.Reconcile(ctx, req)
}
// Benchmark loop: Each iteration needs fresh objects
for b.Loop() {
b.StopTimer()
now := metav1.Now()
runner := NewMinimalEphemeralRunner("default", "test-runner")
runner.Finalizers = []string{ephemeralRunnerFinalizerName}
runner.DeletionTimestamp = &now
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-runner",
},
}
b.StartTimer()
_, err := reconciler.Reconcile(ctx, req)
if err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunner_DeletionWithActionsCleanup benchmarks
// the deletion path with actions finalizer requiring service cleanup
func BenchmarkActionsGithub_Reconcile_EphemeralRunner_DeletionWithActionsCleanup(b *testing.B) {
ctx := context.Background()
// Setup: Create scheme
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Configure fake client to simulate successful cleanup
multiClient := fake.NewMultiClient(
fake.WithClient(
fake.NewClient(
fake.WithRemoveRunner(nil), // Successful cleanup
),
),
)
// Warmup iteration
{
now := metav1.Now()
runner := NewMinimalEphemeralRunner("default", "warmup-runner")
runner.Finalizers = []string{
ephemeralRunnerFinalizerName,
ephemeralRunnerActionsFinalizerName,
}
runner.DeletionTimestamp = &now
runner.Status.RunnerID = 12345
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "warmup-runner",
},
}
_, _ = reconciler.Reconcile(ctx, req)
}
// Benchmark loop
for b.Loop() {
b.StopTimer()
// Create EphemeralRunner marked for deletion with both finalizers
now := metav1.Now()
runner := NewMinimalEphemeralRunner("default", "test-runner")
runner.Finalizers = []string{
ephemeralRunnerFinalizerName,
ephemeralRunnerActionsFinalizerName,
}
runner.DeletionTimestamp = &now
runner.Status.RunnerID = 12345
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-runner",
},
}
b.StartTimer()
_, err := reconciler.Reconcile(ctx, req)
if err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunner_RetryableError benchmarks the
// path where a retryable error occurs during reconciliation
func BenchmarkActionsGithub_Reconcile_EphemeralRunner_RetryableError(b *testing.B) {
ctx := context.Background()
// Setup: Create scheme
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Configure fake client to return transient error
retryableErr := errors.New("transient API error")
multiClient := fake.NewMultiClient(
fake.WithClient(
fake.NewClient(
fake.WithRemoveRunner(retryableErr),
),
),
)
// Warmup iteration
{
now := metav1.Now()
runner := NewMinimalEphemeralRunner("default", "warmup-runner")
runner.Finalizers = []string{
ephemeralRunnerFinalizerName,
ephemeralRunnerActionsFinalizerName,
}
runner.DeletionTimestamp = &now
runner.Status.RunnerID = 12345
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "warmup-runner",
},
}
_, _ = reconciler.Reconcile(ctx, req)
}
// Benchmark loop
for b.Loop() {
b.StopTimer()
// Create EphemeralRunner that will trigger error during cleanup
now := metav1.Now()
runner := NewMinimalEphemeralRunner("default", "test-runner")
runner.Finalizers = []string{
ephemeralRunnerFinalizerName,
ephemeralRunnerActionsFinalizerName,
}
runner.DeletionTimestamp = &now
runner.Status.RunnerID = 12345
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-runner",
},
}
b.StartTimer()
result, err := reconciler.Reconcile(ctx, req)
// Verify error is returned (expected behavior for retryable errors)
if err == nil {
b.Fatalf("expected error, got nil")
}
if result.RequeueAfter != 0 {
b.Fatalf("expected no requeue delay on error, got %v", result.RequeueAfter)
}
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunner_DonePhaseCleanup benchmarks
// the path where an EphemeralRunner in Done phase triggers resource cleanup
func BenchmarkActionsGithub_Reconcile_EphemeralRunner_DonePhaseCleanup(b *testing.B) {
ctx := context.Background()
// Setup: Create scheme
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
multiClient := NewFakeMultiClient()
// Warmup iteration
{
runner := NewMinimalEphemeralRunner("default", "warmup-runner")
runner.Finalizers = []string{
ephemeralRunnerFinalizerName,
ephemeralRunnerActionsFinalizerName,
}
runner.Status.Phase = "Succeeded"
runner.Spec.GitHubConfigSecret = "config-secret"
// Create a pod that will be cleaned up
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "warmup-runner",
Namespace: "default",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "EphemeralRunner",
Name: "warmup-runner",
UID: runner.UID,
},
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "runner",
Image: "ghcr.io/actions/runner:latest",
},
},
},
}
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, pod, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "warmup-runner",
},
}
_, _ = reconciler.Reconcile(ctx, req)
}
// Benchmark loop
for b.Loop() {
b.StopTimer()
// Create EphemeralRunner in Done phase (Succeeded)
runner := NewMinimalEphemeralRunner("default", "test-runner")
runner.Finalizers = []string{
ephemeralRunnerFinalizerName,
ephemeralRunnerActionsFinalizerName,
}
runner.Status.Phase = "Succeeded"
runner.Spec.GitHubConfigSecret = "config-secret"
// Create a pod that will be cleaned up
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-runner",
Namespace: "default",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "EphemeralRunner",
Name: "test-runner",
UID: runner.UID,
},
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "runner",
Image: "ghcr.io/actions/runner:latest",
},
},
},
}
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, pod, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-runner",
},
}
b.StartTimer()
_, err := reconciler.Reconcile(ctx, req)
if err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunner_RequeueAfter benchmarks the
// path where cleanup is not finished and a requeue is requested
func BenchmarkActionsGithub_Reconcile_EphemeralRunner_RequeueAfter(b *testing.B) {
ctx := context.Background()
// Setup: Create scheme
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
multiClient := fake.NewMultiClient(
fake.WithClient(
fake.NewClient(
fake.WithRemoveRunner(scaleset.JobStillRunningError),
),
),
)
// Warmup iteration
{
now := metav1.Now()
runner := NewMinimalEphemeralRunner("default", "warmup-runner")
runner.Finalizers = []string{
ephemeralRunnerFinalizerName,
ephemeralRunnerActionsFinalizerName,
}
runner.DeletionTimestamp = &now
runner.Status.RunnerID = 12345
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "warmup-runner",
},
}
_, _ = reconciler.Reconcile(ctx, req)
}
// Benchmark loop
for b.Loop() {
b.StopTimer()
now := metav1.Now()
runner := NewMinimalEphemeralRunner("default", "test-runner")
runner.Finalizers = []string{
ephemeralRunnerFinalizerName,
ephemeralRunnerActionsFinalizerName,
}
runner.DeletionTimestamp = &now
runner.Status.RunnerID = 12345
runner.Spec.GitHubConfigSecret = "config-secret"
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config-secret",
Namespace: "default",
},
Data: map[string][]byte{
"github_token": []byte("test-token"),
},
}
k8sClient := clientfake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runner, configSecret).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunner{}).
Build()
reconciler := &EphemeralRunnerReconciler{
Client: k8sClient,
Scheme: scheme,
Log: logr.Discard(),
ResourceBuilder: ResourceBuilder{
SecretResolver: secretresolver.New(k8sClient, multiClient),
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-runner",
},
}
b.StartTimer()
result, err := reconciler.Reconcile(ctx, req)
if err != nil {
b.Fatalf("unexpected error: %v", err)
}
if result.RequeueAfter != 30*time.Second {
b.Fatalf("expected RequeueAfter=30s, got %v", result.RequeueAfter)
}
}
}
@@ -0,0 +1,418 @@
/*
Copyright 2020 The actions-runner-controller authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package actionsgithubcom
import (
"context"
"fmt"
"testing"
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
// BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_NoOp benchmarks the steady-state
// no-op reconciliation path where the EphemeralRunnerSet already has finalizer,
// correct spec hash, and the desired number of runners
func BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_NoOp(b *testing.B) {
testCases := []struct {
name string
replicas int
}{
{name: "small_3_replicas", replicas: 3},
{name: "medium_10_replicas", replicas: 10},
{name: "large_50_replicas", replicas: 50},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
ers := NewMinimalEphemeralRunnerSet("default", "test-ers")
ers.Spec.Replicas = tc.replicas
controllerutil.AddFinalizer(ers, EphemeralRunnerSetFinalizerName)
integrityHash := ephemeralRunnerSetIntegrityHash(ers)
if ers.Annotations == nil {
ers.Annotations = make(map[string]string)
}
ers.Annotations[AnnotationKeyIntegrityHash] = integrityHash
ers.Status.CurrentReplicas = tc.replicas
ers.Status.Phase = actionsv1alpha1.EphemeralRunnerSetPhaseRunning
runners := make([]client.Object, 0, tc.replicas+1)
runners = append(runners, ers)
for i := 0; i < tc.replicas; i++ {
runner := NewMinimalEphemeralRunner("default", fmt.Sprintf("test-runner-%d", i))
runner.Status.Phase = actionsv1alpha1.EphemeralRunnerPhaseRunning
runner.Status.RunnerID = i + 100
runner.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "EphemeralRunnerSet",
Name: ers.Name,
UID: ers.UID,
Controller: func() *bool { t := true; return &t }(),
},
}
runners = append(runners, runner)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runners...).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunnerSet{}, &actionsv1alpha1.EphemeralRunner{}).
WithIndex(&actionsv1alpha1.EphemeralRunner{}, resourceOwnerKey, newGroupVersionOwnerKindIndexer("EphemeralRunnerSet")).
Build()
reconciler := &EphemeralRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-ers",
},
}
WarmupIteration(b, func() {
_, _ = reconciler.Reconcile(ctx, req)
})
b.ResetTimer()
for b.Loop() {
_, _ = reconciler.Reconcile(ctx, req)
}
})
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_ScaleUp benchmarks the scale-up
// reconciliation path where new EphemeralRunners need to be created
func BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_ScaleUp(b *testing.B) {
testCases := []struct {
name string
current int
desired int
scaleUpBy int
}{
{name: "small_scale_0_to_3", current: 0, desired: 3, scaleUpBy: 3},
{name: "medium_scale_5_to_15", current: 5, desired: 15, scaleUpBy: 10},
{name: "large_scale_10_to_60", current: 10, desired: 60, scaleUpBy: 50},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
b.ResetTimer()
for b.Loop() {
b.StopTimer()
ers := NewMinimalEphemeralRunnerSet("default", "test-ers")
ers.Spec.Replicas = tc.desired
controllerutil.AddFinalizer(ers, EphemeralRunnerSetFinalizerName)
integrityHash := ephemeralRunnerSetIntegrityHash(ers)
if ers.Annotations == nil {
ers.Annotations = make(map[string]string)
}
ers.Annotations[AnnotationKeyIntegrityHash] = integrityHash
ers.Status.CurrentReplicas = tc.current
ers.Status.Phase = actionsv1alpha1.EphemeralRunnerSetPhaseRunning
runners := make([]client.Object, 0, tc.current+1)
runners = append(runners, ers)
for j := 0; j < tc.current; j++ {
runner := NewMinimalEphemeralRunner("default", fmt.Sprintf("test-runner-%d", j))
runner.Status.Phase = actionsv1alpha1.EphemeralRunnerPhaseRunning
runner.Status.RunnerID = j + 100
runner.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "EphemeralRunnerSet",
Name: ers.Name,
UID: ers.UID,
Controller: func() *bool { t := true; return &t }(),
},
}
runners = append(runners, runner)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runners...).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunnerSet{}, &actionsv1alpha1.EphemeralRunner{}).
WithIndex(&actionsv1alpha1.EphemeralRunner{}, resourceOwnerKey, newGroupVersionOwnerKindIndexer("EphemeralRunnerSet")).
Build()
reconciler := &EphemeralRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-ers",
},
}
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
})
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_FinalizerAdd benchmarks the
// reconciliation path where the finalizer needs to be added to a new EphemeralRunnerSet
func BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_FinalizerAdd(b *testing.B) {
testCases := []struct {
name string
replicas int
}{
{name: "small_3_replicas", replicas: 3},
{name: "medium_10_replicas", replicas: 10},
{name: "large_50_replicas", replicas: 50},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
b.ResetTimer()
for b.Loop() {
b.StopTimer()
ers := NewMinimalEphemeralRunnerSet("default", "test-ers")
ers.Spec.Replicas = tc.replicas
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ers).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunnerSet{}, &actionsv1alpha1.EphemeralRunner{}).
WithIndex(&actionsv1alpha1.EphemeralRunner{}, resourceOwnerKey, newGroupVersionOwnerKindIndexer("EphemeralRunnerSet")).
Build()
reconciler := &EphemeralRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-ers",
},
}
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
})
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_HashChangeDetection benchmarks the
// reconciliation path that detects a spec hash change (but before cleanup begins)
func BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_HashChangeDetection(b *testing.B) {
testCases := []struct {
name string
replicas int
}{
{name: "small_5_replicas", replicas: 5},
{name: "medium_20_replicas", replicas: 20},
{name: "large_100_replicas", replicas: 100},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
b.ResetTimer()
for b.Loop() {
b.StopTimer()
ers := NewMinimalEphemeralRunnerSet("default", "test-ers")
ers.Spec.Replicas = tc.replicas
controllerutil.AddFinalizer(ers, EphemeralRunnerSetFinalizerName)
if ers.Annotations == nil {
ers.Annotations = make(map[string]string)
}
ers.Annotations[AnnotationKeyIntegrityHash] = "outdated-hash-12345"
ers.Status.CurrentReplicas = tc.replicas
ers.Status.Phase = actionsv1alpha1.EphemeralRunnerSetPhaseRunning
runners := make([]client.Object, 0, tc.replicas+1)
runners = append(runners, ers)
for j := 0; j < tc.replicas; j++ {
runner := NewMinimalEphemeralRunner("default", fmt.Sprintf("test-runner-%d", j))
runner.Status.Phase = actionsv1alpha1.EphemeralRunnerPhaseSucceeded
runner.Status.RunnerID = j + 100
runner.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "EphemeralRunnerSet",
Name: ers.Name,
UID: ers.UID,
Controller: func() *bool { t := true; return &t }(),
},
}
runners = append(runners, runner)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runners...).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunnerSet{}, &actionsv1alpha1.EphemeralRunner{}).
WithIndex(&actionsv1alpha1.EphemeralRunner{}, resourceOwnerKey, newGroupVersionOwnerKindIndexer("EphemeralRunnerSet")).
Build()
reconciler := &EphemeralRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-ers",
},
}
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
})
}
}
// BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_OutdatedPhaseStatusUpdate benchmarks the
// reconciliation path where the EphemeralRunnerSet is in Outdated phase with only finished runners
func BenchmarkActionsGithub_Reconcile_EphemeralRunnerSet_OutdatedPhaseStatusUpdate(b *testing.B) {
testCases := []struct {
name string
runnerCount int
}{
{name: "small_5_runners", runnerCount: 5},
{name: "medium_20_runners", runnerCount: 20},
{name: "large_100_runners", runnerCount: 100},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
scheme := runtime.NewScheme()
_ = actionsv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
b.ResetTimer()
for b.Loop() {
b.StopTimer()
ers := NewMinimalEphemeralRunnerSet("default", "test-ers")
ers.Spec.Replicas = tc.runnerCount
controllerutil.AddFinalizer(ers, EphemeralRunnerSetFinalizerName)
integrityHash := ephemeralRunnerSetIntegrityHash(ers)
if ers.Annotations == nil {
ers.Annotations = make(map[string]string)
}
ers.Annotations[AnnotationKeyIntegrityHash] = integrityHash
ers.Status.CurrentReplicas = tc.runnerCount
ers.Status.Phase = actionsv1alpha1.EphemeralRunnerSetPhaseOutdated
runners := make([]client.Object, 0, tc.runnerCount+1)
runners = append(runners, ers)
for j := 0; j < tc.runnerCount; j++ {
runner := NewMinimalEphemeralRunner("default", fmt.Sprintf("test-runner-%d", j))
runner.Status.Phase = actionsv1alpha1.EphemeralRunnerPhaseSucceeded
runner.Status.RunnerID = j + 100
runner.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: actionsv1alpha1.GroupVersion.String(),
Kind: "EphemeralRunnerSet",
Name: ers.Name,
UID: ers.UID,
Controller: func() *bool { t := true; return &t }(),
},
}
runners = append(runners, runner)
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(runners...).
WithStatusSubresource(&actionsv1alpha1.EphemeralRunnerSet{}, &actionsv1alpha1.EphemeralRunner{}).
WithIndex(&actionsv1alpha1.EphemeralRunner{}, resourceOwnerKey, newGroupVersionOwnerKindIndexer("EphemeralRunnerSet")).
Build()
reconciler := &EphemeralRunnerSetReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
}
ctx := context.Background()
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "test-ers",
},
}
b.StartTimer()
_, _ = reconciler.Reconcile(ctx, req)
}
})
}
}