2020-06-27 17:26:46 +09:00
package controllers
import (
"fmt"
2020-10-07 17:00:44 -07:00
"net/http/httptest"
"net/url"
"testing"
2020-06-27 17:26:46 +09:00
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
"github.com/summerwind/actions-runner-controller/github"
"github.com/summerwind/actions-runner-controller/github/fake"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)
func newGithubClient ( server * httptest . Server ) * github . Client {
2020-10-28 15:15:53 +02:00
c := github . Config {
Token : "token" ,
}
client , err := c . NewClient ()
2020-06-27 17:26:46 +09:00
if err != nil {
panic ( err )
}
baseURL , err := url . Parse ( server . URL + "/" )
if err != nil {
panic ( err )
}
client . Client . BaseURL = baseURL
return client
}
func TestDetermineDesiredReplicas_RepositoryRunner ( t * testing . T ) {
intPtr := func ( v int ) * int {
return & v
}
metav1Now := metav1 . Now ()
testcases := [] struct {
repo string
org string
fixed * int
max * int
min * int
sReplicas * int
sTime * metav1 . Time
workflowRuns string
2020-10-07 17:00:44 -07:00
workflowJobs map [ int ] string
2020-06-27 17:26:46 +09:00
want int
err string
}{
2020-10-07 17:00:44 -07:00
// Legacy functionality
2020-06-27 17:26:46 +09:00
// 3 demanded, max at 3
{
repo : "test/valid" ,
min : intPtr ( 2 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 3 ,
},
// 2 demanded, max at 3, currently 3, delay scaling down due to grace period
{
repo : "test/valid" ,
min : intPtr ( 2 ),
max : intPtr ( 3 ),
sReplicas : intPtr ( 3 ),
sTime : & metav1Now ,
workflowRuns : `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 3 ,
},
// 3 demanded, max at 2
{
repo : "test/valid" ,
min : intPtr ( 2 ),
max : intPtr ( 2 ),
workflowRuns : `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 2 ,
},
// 2 demanded, min at 2
{
repo : "test/valid" ,
min : intPtr ( 2 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 3, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 2 ,
},
// 1 demanded, min at 2
{
repo : "test/valid" ,
min : intPtr ( 2 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"` ,
want : 2 ,
},
// 1 demanded, min at 2
{
repo : "test/valid" ,
min : intPtr ( 2 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 2 ,
},
// 1 demanded, min at 1
{
repo : "test/valid" ,
min : intPtr ( 1 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"` ,
want : 1 ,
},
// 1 demanded, min at 1
{
repo : "test/valid" ,
min : intPtr ( 1 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 1 ,
},
// fixed at 3
{
2020-07-19 18:42:06 +09:00
repo : "test/valid" ,
min : intPtr ( 1 ),
max : intPtr ( 3 ),
fixed : intPtr ( 3 ),
workflowRuns : `{"total_count": 4, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 3 ,
2020-06-27 17:26:46 +09:00
},
2020-10-07 17:00:44 -07:00
// Job-level autoscaling
// 5 requested from 3 workflows
{
repo : "test/valid" ,
min : intPtr ( 2 ),
max : intPtr ( 10 ),
workflowRuns : `{"total_count": 4, "workflow_runs":[{"id": 1, "status":"queued"}, {"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}, {"status":"completed"}]}"` ,
workflowJobs : map [ int ] string {
1 : `{"jobs": [{"status":"queued"}, {"status":"queued"}]}` ,
2 : `{"jobs": [{"status": "in_progress"}, {"status":"completed"}]}` ,
3 : `{"jobs": [{"status": "in_progress"}, {"status":"queued"}]}` ,
},
want : 5 ,
},
2020-06-27 17:26:46 +09:00
}
for i := range testcases {
tc := testcases [ i ]
log := zap . New ( func ( o * zap . Options ) {
o . Development = true
})
scheme := runtime . NewScheme ()
_ = clientgoscheme . AddToScheme ( scheme )
_ = v1alpha1 . AddToScheme ( scheme )
t . Run ( fmt . Sprintf ( "case %d" , i ), func ( t * testing . T ) {
2021-02-07 17:37:27 +09:00
server := fake . NewServer (
fake . WithListRepositoryWorkflowRunsResponse ( 200 , tc . workflowRuns ),
fake . WithListWorkflowJobsResponse ( 200 , tc . workflowJobs ),
fake . WithListRunnersResponse ( 200 , fake . RunnersListBody ),
)
2020-06-27 17:26:46 +09:00
defer server . Close ()
client := newGithubClient ( server )
2020-07-19 18:42:06 +09:00
h := & HorizontalRunnerAutoscalerReconciler {
2020-06-27 17:26:46 +09:00
Log : log ,
GitHubClient : client ,
Scheme : scheme ,
}
rd := v1alpha1 . RunnerDeployment {
TypeMeta : metav1 . TypeMeta {},
2020-07-19 18:42:06 +09:00
ObjectMeta : metav1 . ObjectMeta {
Name : "testrd" ,
},
2020-06-27 17:26:46 +09:00
Spec : v1alpha1 . RunnerDeploymentSpec {
Template : v1alpha1 . RunnerTemplate {
Spec : v1alpha1 . RunnerSpec {
Repository : tc . repo ,
},
},
2020-07-19 18:42:06 +09:00
Replicas : tc . fixed ,
},
Status : v1alpha1 . RunnerDeploymentStatus {
Replicas : tc . sReplicas ,
},
}
hra := v1alpha1 . HorizontalRunnerAutoscaler {
Spec : v1alpha1 . HorizontalRunnerAutoscalerSpec {
2020-06-27 17:26:46 +09:00
MaxReplicas : tc . max ,
MinReplicas : tc . min ,
},
2020-07-19 18:42:06 +09:00
Status : v1alpha1 . HorizontalRunnerAutoscalerStatus {
DesiredReplicas : tc . sReplicas ,
2020-06-27 17:26:46 +09:00
LastSuccessfulScaleOutTime : tc . sTime ,
},
}
2020-07-19 18:42:06 +09:00
got , err := h . computeReplicas ( rd , hra )
2020-06-27 17:26:46 +09:00
if err != nil {
if tc . err == "" {
t . Fatalf ( "unexpected error: expected none, got %v" , err )
} else if err . Error () != tc . err {
t . Fatalf ( "unexpected error: expected %v, got %v" , tc . err , err )
}
return
}
if got == nil {
t . Fatalf ( "unexpected value of rs.Spec.Replicas: nil" )
}
if * got != tc . want {
t . Errorf ( "%d: incorrect desired replicas: want %d, got %d" , i , tc . want , * got )
}
})
}
}
2020-07-03 09:05:46 +09:00
func TestDetermineDesiredReplicas_OrganizationalRunner ( t * testing . T ) {
intPtr := func ( v int ) * int {
return & v
}
metav1Now := metav1 . Now ()
testcases := [] struct {
repos [] string
org string
fixed * int
max * int
min * int
sReplicas * int
sTime * metav1 . Time
workflowRuns string
2020-10-07 17:00:44 -07:00
workflowJobs map [ int ] string
2020-07-03 09:05:46 +09:00
want int
err string
}{
// 3 demanded, max at 3
{
org : "test" ,
repos : [] string { "valid" },
min : intPtr ( 2 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 3 ,
},
// 2 demanded, max at 3, currently 3, delay scaling down due to grace period
{
org : "test" ,
repos : [] string { "valid" },
min : intPtr ( 2 ),
max : intPtr ( 3 ),
sReplicas : intPtr ( 3 ),
sTime : & metav1Now ,
workflowRuns : `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 3 ,
},
// 3 demanded, max at 2
{
org : "test" ,
repos : [] string { "valid" },
min : intPtr ( 2 ),
max : intPtr ( 2 ),
workflowRuns : `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 2 ,
},
// 2 demanded, min at 2
{
org : "test" ,
repos : [] string { "valid" },
min : intPtr ( 2 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 3, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 2 ,
},
// 1 demanded, min at 2
{
org : "test" ,
repos : [] string { "valid" },
min : intPtr ( 2 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"` ,
want : 2 ,
},
// 1 demanded, min at 2
{
org : "test" ,
repos : [] string { "valid" },
min : intPtr ( 2 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 2 ,
},
// 1 demanded, min at 1
{
org : "test" ,
repos : [] string { "valid" },
min : intPtr ( 1 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"` ,
want : 1 ,
},
// 1 demanded, min at 1
{
org : "test" ,
repos : [] string { "valid" },
min : intPtr ( 1 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 1 ,
},
// fixed at 3
{
2020-07-19 18:42:06 +09:00
org : "test" ,
repos : [] string { "valid" },
fixed : intPtr ( 1 ),
min : intPtr ( 1 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 3 ,
2020-07-03 09:05:46 +09:00
},
// org runner, fixed at 3
{
2020-07-19 18:42:06 +09:00
org : "test" ,
repos : [] string { "valid" },
fixed : intPtr ( 1 ),
min : intPtr ( 1 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"` ,
want : 3 ,
2020-07-03 09:05:46 +09:00
},
// org runner, 1 demanded, min at 1, no repos
{
org : "test" ,
min : intPtr ( 1 ),
max : intPtr ( 3 ),
workflowRuns : `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"` ,
err : "validating autoscaling metrics: spec.autoscaling.metrics[].repositoryNames is required and must have one more more entries for organizational runner deployment" ,
},
2020-10-07 17:00:44 -07:00
// Job-level autoscaling
// 5 requested from 3 workflows
{
org : "test" ,
repos : [] string { "valid" },
min : intPtr ( 2 ),
max : intPtr ( 10 ),
workflowRuns : `{"total_count": 4, "workflow_runs":[{"id": 1, "status":"queued"}, {"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}, {"status":"completed"}]}"` ,
workflowJobs : map [ int ] string {
1 : `{"jobs": [{"status":"queued"}, {"status":"queued"}]}` ,
2 : `{"jobs": [{"status": "in_progress"}, {"status":"completed"}]}` ,
3 : `{"jobs": [{"status": "in_progress"}, {"status":"queued"}]}` ,
},
want : 5 ,
},
2020-07-03 09:05:46 +09:00
}
for i := range testcases {
tc := testcases [ i ]
log := zap . New ( func ( o * zap . Options ) {
o . Development = true
})
scheme := runtime . NewScheme ()
_ = clientgoscheme . AddToScheme ( scheme )
_ = v1alpha1 . AddToScheme ( scheme )
t . Run ( fmt . Sprintf ( "case %d" , i ), func ( t * testing . T ) {
2021-02-07 17:37:27 +09:00
server := fake . NewServer (
fake . WithListRepositoryWorkflowRunsResponse ( 200 , tc . workflowRuns ),
fake . WithListWorkflowJobsResponse ( 200 , tc . workflowJobs ),
fake . WithListRunnersResponse ( 200 , fake . RunnersListBody ),
)
2020-07-03 09:05:46 +09:00
defer server . Close ()
client := newGithubClient ( server )
2020-07-19 18:42:06 +09:00
h := & HorizontalRunnerAutoscalerReconciler {
2020-07-03 09:05:46 +09:00
Log : log ,
Scheme : scheme ,
2020-07-19 18:42:06 +09:00
GitHubClient : client ,
2020-07-03 09:05:46 +09:00
}
rd := v1alpha1 . RunnerDeployment {
2020-07-19 18:42:06 +09:00
ObjectMeta : metav1 . ObjectMeta {
Name : "testrd" ,
},
2020-07-03 09:05:46 +09:00
Spec : v1alpha1 . RunnerDeploymentSpec {
Template : v1alpha1 . RunnerTemplate {
Spec : v1alpha1 . RunnerSpec {
Organization : tc . org ,
},
},
2020-07-19 18:42:06 +09:00
Replicas : tc . fixed ,
2020-07-03 09:05:46 +09:00
},
Status : v1alpha1 . RunnerDeploymentStatus {
2020-07-19 18:42:06 +09:00
Replicas : tc . sReplicas ,
},
}
hra := v1alpha1 . HorizontalRunnerAutoscaler {
Spec : v1alpha1 . HorizontalRunnerAutoscalerSpec {
ScaleTargetRef : v1alpha1 . ScaleTargetRef {
Name : "testrd" ,
},
MaxReplicas : tc . max ,
MinReplicas : tc . min ,
Metrics : [] v1alpha1 . MetricSpec {
{
Type : v1alpha1 . AutoscalingMetricTypeTotalNumberOfQueuedAndInProgressWorkflowRuns ,
RepositoryNames : tc . repos ,
},
},
},
Status : v1alpha1 . HorizontalRunnerAutoscalerStatus {
DesiredReplicas : tc . sReplicas ,
2020-07-03 09:05:46 +09:00
LastSuccessfulScaleOutTime : tc . sTime ,
},
}
2020-07-19 18:42:06 +09:00
got , err := h . computeReplicas ( rd , hra )
2020-07-03 09:05:46 +09:00
if err != nil {
if tc . err == "" {
t . Fatalf ( "unexpected error: expected none, got %v" , err )
} else if err . Error () != tc . err {
t . Fatalf ( "unexpected error: expected %v, got %v" , tc . err , err )
}
return
}
if got == nil {
2020-07-19 18:42:06 +09:00
t . Fatalf ( "unexpected value of rs.Spec.Replicas: nil, wanted %v" , tc . want )
2020-07-03 09:05:46 +09:00
}
if * got != tc . want {
t . Errorf ( "%d: incorrect desired replicas: want %d, got %d" , i , tc . want , * got )
}
})
}
}