From 4733edc20d157e63b9d251d6f61a0aa9759f3cb7 Mon Sep 17 00:00:00 2001 From: Yusuke Kuoka Date: Sun, 2 Aug 2020 11:49:25 +0900 Subject: [PATCH] Add scaling-down scenario to integration test --- controllers/integration_test.go | 70 +++++++++++++++++++++++++++++---- github/fake/fake.go | 57 +++++++++++++++------------ github/fake/options.go | 26 +++++------- 3 files changed, 103 insertions(+), 50 deletions(-) diff --git a/controllers/integration_test.go b/controllers/integration_test.go index e0dc6102bd..35b830ddd9 100644 --- a/controllers/integration_test.go +++ b/controllers/integration_test.go @@ -19,18 +19,32 @@ import ( actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1" ) +type testEnvironment struct { + Namespace *corev1.Namespace + Responses *fake.FixedResponses +} + +var ( + workflowRunsFor3Replicas = `{"total_count": 5, "workflow_runs":[{"status":"queued"}, {"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"` + workflowRunsFor1Replicas = `{"total_count": 6, "workflow_runs":[{"status":"queued"}, {"status":"completed"}, {"status":"completed"}, {"status":"completed"}, {"status":"completed"}]}"` +) + // SetupIntegrationTest will set up a testing environment. // This includes: // * creating a Namespace to be used during the test // * starting all the reconcilers // * stopping all the reconcilers after the test ends // Call this function at the start of each of your tests. -func SetupIntegrationTest(ctx context.Context) *corev1.Namespace { +func SetupIntegrationTest(ctx context.Context) *testEnvironment { var stopCh chan struct{} ns := &corev1.Namespace{} - workflowRuns := `{"total_count": 5, "workflow_runs":[{"status":"queued"}, {"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"` - server := fake.NewServer(fake.WithListRepositoryWorkflowRunsResponse(200, workflowRuns)) + responses := &fake.FixedResponses{} + responses.ListRepositoryWorkflowRuns = &fake.Handler{ + Status: 200, + Body: workflowRunsFor3Replicas, + } + server := fake.NewServer(fake.WithFixedResponses(responses)) BeforeEach(func() { stopCh = make(chan struct{}) @@ -91,12 +105,14 @@ func SetupIntegrationTest(ctx context.Context) *corev1.Namespace { Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace") }) - return ns + return &testEnvironment{Namespace: ns, Responses: responses} } var _ = Context("Inside of a new namespace", func() { ctx := context.TODO() - ns := SetupIntegrationTest(ctx) + env := SetupIntegrationTest(ctx) + ns := env.Namespace + responses := env.Responses Describe("when no existing resources exist", func() { @@ -199,8 +215,9 @@ var _ = Context("Inside of a new namespace", func() { time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(2)) } + // Scale-up to 3 replicas { - rs := &actionsv1alpha1.HorizontalRunnerAutoscaler{ + hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: ns.Name, @@ -216,9 +233,9 @@ var _ = Context("Inside of a new namespace", func() { }, } - err := k8sClient.Create(ctx, rs) + err := k8sClient.Create(ctx, hra) - Expect(err).NotTo(HaveOccurred(), "failed to create test RunnerDeployment resource") + Expect(err).NotTo(HaveOccurred(), "failed to create test HorizontalRunnerAutoscaler resource") runnerSets := actionsv1alpha1.RunnerReplicaSetList{Items: []actionsv1alpha1.RunnerReplicaSet{}} @@ -249,6 +266,43 @@ var _ = Context("Inside of a new namespace", func() { }, time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(3)) } + + // Scale-down to 1 replica + { + responses.ListRepositoryWorkflowRuns.Body = workflowRunsFor1Replicas + + var hra actionsv1alpha1.HorizontalRunnerAutoscaler + + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns.Name, Name: name}, &hra) + + Expect(err).NotTo(HaveOccurred(), "failed to get test HorizontalRunnerAutoscaler resource") + + hra.Annotations = map[string]string{ + "force-update": "1", + } + + err = k8sClient.Update(ctx, &hra) + + Expect(err).NotTo(HaveOccurred(), "failed to get test HorizontalRunnerAutoscaler resource") + + Eventually( + func() int { + var runnerSets actionsv1alpha1.RunnerReplicaSetList + + err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns.Name)) + if err != nil { + logf.Log.Error(err, "list runner sets") + } + + if len(runnerSets.Items) == 0 { + logf.Log.Info("No runnerreplicasets exist yet") + return -1 + } + + return *runnerSets.Items[0].Spec.Replicas + }, + time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(1)) + } }) }) }) diff --git a/github/fake/fake.go b/github/fake/fake.go index 0d409b6bb2..66c3e93f7b 100644 --- a/github/fake/fake.go +++ b/github/fake/fake.go @@ -21,111 +21,116 @@ const ( ` ) -type handler struct { +type Handler struct { Status int Body string } -func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { +func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.WriteHeader(h.Status) fmt.Fprintf(w, h.Body) } +type ServerConfig struct { + *FixedResponses +} + // NewServer creates a fake server for running unit tests func NewServer(opts ...Option) *httptest.Server { - var responses FixedResponses + config := ServerConfig{ + FixedResponses: &FixedResponses{}, + } for _, o := range opts { - o(&responses) + o(&config) } - routes := map[string]handler{ + routes := map[string]*Handler{ // For CreateRegistrationToken - "/repos/test/valid/actions/runners/registration-token": handler{ + "/repos/test/valid/actions/runners/registration-token": &Handler{ Status: http.StatusCreated, Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)), }, - "/repos/test/invalid/actions/runners/registration-token": handler{ + "/repos/test/invalid/actions/runners/registration-token": &Handler{ Status: http.StatusOK, Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)), }, - "/repos/test/error/actions/runners/registration-token": handler{ + "/repos/test/error/actions/runners/registration-token": &Handler{ Status: http.StatusBadRequest, Body: "", }, - "/orgs/test/actions/runners/registration-token": handler{ + "/orgs/test/actions/runners/registration-token": &Handler{ Status: http.StatusCreated, Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)), }, - "/orgs/invalid/actions/runners/registration-token": handler{ + "/orgs/invalid/actions/runners/registration-token": &Handler{ Status: http.StatusOK, Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)), }, - "/orgs/error/actions/runners/registration-token": handler{ + "/orgs/error/actions/runners/registration-token": &Handler{ Status: http.StatusBadRequest, Body: "", }, // For ListRunners - "/repos/test/valid/actions/runners": handler{ + "/repos/test/valid/actions/runners": &Handler{ Status: http.StatusOK, Body: RunnersListBody, }, - "/repos/test/invalid/actions/runners": handler{ + "/repos/test/invalid/actions/runners": &Handler{ Status: http.StatusNoContent, Body: "", }, - "/repos/test/error/actions/runners": handler{ + "/repos/test/error/actions/runners": &Handler{ Status: http.StatusBadRequest, Body: "", }, - "/orgs/test/actions/runners": handler{ + "/orgs/test/actions/runners": &Handler{ Status: http.StatusOK, Body: RunnersListBody, }, - "/orgs/invalid/actions/runners": handler{ + "/orgs/invalid/actions/runners": &Handler{ Status: http.StatusNoContent, Body: "", }, - "/orgs/error/actions/runners": handler{ + "/orgs/error/actions/runners": &Handler{ Status: http.StatusBadRequest, Body: "", }, // For RemoveRunner - "/repos/test/valid/actions/runners/1": handler{ + "/repos/test/valid/actions/runners/1": &Handler{ Status: http.StatusNoContent, Body: "", }, - "/repos/test/invalid/actions/runners/1": handler{ + "/repos/test/invalid/actions/runners/1": &Handler{ Status: http.StatusOK, Body: "", }, - "/repos/test/error/actions/runners/1": handler{ + "/repos/test/error/actions/runners/1": &Handler{ Status: http.StatusBadRequest, Body: "", }, - "/orgs/test/actions/runners/1": handler{ + "/orgs/test/actions/runners/1": &Handler{ Status: http.StatusNoContent, Body: "", }, - "/orgs/invalid/actions/runners/1": handler{ + "/orgs/invalid/actions/runners/1": &Handler{ Status: http.StatusOK, Body: "", }, - "/orgs/error/actions/runners/1": handler{ + "/orgs/error/actions/runners/1": &Handler{ Status: http.StatusBadRequest, Body: "", }, // For auto-scaling based on the number of queued(pending) workflow runs - "/repos/test/valid/actions/runs": responses.listRepositoryWorkflowRuns.handler(), + "/repos/test/valid/actions/runs": config.FixedResponses.ListRepositoryWorkflowRuns, } mux := http.NewServeMux() for path, handler := range routes { - h := handler - mux.Handle(path, &h) + mux.Handle(path, handler) } return httptest.NewServer(mux) diff --git a/github/fake/options.go b/github/fake/options.go index 3b79229972..b50b04e9e8 100644 --- a/github/fake/options.go +++ b/github/fake/options.go @@ -1,28 +1,22 @@ package fake type FixedResponses struct { - listRepositoryWorkflowRuns FixedResponse + ListRepositoryWorkflowRuns *Handler } -type FixedResponse struct { - Status int - Body string -} - -func (r FixedResponse) handler() handler { - return handler{ - Status: r.Status, - Body: r.Body, - } -} - -type Option func(responses *FixedResponses) +type Option func(*ServerConfig) func WithListRepositoryWorkflowRunsResponse(status int, body string) Option { - return func(r *FixedResponses) { - r.listRepositoryWorkflowRuns = FixedResponse{ + return func(c *ServerConfig) { + c.FixedResponses.ListRepositoryWorkflowRuns = &Handler{ Status: status, Body: body, } } } + +func WithFixedResponses(responses *FixedResponses) Option { + return func(c *ServerConfig) { + c.FixedResponses = responses + } +}