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

115 lines
4.6 KiB
Go

package actionsgithubcom
import (
"context"
"testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
"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"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
// TestUpdateStatusStaleOutdatedRunnersNeverSetTheOutdatedPhase pins the
// invariant that makes the Outdated early return in Reconcile safe.
//
// The Outdated branch returns through cleanUpEphemeralRunners without
// recomputing the phase, so the stale-runner recovery block further down is
// unreachable while the set is already Outdated. That is only harmless because
// a stale runner cannot put the set into the Outdated phase in the first place:
// updateStatus switches to Outdated on len(outdated) alone, and a runner whose
// revision predates the applied one is classified staleOutdated instead.
//
// The case that matters for upgrades is a runner carrying no revision
// annotation at all. It parses to revision 0, so on a set that has applied any
// revision above 0 it is stale, not outdated, and cannot strand the set in a
// phase that stops it scaling. Stale runners are still collected -- terminated()
// includes them -- they simply are not evidence about the current runner spec.
//
// Recomputing the phase inside the Outdated branch instead, as review has twice
// proposed, would not fix anything here and would break the phase outright:
// cleanUpEphemeralRunners deletes terminated(), which includes outdated, so
// "no outdated runners remain" is trivially true immediately after it runs. The
// set would return to Running against an unchanged spec, scale a replacement
// runner from that same spec, and be told Outdated again on the next pass.
func TestUpdateStatusStaleOutdatedRunnersNeverSetTheOutdatedPhase(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
require.NoError(t, v1alpha1.AddToScheme(scheme))
legacyRunner := func() v1alpha1.EphemeralRunner {
return v1alpha1.EphemeralRunner{
ObjectMeta: metav1.ObjectMeta{Name: "legacy-runner", Namespace: "default"},
Status: v1alpha1.EphemeralRunnerStatus{Phase: v1alpha1.EphemeralRunnerPhaseOutdated},
}
}
tests := []struct {
name string
appliedRevision int64
startingPhase v1alpha1.EphemeralRunnerSetPhase
wantPhase v1alpha1.EphemeralRunnerSetPhase
}{
{
// The upgrade case from review: an annotation-less runner on a set
// that has already applied a revision must not stop it scaling.
name: "legacy runner on an upgraded set leaves the phase Running",
appliedRevision: 4,
startingPhase: v1alpha1.EphemeralRunnerSetPhaseRunning,
wantPhase: v1alpha1.EphemeralRunnerSetPhaseRunning,
},
{
// Before any revision is applied the same runner is genuinely
// outdated, which is what keeps upgrades behaving as they do today.
name: "legacy runner before any revision is applied still reports Outdated",
appliedRevision: 0,
startingPhase: v1alpha1.EphemeralRunnerSetPhaseRunning,
wantPhase: v1alpha1.EphemeralRunnerSetPhaseOutdated,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ephemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
ObjectMeta: metav1.ObjectMeta{Name: "test-ers", Namespace: "default"},
Status: v1alpha1.EphemeralRunnerSetStatus{
AppliedActionableRevision: tt.appliedRevision,
Phase: tt.startingPhase,
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ephemeralRunnerSet).
WithStatusSubresource(&v1alpha1.EphemeralRunnerSet{}).
Build()
reconciler := &EphemeralRunnerSetReconciler{
Client: fakeClient,
Log: logr.Discard(),
Scheme: scheme,
}
list := &v1alpha1.EphemeralRunnerList{Items: []v1alpha1.EphemeralRunner{legacyRunner()}}
state := newEphemeralRunnersByStates(list, tt.appliedRevision)
require.NoError(t, reconciler.updateStatus(context.Background(), ephemeralRunnerSet, state, logr.Discard()))
var patched v1alpha1.EphemeralRunnerSet
require.NoError(t, fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "default", Name: "test-ers"}, &patched))
assert.Equal(
t,
tt.wantPhase,
patched.Status.Phase,
"a stale runner must not drive the set into a phase that stops it scaling, because the Outdated branch returns without recomputing the phase",
)
})
}
}