2020-02-21 03:01:52 +09:00
|
|
|
package controllers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2020-03-15 18:08:11 +09:00
|
|
|
"math/rand"
|
2020-10-05 01:06:37 +01:00
|
|
|
"net/http/httptest"
|
2020-03-15 18:08:11 +09:00
|
|
|
"time"
|
|
|
|
|
|
2020-02-21 03:01:52 +09:00
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
|
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
|
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
|
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
|
|
|
|
|
|
|
|
. "github.com/onsi/ginkgo"
|
|
|
|
|
. "github.com/onsi/gomega"
|
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
|
|
2021-06-22 17:55:06 +09:00
|
|
|
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
|
|
|
|
"github.com/actions-runner-controller/actions-runner-controller/github/fake"
|
2020-10-05 01:06:37 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
runnersList *fake.RunnersList
|
|
|
|
|
server *httptest.Server
|
2020-02-21 03:01:52 +09:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// SetupTest will set up a testing environment.
|
|
|
|
|
// This includes:
|
|
|
|
|
// * creating a Namespace to be used during the test
|
|
|
|
|
// * starting the 'RunnerReconciler'
|
2020-03-10 09:14:11 +09:00
|
|
|
// * stopping the 'RunnerReplicaSetReconciler" after the test ends
|
2020-02-21 03:01:52 +09:00
|
|
|
// Call this function at the start of each of your tests.
|
2021-06-22 17:10:09 +09:00
|
|
|
func SetupTest(ctx2 context.Context) *corev1.Namespace {
|
|
|
|
|
var ctx context.Context
|
|
|
|
|
var cancel func()
|
2020-02-21 03:01:52 +09:00
|
|
|
ns := &corev1.Namespace{}
|
|
|
|
|
|
|
|
|
|
BeforeEach(func() {
|
2021-06-22 17:10:09 +09:00
|
|
|
ctx, cancel = context.WithCancel(ctx2)
|
2020-02-21 03:01:52 +09:00
|
|
|
*ns = corev1.Namespace{
|
|
|
|
|
ObjectMeta: metav1.ObjectMeta{Name: "testns-" + randStringRunes(5)},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := k8sClient.Create(ctx, ns)
|
|
|
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")
|
|
|
|
|
|
2021-03-19 16:14:15 +09:00
|
|
|
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
|
|
|
|
|
Namespace: ns.Name,
|
|
|
|
|
})
|
2020-02-21 03:01:52 +09:00
|
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create manager")
|
|
|
|
|
|
2020-10-05 01:06:37 +01:00
|
|
|
runnersList = fake.NewRunnersList()
|
|
|
|
|
server = runnersList.GetServer()
|
|
|
|
|
ghClient := newGithubClient(server)
|
|
|
|
|
|
2020-03-10 09:14:11 +09:00
|
|
|
controller := &RunnerReplicaSetReconciler{
|
2020-10-05 01:06:37 +01:00
|
|
|
Client: mgr.GetClient(),
|
|
|
|
|
Scheme: scheme.Scheme,
|
|
|
|
|
Log: logf.Log,
|
|
|
|
|
Recorder: mgr.GetEventRecorderFor("runnerreplicaset-controller"),
|
|
|
|
|
GitHubClient: ghClient,
|
2021-02-23 08:05:25 +09:00
|
|
|
Name: "runnerreplicaset-" + ns.Name,
|
2020-02-21 03:01:52 +09:00
|
|
|
}
|
|
|
|
|
err = controller.SetupWithManager(mgr)
|
|
|
|
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
defer GinkgoRecover()
|
|
|
|
|
|
2021-06-22 17:10:09 +09:00
|
|
|
err := mgr.Start(ctx)
|
2020-02-21 03:01:52 +09:00
|
|
|
Expect(err).NotTo(HaveOccurred(), "failed to start manager")
|
|
|
|
|
}()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
AfterEach(func() {
|
2021-06-22 17:10:09 +09:00
|
|
|
defer cancel()
|
2020-02-21 03:01:52 +09:00
|
|
|
|
2020-10-05 01:06:37 +01:00
|
|
|
server.Close()
|
2020-02-21 03:01:52 +09:00
|
|
|
err := k8sClient.Delete(ctx, ns)
|
|
|
|
|
Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return ns
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
|
|
|
|
|
|
|
|
|
func randStringRunes(n int) string {
|
|
|
|
|
b := make([]rune, n)
|
|
|
|
|
for i := range b {
|
|
|
|
|
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
|
|
|
|
}
|
|
|
|
|
return string(b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func intPtr(v int) *int {
|
|
|
|
|
return &v
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var _ = Context("Inside of a new namespace", func() {
|
|
|
|
|
ctx := context.TODO()
|
|
|
|
|
ns := SetupTest(ctx)
|
2022-03-05 12:13:22 +00:00
|
|
|
name := "example-runnerreplicaset"
|
2020-02-21 03:01:52 +09:00
|
|
|
|
2022-03-05 12:13:22 +00:00
|
|
|
getRunnerCount := func() int {
|
|
|
|
|
runners := actionsv1alpha1.RunnerList{Items: []actionsv1alpha1.Runner{}}
|
2020-02-21 03:01:52 +09:00
|
|
|
|
2022-03-05 12:13:22 +00:00
|
|
|
selector, err := metav1.LabelSelectorAsSelector(
|
|
|
|
|
&metav1.LabelSelector{
|
|
|
|
|
MatchLabels: map[string]string{
|
|
|
|
|
"foo": "bar",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logf.Log.Error(err, "failed to create labelselector")
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = k8sClient.List(
|
|
|
|
|
ctx,
|
|
|
|
|
&runners,
|
|
|
|
|
client.InNamespace(ns.Name),
|
|
|
|
|
client.MatchingLabelsSelector{Selector: selector},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logf.Log.Error(err, "list runners")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runnersList.Sync(runners.Items)
|
|
|
|
|
|
|
|
|
|
return len(runners.Items)
|
|
|
|
|
}
|
2020-02-21 03:01:52 +09:00
|
|
|
|
2022-03-05 12:13:22 +00:00
|
|
|
Describe("RunnerReplicaSet", func() {
|
|
|
|
|
It("should create a new Runner resource from the specified template", func() {
|
2020-02-21 03:01:52 +09:00
|
|
|
{
|
2020-03-10 09:14:11 +09:00
|
|
|
rs := &actionsv1alpha1.RunnerReplicaSet{
|
2020-02-21 03:01:52 +09:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
|
Name: name,
|
|
|
|
|
Namespace: ns.Name,
|
|
|
|
|
},
|
2020-03-10 09:14:11 +09:00
|
|
|
Spec: actionsv1alpha1.RunnerReplicaSetSpec{
|
2020-02-21 03:01:52 +09:00
|
|
|
Replicas: intPtr(1),
|
2021-03-05 10:15:39 +09:00
|
|
|
Selector: &metav1.LabelSelector{
|
|
|
|
|
MatchLabels: map[string]string{
|
|
|
|
|
"foo": "bar",
|
|
|
|
|
},
|
|
|
|
|
},
|
2020-02-26 21:23:23 +09:00
|
|
|
Template: actionsv1alpha1.RunnerTemplate{
|
2021-03-05 10:15:39 +09:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
|
Labels: map[string]string{
|
|
|
|
|
"foo": "bar",
|
|
|
|
|
},
|
|
|
|
|
},
|
2020-02-26 21:23:23 +09:00
|
|
|
Spec: actionsv1alpha1.RunnerSpec{
|
2021-06-22 17:10:09 +09:00
|
|
|
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
|
|
|
|
Repository: "test/valid",
|
|
|
|
|
Image: "bar",
|
|
|
|
|
},
|
|
|
|
|
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
|
|
|
|
Env: []corev1.EnvVar{
|
|
|
|
|
{Name: "FOO", Value: "FOOVALUE"},
|
|
|
|
|
},
|
2020-02-26 21:23:23 +09:00
|
|
|
},
|
2020-02-21 03:01:52 +09:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := k8sClient.Create(ctx, rs)
|
|
|
|
|
|
2020-03-10 09:14:11 +09:00
|
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create test RunnerReplicaSet resource")
|
2020-02-21 03:01:52 +09:00
|
|
|
|
|
|
|
|
Eventually(
|
2022-03-05 12:13:22 +00:00
|
|
|
getRunnerCount,
|
|
|
|
|
time.Second*5, time.Second).Should(BeEquivalentTo(1))
|
2020-02-21 03:01:52 +09:00
|
|
|
}
|
2022-03-05 12:13:22 +00:00
|
|
|
})
|
2020-02-21 03:01:52 +09:00
|
|
|
|
2022-03-05 12:13:22 +00:00
|
|
|
It("should create 2 runners when specified 2 replicas", func() {
|
2020-02-21 03:01:52 +09:00
|
|
|
{
|
2022-03-05 12:13:22 +00:00
|
|
|
rs := &actionsv1alpha1.RunnerReplicaSet{
|
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
|
Name: name,
|
|
|
|
|
Namespace: ns.Name,
|
|
|
|
|
},
|
|
|
|
|
Spec: actionsv1alpha1.RunnerReplicaSetSpec{
|
|
|
|
|
Replicas: intPtr(2),
|
|
|
|
|
Selector: &metav1.LabelSelector{
|
|
|
|
|
MatchLabels: map[string]string{
|
|
|
|
|
"foo": "bar",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Template: actionsv1alpha1.RunnerTemplate{
|
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
|
Labels: map[string]string{
|
2021-03-05 10:15:39 +09:00
|
|
|
"foo": "bar",
|
|
|
|
|
},
|
|
|
|
|
},
|
2022-03-05 12:13:22 +00:00
|
|
|
Spec: actionsv1alpha1.RunnerSpec{
|
|
|
|
|
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
|
|
|
|
Repository: "test/valid",
|
|
|
|
|
Image: "bar",
|
|
|
|
|
},
|
|
|
|
|
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
|
|
|
|
Env: []corev1.EnvVar{
|
|
|
|
|
{Name: "FOO", Value: "FOOVALUE"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2020-02-21 03:01:52 +09:00
|
|
|
},
|
2022-03-05 12:13:22 +00:00
|
|
|
}
|
2020-02-21 03:01:52 +09:00
|
|
|
|
2022-03-05 12:13:22 +00:00
|
|
|
err := k8sClient.Create(ctx, rs)
|
2020-02-21 03:01:52 +09:00
|
|
|
|
2022-03-05 12:13:22 +00:00
|
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create test RunnerReplicaSet resource")
|
2020-02-21 03:01:52 +09:00
|
|
|
|
|
|
|
|
Eventually(
|
2022-03-05 12:13:22 +00:00
|
|
|
getRunnerCount,
|
|
|
|
|
time.Second*5, time.Second).Should(BeEquivalentTo(2))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("should not create any runners when specified 0 replicas", func() {
|
|
|
|
|
{
|
|
|
|
|
rs := &actionsv1alpha1.RunnerReplicaSet{
|
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
|
Name: name,
|
|
|
|
|
Namespace: ns.Name,
|
|
|
|
|
},
|
|
|
|
|
Spec: actionsv1alpha1.RunnerReplicaSetSpec{
|
|
|
|
|
Replicas: intPtr(0),
|
|
|
|
|
Selector: &metav1.LabelSelector{
|
feat: Support for scaling from/to zero (#465)
This is an attempt to support scaling from/to zero.
The basic idea is that we create a one-off "registration-only" runner pod on RunnerReplicaSet being scaled to zero, so that there is one "offline" runner, which enables GitHub Actions to queue jobs instead of discarding those.
GitHub Actions seems to immediately throw away the new job when there are no runners at all. Generally, having runners of any status, `busy`, `idle`, or `offline` would prevent GitHub actions from failing jobs. But retaining `busy` or `idle` runners means that we need to keep runner pods running, which conflicts with our desired to scale to/from zero, hence we retain `offline` runners.
In this change, I enhanced the runnerreplicaset controller to create a registration-only runner on very beginning of its reconciliation logic, only when a runnerreplicaset is scaled to zero. The runner controller creates the registration-only runner pod, waits for it to become "offline", and then removes the runner pod. The runner on GitHub stays `offline`, until the runner resource on K8s is deleted. As we remove the registration-only runner pod as soon as it registers, this doesn't block cluster-autoscaler.
Related to #447
2021-05-02 16:11:36 +09:00
|
|
|
MatchLabels: map[string]string{
|
|
|
|
|
"foo": "bar",
|
|
|
|
|
},
|
2022-03-05 12:13:22 +00:00
|
|
|
},
|
|
|
|
|
Template: actionsv1alpha1.RunnerTemplate{
|
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
|
Labels: map[string]string{
|
|
|
|
|
"foo": "bar",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Spec: actionsv1alpha1.RunnerSpec{
|
|
|
|
|
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
|
|
|
|
Repository: "test/valid",
|
|
|
|
|
Image: "bar",
|
|
|
|
|
},
|
|
|
|
|
RunnerPodSpec: actionsv1alpha1.RunnerPodSpec{
|
|
|
|
|
Env: []corev1.EnvVar{
|
|
|
|
|
{Name: "FOO", Value: "FOOVALUE"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
feat: Support for scaling from/to zero (#465)
This is an attempt to support scaling from/to zero.
The basic idea is that we create a one-off "registration-only" runner pod on RunnerReplicaSet being scaled to zero, so that there is one "offline" runner, which enables GitHub Actions to queue jobs instead of discarding those.
GitHub Actions seems to immediately throw away the new job when there are no runners at all. Generally, having runners of any status, `busy`, `idle`, or `offline` would prevent GitHub actions from failing jobs. But retaining `busy` or `idle` runners means that we need to keep runner pods running, which conflicts with our desired to scale to/from zero, hence we retain `offline` runners.
In this change, I enhanced the runnerreplicaset controller to create a registration-only runner on very beginning of its reconciliation logic, only when a runnerreplicaset is scaled to zero. The runner controller creates the registration-only runner pod, waits for it to become "offline", and then removes the runner pod. The runner on GitHub stays `offline`, until the runner resource on K8s is deleted. As we remove the registration-only runner pod as soon as it registers, this doesn't block cluster-autoscaler.
Related to #447
2021-05-02 16:11:36 +09:00
|
|
|
|
2022-03-05 12:13:22 +00:00
|
|
|
err := k8sClient.Create(ctx, rs)
|
2020-02-21 03:01:52 +09:00
|
|
|
|
2022-03-05 12:13:22 +00:00
|
|
|
Expect(err).NotTo(HaveOccurred(), "failed to create test RunnerReplicaSet resource")
|
2020-10-05 01:06:37 +01:00
|
|
|
|
2022-03-05 12:13:22 +00:00
|
|
|
Consistently(
|
|
|
|
|
getRunnerCount,
|
|
|
|
|
time.Second*5, time.Second).Should(BeEquivalentTo(0))
|
2020-02-21 03:01:52 +09:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|