mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 11:41:27 +00:00
This introduces a linter to PRs to help with code reviews and code hygiene. I've also gone ahead and fixed (or ignored) the existing lints. I've only setup the default linters right now. There are many more options that are documented at https://golangci-lint.run/. The GitHub Action should add appropriate annotations to the lint job for the PR. Contributors can also lint locally using `make lint`.
640 lines
14 KiB
Go
640 lines
14 KiB
Go
package controllers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
|
"github.com/go-logr/logr"
|
|
"github.com/google/go-github/v47/github"
|
|
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/client/fake"
|
|
)
|
|
|
|
var (
|
|
sc = runtime.NewScheme()
|
|
)
|
|
|
|
func init() {
|
|
_ = clientgoscheme.AddToScheme(sc)
|
|
_ = actionsv1alpha1.AddToScheme(sc)
|
|
}
|
|
|
|
func TestOrgWebhookCheckRun(t *testing.T) {
|
|
f, err := os.Open("testdata/org_webhook_check_run_payload.json")
|
|
if err != nil {
|
|
t.Fatalf("could not open the fixture: %s", err)
|
|
}
|
|
defer f.Close()
|
|
var e github.CheckRunEvent
|
|
if err := json.NewDecoder(f).Decode(&e); err != nil {
|
|
t.Fatalf("invalid json: %s", err)
|
|
}
|
|
testServer(t,
|
|
"check_run",
|
|
&e,
|
|
200,
|
|
"no horizontalrunnerautoscaler to scale for this github event",
|
|
)
|
|
}
|
|
|
|
func TestRepoWebhookCheckRun(t *testing.T) {
|
|
f, err := os.Open("testdata/repo_webhook_check_run_payload.json")
|
|
if err != nil {
|
|
t.Fatalf("could not open the fixture: %s", err)
|
|
}
|
|
defer f.Close()
|
|
var e github.CheckRunEvent
|
|
if err := json.NewDecoder(f).Decode(&e); err != nil {
|
|
t.Fatalf("invalid json: %s", err)
|
|
}
|
|
testServer(t,
|
|
"check_run",
|
|
&e,
|
|
200,
|
|
"no horizontalrunnerautoscaler to scale for this github event",
|
|
)
|
|
}
|
|
|
|
func TestWebhookPullRequest(t *testing.T) {
|
|
testServer(t,
|
|
"pull_request",
|
|
&github.PullRequestEvent{
|
|
PullRequest: &github.PullRequest{
|
|
Base: &github.PullRequestBranch{
|
|
Ref: github.String("main"),
|
|
},
|
|
},
|
|
Repo: &github.Repository{
|
|
Name: github.String("myorg/myrepo"),
|
|
Organization: &github.Organization{
|
|
Name: github.String("myorg"),
|
|
},
|
|
},
|
|
Action: github.String("created"),
|
|
},
|
|
200,
|
|
"no horizontalrunnerautoscaler to scale for this github event",
|
|
)
|
|
}
|
|
|
|
func TestWebhookPush(t *testing.T) {
|
|
testServer(t,
|
|
"push",
|
|
&github.PushEvent{
|
|
Repo: &github.PushEventRepository{
|
|
Name: github.String("myrepo"),
|
|
Organization: github.String("myorg"),
|
|
},
|
|
},
|
|
200,
|
|
"no horizontalrunnerautoscaler to scale for this github event",
|
|
)
|
|
}
|
|
|
|
func TestWebhookPing(t *testing.T) {
|
|
testServer(t,
|
|
"ping",
|
|
&github.PingEvent{
|
|
Zen: github.String("zen"),
|
|
},
|
|
200,
|
|
"pong",
|
|
)
|
|
}
|
|
|
|
func TestWebhookWorkflowJob(t *testing.T) {
|
|
setupTest := func() github.WorkflowJobEvent {
|
|
f, err := os.Open("testdata/org_webhook_workflow_job_payload.json")
|
|
if err != nil {
|
|
t.Fatalf("could not open the fixture: %s", err)
|
|
}
|
|
defer f.Close()
|
|
var e github.WorkflowJobEvent
|
|
if err := json.NewDecoder(f).Decode(&e); err != nil {
|
|
t.Fatalf("invalid json: %s", err)
|
|
}
|
|
|
|
return e
|
|
}
|
|
t.Run("Successful", func(t *testing.T) {
|
|
e := setupTest()
|
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
|
ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
|
|
Name: "test-name",
|
|
},
|
|
ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
|
|
{
|
|
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
|
WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
rd := &actionsv1alpha1.RunnerDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.RunnerDeploymentSpec{
|
|
Template: actionsv1alpha1.RunnerTemplate{
|
|
Spec: actionsv1alpha1.RunnerSpec{
|
|
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
|
Organization: "MYORG",
|
|
Labels: []string{"label1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
initObjs := []runtime.Object{hra, rd}
|
|
|
|
testServerWithInitObjs(t,
|
|
"workflow_job",
|
|
&e,
|
|
200,
|
|
"scaled test-name by 1",
|
|
initObjs,
|
|
)
|
|
})
|
|
t.Run("WrongLabels", func(t *testing.T) {
|
|
e := setupTest()
|
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
|
ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
|
|
Name: "test-name",
|
|
},
|
|
ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
|
|
{
|
|
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
|
WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
rd := &actionsv1alpha1.RunnerDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.RunnerDeploymentSpec{
|
|
Template: actionsv1alpha1.RunnerTemplate{
|
|
Spec: actionsv1alpha1.RunnerSpec{
|
|
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
|
Organization: "MYORG",
|
|
Labels: []string{"bad-label"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
initObjs := []runtime.Object{hra, rd}
|
|
|
|
testServerWithInitObjs(t,
|
|
"workflow_job",
|
|
&e,
|
|
200,
|
|
"no horizontalrunnerautoscaler to scale for this github event",
|
|
initObjs,
|
|
)
|
|
})
|
|
// This test verifies that the old way of matching labels doesn't work anymore
|
|
t.Run("OldLabels", func(t *testing.T) {
|
|
e := setupTest()
|
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
|
ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
|
|
Name: "test-name",
|
|
},
|
|
ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
|
|
{
|
|
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
|
WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
rd := &actionsv1alpha1.RunnerDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.RunnerDeploymentSpec{
|
|
Template: actionsv1alpha1.RunnerTemplate{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"label1": "label1",
|
|
},
|
|
},
|
|
Spec: actionsv1alpha1.RunnerSpec{
|
|
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
|
Organization: "MYORG",
|
|
Labels: []string{"bad-label"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
initObjs := []runtime.Object{hra, rd}
|
|
|
|
testServerWithInitObjs(t,
|
|
"workflow_job",
|
|
&e,
|
|
200,
|
|
"no horizontalrunnerautoscaler to scale for this github event",
|
|
initObjs,
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestWebhookWorkflowJobWithSelfHostedLabel(t *testing.T) {
|
|
setupTest := func() github.WorkflowJobEvent {
|
|
f, err := os.Open("testdata/org_webhook_workflow_job_with_self_hosted_label_payload.json")
|
|
if err != nil {
|
|
t.Fatalf("could not open the fixture: %s", err)
|
|
}
|
|
defer f.Close()
|
|
var e github.WorkflowJobEvent
|
|
if err := json.NewDecoder(f).Decode(&e); err != nil {
|
|
t.Fatalf("invalid json: %s", err)
|
|
}
|
|
|
|
return e
|
|
}
|
|
t.Run("Successful", func(t *testing.T) {
|
|
e := setupTest()
|
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
|
ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
|
|
Name: "test-name",
|
|
},
|
|
ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
|
|
{
|
|
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
|
WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
rd := &actionsv1alpha1.RunnerDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.RunnerDeploymentSpec{
|
|
Template: actionsv1alpha1.RunnerTemplate{
|
|
Spec: actionsv1alpha1.RunnerSpec{
|
|
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
|
Organization: "MYORG",
|
|
Labels: []string{"label1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
initObjs := []runtime.Object{hra, rd}
|
|
|
|
testServerWithInitObjs(t,
|
|
"workflow_job",
|
|
&e,
|
|
200,
|
|
"scaled test-name by 1",
|
|
initObjs,
|
|
)
|
|
})
|
|
t.Run("WrongLabels", func(t *testing.T) {
|
|
e := setupTest()
|
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
|
ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
|
|
Name: "test-name",
|
|
},
|
|
ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
|
|
{
|
|
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
|
WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
rd := &actionsv1alpha1.RunnerDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.RunnerDeploymentSpec{
|
|
Template: actionsv1alpha1.RunnerTemplate{
|
|
Spec: actionsv1alpha1.RunnerSpec{
|
|
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
|
Organization: "MYORG",
|
|
Labels: []string{"bad-label"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
initObjs := []runtime.Object{hra, rd}
|
|
|
|
testServerWithInitObjs(t,
|
|
"workflow_job",
|
|
&e,
|
|
200,
|
|
"no horizontalrunnerautoscaler to scale for this github event",
|
|
initObjs,
|
|
)
|
|
})
|
|
// This test verifies that the old way of matching labels doesn't work anymore
|
|
t.Run("OldLabels", func(t *testing.T) {
|
|
e := setupTest()
|
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
|
ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
|
|
Name: "test-name",
|
|
},
|
|
ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
|
|
{
|
|
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
|
WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
rd := &actionsv1alpha1.RunnerDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-name",
|
|
},
|
|
Spec: actionsv1alpha1.RunnerDeploymentSpec{
|
|
Template: actionsv1alpha1.RunnerTemplate{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"label1": "label1",
|
|
},
|
|
},
|
|
Spec: actionsv1alpha1.RunnerSpec{
|
|
RunnerConfig: actionsv1alpha1.RunnerConfig{
|
|
Organization: "MYORG",
|
|
Labels: []string{"bad-label"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
initObjs := []runtime.Object{hra, rd}
|
|
|
|
testServerWithInitObjs(t,
|
|
"workflow_job",
|
|
&e,
|
|
200,
|
|
"no horizontalrunnerautoscaler to scale for this github event",
|
|
initObjs,
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestGetRequest(t *testing.T) {
|
|
hra := HorizontalRunnerAutoscalerGitHubWebhook{}
|
|
request, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
recorder := httptest.ResponseRecorder{}
|
|
|
|
hra.Handle(&recorder, request)
|
|
response := recorder.Result()
|
|
|
|
if response.StatusCode != http.StatusOK {
|
|
t.Errorf("want %d, got %d", http.StatusOK, response.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestGetValidCapacityReservations(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
|
CapacityReservations: []actionsv1alpha1.CapacityReservation{
|
|
{
|
|
ExpirationTime: metav1.Time{Time: now.Add(-time.Second)},
|
|
Replicas: 1,
|
|
},
|
|
{
|
|
ExpirationTime: metav1.Time{Time: now},
|
|
Replicas: 2,
|
|
},
|
|
{
|
|
ExpirationTime: metav1.Time{Time: now.Add(time.Second)},
|
|
Replicas: 3,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
revs := getValidCapacityReservations(hra)
|
|
|
|
var count int
|
|
|
|
for _, r := range revs {
|
|
count += r.Replicas
|
|
}
|
|
|
|
want := 3
|
|
|
|
if count != want {
|
|
t.Errorf("want %d, got %d", want, count)
|
|
}
|
|
}
|
|
|
|
func installTestLogger(webhook *HorizontalRunnerAutoscalerGitHubWebhook) *bytes.Buffer {
|
|
logs := &bytes.Buffer{}
|
|
|
|
sink := &testLogSink{
|
|
name: "testlog",
|
|
writer: logs,
|
|
}
|
|
|
|
log := logr.New(sink)
|
|
|
|
webhook.Log = log
|
|
|
|
return logs
|
|
}
|
|
|
|
func testServerWithInitObjs(t *testing.T, eventType string, event interface{}, wantCode int, wantBody string, initObjs []runtime.Object) {
|
|
t.Helper()
|
|
|
|
hraWebhook := &HorizontalRunnerAutoscalerGitHubWebhook{}
|
|
|
|
client := fake.NewClientBuilder().WithScheme(sc).WithRuntimeObjects(initObjs...).Build()
|
|
|
|
logs := installTestLogger(hraWebhook)
|
|
|
|
defer func() {
|
|
if t.Failed() {
|
|
t.Logf("diagnostics: %s", logs.String())
|
|
}
|
|
}()
|
|
|
|
hraWebhook.Client = client
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", hraWebhook.Handle)
|
|
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
resp, err := sendWebhook(server, eventType, event)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() {
|
|
if resp != nil {
|
|
resp.Body.Close()
|
|
}
|
|
}()
|
|
|
|
if resp.StatusCode != wantCode {
|
|
t.Error("status:", resp.StatusCode)
|
|
}
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if string(respBody) != wantBody {
|
|
t.Fatal("body:", string(respBody))
|
|
}
|
|
}
|
|
|
|
func testServer(t *testing.T, eventType string, event interface{}, wantCode int, wantBody string) {
|
|
var initObjs []runtime.Object
|
|
testServerWithInitObjs(t, eventType, event, wantCode, wantBody, initObjs)
|
|
}
|
|
|
|
func sendWebhook(server *httptest.Server, eventType string, event interface{}) (*http.Response, error) {
|
|
jsonBuf := &bytes.Buffer{}
|
|
enc := json.NewEncoder(jsonBuf)
|
|
enc.SetIndent(" ", "")
|
|
err := enc.Encode(event)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("[bug in test] encoding event to json: %+v", err)
|
|
}
|
|
|
|
reqBody := jsonBuf.Bytes()
|
|
|
|
u, err := url.Parse(server.URL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing server url: %v", err)
|
|
}
|
|
|
|
req := &http.Request{
|
|
Method: http.MethodPost,
|
|
URL: u,
|
|
Header: map[string][]string{
|
|
"X-GitHub-Event": {eventType},
|
|
"Content-Type": {"application/json"},
|
|
},
|
|
Body: io.NopCloser(bytes.NewBuffer(reqBody)),
|
|
}
|
|
|
|
return http.DefaultClient.Do(req)
|
|
}
|
|
|
|
// testLogSink is a sample logr.Logger that logs in-memory.
|
|
// It's only for testing log outputs.
|
|
type testLogSink struct {
|
|
name string
|
|
keyValues map[string]interface{}
|
|
|
|
writer io.Writer
|
|
}
|
|
|
|
var _ logr.LogSink = &testLogSink{}
|
|
|
|
func (l *testLogSink) Init(_ logr.RuntimeInfo) {
|
|
|
|
}
|
|
|
|
func (l *testLogSink) Info(_ int, msg string, kvs ...interface{}) {
|
|
fmt.Fprintf(l.writer, "%s] %s\t", l.name, msg)
|
|
for k, v := range l.keyValues {
|
|
fmt.Fprintf(l.writer, "%s=%+v ", k, v)
|
|
}
|
|
for i := 0; i < len(kvs); i += 2 {
|
|
fmt.Fprintf(l.writer, "%s=%+v ", kvs[i], kvs[i+1])
|
|
}
|
|
fmt.Fprintf(l.writer, "\n")
|
|
}
|
|
|
|
func (*testLogSink) Enabled(level int) bool {
|
|
return true
|
|
}
|
|
|
|
func (l *testLogSink) Error(err error, msg string, kvs ...interface{}) {
|
|
kvs = append(kvs, "error", err)
|
|
l.Info(0, msg, kvs...)
|
|
}
|
|
|
|
func (l *testLogSink) WithName(name string) logr.LogSink {
|
|
return &testLogSink{
|
|
name: l.name + "." + name,
|
|
keyValues: l.keyValues,
|
|
writer: l.writer,
|
|
}
|
|
}
|
|
|
|
func (l *testLogSink) WithValues(kvs ...interface{}) logr.LogSink {
|
|
newMap := make(map[string]interface{}, len(l.keyValues)+len(kvs)/2)
|
|
for k, v := range l.keyValues {
|
|
newMap[k] = v
|
|
}
|
|
for i := 0; i < len(kvs); i += 2 {
|
|
newMap[kvs[i].(string)] = kvs[i+1]
|
|
}
|
|
return &testLogSink{
|
|
name: l.name,
|
|
keyValues: newMap,
|
|
writer: l.writer,
|
|
}
|
|
}
|