1922 lines
68 KiB
Go
1922 lines
68 KiB
Go
package actionsgithubcom
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/actions/actions-runner-controller/controllers/actions.github.com/metrics"
|
|
"github.com/actions/actions-runner-controller/github/actions"
|
|
"github.com/actions/scaleset"
|
|
prometheusdto "github.com/prometheus/client_model/go"
|
|
corev1 "k8s.io/api/core/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
controllerMetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"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/multiclient/fake"
|
|
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
|
|
)
|
|
|
|
const (
|
|
ephemeralRunnerSetTestTimeout = time.Second * 20
|
|
ephemeralRunnerSetTestInterval = time.Millisecond * 250
|
|
)
|
|
|
|
var registerActionsGithubMetricsForTest sync.Once
|
|
|
|
func TestPrecomputedConstants(t *testing.T) {
|
|
require.Equal(t, len(failedRunnerBackoff), maxFailures+1)
|
|
}
|
|
|
|
func expectEphemeralRunnerPhase(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, phase v1alpha1.EphemeralRunnerPhase) {
|
|
updated := new(v1alpha1.EphemeralRunner)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get ephemeral runner")
|
|
Expect(updated.Status.Phase).To(Equal(phase))
|
|
}
|
|
|
|
func expectEphemeralRunnerPhaseMetric(ephemeralRunner *v1alpha1.EphemeralRunner, phase v1alpha1.EphemeralRunnerPhase, expected float64) {
|
|
metricName := ephemeralRunnerPhaseMetricName(phase)
|
|
Expect(metricName).NotTo(BeEmpty(), "unexpected ephemeral runner phase")
|
|
|
|
parsedURL, err := actions.ParseGitHubConfigFromURL(ephemeralRunner.Spec.GitHubConfigURL)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to parse GitHub config URL")
|
|
|
|
value, err := ephemeralRunnerPhaseMetricValue(metricName, map[string]string{
|
|
"name": ephemeralRunner.Labels[LabelKeyGitHubScaleSetName],
|
|
"namespace": ephemeralRunner.Labels[LabelKeyGitHubScaleSetNamespace],
|
|
"repository": parsedURL.Repository,
|
|
"organization": parsedURL.Organization,
|
|
"enterprise": parsedURL.Enterprise,
|
|
})
|
|
Expect(err).NotTo(HaveOccurred(), "failed to gather ephemeral runner phase metrics")
|
|
Expect(value).To(Equal(expected))
|
|
}
|
|
|
|
func ephemeralRunnerPhaseMetricName(phase v1alpha1.EphemeralRunnerPhase) string {
|
|
switch phase {
|
|
case v1alpha1.EphemeralRunnerPhasePending:
|
|
return "gha_controller_pending_ephemeral_runners"
|
|
case v1alpha1.EphemeralRunnerPhaseRunning:
|
|
return "gha_controller_running_ephemeral_runners"
|
|
case v1alpha1.EphemeralRunnerPhaseSucceeded:
|
|
return "gha_controller_succeeded_ephemeral_runners"
|
|
case v1alpha1.EphemeralRunnerPhaseFailed:
|
|
return "gha_controller_failed_ephemeral_runners"
|
|
case v1alpha1.EphemeralRunnerPhaseOutdated:
|
|
return "gha_controller_outdated_ephemeral_runners"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func ephemeralRunnerPhaseMetricValue(metricName string, labels map[string]string) (float64, error) {
|
|
metricFamilies, err := controllerMetrics.Registry.Gather()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
for _, metricFamily := range metricFamilies {
|
|
if metricFamily.GetName() != metricName {
|
|
continue
|
|
}
|
|
|
|
for _, metric := range metricFamily.GetMetric() {
|
|
if metricHasLabels(metric, labels) {
|
|
if metric.GetGauge() == nil {
|
|
return 0, nil
|
|
}
|
|
return metric.GetGauge().GetValue(), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0, nil
|
|
}
|
|
|
|
func metricHasLabels(metric *prometheusdto.Metric, labels map[string]string) bool {
|
|
metricLabels := map[string]string{}
|
|
for _, label := range metric.GetLabel() {
|
|
metricLabels[label.GetName()] = label.GetValue()
|
|
}
|
|
|
|
for name, value := range labels {
|
|
if metricLabels[name] != value {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
var _ = Describe("Test EphemeralRunnerSet controller", func() {
|
|
var ctx context.Context
|
|
var mgr ctrl.Manager
|
|
var autoscalingNS *corev1.Namespace
|
|
var ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet
|
|
var configSecret *corev1.Secret
|
|
|
|
BeforeEach(func() {
|
|
ctx = context.Background()
|
|
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
|
|
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
|
|
|
|
controller := &EphemeralRunnerSetReconciler{
|
|
Client: mgr.GetClient(),
|
|
Scheme: mgr.GetScheme(),
|
|
Log: logf.Log,
|
|
ResourceBuilder: ResourceBuilder{
|
|
SecretResolver: secretresolver.New(mgr.GetClient(), fake.NewMultiClient(
|
|
fake.WithClient(
|
|
fake.NewClient(
|
|
fake.WithRemoveRunner(nil),
|
|
),
|
|
),
|
|
)),
|
|
},
|
|
}
|
|
err := controller.SetupWithManager(mgr)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
|
|
|
ephemeralRunnerSet = &v1alpha1.EphemeralRunnerSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-asrs",
|
|
Namespace: autoscalingNS.Name,
|
|
Annotations: map[string]string{
|
|
"arc.test/runner-set-annotation": "initial",
|
|
},
|
|
},
|
|
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
|
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
|
|
GitHubConfigURL: "https://github.com/owner/repo",
|
|
GitHubConfigSecret: configSecret.Name,
|
|
RunnerScaleSetID: 100,
|
|
PodTemplateSpec: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "runner",
|
|
Image: "ghcr.io/actions/runner",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(ctx, ephemeralRunnerSet)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create EphemeralRunnerSet")
|
|
|
|
startManagers(GinkgoT(), mgr)
|
|
})
|
|
|
|
Context("When creating a new EphemeralRunnerSet", func() {
|
|
It("It should create/add all required resources for a new EphemeralRunnerSet (finalizer)", func() {
|
|
// Check if finalizer is added
|
|
created := new(v1alpha1.EphemeralRunnerSet)
|
|
Eventually(
|
|
func() (string, error) {
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, created)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(created.Finalizers) == 0 {
|
|
return "", nil
|
|
}
|
|
return created.Finalizers[0], nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(EphemeralRunnerSetFinalizerName), "EphemeralRunnerSet should have a finalizer")
|
|
|
|
// Check if the number of ephemeral runners are stay 0
|
|
Consistently(
|
|
func() (int, error) {
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
if err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(0), "No EphemeralRunner should be created")
|
|
|
|
// Check if the status is initialized
|
|
Consistently(
|
|
func() (v1alpha1.EphemeralRunnerSetPhase, error) {
|
|
runnerSet := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, runnerSet)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return runnerSet.Status.Phase, nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(v1alpha1.EphemeralRunnerSetPhaseRunning), "EphemeralRunnerSet status should be running")
|
|
|
|
// Scaling up the EphemeralRunnerSet
|
|
updated := created.DeepCopy()
|
|
updated.Spec.Replicas = 5
|
|
err := k8sClient.Update(ctx, updated)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
// Check if the number of ephemeral runners are created
|
|
Eventually(
|
|
func() (int, error) {
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
if err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
// Set status to simulate a configured EphemeralRunner
|
|
refetch := false
|
|
for i, runner := range runnerList.Items {
|
|
if runner.Status.RunnerID == 0 {
|
|
updatedRunner := runner.DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
updatedRunner.Status.RunnerID = i + 100
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
refetch = true
|
|
}
|
|
}
|
|
|
|
if refetch {
|
|
if err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace); err != nil {
|
|
return -1, err
|
|
}
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(5), "5 EphemeralRunner should be created")
|
|
|
|
// Check if the status stays running
|
|
Eventually(
|
|
func() (v1alpha1.EphemeralRunnerSetPhase, error) {
|
|
runnerSet := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, runnerSet)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return runnerSet.Status.Phase, nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(v1alpha1.EphemeralRunnerSetPhaseRunning), "EphemeralRunnerSet status should be running")
|
|
})
|
|
})
|
|
|
|
Context("When deleting a new EphemeralRunnerSet", func() {
|
|
It("It should cleanup all resources for a deleting EphemeralRunnerSet before removing it", func() {
|
|
created := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, created)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
// Scale up the EphemeralRunnerSet
|
|
updated := created.DeepCopy()
|
|
updated.Spec.Replicas = 5
|
|
err = k8sClient.Update(ctx, updated)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
// Wait for the EphemeralRunnerSet to be scaled up
|
|
Eventually(
|
|
func() (int, error) {
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
if err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
// Set status to simulate a configured EphemeralRunner
|
|
refetch := false
|
|
for i, runner := range runnerList.Items {
|
|
if runner.Status.RunnerID == 0 {
|
|
updatedRunner := runner.DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
updatedRunner.Status.RunnerID = i + 100
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
refetch = true
|
|
}
|
|
}
|
|
|
|
if refetch {
|
|
if err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace); err != nil {
|
|
return -1, err
|
|
}
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(5), "5 EphemeralRunner should be created")
|
|
|
|
// Delete the EphemeralRunnerSet
|
|
err = k8sClient.Delete(ctx, created)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to delete EphemeralRunnerSet")
|
|
|
|
// Check if all ephemeral runners are deleted
|
|
Eventually(
|
|
func() (int, error) {
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
if err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(0), "All EphemeralRunner should be deleted")
|
|
|
|
// Check if the EphemeralRunnerSet is deleted
|
|
Eventually(
|
|
func() error {
|
|
deleted := new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, deleted)
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return fmt.Errorf("EphemeralRunnerSet is not deleted")
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(Succeed(), "EphemeralRunnerSet should be deleted")
|
|
})
|
|
})
|
|
|
|
Context("When a new EphemeralRunnerSet scale up and down", func() {
|
|
It("Should scale up with patch ID 0", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 5
|
|
updated.Spec.PatchID = 0
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
if err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(5), "5 EphemeralRunner should be created")
|
|
})
|
|
|
|
It("propagates updated EphemeralRunnerSet annotations to newly created EphemeralRunners", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 1
|
|
updated.Spec.PatchID = 0
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to scale EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func(g Gomega) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
g.Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunners")
|
|
g.Expect(runnerList.Items).To(HaveLen(1))
|
|
g.Expect(runnerList.Items[0].Annotations["arc.test/runner-set-annotation"]).To(Equal("initial"))
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(Succeed())
|
|
|
|
ers = new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated = ers.DeepCopy()
|
|
updated.Annotations["arc.test/runner-set-annotation"] = "updated"
|
|
updated.Annotations["arc.test/new-runner-set-annotation"] = "added"
|
|
updated.Spec.Replicas = 2
|
|
updated.Spec.PatchID = 1
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet annotations")
|
|
|
|
Eventually(
|
|
func(g Gomega) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
g.Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunners")
|
|
g.Expect(runnerList.Items).To(HaveLen(2))
|
|
|
|
annotationsByValue := map[string]int{}
|
|
var updatedRunnerHasNewAnnotation bool
|
|
for _, runner := range runnerList.Items {
|
|
annotationsByValue[runner.Annotations["arc.test/runner-set-annotation"]]++
|
|
if runner.Annotations["arc.test/runner-set-annotation"] == "updated" && runner.Annotations["arc.test/new-runner-set-annotation"] == "added" {
|
|
updatedRunnerHasNewAnnotation = true
|
|
}
|
|
}
|
|
g.Expect(annotationsByValue["initial"]).To(Equal(1))
|
|
g.Expect(annotationsByValue["updated"]).To(Equal(1))
|
|
g.Expect(updatedRunnerHasNewAnnotation).To(BeTrue())
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(Succeed())
|
|
})
|
|
|
|
It("Should scale up when patch ID changes", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 1
|
|
updated.Spec.PatchID = 0
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
if err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(1), "1 EphemeralRunner should be created")
|
|
|
|
ers = new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated = ers.DeepCopy()
|
|
updated.Spec.Replicas = 2
|
|
updated.Spec.PatchID = 1
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
|
})
|
|
|
|
It("Should clean up finished ephemeral runner when scaling down", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 2
|
|
updated.Spec.PatchID = 1
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
|
|
|
updatedRunner := runnerList.Items[0].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
updatedRunner = runnerList.Items[1].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
// Keep the ephemeral runner until the next patch
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "1 EphemeralRunner should be up")
|
|
|
|
// The listener was slower to patch the completed, but we should still have 1 running
|
|
ers = new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated = ers.DeepCopy()
|
|
updated.Spec.Replicas = 1
|
|
updated.Spec.PatchID = 2
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(1), "1 Ephemeral runner should be up")
|
|
})
|
|
|
|
It("Should keep finished ephemeral runners until patch id changes", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 2
|
|
updated.Spec.PatchID = 1
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
|
|
|
updatedRunner := runnerList.Items[0].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
updatedRunner = runnerList.Items[1].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhasePending
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
// confirm they are not deleted
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Consistently(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
5*time.Second,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
|
})
|
|
|
|
It("Should handle double scale up", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 2
|
|
updated.Spec.PatchID = 1
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
|
|
|
updatedRunner := runnerList.Items[0].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
updatedRunner = runnerList.Items[1].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
ers = new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated = ers.DeepCopy()
|
|
updated.Spec.Replicas = 3
|
|
updated.Spec.PatchID = 2
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
// We should have 3 runners, and have no Succeeded ones
|
|
Eventually(
|
|
func() error {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(runnerList.Items) != 3 {
|
|
return fmt.Errorf("Expected 3 runners, got %d", len(runnerList.Items))
|
|
}
|
|
|
|
for _, runner := range runnerList.Items {
|
|
if runner.Status.Phase == v1alpha1.EphemeralRunnerPhaseSucceeded {
|
|
return fmt.Errorf("Runner %s is in Succeeded phase", runner.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeNil(), "3 EphemeralRunner should be created and none should be in Succeeded phase")
|
|
})
|
|
|
|
It("Should handle scale down without removing pending runners", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 2
|
|
updated.Spec.PatchID = 1
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
|
|
|
updatedRunner := runnerList.Items[0].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
updatedRunner = runnerList.Items[1].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhasePending
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
// Wait for these statuses to actually be updated
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() error {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pending := 0
|
|
succeeded := 0
|
|
for _, runner := range runnerList.Items {
|
|
switch runner.Status.Phase {
|
|
case v1alpha1.EphemeralRunnerPhaseSucceeded:
|
|
succeeded++
|
|
case v1alpha1.EphemeralRunnerPhasePending:
|
|
pending++
|
|
}
|
|
}
|
|
|
|
if pending != 1 && succeeded != 1 {
|
|
return fmt.Errorf("Expected 1 runner in Pending and 1 in Succeeded, got %d in Pending and %d in Succeeded", pending, succeeded)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeNil(), "1 EphemeralRunner should be in Pending and 1 in Succeeded phase")
|
|
|
|
// Scale down to 0, while 1 is still pending. This simulates the difference between the desired and actual state
|
|
ers = new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated = ers.DeepCopy()
|
|
updated.Spec.Replicas = 0
|
|
updated.Spec.PatchID = 2
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
// We should have 1 runner up and pending
|
|
Eventually(
|
|
func() error {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(runnerList.Items) != 1 {
|
|
return fmt.Errorf("Expected 1 runner, got %d", len(runnerList.Items))
|
|
}
|
|
|
|
if runnerList.Items[0].Status.Phase != v1alpha1.EphemeralRunnerPhasePending {
|
|
return fmt.Errorf("Expected runner to be in Pending, got %s", runnerList.Items[0].Status.Phase)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeNil(), "1 EphemeralRunner should be created and in Pending phase")
|
|
|
|
// Now, the ephemeral runner finally is done and we can scale down to 0
|
|
updatedRunner = runnerList.Items[0].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(0), "2 EphemeralRunner should be created")
|
|
})
|
|
|
|
It("Should kill pending and running runners if they are up for some reason and the batch contains no jobs", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 2
|
|
updated.Spec.PatchID = 1
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
|
|
|
// Put one runner in Pending and one in Running
|
|
updatedRunner := runnerList.Items[0].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhasePending
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
updatedRunner = runnerList.Items[1].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
// Wait for these statuses to actually be updated
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() error {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pending := 0
|
|
running := 0
|
|
|
|
for _, runner := range runnerList.Items {
|
|
switch runner.Status.Phase {
|
|
case v1alpha1.EphemeralRunnerPhasePending:
|
|
pending++
|
|
case v1alpha1.EphemeralRunnerPhaseRunning:
|
|
running++
|
|
|
|
}
|
|
}
|
|
|
|
if pending != 1 && running != 1 {
|
|
return fmt.Errorf("Expected 1 runner in Pending and 1 in Running, got %d in Pending and %d in Running", pending, running)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeNil(), "1 EphemeralRunner should be in Pending and 1 in Running phase")
|
|
|
|
// Scale down to 0 with patch ID 0. This forces the scale down to self correct on empty batch
|
|
|
|
ers = new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated = ers.DeepCopy()
|
|
updated.Spec.Replicas = 0
|
|
updated.Spec.PatchID = 0
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Consistently(
|
|
func() (int, error) {
|
|
if err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be up since they don't have an ID yet")
|
|
|
|
// Now, let's say ephemeral runner controller patched these ephemeral runners with the registration.
|
|
|
|
updatedRunner = runnerList.Items[0].DeepCopy()
|
|
updatedRunner.Status.RunnerID = 1
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
updatedRunner = runnerList.Items[1].DeepCopy()
|
|
updatedRunner.Status.RunnerID = 2
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
// Now, eventually, they should be deleted
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(0), "0 EphemeralRunner should exist")
|
|
})
|
|
|
|
It("Should replace finished ephemeral runners with new ones", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 2
|
|
updated.Spec.PatchID = 1
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
|
|
|
// Put one runner in Succeeded and one in Running
|
|
updatedRunner := runnerList.Items[0].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[0]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
updatedRunner = runnerList.Items[1].DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runnerList.Items[1]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
|
|
// Wait for these statuses to actually be updated
|
|
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() error {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
succeeded := 0
|
|
running := 0
|
|
|
|
for _, runner := range runnerList.Items {
|
|
switch runner.Status.Phase {
|
|
case v1alpha1.EphemeralRunnerPhaseSucceeded:
|
|
succeeded++
|
|
case v1alpha1.EphemeralRunnerPhaseRunning:
|
|
running++
|
|
}
|
|
}
|
|
|
|
if succeeded != 1 && running != 1 {
|
|
return fmt.Errorf("Expected 1 runner in Succeeded and 1 in Running, got %d in Succeeded and %d in Running", succeeded, running)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeNil(), "1 EphemeralRunner should be in Succeeded and 1 in Running phase")
|
|
|
|
// Now, let's simulate replacement. The desired count is still 2.
|
|
// This simulates that we got 1 job assigned, and 1 job completed.
|
|
|
|
ers = new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated = ers.DeepCopy()
|
|
updated.Spec.Replicas = 2
|
|
updated.Spec.PatchID = 2
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() error {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(runnerList.Items) != 2 {
|
|
return fmt.Errorf("Expected 2 runners, got %d", len(runnerList.Items))
|
|
}
|
|
|
|
for _, runner := range runnerList.Items {
|
|
if runner.Status.Phase == v1alpha1.EphemeralRunnerPhaseSucceeded {
|
|
return fmt.Errorf("Expected no runners in Succeeded phase, got one")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeNil(), "2 EphemeralRunner should be created and none should be in Succeeded phase")
|
|
})
|
|
|
|
It("Should delete idle runners, keep busy runners, and create new runners when the spec changes", func() {
|
|
ers := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updated := ers.DeepCopy()
|
|
updated.Spec.Replicas = 3
|
|
updated.Spec.PatchID = 0
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(3), "3 EphemeralRunner should be created")
|
|
|
|
idleRunnerNames := map[string]struct{}{}
|
|
for i := 0; i < 2; i++ {
|
|
idleRunner := runnerList.Items[i].DeepCopy()
|
|
idleRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
idleRunner.Status.RunnerID = i + 101
|
|
err = k8sClient.Status().Patch(ctx, idleRunner, client.MergeFrom(&runnerList.Items[i]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update idle EphemeralRunner")
|
|
idleRunnerNames[idleRunner.Name] = struct{}{}
|
|
}
|
|
|
|
busyRunner := runnerList.Items[2].DeepCopy()
|
|
busyRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
busyRunner.Status.RunnerID = 103
|
|
busyRunner.Status.JobID = "job-1"
|
|
busyRunner.Status.WorkflowRunID = 9001
|
|
err = k8sClient.Status().Patch(ctx, busyRunner, client.MergeFrom(&runnerList.Items[2]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update busy EphemeralRunner")
|
|
|
|
busyRunnerName := busyRunner.Name
|
|
|
|
ers = new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, ers)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to re-fetch EphemeralRunnerSet")
|
|
|
|
updated = ers.DeepCopy()
|
|
updated.Spec.EphemeralRunnerSpec.PodTemplateSpec.Spec.Containers[0].Image = "ghcr.io/actions/runner:new"
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(ers))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to patch EphemeralRunnerSet with new spec")
|
|
|
|
Eventually(
|
|
func() error {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(runnerList.Items) != 3 {
|
|
return fmt.Errorf("expected 3 runners after spec update, got %d", len(runnerList.Items))
|
|
}
|
|
|
|
busyRunnerFound := false
|
|
newSpecRunnerCount := 0
|
|
for _, runner := range runnerList.Items {
|
|
if _, ok := idleRunnerNames[runner.Name]; ok {
|
|
return fmt.Errorf("expected idle runner %s to be deleted", runner.Name)
|
|
}
|
|
|
|
if runner.Name == busyRunnerName {
|
|
busyRunnerFound = true
|
|
if !runner.HasJob() {
|
|
return fmt.Errorf("expected remaining runner to still be busy")
|
|
}
|
|
if runner.Spec.PodTemplateSpec.Spec.Containers[0].Image != "ghcr.io/actions/runner" {
|
|
return fmt.Errorf("expected busy runner to keep original image, got %s", runner.Spec.PodTemplateSpec.Spec.Containers[0].Image)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if len(runner.Spec.PodTemplateSpec.Spec.Containers) == 0 {
|
|
return fmt.Errorf("new runner has empty container spec")
|
|
}
|
|
|
|
if runner.Spec.PodTemplateSpec.Spec.Containers[0].Image != "ghcr.io/actions/runner:new" {
|
|
return fmt.Errorf("expected new runner image to be updated, got %s", runner.Spec.PodTemplateSpec.Spec.Containers[0].Image)
|
|
}
|
|
|
|
newSpecRunnerCount++
|
|
}
|
|
|
|
if !busyRunnerFound {
|
|
return fmt.Errorf("expected busy runner %s to remain", busyRunnerName)
|
|
}
|
|
|
|
if newSpecRunnerCount != 2 {
|
|
return fmt.Errorf("expected 2 runners with updated spec, got %d", newSpecRunnerCount)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeNil(), "busy runner should stay while idle runners are replaced with the updated spec")
|
|
})
|
|
|
|
It("Should update status on Ephemeral Runner state changes", func() {
|
|
created := new(v1alpha1.EphemeralRunnerSet)
|
|
Eventually(
|
|
func() error {
|
|
return k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, created)
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(Succeed(), "EphemeralRunnerSet should be created")
|
|
|
|
// Scale up the EphemeralRunnerSet
|
|
updated := created.DeepCopy()
|
|
updated.Spec.Replicas = 3
|
|
err := k8sClient.Update(ctx, updated)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet replica count")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (bool, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if len(runnerList.Items) != 3 {
|
|
return false, err
|
|
}
|
|
|
|
var pendingOriginal *v1alpha1.EphemeralRunner
|
|
var runningOriginal *v1alpha1.EphemeralRunner
|
|
var failedOriginal *v1alpha1.EphemeralRunner
|
|
var empty []*v1alpha1.EphemeralRunner
|
|
for _, runner := range runnerList.Items {
|
|
switch runner.Status.RunnerID {
|
|
case 101:
|
|
pendingOriginal = runner.DeepCopy()
|
|
case 102:
|
|
runningOriginal = runner.DeepCopy()
|
|
case 103:
|
|
failedOriginal = runner.DeepCopy()
|
|
default:
|
|
empty = append(empty, runner.DeepCopy())
|
|
}
|
|
}
|
|
|
|
refetch := false
|
|
if pendingOriginal == nil { // if NO pending
|
|
refetch = true
|
|
pendingOriginal = empty[0]
|
|
empty = empty[1:]
|
|
|
|
pending := pendingOriginal.DeepCopy()
|
|
pending.Status.RunnerID = 101
|
|
pending.Status.Phase = v1alpha1.EphemeralRunnerPhasePending
|
|
|
|
err = k8sClient.Status().Patch(ctx, pending, client.MergeFrom(pendingOriginal))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
if runningOriginal == nil { // if NO running
|
|
refetch = true
|
|
runningOriginal = empty[0]
|
|
empty = empty[1:]
|
|
running := runningOriginal.DeepCopy()
|
|
running.Status.RunnerID = 102
|
|
running.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
|
|
err = k8sClient.Status().Patch(ctx, running, client.MergeFrom(runningOriginal))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
if failedOriginal == nil { // if NO failed
|
|
refetch = true
|
|
failedOriginal = empty[0]
|
|
|
|
failed := pendingOriginal.DeepCopy()
|
|
failed.Status.RunnerID = 103
|
|
failed.Status.Phase = v1alpha1.EphemeralRunnerPhaseFailed
|
|
|
|
err = k8sClient.Status().Patch(ctx, failed, client.MergeFrom(failedOriginal))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
return !refetch, nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeTrue(), "Failed to eventually update to one pending, one running and one failed")
|
|
|
|
desiredStatus := v1alpha1.EphemeralRunnerSetStatus{
|
|
Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
|
|
}
|
|
Eventually(
|
|
func() (v1alpha1.EphemeralRunnerSetStatus, error) {
|
|
updated := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, updated)
|
|
if err != nil {
|
|
return v1alpha1.EphemeralRunnerSetStatus{}, err
|
|
}
|
|
return updated.Status, nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(desiredStatus), "Status is not eventually updated to the desired one")
|
|
|
|
updated = new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, updated)
|
|
Expect(err).NotTo(HaveOccurred(), "Failed to fetch ephemeral runner set")
|
|
|
|
updatedOriginal := updated.DeepCopy()
|
|
updated.Spec.Replicas = 0
|
|
|
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(updatedOriginal))
|
|
Expect(err).NotTo(HaveOccurred(), "Failed to patch ephemeral runner set with 0 replicas")
|
|
|
|
Eventually(
|
|
func() (int, error) {
|
|
runnerList = new(v1alpha1.EphemeralRunnerList)
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(1), "Failed to eventually scale down")
|
|
|
|
desiredStatus = v1alpha1.EphemeralRunnerSetStatus{
|
|
Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
|
|
}
|
|
|
|
Eventually(
|
|
func() (v1alpha1.EphemeralRunnerSetStatus, error) {
|
|
updated := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, updated)
|
|
if err != nil {
|
|
return v1alpha1.EphemeralRunnerSetStatus{}, err
|
|
}
|
|
return updated.Status, nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(desiredStatus), "Status is not eventually updated to the desired one")
|
|
|
|
err = k8sClient.Delete(ctx, &runnerList.Items[0])
|
|
Expect(err).To(BeNil(), "Failed to delete failed ephemeral runner")
|
|
|
|
desiredStatus = v1alpha1.EphemeralRunnerSetStatus{
|
|
Phase: v1alpha1.EphemeralRunnerSetPhaseRunning,
|
|
}
|
|
Eventually(
|
|
func() (v1alpha1.EphemeralRunnerSetStatus, error) {
|
|
updated := new(v1alpha1.EphemeralRunnerSet)
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, updated)
|
|
if err != nil {
|
|
return v1alpha1.EphemeralRunnerSetStatus{}, err
|
|
}
|
|
return updated.Status, nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(desiredStatus), "Status is not eventually updated to the desired one")
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("EphemeralRunner phase metrics", func() {
|
|
var ctx context.Context
|
|
var autoscalingNS *corev1.Namespace
|
|
var mgr ctrl.Manager
|
|
var configSecret *corev1.Secret
|
|
var controller *EphemeralRunnerReconciler
|
|
var ephemeralRunner *v1alpha1.EphemeralRunner
|
|
var request ctrl.Request
|
|
|
|
BeforeEach(func() {
|
|
registerActionsGithubMetricsForTest.Do(func() {
|
|
metrics.RegisterMetrics()
|
|
})
|
|
|
|
ephemeralRunnerPhaseMetrics.Lock()
|
|
ephemeralRunnerPhaseMetrics.phases = map[types.NamespacedName]v1alpha1.EphemeralRunnerPhase{}
|
|
ephemeralRunnerPhaseMetrics.Unlock()
|
|
|
|
ctx = context.Background()
|
|
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
|
|
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
|
|
|
|
controller = &EphemeralRunnerReconciler{
|
|
Client: k8sClient,
|
|
Scheme: mgr.GetScheme(),
|
|
Log: logf.Log,
|
|
PublishMetrics: true,
|
|
ResourceBuilder: ResourceBuilder{
|
|
SecretResolver: secretresolver.New(k8sClient, fake.NewMultiClient(
|
|
fake.WithClient(
|
|
fake.NewClient(
|
|
fake.WithGenerateJitRunnerConfig(
|
|
&scaleset.RunnerScaleSetJitRunnerConfig{
|
|
Runner: &scaleset.RunnerReference{ID: 100, Name: "test-runner"},
|
|
EncodedJITConfig: "fake-jit-config",
|
|
},
|
|
nil,
|
|
),
|
|
),
|
|
),
|
|
)),
|
|
},
|
|
}
|
|
|
|
ephemeralRunner = &v1alpha1.EphemeralRunner{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-runner",
|
|
Namespace: autoscalingNS.Name,
|
|
Labels: map[string]string{
|
|
LabelKeyGitHubScaleSetName: "test-scale-set",
|
|
LabelKeyGitHubScaleSetNamespace: autoscalingNS.Name,
|
|
},
|
|
},
|
|
Spec: v1alpha1.EphemeralRunnerSpec{
|
|
GitHubConfigURL: "https://github.com/owner/repo",
|
|
GitHubConfigSecret: configSecret.Name,
|
|
RunnerScaleSetID: 100,
|
|
PodTemplateSpec: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: v1alpha1.EphemeralRunnerContainerName,
|
|
Image: "ghcr.io/actions/runner",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := k8sClient.Create(ctx, ephemeralRunner)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create ephemeral runner")
|
|
|
|
request = ctrl.Request{NamespacedName: types.NamespacedName{Namespace: ephemeralRunner.Namespace, Name: ephemeralRunner.Name}}
|
|
})
|
|
|
|
It("publishes pending, running, and succeeded phase transitions", func() {
|
|
_, err := controller.Reconcile(ctx, request)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to reconcile ephemeral runner")
|
|
|
|
pod := new(corev1.Pod)
|
|
Eventually(func() error {
|
|
return k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
|
|
}, ephemeralRunnerSetTestTimeout, ephemeralRunnerSetTestInterval).Should(Succeed(), "expected ephemeral runner pod to be created")
|
|
|
|
podPending := pod.DeepCopy()
|
|
podPending.Status.Phase = corev1.PodPending
|
|
podPending.Status.ContainerStatuses = []corev1.ContainerStatus{
|
|
{
|
|
Name: v1alpha1.EphemeralRunnerContainerName,
|
|
State: corev1.ContainerState{},
|
|
},
|
|
}
|
|
err = k8sClient.Status().Patch(ctx, podPending, client.MergeFrom(pod))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to patch pod to pending")
|
|
|
|
_, err = controller.Reconcile(ctx, request)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to reconcile pending pod")
|
|
expectEphemeralRunnerPhase(ctx, ephemeralRunner, v1alpha1.EphemeralRunnerPhasePending)
|
|
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhasePending, 1)
|
|
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhaseRunning, 0)
|
|
|
|
podRunning := podPending.DeepCopy()
|
|
podRunning.Status.Phase = corev1.PodRunning
|
|
podRunning.Status.ContainerStatuses = []corev1.ContainerStatus{
|
|
{
|
|
Name: v1alpha1.EphemeralRunnerContainerName,
|
|
State: corev1.ContainerState{
|
|
Running: &corev1.ContainerStateRunning{StartedAt: metav1.Now()},
|
|
},
|
|
},
|
|
}
|
|
err = k8sClient.Status().Patch(ctx, podRunning, client.MergeFrom(podPending))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to patch pod to running")
|
|
|
|
_, err = controller.Reconcile(ctx, request)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to reconcile running pod")
|
|
expectEphemeralRunnerPhase(ctx, ephemeralRunner, v1alpha1.EphemeralRunnerPhaseRunning)
|
|
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhasePending, 0)
|
|
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhaseRunning, 1)
|
|
|
|
podSucceeded := podRunning.DeepCopy()
|
|
podSucceeded.Status.Phase = corev1.PodSucceeded
|
|
podSucceeded.Status.ContainerStatuses = []corev1.ContainerStatus{
|
|
{
|
|
Name: v1alpha1.EphemeralRunnerContainerName,
|
|
State: corev1.ContainerState{
|
|
Terminated: &corev1.ContainerStateTerminated{ExitCode: 0},
|
|
},
|
|
},
|
|
}
|
|
err = k8sClient.Status().Patch(ctx, podSucceeded, client.MergeFrom(podRunning))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to patch pod to succeeded")
|
|
|
|
_, err = controller.Reconcile(ctx, request)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to reconcile succeeded pod")
|
|
expectEphemeralRunnerPhase(ctx, ephemeralRunner, v1alpha1.EphemeralRunnerPhaseSucceeded)
|
|
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhaseRunning, 0)
|
|
expectEphemeralRunnerPhaseMetric(ephemeralRunner, v1alpha1.EphemeralRunnerPhaseSucceeded, 1)
|
|
})
|
|
})
|
|
|
|
var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func() {
|
|
var ctx context.Context
|
|
var mgr ctrl.Manager
|
|
var autoscalingNS *corev1.Namespace
|
|
var ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet
|
|
var configSecret *corev1.Secret
|
|
|
|
BeforeEach(func() {
|
|
ctx = context.Background()
|
|
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
|
|
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
|
|
|
|
controller := &EphemeralRunnerSetReconciler{
|
|
Client: mgr.GetClient(),
|
|
Scheme: mgr.GetScheme(),
|
|
Log: logf.Log,
|
|
ResourceBuilder: ResourceBuilder{
|
|
SecretResolver: secretresolver.New(mgr.GetClient(), multiclient.NewScaleset()),
|
|
},
|
|
}
|
|
err := controller.SetupWithManager(mgr)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
|
|
|
startManagers(GinkgoT(), mgr)
|
|
})
|
|
|
|
It("should create a proxy secret and delete the proxy secreat after the runner-set is deleted", func() {
|
|
secretCredentials := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "proxy-credentials",
|
|
Namespace: autoscalingNS.Name,
|
|
},
|
|
Data: map[string][]byte{
|
|
"username": []byte("username"),
|
|
"password": []byte("password"),
|
|
},
|
|
}
|
|
|
|
err := k8sClient.Create(ctx, secretCredentials)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create secret credentials")
|
|
|
|
ephemeralRunnerSet = &v1alpha1.EphemeralRunnerSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-asrs",
|
|
Namespace: autoscalingNS.Name,
|
|
},
|
|
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
|
Replicas: 1,
|
|
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
|
|
GitHubConfigURL: "http://example.com/owner/repo",
|
|
GitHubConfigSecret: configSecret.Name,
|
|
RunnerScaleSetID: 100,
|
|
Proxy: &v1alpha1.ProxyConfig{
|
|
HTTP: &v1alpha1.ProxyServerConfig{
|
|
Url: "http://proxy.example.com",
|
|
CredentialSecretRef: secretCredentials.Name,
|
|
},
|
|
HTTPS: &v1alpha1.ProxyServerConfig{
|
|
Url: "https://proxy.example.com",
|
|
CredentialSecretRef: secretCredentials.Name,
|
|
},
|
|
NoProxy: []string{"example.com", "example.org"},
|
|
},
|
|
PodTemplateSpec: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "runner",
|
|
Image: "ghcr.io/actions/runner",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(ctx, ephemeralRunnerSet)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create EphemeralRunnerSet")
|
|
|
|
Eventually(
|
|
func(g Gomega) {
|
|
// Compiled / flattened proxy secret should exist at this point
|
|
actualProxySecret := &corev1.Secret{}
|
|
err = k8sClient.Get(ctx, client.ObjectKey{
|
|
Namespace: autoscalingNS.Name,
|
|
Name: proxyEphemeralRunnerSetSecretName(ephemeralRunnerSet),
|
|
}, actualProxySecret)
|
|
g.Expect(err).NotTo(HaveOccurred(), "failed to get compiled / flattened proxy secret")
|
|
|
|
secretFetcher := func(name string) (*corev1.Secret, error) {
|
|
secret := &corev1.Secret{}
|
|
err = k8sClient.Get(ctx, client.ObjectKey{
|
|
Namespace: autoscalingNS.Name,
|
|
Name: name,
|
|
}, secret)
|
|
return secret, err
|
|
}
|
|
|
|
// Assert that the proxy secret is created with the correct values
|
|
expectedData, err := ephemeralRunnerSet.Spec.EphemeralRunnerSpec.Proxy.ToSecretData(secretFetcher)
|
|
g.Expect(err).NotTo(HaveOccurred(), "failed to get proxy secret data")
|
|
g.Expect(actualProxySecret.Data).To(Equal(expectedData))
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(Succeed(), "compiled / flattened proxy secret should exist")
|
|
|
|
Eventually(func(g Gomega) {
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
g.Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunners")
|
|
|
|
for _, runner := range runnerList.Items {
|
|
g.Expect(runner.Spec.ProxySecretRef).To(Equal(proxyEphemeralRunnerSetSecretName(ephemeralRunnerSet)))
|
|
}
|
|
}, ephemeralRunnerSetTestTimeout, ephemeralRunnerSetTestInterval).Should(Succeed(), "EphemeralRunners should have a reference to the proxy secret")
|
|
|
|
// patch ephemeral runner set to have 0 replicas
|
|
patch := client.MergeFrom(ephemeralRunnerSet.DeepCopy())
|
|
ephemeralRunnerSet.Spec.Replicas = 0
|
|
err = k8sClient.Patch(ctx, ephemeralRunnerSet, patch)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to patch EphemeralRunnerSet")
|
|
|
|
// Set pods to PodSucceeded to simulate an actual EphemeralRunner stopping
|
|
Eventually(
|
|
func(g Gomega) (int, error) {
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
// Set status to simulate a configured EphemeralRunner
|
|
refetch := false
|
|
for i, runner := range runnerList.Items {
|
|
if runner.Status.RunnerID == 0 {
|
|
updatedRunner := runner.DeepCopy()
|
|
updatedRunner.Status.Phase = v1alpha1.EphemeralRunnerPhaseSucceeded
|
|
updatedRunner.Status.RunnerID = i + 100
|
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
|
refetch = true
|
|
}
|
|
}
|
|
|
|
if refetch {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(1), "1 EphemeralRunner should exist")
|
|
|
|
// Delete the EphemeralRunnerSet
|
|
err = k8sClient.Delete(ctx, ephemeralRunnerSet)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to delete EphemeralRunnerSet")
|
|
|
|
Eventually(
|
|
func(g Gomega) (int, error) {
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(0), "EphemeralRunners should be deleted")
|
|
|
|
// Assert that the proxy secret is deleted
|
|
Eventually(
|
|
func(g Gomega) {
|
|
proxySecret := &corev1.Secret{}
|
|
err = k8sClient.Get(ctx, client.ObjectKey{
|
|
Namespace: autoscalingNS.Name,
|
|
Name: proxyEphemeralRunnerSetSecretName(ephemeralRunnerSet),
|
|
}, proxySecret)
|
|
g.Expect(err).To(HaveOccurred(), "proxy secret should be deleted")
|
|
g.Expect(kerrors.IsNotFound(err)).To(BeTrue(), "proxy secret should be deleted")
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(Succeed(), "proxy secret should be deleted")
|
|
})
|
|
|
|
It("should configure the actions client to use proxy details", func() {
|
|
secretCredentials := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "proxy-credentials",
|
|
Namespace: autoscalingNS.Name,
|
|
},
|
|
Data: map[string][]byte{
|
|
"username": []byte("test"),
|
|
"password": []byte("password"),
|
|
},
|
|
}
|
|
|
|
err := k8sClient.Create(ctx, secretCredentials)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create secret credentials")
|
|
|
|
proxySuccessfulllyCalled := false
|
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
header := r.Header.Get("Proxy-Authorization")
|
|
Expect(header).NotTo(BeEmpty())
|
|
|
|
header = strings.TrimPrefix(header, "Basic ")
|
|
decoded, err := base64.StdEncoding.DecodeString(header)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(string(decoded)).To(Equal("test:password"))
|
|
|
|
proxySuccessfulllyCalled = true
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
GinkgoT().Cleanup(func() {
|
|
proxy.Close()
|
|
})
|
|
|
|
ephemeralRunnerSet = &v1alpha1.EphemeralRunnerSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-asrs",
|
|
Namespace: autoscalingNS.Name,
|
|
},
|
|
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
|
Replicas: 1,
|
|
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
|
|
GitHubConfigURL: "http://example.com/owner/repo",
|
|
GitHubConfigSecret: configSecret.Name,
|
|
RunnerScaleSetID: 100,
|
|
Proxy: &v1alpha1.ProxyConfig{
|
|
HTTP: &v1alpha1.ProxyServerConfig{
|
|
Url: proxy.URL,
|
|
CredentialSecretRef: "proxy-credentials",
|
|
},
|
|
},
|
|
PodTemplateSpec: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "runner",
|
|
Image: "ghcr.io/actions/runner",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(ctx, ephemeralRunnerSet)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(1), "failed to create ephemeral runner")
|
|
|
|
runner := runnerList.Items[0].DeepCopy()
|
|
runner.Status.Phase = v1alpha1.EphemeralRunnerPhaseRunning
|
|
runner.Status.RunnerID = 100
|
|
err = k8sClient.Status().Patch(ctx, runner, client.MergeFrom(&runnerList.Items[0]))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update ephemeral runner status")
|
|
|
|
runnerSet := new(v1alpha1.EphemeralRunnerSet)
|
|
err = k8sClient.Get(ctx, client.ObjectKey{Namespace: ephemeralRunnerSet.Namespace, Name: ephemeralRunnerSet.Name}, runnerSet)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
|
|
|
updatedRunnerSet := runnerSet.DeepCopy()
|
|
updatedRunnerSet.Spec.Replicas = 0
|
|
err = k8sClient.Patch(ctx, updatedRunnerSet, client.MergeFrom(runnerSet))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
|
|
|
Eventually(
|
|
func() bool {
|
|
return proxySuccessfulllyCalled
|
|
},
|
|
2*time.Second,
|
|
ephemeralRunnerInterval,
|
|
).Should(BeEquivalentTo(true))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func() {
|
|
var ctx context.Context
|
|
var mgr ctrl.Manager
|
|
var autoscalingNS *corev1.Namespace
|
|
var ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet
|
|
var configSecret *corev1.Secret
|
|
var rootCAConfigMap *corev1.ConfigMap
|
|
|
|
BeforeEach(func() {
|
|
ctx = context.Background()
|
|
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
|
|
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
|
|
|
|
cert, err := os.ReadFile(filepath.Join(
|
|
"../../",
|
|
"github",
|
|
"actions",
|
|
"testdata",
|
|
"rootCA.crt",
|
|
))
|
|
Expect(err).NotTo(HaveOccurred(), "failed to read root CA cert")
|
|
rootCAConfigMap = &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "root-ca-configmap",
|
|
Namespace: autoscalingNS.Name,
|
|
},
|
|
Data: map[string]string{
|
|
"rootCA.crt": string(cert),
|
|
},
|
|
}
|
|
err = k8sClient.Create(ctx, rootCAConfigMap)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs")
|
|
|
|
controller := &EphemeralRunnerSetReconciler{
|
|
Client: mgr.GetClient(),
|
|
Scheme: mgr.GetScheme(),
|
|
Log: logf.Log,
|
|
ResourceBuilder: ResourceBuilder{
|
|
SecretResolver: secretresolver.New(mgr.GetClient(), multiclient.NewScaleset()),
|
|
},
|
|
}
|
|
err = controller.SetupWithManager(mgr)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
|
|
|
startManagers(GinkgoT(), mgr)
|
|
})
|
|
|
|
It("should be able to make requests to a server using root CAs", func() {
|
|
ephemeralRunnerSet = &v1alpha1.EphemeralRunnerSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-asrs",
|
|
Namespace: autoscalingNS.Name,
|
|
},
|
|
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
|
Replicas: 1,
|
|
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
|
|
GitHubConfigURL: "https://github.example.com/api/v3",
|
|
GitHubConfigSecret: configSecret.Name,
|
|
GitHubServerTLS: &v1alpha1.TLSConfig{
|
|
CertificateFrom: &v1alpha1.TLSCertificateSource{
|
|
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
|
|
LocalObjectReference: corev1.LocalObjectReference{
|
|
Name: rootCAConfigMap.Name,
|
|
},
|
|
Key: "rootCA.crt",
|
|
},
|
|
},
|
|
},
|
|
RunnerScaleSetID: 100,
|
|
PodTemplateSpec: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "runner",
|
|
Image: "ghcr.io/actions/runner",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := k8sClient.Create(ctx, ephemeralRunnerSet)
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create EphemeralRunnerSet")
|
|
|
|
runnerList := new(v1alpha1.EphemeralRunnerList)
|
|
Eventually(
|
|
func() (int, error) {
|
|
err := listEphemeralRunnersAndRemoveFinalizers(ctx, k8sClient, runnerList, ephemeralRunnerSet.Namespace)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return len(runnerList.Items), nil
|
|
},
|
|
ephemeralRunnerSetTestTimeout,
|
|
ephemeralRunnerSetTestInterval,
|
|
).Should(BeEquivalentTo(1), "failed to create ephemeral runner")
|
|
|
|
// Verify that the TLS configuration is properly propagated to the runner
|
|
runner := runnerList.Items[0].DeepCopy()
|
|
Expect(runner.Spec.GitHubServerTLS).NotTo(BeNil(), "runner tls config should not be nil")
|
|
Expect(runner.Spec.GitHubServerTLS).To(BeEquivalentTo(ephemeralRunnerSet.Spec.EphemeralRunnerSpec.GitHubServerTLS), "runner tls config should be correct")
|
|
})
|
|
})
|
|
|
|
// helper function to remove ephemeral runners since in the test, ephemeral runner reconciler is not started
|
|
func listEphemeralRunnersAndRemoveFinalizers(ctx context.Context, k8sClient client.Client, list *v1alpha1.EphemeralRunnerList, namespace string) error {
|
|
err := k8sClient.List(ctx, list, client.InNamespace(namespace))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Since we are not starting ephemeral runner reconciler, ignore
|
|
liveItems := make([]v1alpha1.EphemeralRunner, 0)
|
|
for _, item := range list.Items {
|
|
if !item.DeletionTimestamp.IsZero() {
|
|
original := item.DeepCopy()
|
|
item.Finalizers = []string{}
|
|
if err := k8sClient.Patch(ctx, &item, client.MergeFrom(original)); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
liveItems = append(liveItems, item)
|
|
}
|
|
list.Items = liveItems
|
|
return nil
|
|
}
|