Skip to content
This repository was archived by the owner on Jul 5, 2023. It is now read-only.

Commit 85b74ff

Browse files
authored
Merge pull request #5 from scalyr/OPS-5990-autoscale-on-jobs
Ops 5990 autoscale on jobs
2 parents 1b5b603 + 3c82c6d commit 85b74ff

File tree

6 files changed

+138
-11
lines changed

6 files changed

+138
-11
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
name: Build
2+
13
on:
24
push:
35
branches:

.github/workflows/test.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- master
7+
paths-ignore:
8+
- 'runner/**'
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
name: Test
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v2
17+
- name: Install kubebuilder
18+
run: |
19+
curl -L -O https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.2.0/kubebuilder_2.2.0_linux_amd64.tar.gz
20+
tar zxvf kubebuilder_2.2.0_linux_amd64.tar.gz
21+
sudo mv kubebuilder_2.2.0_linux_amd64 /usr/local/kubebuilder
22+
- name: Run tests
23+
run: make test

controllers/autoscaling.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
87
"strings"
8+
9+
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
910
)
1011

1112
func (r *HorizontalRunnerAutoscalerReconciler) determineDesiredReplicas(rd v1alpha1.RunnerDeployment, hra v1alpha1.HorizontalRunnerAutoscaler) (*int, error) {
@@ -44,6 +45,38 @@ func (r *HorizontalRunnerAutoscalerReconciler) determineDesiredReplicas(rd v1alp
4445
}
4546

4647
var total, inProgress, queued, completed, unknown int
48+
type callback func()
49+
listWorkflowJobs := func(user string, repoName string, runID int64, fallback_cb callback) {
50+
if runID == 0 {
51+
fallback_cb()
52+
return
53+
}
54+
jobs, _, err := r.GitHubClient.Actions.ListWorkflowJobs(context.TODO(), user, repoName, runID, nil)
55+
if err != nil {
56+
r.Log.Error(err, "Error listing workflow jobs")
57+
fallback_cb()
58+
} else if len(jobs.Jobs) == 0 {
59+
fallback_cb()
60+
} else {
61+
for _, job := range jobs.Jobs {
62+
switch job.GetStatus() {
63+
case "completed":
64+
// We add a case for `completed` so it is not counted in `unknown`.
65+
// And we do not increment the counter for completed because
66+
// that counter only refers to workflows. The reason for
67+
// this is because we do not get a list of jobs for
68+
// completed workflows in order to keep the number of API
69+
// calls to a minimum.
70+
case "in_progress":
71+
inProgress++
72+
case "queued":
73+
queued++
74+
default:
75+
unknown++
76+
}
77+
}
78+
}
79+
}
4780

4881
for _, repo := range repos {
4982
user, repoName := repo[0], repo[1]
@@ -52,20 +85,20 @@ func (r *HorizontalRunnerAutoscalerReconciler) determineDesiredReplicas(rd v1alp
5285
return nil, err
5386
}
5487

55-
for _, r := range list.WorkflowRuns {
88+
for _, run := range list.WorkflowRuns {
5689
total++
5790

5891
// In May 2020, there are only 3 statuses.
5992
// Follow the below links for more details:
6093
// - https://developer.github.com/v3/actions/workflow-runs/#list-repository-workflow-runs
6194
// - https://developer.github.com/v3/checks/runs/#create-a-check-run
62-
switch r.GetStatus() {
95+
switch run.GetStatus() {
6396
case "completed":
6497
completed++
6598
case "in_progress":
66-
inProgress++
99+
listWorkflowJobs(user, repoName, run.GetID(), func() { inProgress++ })
67100
case "queued":
68-
queued++
101+
listWorkflowJobs(user, repoName, run.GetID(), func() { queued++ })
69102
default:
70103
unknown++
71104
}

controllers/autoscaling_test.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ package controllers
22

33
import (
44
"fmt"
5+
"net/http/httptest"
6+
"net/url"
7+
"testing"
8+
59
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
610
"github.com/summerwind/actions-runner-controller/github"
711
"github.com/summerwind/actions-runner-controller/github/fake"
812
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
913
"k8s.io/apimachinery/pkg/runtime"
1014
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
11-
"net/http/httptest"
12-
"net/url"
1315
"sigs.k8s.io/controller-runtime/pkg/log/zap"
14-
"testing"
1516
)
1617

1718
func newGithubClient(server *httptest.Server) *github.Client {
@@ -44,9 +45,11 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
4445
sReplicas *int
4546
sTime *metav1.Time
4647
workflowRuns string
48+
workflowJobs map[int]string
4749
want int
4850
err string
4951
}{
52+
// Legacy functionality
5053
// 3 demanded, max at 3
5154
{
5255
repo: "test/valid",
@@ -122,6 +125,21 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
122125
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
123126
want: 3,
124127
},
128+
129+
// Job-level autoscaling
130+
// 5 requested from 3 workflows
131+
{
132+
repo: "test/valid",
133+
min: intPtr(2),
134+
max: intPtr(10),
135+
workflowRuns: `{"total_count": 4, "workflow_runs":[{"id": 1, "status":"queued"}, {"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}, {"status":"completed"}]}"`,
136+
workflowJobs: map[int]string{
137+
1: `{"jobs": [{"status":"queued"}, {"status":"queued"}]}`,
138+
2: `{"jobs": [{"status": "in_progress"}, {"status":"completed"}]}`,
139+
3: `{"jobs": [{"status": "in_progress"}, {"status":"queued"}]}`,
140+
},
141+
want: 5,
142+
},
125143
}
126144

127145
for i := range testcases {
@@ -136,7 +154,7 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
136154
_ = v1alpha1.AddToScheme(scheme)
137155

138156
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
139-
server := fake.NewServer(fake.WithListRepositoryWorkflowRunsResponse(200, tc.workflowRuns))
157+
server := fake.NewServer(fake.WithListRepositoryWorkflowRunsResponse(200, tc.workflowRuns), fake.WithListWorkflowJobsResponse(200, tc.workflowJobs))
140158
defer server.Close()
141159
client := newGithubClient(server)
142160

@@ -211,6 +229,7 @@ func TestDetermineDesiredReplicas_OrganizationalRunner(t *testing.T) {
211229
sReplicas *int
212230
sTime *metav1.Time
213231
workflowRuns string
232+
workflowJobs map[int]string
214233
want int
215234
err string
216235
}{
@@ -316,6 +335,22 @@ func TestDetermineDesiredReplicas_OrganizationalRunner(t *testing.T) {
316335
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
317336
err: "validating autoscaling metrics: spec.autoscaling.metrics[].repositoryNames is required and must have one more more entries for organizational runner deployment",
318337
},
338+
339+
// Job-level autoscaling
340+
// 5 requested from 3 workflows
341+
{
342+
org: "test",
343+
repos: []string{"valid"},
344+
min: intPtr(2),
345+
max: intPtr(10),
346+
workflowRuns: `{"total_count": 4, "workflow_runs":[{"id": 1, "status":"queued"}, {"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}, {"status":"completed"}]}"`,
347+
workflowJobs: map[int]string{
348+
1: `{"jobs": [{"status":"queued"}, {"status":"queued"}]}`,
349+
2: `{"jobs": [{"status": "in_progress"}, {"status":"completed"}]}`,
350+
3: `{"jobs": [{"status": "in_progress"}, {"status":"queued"}]}`,
351+
},
352+
want: 5,
353+
},
319354
}
320355

321356
for i := range testcases {
@@ -330,7 +365,7 @@ func TestDetermineDesiredReplicas_OrganizationalRunner(t *testing.T) {
330365
_ = v1alpha1.AddToScheme(scheme)
331366

332367
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
333-
server := fake.NewServer(fake.WithListRepositoryWorkflowRunsResponse(200, tc.workflowRuns))
368+
server := fake.NewServer(fake.WithListRepositoryWorkflowRunsResponse(200, tc.workflowRuns), fake.WithListWorkflowJobsResponse(200, tc.workflowJobs))
334369
defer server.Close()
335370
client := newGithubClient(server)
336371

github/fake/fake.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"fmt"
55
"net/http"
66
"net/http/httptest"
7+
"strconv"
8+
"strings"
79
"time"
10+
"unicode"
811
)
912

1013
const (
@@ -31,6 +34,24 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
3134
fmt.Fprintf(w, h.Body)
3235
}
3336

37+
type MapHandler struct {
38+
Status int
39+
Bodies map[int]string
40+
}
41+
42+
func (h *MapHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
43+
// Parse out int key from URL path
44+
key, err := strconv.Atoi(strings.TrimFunc(req.URL.Path, func(r rune) bool { return !unicode.IsNumber(r) }))
45+
if err != nil {
46+
w.WriteHeader(400)
47+
} else if body := h.Bodies[key]; len(body) == 0 {
48+
w.WriteHeader(404)
49+
} else {
50+
w.WriteHeader(h.Status)
51+
fmt.Fprintf(w, body)
52+
}
53+
}
54+
3455
type ServerConfig struct {
3556
*FixedResponses
3657
}
@@ -45,7 +66,7 @@ func NewServer(opts ...Option) *httptest.Server {
4566
o(&config)
4667
}
4768

48-
routes := map[string]*Handler{
69+
routes := map[string]http.Handler{
4970
// For CreateRegistrationToken
5071
"/repos/test/valid/actions/runners/registration-token": &Handler{
5172
Status: http.StatusCreated,
@@ -126,6 +147,9 @@ func NewServer(opts ...Option) *httptest.Server {
126147

127148
// For auto-scaling based on the number of queued(pending) workflow runs
128149
"/repos/test/valid/actions/runs": config.FixedResponses.ListRepositoryWorkflowRuns,
150+
151+
// For auto-scaling based on the number of queued(pending) workflow jobs
152+
"/repos/test/valid/actions/runs/": config.FixedResponses.ListWorkflowJobs,
129153
}
130154

131155
mux := http.NewServeMux()

github/fake/options.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package fake
22

33
type FixedResponses struct {
44
ListRepositoryWorkflowRuns *Handler
5+
ListWorkflowJobs *MapHandler
56
}
67

78
type Option func(*ServerConfig)
@@ -15,6 +16,15 @@ func WithListRepositoryWorkflowRunsResponse(status int, body string) Option {
1516
}
1617
}
1718

19+
func WithListWorkflowJobsResponse(status int, bodies map[int]string) Option {
20+
return func(c *ServerConfig) {
21+
c.FixedResponses.ListWorkflowJobs = &MapHandler{
22+
Status: status,
23+
Bodies: bodies,
24+
}
25+
}
26+
}
27+
1828
func WithFixedResponses(responses *FixedResponses) Option {
1929
return func(c *ServerConfig) {
2030
c.FixedResponses = responses

0 commit comments

Comments
 (0)