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

136 lines
4.5 KiB
Go

package actionsgithubcom
import (
"context"
"testing"
"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
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"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
)
// TestEphemeralRunnerSetIgnoresTheTargetCountOnTheReconcileThatFindsOutdatedRunners
// pins that a rejected runner spec beats the count the listener asked for.
//
// Spec.Replicas is what the listener wanted, computed before any runner had
// reported anything. Once a runner rejects the spec it was given, that count is
// a request to create more runners that will reject it in exactly the same way,
// so it must not be acted on.
//
// The phase alone is not enough to enforce that. Status.Phase only turns
// Outdated at the end of the reconcile that discovers the rejection, so the
// reconcile that finds it first still reaches the scaling block below with a
// phase that still says Running, and scales up against a spec already known to
// be bad. Only the reconcile after that takes the early return. The gate has to
// be the runners themselves.
func TestEphemeralRunnerSetIgnoresTheTargetCountOnTheReconcileThatFindsOutdatedRunners(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
require.NoError(t, v1alpha1.AddToScheme(scheme))
const (
name = "test-ers"
namespace = "default"
)
ephemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Finalizers: []string{EphemeralRunnerSetFinalizerName},
},
Spec: v1alpha1.EphemeralRunnerSetSpec{
// The listener has published a fresh target since the rejection.
// It knows nothing about it: it counts queued jobs, not whether the
// runners it asks for can run at all.
Replicas: 5,
PatchID: 4,
ActionableRevision: 2,
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigURL: "https://github.com/owner/repo",
},
},
Status: v1alpha1.EphemeralRunnerSetStatus{
// Still Running: this is the reconcile that discovers the rejection.
Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
AppliedActionableRevision: 2,
},
}
controller := true
rejectedRunner := &v1alpha1.EphemeralRunner{
ObjectMeta: metav1.ObjectMeta{
Name: "runner-that-rejected-the-spec",
Namespace: namespace,
Annotations: map[string]string{
AnnotationKeyActionableRevision: "2",
AnnotationKeyPatchID: "3",
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: v1alpha1.GroupVersion.String(),
Kind: "EphemeralRunnerSet",
Name: name,
UID: "test-uid",
Controller: &controller,
},
},
},
Status: v1alpha1.EphemeralRunnerStatus{
Phase: v1alpha1.EphemeralRunnerPhaseOutdated,
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ephemeralRunnerSet, rejectedRunner).
WithStatusSubresource(&v1alpha1.EphemeralRunnerSet{}, &v1alpha1.EphemeralRunner{}).
WithIndex(&v1alpha1.EphemeralRunner{}, resourceOwnerKey, newGroupVersionOwnerKindIndexer("EphemeralRunnerSet")).
Build()
resourceCache := NewResourceCache()
reconciler := &EphemeralRunnerSetReconciler{
Client: fakeClient,
APIReader: fakeClient,
Log: logr.Discard(),
Scheme: scheme,
ResourceBuilder: ResourceBuilder{
ResourceCache: &resourceCache,
Scheme: scheme,
},
}
key := types.NamespacedName{Namespace: namespace, Name: name}
_, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
runners := new(v1alpha1.EphemeralRunnerList)
require.NoError(t, fakeClient.List(context.Background(), runners, client.InNamespace(namespace)))
for _, runner := range runners.Items {
require.Equal(
t,
rejectedRunner.Name,
runner.Name,
"no runner may be created from a spec the runners have already rejected, whatever count the listener asked for",
)
}
got := new(v1alpha1.EphemeralRunnerSet)
require.NoError(t, fakeClient.Get(context.Background(), key, got))
require.Equal(
t,
v1alpha1.EphemeralRunnerSetPhaseOutdated,
got.Status.Phase,
"the rejection must be recorded on the same reconcile that found it",
)
}