mirror of
https://github.com/actions-runner-controller/actions-runner-controller.git
synced 2026-09-30 02:35:57 +02:00
193 lines
6.3 KiB
Go
193 lines
6.3 KiB
Go
/*
|
|
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"
|
|
|
|
"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"
|
|
corev1 "k8s.io/api/core/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
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"
|
|
ctrlfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
|
|
)
|
|
|
|
func TestReconcileDefersRunnerIdentityUntilPodReportsStatus(t *testing.T) {
|
|
ctx := context.Background()
|
|
key := types.NamespacedName{Namespace: "default", Name: "test-runner"}
|
|
|
|
scheme := runtime.NewScheme()
|
|
require.NoError(t, corev1.AddToScheme(scheme))
|
|
require.NoError(t, v1alpha1.AddToScheme(scheme))
|
|
|
|
runner := &v1alpha1.EphemeralRunner{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
Finalizers: []string{ephemeralRunnerFinalizerName, ephemeralRunnerActionsFinalizerName},
|
|
},
|
|
Spec: v1alpha1.EphemeralRunnerSpec{
|
|
PodTemplateSpec: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{{Name: v1alpha1.EphemeralRunnerContainerName}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Name: key.Name, Namespace: key.Namespace},
|
|
Data: map[string][]byte{
|
|
jitTokenKey: []byte("jit-token"),
|
|
"runnerId": []byte("7"),
|
|
"runnerName": []byte("test-runner"),
|
|
},
|
|
}
|
|
|
|
failStatusPatch := true
|
|
statusPatchAttempts := 0
|
|
c := ctrlfake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithObjects(runner, secret).
|
|
WithStatusSubresource(&v1alpha1.EphemeralRunner{}, &corev1.Pod{}).
|
|
WithInterceptorFuncs(interceptor.Funcs{
|
|
SubResourcePatch: func(ctx context.Context, clt client.Client, subResourceName string, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
|
|
if _, ok := obj.(*v1alpha1.EphemeralRunner); ok {
|
|
statusPatchAttempts++
|
|
if failStatusPatch {
|
|
failStatusPatch = false
|
|
return errors.New("transient status patch failure")
|
|
}
|
|
}
|
|
return clt.Status().Patch(ctx, obj, patch, opts...)
|
|
},
|
|
}).
|
|
Build()
|
|
|
|
newReconciler := func() *EphemeralRunnerReconciler {
|
|
return &EphemeralRunnerReconciler{
|
|
Client: c,
|
|
Log: logr.Discard(),
|
|
Scheme: scheme,
|
|
ResourceBuilder: ResourceBuilder{
|
|
Scheme: scheme,
|
|
},
|
|
}
|
|
}
|
|
podCount := func() int {
|
|
t.Helper()
|
|
|
|
var pods corev1.PodList
|
|
require.NoError(t, c.List(ctx, &pods, client.InNamespace(key.Namespace)))
|
|
return len(pods.Items)
|
|
}
|
|
getRunner := func() *v1alpha1.EphemeralRunner {
|
|
t.Helper()
|
|
|
|
got := new(v1alpha1.EphemeralRunner)
|
|
require.NoError(t, c.Get(ctx, key, got))
|
|
return got
|
|
}
|
|
|
|
// The first pass creates the Pod but deliberately leaves status unwritten.
|
|
_, err := newReconciler().Reconcile(ctx, ctrl.Request{NamespacedName: key})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, podCount())
|
|
assert.Zero(t, getRunner().Status.RunnerID)
|
|
assert.Empty(t, getRunner().Status.RunnerName)
|
|
assert.Zero(t, statusPatchAttempts, "the runner identity must not delay Pod creation")
|
|
|
|
// Identity remains deferred until the Pod reports a non-terminal container
|
|
// status. A transient failure of that coalesced status patch returns an
|
|
// error for reconciliation to retry without creating another Pod.
|
|
pod := new(corev1.Pod)
|
|
require.NoError(t, c.Get(ctx, key, pod))
|
|
pod.Status.Phase = corev1.PodRunning
|
|
pod.Status.ContainerStatuses = []corev1.ContainerStatus{{
|
|
Name: v1alpha1.EphemeralRunnerContainerName,
|
|
State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}},
|
|
}}
|
|
require.NoError(t, c.Status().Update(ctx, pod))
|
|
|
|
_, err = newReconciler().Reconcile(ctx, ctrl.Request{NamespacedName: key})
|
|
require.ErrorContains(t, err, "failed to update runner status for Phase/Reason/Message/Ready")
|
|
assert.Equal(t, 1, podCount())
|
|
assert.Zero(t, getRunner().Status.RunnerID)
|
|
assert.Empty(t, getRunner().Status.RunnerName)
|
|
|
|
_, err = newReconciler().Reconcile(ctx, ctrl.Request{NamespacedName: key})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, podCount())
|
|
assert.Equal(t, 2, statusPatchAttempts)
|
|
assert.Equal(t, 7, getRunner().Status.RunnerID)
|
|
assert.Equal(t, "test-runner", getRunner().Status.RunnerName)
|
|
}
|
|
|
|
func TestReconcileRejectsMalformedJITSecretBeforeCreatingPod(t *testing.T) {
|
|
ctx := context.Background()
|
|
key := types.NamespacedName{Namespace: "default", Name: "test-runner"}
|
|
|
|
scheme := runtime.NewScheme()
|
|
require.NoError(t, corev1.AddToScheme(scheme))
|
|
require.NoError(t, v1alpha1.AddToScheme(scheme))
|
|
|
|
runner := newExampleRunner(key.Name, key.Namespace, "github-config-secret")
|
|
runner.Finalizers = []string{ephemeralRunnerFinalizerName, ephemeralRunnerActionsFinalizerName}
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Name: key.Name, Namespace: key.Namespace},
|
|
Data: map[string][]byte{
|
|
jitTokenKey: []byte("jit-token"),
|
|
"runnerName": []byte(key.Name),
|
|
},
|
|
}
|
|
c := ctrlfake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithObjects(runner, secret).
|
|
WithStatusSubresource(&v1alpha1.EphemeralRunner{}).
|
|
Build()
|
|
|
|
reconciler := &EphemeralRunnerReconciler{
|
|
Client: c,
|
|
APIReader: c,
|
|
Log: logr.Discard(),
|
|
Scheme: scheme,
|
|
ResourceBuilder: ResourceBuilder{
|
|
Scheme: scheme,
|
|
},
|
|
}
|
|
|
|
result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: key})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 500*time.Millisecond, result.RequeueAfter)
|
|
|
|
err = c.Get(ctx, key, new(corev1.Pod))
|
|
assert.True(t, kerrors.IsNotFound(err), "a malformed JIT Secret must never produce a Pod")
|
|
|
|
err = c.Get(ctx, key, new(corev1.Secret))
|
|
assert.True(t, kerrors.IsNotFound(err), "the malformed JIT Secret must be deleted")
|
|
}
|