mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-11 12:06:57 +00:00
Introduce new preview auto-scaling mode for ARC. (#2153)
Co-authored-by: Cory Miller <cory-miller@github.com> Co-authored-by: Nikola Jokic <nikola-jokic@github.com> Co-authored-by: Ava Stancu <AvaStancu@github.com> Co-authored-by: Ferenc Hammerl <fhammerl@github.com> Co-authored-by: Francesco Renzi <rentziass@github.com> Co-authored-by: Bassem Dghaidi <Link-@github.com>
This commit is contained in:
129
cmd/githubrunnerscalesetlistener/autoScalerKubernetesManager.go
Normal file
129
cmd/githubrunnerscalesetlistener/autoScalerKubernetesManager.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type AutoScalerKubernetesManager struct {
|
||||
*kubernetes.Clientset
|
||||
|
||||
logger logr.Logger
|
||||
}
|
||||
|
||||
func NewKubernetesManager(logger *logr.Logger) (*AutoScalerKubernetesManager, error) {
|
||||
conf, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubeClient, err := kubernetes.NewForConfig(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var manager = &AutoScalerKubernetesManager{
|
||||
Clientset: kubeClient,
|
||||
logger: logger.WithName("KubernetesManager"),
|
||||
}
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
func (k *AutoScalerKubernetesManager) ScaleEphemeralRunnerSet(ctx context.Context, namespace, resourceName string, runnerCount int) error {
|
||||
original := &v1alpha1.EphemeralRunnerSet{
|
||||
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
||||
Replicas: -1,
|
||||
},
|
||||
}
|
||||
originalJson, err := json.Marshal(original)
|
||||
if err != nil {
|
||||
k.logger.Error(err, "could not marshal empty ephemeral runner set")
|
||||
}
|
||||
|
||||
patch := &v1alpha1.EphemeralRunnerSet{
|
||||
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
||||
Replicas: runnerCount,
|
||||
},
|
||||
}
|
||||
patchJson, err := json.Marshal(patch)
|
||||
if err != nil {
|
||||
k.logger.Error(err, "could not marshal patch ephemeral runner set")
|
||||
}
|
||||
mergePatch, err := jsonpatch.CreateMergePatch(originalJson, patchJson)
|
||||
if err != nil {
|
||||
k.logger.Error(err, "could not create merge patch json for ephemeral runner set")
|
||||
}
|
||||
|
||||
k.logger.Info("Created merge patch json for EphemeralRunnerSet update", "json", string(mergePatch))
|
||||
|
||||
patchedEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{}
|
||||
err = k.RESTClient().
|
||||
Patch(types.MergePatchType).
|
||||
Prefix("apis", "actions.github.com", "v1alpha1").
|
||||
Namespace(namespace).
|
||||
Resource("EphemeralRunnerSets").
|
||||
Name(resourceName).
|
||||
Body([]byte(mergePatch)).
|
||||
Do(ctx).
|
||||
Into(patchedEphemeralRunnerSet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not patch ephemeral runner set , patch JSON: %s, error: %w", string(mergePatch), err)
|
||||
}
|
||||
|
||||
k.logger.Info("Ephemeral runner set scaled.", "namespace", namespace, "name", resourceName, "replicas", patchedEphemeralRunnerSet.Spec.Replicas)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *AutoScalerKubernetesManager) UpdateEphemeralRunnerWithJobInfo(ctx context.Context, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName string, workflowRunId, jobRequestId int64) error {
|
||||
original := &v1alpha1.EphemeralRunner{}
|
||||
originalJson, err := json.Marshal(original)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal empty ephemeral runner, error: %w", err)
|
||||
}
|
||||
|
||||
patch := &v1alpha1.EphemeralRunner{
|
||||
Status: v1alpha1.EphemeralRunnerStatus{
|
||||
JobRequestId: jobRequestId,
|
||||
JobRepositoryName: fmt.Sprintf("%s/%s", ownerName, repositoryName),
|
||||
WorkflowRunId: workflowRunId,
|
||||
JobWorkflowRef: jobWorkflowRef,
|
||||
JobDisplayName: jobDisplayName,
|
||||
},
|
||||
}
|
||||
patchedJson, err := json.Marshal(patch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal patched ephemeral runner, error: %w", err)
|
||||
}
|
||||
|
||||
mergePatch, err := jsonpatch.CreateMergePatch(originalJson, patchedJson)
|
||||
if err != nil {
|
||||
k.logger.Error(err, "could not create merge patch json for ephemeral runner")
|
||||
}
|
||||
|
||||
k.logger.Info("Created merge patch json for EphemeralRunner status update", "json", string(mergePatch))
|
||||
|
||||
patchedStatus := &v1alpha1.EphemeralRunner{}
|
||||
err = k.RESTClient().
|
||||
Patch(types.MergePatchType).
|
||||
Prefix("apis", "actions.github.com", "v1alpha1").
|
||||
Namespace(namespace).
|
||||
Resource("EphemeralRunners").
|
||||
Name(resourceName).
|
||||
SubResource("status").
|
||||
Body(mergePatch).
|
||||
Do(ctx).
|
||||
Into(patchedStatus)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not patch ephemeral runner status, patch JSON: %s, error: %w", string(mergePatch), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
184
cmd/githubrunnerscalesetlistener/autoScalerMessageListener.go
Normal file
184
cmd/githubrunnerscalesetlistener/autoScalerMessageListener.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/actions/actions-runner-controller/github/actions"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
sessionCreationMaxRetryCount = 10
|
||||
)
|
||||
|
||||
type devContextKey bool
|
||||
|
||||
var testIgnoreSleep devContextKey = true
|
||||
|
||||
type AutoScalerClient struct {
|
||||
client actions.SessionService
|
||||
logger logr.Logger
|
||||
|
||||
lastMessageId int64
|
||||
initialMessage *actions.RunnerScaleSetMessage
|
||||
}
|
||||
|
||||
func NewAutoScalerClient(
|
||||
ctx context.Context,
|
||||
client actions.ActionsService,
|
||||
logger *logr.Logger,
|
||||
runnerScaleSetId int,
|
||||
options ...func(*AutoScalerClient),
|
||||
) (*AutoScalerClient, error) {
|
||||
listener := AutoScalerClient{
|
||||
logger: logger.WithName("auto_scaler"),
|
||||
}
|
||||
|
||||
session, initialMessage, err := createSession(ctx, &listener.logger, client, runnerScaleSetId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to create session. %w", err)
|
||||
}
|
||||
|
||||
listener.lastMessageId = 0
|
||||
listener.initialMessage = initialMessage
|
||||
listener.client = newSessionClient(client, logger, session)
|
||||
|
||||
for _, option := range options {
|
||||
option(&listener)
|
||||
}
|
||||
|
||||
return &listener, nil
|
||||
}
|
||||
|
||||
func createSession(ctx context.Context, logger *logr.Logger, client actions.ActionsService, runnerScaleSetId int) (*actions.RunnerScaleSetSession, *actions.RunnerScaleSetMessage, error) {
|
||||
hostName, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostName = uuid.New().String()
|
||||
logger.Info("could not get hostname, fail back to a random string.", "fallback", hostName)
|
||||
}
|
||||
|
||||
var runnerScaleSetSession *actions.RunnerScaleSetSession
|
||||
var retryCount int
|
||||
for {
|
||||
runnerScaleSetSession, err = client.CreateMessageSession(ctx, runnerScaleSetId, hostName)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
clientSideError := &actions.HttpClientSideError{}
|
||||
if errors.As(err, &clientSideError) && clientSideError.Code != http.StatusConflict {
|
||||
logger.Info("unable to create message session. The error indicates something is wrong on the client side, won't make any retry.")
|
||||
return nil, nil, fmt.Errorf("create message session http request failed. %w", err)
|
||||
}
|
||||
|
||||
retryCount++
|
||||
if retryCount >= sessionCreationMaxRetryCount {
|
||||
return nil, nil, fmt.Errorf("create message session failed since it exceed %d retry limit. %w", sessionCreationMaxRetryCount, err)
|
||||
}
|
||||
|
||||
logger.Info("unable to create message session. Will try again in 30 seconds", "error", err.Error())
|
||||
if ok := ctx.Value(testIgnoreSleep); ok == nil {
|
||||
time.Sleep(getRandomDuration(30, 45))
|
||||
}
|
||||
}
|
||||
|
||||
statistics, _ := json.Marshal(runnerScaleSetSession.Statistics)
|
||||
logger.Info("current runner scale set statistics.", "statistics", string(statistics))
|
||||
|
||||
if runnerScaleSetSession.Statistics.TotalAvailableJobs > 0 || runnerScaleSetSession.Statistics.TotalAssignedJobs > 0 {
|
||||
acquirableJobs, err := client.GetAcquirableJobs(ctx, runnerScaleSetId)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("get acquirable jobs failed. %w", err)
|
||||
}
|
||||
|
||||
acquirableJobsJson, err := json.Marshal(acquirableJobs.Jobs)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("marshal acquirable jobs failed. %w", err)
|
||||
}
|
||||
|
||||
initialMessage := &actions.RunnerScaleSetMessage{
|
||||
MessageId: 0,
|
||||
MessageType: "RunnerScaleSetJobMessages",
|
||||
Statistics: runnerScaleSetSession.Statistics,
|
||||
Body: string(acquirableJobsJson),
|
||||
}
|
||||
|
||||
return runnerScaleSetSession, initialMessage, nil
|
||||
}
|
||||
|
||||
return runnerScaleSetSession, nil, nil
|
||||
}
|
||||
|
||||
func (m *AutoScalerClient) Close() error {
|
||||
m.logger.Info("closing.")
|
||||
return m.client.Close()
|
||||
}
|
||||
|
||||
func (m *AutoScalerClient) GetRunnerScaleSetMessage(ctx context.Context, handler func(msg *actions.RunnerScaleSetMessage) error) error {
|
||||
if m.initialMessage != nil {
|
||||
err := handler(m.initialMessage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fail to process initial message. %w", err)
|
||||
}
|
||||
|
||||
m.initialMessage = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
message, err := m.client.GetMessage(ctx, m.lastMessageId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get message failed from refreshing client. %w", err)
|
||||
}
|
||||
|
||||
if message == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err = handler(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handle message failed. %w", err)
|
||||
}
|
||||
|
||||
m.lastMessageId = message.MessageId
|
||||
|
||||
return m.deleteMessage(ctx, message.MessageId)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AutoScalerClient) deleteMessage(ctx context.Context, messageId int64) error {
|
||||
err := m.client.DeleteMessage(ctx, messageId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete message failed from refreshing client. %w", err)
|
||||
}
|
||||
|
||||
m.logger.Info("deleted message.", "messageId", messageId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AutoScalerClient) AcquireJobsForRunnerScaleSet(ctx context.Context, requestIds []int64) error {
|
||||
m.logger.Info("acquiring jobs.", "request count", len(requestIds), "requestIds", fmt.Sprint(requestIds))
|
||||
if len(requestIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ids, err := m.client.AcquireJobs(ctx, requestIds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("acquire jobs failed from refreshing client. %w", err)
|
||||
}
|
||||
|
||||
m.logger.Info("acquired jobs.", "requested", len(requestIds), "acquired", len(ids))
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRandomDuration(minSeconds, maxSeconds int) time.Duration {
|
||||
return time.Duration(rand.Intn(maxSeconds-minSeconds)+minSeconds) * time.Second
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/actions/actions-runner-controller/github/actions"
|
||||
"github.com/actions/actions-runner-controller/logging"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateSession(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
assert.Equal(t, session, session, "Session is not correct")
|
||||
assert.Nil(t, asClient.initialMessage, "Initial message should be nil")
|
||||
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestCreateSession_CreateInitMessage(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAvailableJobs: 1,
|
||||
TotalAssignedJobs: 5,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{
|
||||
Count: 1,
|
||||
Jobs: []actions.AcquirableJob{
|
||||
{
|
||||
RunnerRequestId: 1,
|
||||
OwnerName: "owner",
|
||||
RepositoryName: "repo",
|
||||
AcquireJobUrl: "https://github.com",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
assert.Equal(t, session, session, "Session is not correct")
|
||||
assert.NotNil(t, asClient.initialMessage, "Initial message should not be nil")
|
||||
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0")
|
||||
assert.Equal(t, int64(0), asClient.initialMessage.MessageId, "Initial message id should be 0")
|
||||
assert.Equal(t, "RunnerScaleSetJobMessages", asClient.initialMessage.MessageType, "Initial message type should be RunnerScaleSetJobMessages")
|
||||
assert.Equal(t, 5, asClient.initialMessage.Statistics.TotalAssignedJobs, "Initial message total assigned jobs should be 5")
|
||||
assert.Equal(t, 1, asClient.initialMessage.Statistics.TotalAvailableJobs, "Initial message total available jobs should be 1")
|
||||
assert.Equal(t, "[{\"acquireJobUrl\":\"https://github.com\",\"messageType\":\"\",\"runnerRequestId\":1,\"repositoryName\":\"repo\",\"ownerName\":\"owner\",\"jobWorkflowRef\":\"\",\"eventName\":\"\",\"requestLabels\":null}]", asClient.initialMessage.Body, "Initial message body is not correct")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestCreateSession_CreateInitMessageWithOnlyAssignedJobs(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAssignedJobs: 5,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{
|
||||
Count: 0,
|
||||
Jobs: []actions.AcquirableJob{},
|
||||
}, nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
assert.Equal(t, session, session, "Session is not correct")
|
||||
assert.NotNil(t, asClient.initialMessage, "Initial message should not be nil")
|
||||
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0")
|
||||
assert.Equal(t, int64(0), asClient.initialMessage.MessageId, "Initial message id should be 0")
|
||||
assert.Equal(t, "RunnerScaleSetJobMessages", asClient.initialMessage.MessageType, "Initial message type should be RunnerScaleSetJobMessages")
|
||||
assert.Equal(t, 5, asClient.initialMessage.Statistics.TotalAssignedJobs, "Initial message total assigned jobs should be 5")
|
||||
assert.Equal(t, 0, asClient.initialMessage.Statistics.TotalAvailableJobs, "Initial message total available jobs should be 0")
|
||||
assert.Equal(t, "[]", asClient.initialMessage.Body, "Initial message body is not correct")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestCreateSession_CreateInitMessageFailed(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAvailableJobs: 1,
|
||||
TotalAssignedJobs: 5,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(nil, fmt.Errorf("error"))
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||
|
||||
assert.ErrorContains(t, err, "get acquirable jobs failed. error", "Unexpected error")
|
||||
assert.Nil(t, asClient, "Client should be nil")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestCreateSession_RetrySessionConflict(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.WithValue(context.Background(), testIgnoreSleep, true)
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(nil, &actions.HttpClientSideError{
|
||||
Code: 409,
|
||||
}).Once()
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil).Once()
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
assert.Equal(t, session, session, "Session is not correct")
|
||||
assert.Nil(t, asClient.initialMessage, "Initial message should be nil")
|
||||
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestCreateSession_RetrySessionConflict_RunOutOfRetry(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.WithValue(context.Background(), testIgnoreSleep, true)
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(nil, &actions.HttpClientSideError{
|
||||
Code: 409,
|
||||
})
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||
|
||||
assert.Error(t, err, "Error should be returned")
|
||||
assert.Nil(t, asClient, "AutoScaler should be nil")
|
||||
assert.True(t, mockActionsClient.AssertNumberOfCalls(t, "CreateMessageSession", sessionCreationMaxRetryCount), "CreateMessageSession should be called 10 times")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestCreateSession_NotRetryOnGeneralException(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.WithValue(context.Background(), testIgnoreSleep, true)
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(nil, &actions.HttpClientSideError{
|
||||
Code: 403,
|
||||
})
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||
|
||||
assert.Error(t, err, "Error should be returned")
|
||||
assert.Nil(t, asClient, "AutoScaler should be nil")
|
||||
assert.True(t, mockActionsClient.AssertNumberOfCalls(t, "CreateMessageSession", 1), "CreateMessageSession should be called 1 time and not retry on generic error")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestDeleteSession(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockSessionClient.On("Close").Return(nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.Close()
|
||||
assert.NoError(t, err, "Error deleting session")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestDeleteSession_Failed(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockSessionClient.On("Close").Return(fmt.Errorf("error"))
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.Close()
|
||||
assert.Error(t, err, "Error should be returned")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestGetRunnerScaleSetMessage(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "test",
|
||||
Body: "test",
|
||||
}, nil)
|
||||
mockSessionClient.On("DeleteMessage", ctx, int64(1)).Return(nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.NoError(t, err, "Error getting message")
|
||||
assert.Equal(t, int64(1), asClient.lastMessageId, "Last message id should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestGetRunnerScaleSetMessage_HandleFailed(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "test",
|
||||
Body: "test",
|
||||
}, nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||
return fmt.Errorf("error")
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "handle message failed. error", "Error getting message")
|
||||
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestGetRunnerScaleSetMessage_HandleInitialMessage(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAvailableJobs: 1,
|
||||
TotalAssignedJobs: 2,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{
|
||||
Count: 1,
|
||||
Jobs: []actions.AcquirableJob{
|
||||
{
|
||||
RunnerRequestId: 1,
|
||||
OwnerName: "owner",
|
||||
RepositoryName: "repo",
|
||||
AcquireJobUrl: "https://github.com",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
require.NotNil(t, asClient.initialMessage, "Initial message should be set")
|
||||
|
||||
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.NoError(t, err, "Error getting message")
|
||||
assert.Nil(t, asClient.initialMessage, "Initial message should be nil")
|
||||
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestGetRunnerScaleSetMessage_HandleInitialMessageFailed(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAvailableJobs: 1,
|
||||
TotalAssignedJobs: 2,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{
|
||||
Count: 1,
|
||||
Jobs: []actions.AcquirableJob{
|
||||
{
|
||||
RunnerRequestId: 1,
|
||||
OwnerName: "owner",
|
||||
RepositoryName: "repo",
|
||||
AcquireJobUrl: "https://github.com",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
require.NotNil(t, asClient.initialMessage, "Initial message should be set")
|
||||
|
||||
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||
return fmt.Errorf("error")
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "fail to process initial message. error", "Error getting message")
|
||||
assert.NotNil(t, asClient.initialMessage, "Initial message should be nil")
|
||||
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestGetRunnerScaleSetMessage_RetryUntilGetMessage(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(nil, nil).Times(3)
|
||||
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "test",
|
||||
Body: "test",
|
||||
}, nil).Once()
|
||||
mockSessionClient.On("DeleteMessage", ctx, int64(1)).Return(nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.NoError(t, err, "Error getting message")
|
||||
assert.Equal(t, int64(1), asClient.lastMessageId, "Last message id should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestGetRunnerScaleSetMessage_ErrorOnGetMessage(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(nil, fmt.Errorf("error"))
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||
return fmt.Errorf("Should not be called")
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "get message failed from refreshing client. error", "Error should be returned")
|
||||
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestDeleteRunnerScaleSetMessage_Error(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "test",
|
||||
Body: "test",
|
||||
}, nil)
|
||||
mockSessionClient.On("DeleteMessage", ctx, int64(1)).Return(fmt.Errorf("error"))
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "delete message failed from refreshing client. error", "Error getting message")
|
||||
assert.Equal(t, int64(1), asClient.lastMessageId, "Last message id should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestAcquireJobsForRunnerScaleSet(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockSessionClient.On("AcquireJobs", ctx, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return([]int64{1, 2, 3}, nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.AcquireJobsForRunnerScaleSet(ctx, []int64{1, 2, 3})
|
||||
assert.NoError(t, err, "Error acquiring jobs")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestAcquireJobsForRunnerScaleSet_SkipEmptyList(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.AcquireJobsForRunnerScaleSet(ctx, []int64{})
|
||||
assert.NoError(t, err, "Error acquiring jobs")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestAcquireJobsForRunnerScaleSet_Failed(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
mockSessionClient := &actions.MockSessionService{}
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||
}
|
||||
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||
mockSessionClient.On("AcquireJobs", ctx, mock.Anything).Return(nil, fmt.Errorf("error"))
|
||||
|
||||
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||
asc.client = mockSessionClient
|
||||
})
|
||||
require.NoError(t, err, "Error creating autoscaler client")
|
||||
|
||||
err = asClient.AcquireJobsForRunnerScaleSet(ctx, []int64{1, 2, 3})
|
||||
assert.ErrorContains(t, err, "acquire jobs failed from refreshing client. error", "Expect error acquiring jobs")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
185
cmd/githubrunnerscalesetlistener/autoScalerService.go
Normal file
185
cmd/githubrunnerscalesetlistener/autoScalerService.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/actions/actions-runner-controller/github/actions"
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
type ScaleSettings struct {
|
||||
Namespace string
|
||||
ResourceName string
|
||||
MinRunners int
|
||||
MaxRunners int
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ctx context.Context
|
||||
logger logr.Logger
|
||||
rsClient RunnerScaleSetClient
|
||||
kubeManager KubernetesManager
|
||||
settings *ScaleSettings
|
||||
currentRunnerCount int
|
||||
}
|
||||
|
||||
func NewService(
|
||||
ctx context.Context,
|
||||
rsClient RunnerScaleSetClient,
|
||||
manager KubernetesManager,
|
||||
settings *ScaleSettings,
|
||||
options ...func(*Service),
|
||||
) *Service {
|
||||
s := &Service{
|
||||
ctx: ctx,
|
||||
rsClient: rsClient,
|
||||
kubeManager: manager,
|
||||
settings: settings,
|
||||
currentRunnerCount: 0,
|
||||
logger: logr.FromContextOrDiscard(ctx),
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(s)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Service) Start() error {
|
||||
if s.settings.MinRunners > 0 {
|
||||
s.logger.Info("scale to match minimal runners.")
|
||||
err := s.scaleForAssignedJobCount(0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not scale to match minimal runners. %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
s.logger.Info("waiting for message...")
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
s.logger.Info("service is stopped.")
|
||||
return nil
|
||||
default:
|
||||
err := s.rsClient.GetRunnerScaleSetMessage(s.ctx, s.processMessage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get and process message. %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) processMessage(message *actions.RunnerScaleSetMessage) error {
|
||||
s.logger.Info("process message.", "messageId", message.MessageId, "messageType", message.MessageType)
|
||||
if message.Statistics == nil {
|
||||
return fmt.Errorf("can't process message with empty statistics")
|
||||
}
|
||||
|
||||
s.logger.Info("current runner scale set statistics.",
|
||||
"available jobs", message.Statistics.TotalAvailableJobs,
|
||||
"acquired jobs", message.Statistics.TotalAcquiredJobs,
|
||||
"assigned jobs", message.Statistics.TotalAssignedJobs,
|
||||
"running jobs", message.Statistics.TotalRunningJobs,
|
||||
"registered runners", message.Statistics.TotalRegisteredRunners,
|
||||
"busy runners", message.Statistics.TotalBusyRunners,
|
||||
"idle runners", message.Statistics.TotalIdleRunners)
|
||||
|
||||
if message.MessageType != "RunnerScaleSetJobMessages" {
|
||||
s.logger.Info("skip message with unknown message type.", "messageType", message.MessageType)
|
||||
return nil
|
||||
}
|
||||
|
||||
var batchedMessages []json.RawMessage
|
||||
if err := json.NewDecoder(strings.NewReader(message.Body)).Decode(&batchedMessages); err != nil {
|
||||
return fmt.Errorf("could not decode job messages. %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("process batched runner scale set job messages.", "messageId", message.MessageId, "batchSize", len(batchedMessages))
|
||||
|
||||
var availableJobs []int64
|
||||
for _, message := range batchedMessages {
|
||||
var messageType actions.JobMessageType
|
||||
if err := json.Unmarshal(message, &messageType); err != nil {
|
||||
return fmt.Errorf("could not decode job message type. %w", err)
|
||||
}
|
||||
|
||||
switch messageType.MessageType {
|
||||
case "JobAvailable":
|
||||
var jobAvailable actions.JobAvailable
|
||||
if err := json.Unmarshal(message, &jobAvailable); err != nil {
|
||||
return fmt.Errorf("could not decode job available message. %w", err)
|
||||
}
|
||||
s.logger.Info("job available message received.", "RequestId", jobAvailable.RunnerRequestId)
|
||||
availableJobs = append(availableJobs, jobAvailable.RunnerRequestId)
|
||||
case "JobAssigned":
|
||||
var jobAssigned actions.JobAssigned
|
||||
if err := json.Unmarshal(message, &jobAssigned); err != nil {
|
||||
return fmt.Errorf("could not decode job assigned message. %w", err)
|
||||
}
|
||||
s.logger.Info("job assigned message received.", "RequestId", jobAssigned.RunnerRequestId)
|
||||
case "JobStarted":
|
||||
var jobStarted actions.JobStarted
|
||||
if err := json.Unmarshal(message, &jobStarted); err != nil {
|
||||
return fmt.Errorf("could not decode job started message. %w", err)
|
||||
}
|
||||
s.logger.Info("job started message received.", "RequestId", jobStarted.RunnerRequestId, "RunnerId", jobStarted.RunnerId)
|
||||
s.updateJobInfoForRunner(jobStarted)
|
||||
case "JobCompleted":
|
||||
var jobCompleted actions.JobCompleted
|
||||
if err := json.Unmarshal(message, &jobCompleted); err != nil {
|
||||
return fmt.Errorf("could not decode job completed message. %w", err)
|
||||
}
|
||||
s.logger.Info("job completed message received.", "RequestId", jobCompleted.RunnerRequestId, "Result", jobCompleted.Result, "RunnerId", jobCompleted.RunnerId, "RunnerName", jobCompleted.RunnerName)
|
||||
default:
|
||||
s.logger.Info("unknown job message type.", "messageType", messageType.MessageType)
|
||||
}
|
||||
}
|
||||
|
||||
err := s.rsClient.AcquireJobsForRunnerScaleSet(s.ctx, availableJobs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not acquire jobs. %w", err)
|
||||
}
|
||||
|
||||
return s.scaleForAssignedJobCount(message.Statistics.TotalAssignedJobs)
|
||||
}
|
||||
|
||||
func (s *Service) scaleForAssignedJobCount(count int) error {
|
||||
targetRunnerCount := int(math.Max(math.Min(float64(s.settings.MaxRunners), float64(count)), float64(s.settings.MinRunners)))
|
||||
if targetRunnerCount != s.currentRunnerCount {
|
||||
s.logger.Info("try scale runner request up/down base on assigned job count",
|
||||
"assigned job", count,
|
||||
"decision", targetRunnerCount,
|
||||
"min", s.settings.MinRunners,
|
||||
"max", s.settings.MaxRunners,
|
||||
"currentRunnerCount", s.currentRunnerCount)
|
||||
err := s.kubeManager.ScaleEphemeralRunnerSet(s.ctx, s.settings.Namespace, s.settings.ResourceName, targetRunnerCount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not scale ephemeral runner set (%s/%s). %w", s.settings.Namespace, s.settings.ResourceName, err)
|
||||
}
|
||||
|
||||
s.currentRunnerCount = targetRunnerCount
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateJobInfoForRunner updates the ephemeral runner with the job info and this is best effort since the info is only for better telemetry
|
||||
func (s *Service) updateJobInfoForRunner(jobInfo actions.JobStarted) {
|
||||
s.logger.Info("update job info for runner",
|
||||
"runnerName", jobInfo.RunnerName,
|
||||
"ownerName", jobInfo.OwnerName,
|
||||
"repoName", jobInfo.RepositoryName,
|
||||
"workflowRef", jobInfo.JobWorkflowRef,
|
||||
"workflowRunId", jobInfo.WorkflowRunId,
|
||||
"jobDisplayName", jobInfo.JobDisplayName,
|
||||
"requestId", jobInfo.RunnerRequestId)
|
||||
err := s.kubeManager.UpdateEphemeralRunnerWithJobInfo(s.ctx, s.settings.Namespace, jobInfo.RunnerName, jobInfo.OwnerName, jobInfo.RepositoryName, jobInfo.JobWorkflowRef, jobInfo.JobDisplayName, jobInfo.WorkflowRunId, jobInfo.RunnerRequestId)
|
||||
if err != nil {
|
||||
s.logger.Error(err, "could not update ephemeral runner with job info", "runnerName", jobInfo.RunnerName, "requestId", jobInfo.RunnerRequestId)
|
||||
}
|
||||
}
|
||||
631
cmd/githubrunnerscalesetlistener/autoScalerService_test.go
Normal file
631
cmd/githubrunnerscalesetlistener/autoScalerService_test.go
Normal file
@@ -0,0 +1,631 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/actions/actions-runner-controller/github/actions"
|
||||
"github.com/actions/actions-runner-controller/logging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, logger, service.logger)
|
||||
}
|
||||
|
||||
func TestStart(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||
|
||||
err := service.Start()
|
||||
|
||||
assert.NoError(t, err, "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestStart_ScaleToMinRunners(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 5,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||
|
||||
err := service.Start()
|
||||
|
||||
assert.NoError(t, err, "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestStart_ScaleToMinRunnersFailed(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 5,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Return(fmt.Errorf("error")).Once()
|
||||
|
||||
err := service.Start()
|
||||
|
||||
assert.ErrorContains(t, err, "could not scale to match minimal runners", "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestStart_GetMultipleMessages(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Return(nil).Times(5)
|
||||
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||
|
||||
err := service.Start()
|
||||
|
||||
assert.NoError(t, err, "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestStart_ErrorOnMessage(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Return(nil).Times(2)
|
||||
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Return(fmt.Errorf("error")).Once()
|
||||
|
||||
err := service.Start()
|
||||
|
||||
assert.ErrorContains(t, err, "could not get and process message. error", "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestProcessMessage_NoStatistic(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
|
||||
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "test",
|
||||
Body: "test",
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "can't process message with empty statistics", "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestProcessMessage_IgnoreUnknownMessageType(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
|
||||
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "unknown",
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAvailableJobs: 1,
|
||||
},
|
||||
Body: "[]",
|
||||
})
|
||||
|
||||
assert.NoError(t, err, "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestProcessMessage_InvalidBatchMessageJson(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
|
||||
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "RunnerScaleSetJobMessages",
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAvailableJobs: 1,
|
||||
},
|
||||
Body: "invalid json",
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "could not decode job messages", "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestProcessMessage_InvalidJobMessageJson(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
|
||||
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "RunnerScaleSetJobMessages",
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAvailableJobs: 1,
|
||||
},
|
||||
Body: "[\"something\", \"test\"]",
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "could not decode job message type", "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestProcessMessage_MultipleMessages(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 1,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 3 && ids[1] == 4 })).Return(nil).Once()
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 2).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||
|
||||
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "RunnerScaleSetJobMessages",
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAssignedJobs: 2,
|
||||
TotalAvailableJobs: 2,
|
||||
},
|
||||
Body: "[{\"messageType\":\"JobAvailable\", \"runnerRequestId\": 3},{\"messageType\":\"JobAvailable\", \"runnerRequestId\": 4},{\"messageType\":\"JobAssigned\", \"runnerRequestId\": 2}, {\"messageType\":\"JobCompleted\", \"runnerRequestId\": 1, \"result\":\"succeed\"},{\"messageType\":\"unknown\"}]",
|
||||
})
|
||||
|
||||
assert.NoError(t, err, "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestProcessMessage_AcquireJobsFailed(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 })).Return(fmt.Errorf("error")).Once()
|
||||
|
||||
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "RunnerScaleSetJobMessages",
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAssignedJobs: 1,
|
||||
TotalAvailableJobs: 1,
|
||||
},
|
||||
Body: "[{\"messageType\":\"JobAvailable\", \"runnerRequestId\": 1}]",
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "could not acquire jobs. error", "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestScaleForAssignedJobCount_DeDupScale(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 0,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 2).Return(nil).Once()
|
||||
|
||||
err := service.scaleForAssignedJobCount(2)
|
||||
require.NoError(t, err, "Unexpected error")
|
||||
err = service.scaleForAssignedJobCount(2)
|
||||
require.NoError(t, err, "Unexpected error")
|
||||
err = service.scaleForAssignedJobCount(2)
|
||||
require.NoError(t, err, "Unexpected error")
|
||||
err = service.scaleForAssignedJobCount(2)
|
||||
|
||||
assert.NoError(t, err, "Unexpected error")
|
||||
assert.Equal(t, 2, service.currentRunnerCount, "Unexpected runner count")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestScaleForAssignedJobCount_ScaleWithinMinMax(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 1,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 1).Return(nil).Once()
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 3).Return(nil).Once()
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Return(nil).Once()
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 1).Return(nil).Once()
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Return(nil).Once()
|
||||
|
||||
err := service.scaleForAssignedJobCount(0)
|
||||
require.NoError(t, err, "Unexpected error")
|
||||
err = service.scaleForAssignedJobCount(3)
|
||||
require.NoError(t, err, "Unexpected error")
|
||||
err = service.scaleForAssignedJobCount(5)
|
||||
require.NoError(t, err, "Unexpected error")
|
||||
err = service.scaleForAssignedJobCount(1)
|
||||
require.NoError(t, err, "Unexpected error")
|
||||
err = service.scaleForAssignedJobCount(10)
|
||||
|
||||
assert.NoError(t, err, "Unexpected error")
|
||||
assert.Equal(t, 5, service.currentRunnerCount, "Unexpected runner count")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestScaleForAssignedJobCount_ScaleFailed(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 1,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 2).Return(fmt.Errorf("error"))
|
||||
|
||||
err := service.scaleForAssignedJobCount(2)
|
||||
|
||||
assert.ErrorContains(t, err, "could not scale ephemeral runner set (namespace/resource). error", "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestProcessMessage_JobStartedMessage(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 1,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
service.currentRunnerCount = 1
|
||||
|
||||
mockKubeManager.On("UpdateEphemeralRunnerWithJobInfo", ctx, service.settings.Namespace, "runner1", "owner1", "repo1", ".github/workflows/ci.yaml", "job1", int64(100), int64(3)).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||
mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return len(ids) == 0 })).Return(nil).Once()
|
||||
|
||||
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "RunnerScaleSetJobMessages",
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAssignedJobs: 1,
|
||||
TotalAvailableJobs: 0,
|
||||
},
|
||||
Body: "[{\"messageType\":\"JobStarted\", \"runnerRequestId\": 3, \"runnerId\": 1, \"runnerName\": \"runner1\", \"ownerName\": \"owner1\", \"repositoryName\": \"repo1\", \"jobWorkflowRef\": \".github/workflows/ci.yaml\", \"jobDisplayName\": \"job1\", \"workflowRunId\": 100 }]",
|
||||
})
|
||||
|
||||
assert.NoError(t, err, "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestProcessMessage_JobStartedMessageIgnoreRunnerUpdateError(t *testing.T) {
|
||||
mockRsClient := &MockRunnerScaleSetClient{}
|
||||
mockKubeManager := &MockKubernetesManager{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
service := NewService(
|
||||
ctx,
|
||||
mockRsClient,
|
||||
mockKubeManager,
|
||||
&ScaleSettings{
|
||||
Namespace: "namespace",
|
||||
ResourceName: "resource",
|
||||
MinRunners: 1,
|
||||
MaxRunners: 5,
|
||||
},
|
||||
func(s *Service) {
|
||||
s.logger = logger
|
||||
},
|
||||
)
|
||||
service.currentRunnerCount = 1
|
||||
|
||||
mockKubeManager.On("UpdateEphemeralRunnerWithJobInfo", ctx, service.settings.Namespace, "runner1", "owner1", "repo1", ".github/workflows/ci.yaml", "job1", int64(100), int64(3)).Run(func(args mock.Arguments) { cancel() }).Return(fmt.Errorf("error")).Once()
|
||||
mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return len(ids) == 0 })).Return(nil).Once()
|
||||
|
||||
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "RunnerScaleSetJobMessages",
|
||||
Statistics: &actions.RunnerScaleSetStatistic{
|
||||
TotalAssignedJobs: 0,
|
||||
TotalAvailableJobs: 0,
|
||||
},
|
||||
Body: "[{\"messageType\":\"JobStarted\", \"runnerRequestId\": 3, \"runnerId\": 1, \"runnerName\": \"runner1\", \"ownerName\": \"owner1\", \"repositoryName\": \"repo1\", \"jobWorkflowRef\": \".github/workflows/ci.yaml\", \"jobDisplayName\": \"job1\", \"workflowRunId\": 100 }]",
|
||||
})
|
||||
|
||||
assert.NoError(t, err, "Unexpected error")
|
||||
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
12
cmd/githubrunnerscalesetlistener/kubernetesManager.go
Normal file
12
cmd/githubrunnerscalesetlistener/kubernetesManager.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
//go:generate mockery --inpackage --name=KubernetesManager
|
||||
type KubernetesManager interface {
|
||||
ScaleEphemeralRunnerSet(ctx context.Context, namespace, resourceName string, runnerCount int) error
|
||||
|
||||
UpdateEphemeralRunnerWithJobInfo(ctx context.Context, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName string, jobRequestId, workflowRunId int64) error
|
||||
}
|
||||
151
cmd/githubrunnerscalesetlistener/main.go
Normal file
151
cmd/githubrunnerscalesetlistener/main.go
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
Copyright 2021 The actions-runner-controller authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/actions/actions-runner-controller/github/actions"
|
||||
"github.com/actions/actions-runner-controller/logging"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
type RunnerScaleSetListenerConfig struct {
|
||||
ConfigureUrl string `split_words:"true"`
|
||||
AppID int64 `split_words:"true"`
|
||||
AppInstallationID int64 `split_words:"true"`
|
||||
AppPrivateKey string `split_words:"true"`
|
||||
Token string `split_words:"true"`
|
||||
EphemeralRunnerSetNamespace string `split_words:"true"`
|
||||
EphemeralRunnerSetName string `split_words:"true"`
|
||||
MaxRunners int `split_words:"true"`
|
||||
MinRunners int `split_words:"true"`
|
||||
RunnerScaleSetId int `split_words:"true"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: creating logger: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var rc RunnerScaleSetListenerConfig
|
||||
if err := envconfig.Process("github", &rc); err != nil {
|
||||
logger.Error(err, "Error: processing environment variables for RunnerScaleSetListenerConfig")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Validate all inputs
|
||||
if err := validateConfig(&rc); err != nil {
|
||||
logger.Error(err, "Inputs validation failed")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := run(rc, logger); err != nil {
|
||||
logger.Error(err, "Run error")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(rc RunnerScaleSetListenerConfig, logger logr.Logger) error {
|
||||
// Create root context and hook with sigint and sigterm
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
creds := &actions.ActionsAuth{}
|
||||
if rc.Token != "" {
|
||||
creds.Token = rc.Token
|
||||
} else {
|
||||
creds.AppCreds = &actions.GitHubAppAuth{
|
||||
AppID: rc.AppID,
|
||||
AppInstallationID: rc.AppInstallationID,
|
||||
AppPrivateKey: rc.AppPrivateKey,
|
||||
}
|
||||
}
|
||||
|
||||
actionsServiceClient, err := actions.NewClient(ctx, rc.ConfigureUrl, creds, "actions-runner-controller", logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create an Actions Service client: %w", err)
|
||||
}
|
||||
|
||||
// Create message listener
|
||||
autoScalerClient, err := NewAutoScalerClient(ctx, actionsServiceClient, &logger, rc.RunnerScaleSetId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create a message listener: %w", err)
|
||||
}
|
||||
defer autoScalerClient.Close()
|
||||
|
||||
// Create kube manager and scale controller
|
||||
kubeManager, err := NewKubernetesManager(&logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create kubernetes manager: %w", err)
|
||||
}
|
||||
|
||||
scaleSettings := &ScaleSettings{
|
||||
Namespace: rc.EphemeralRunnerSetNamespace,
|
||||
ResourceName: rc.EphemeralRunnerSetName,
|
||||
MaxRunners: rc.MaxRunners,
|
||||
MinRunners: rc.MinRunners,
|
||||
}
|
||||
|
||||
service := NewService(ctx, autoScalerClient, kubeManager, scaleSettings, func(s *Service) {
|
||||
s.logger = logger.WithName("service")
|
||||
})
|
||||
|
||||
// Start listening for messages
|
||||
if err = service.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start message queue listener: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateConfig(config *RunnerScaleSetListenerConfig) error {
|
||||
if len(config.ConfigureUrl) == 0 {
|
||||
return fmt.Errorf("GitHubConfigUrl is not provided")
|
||||
}
|
||||
|
||||
if len(config.EphemeralRunnerSetNamespace) == 0 || len(config.EphemeralRunnerSetName) == 0 {
|
||||
return fmt.Errorf("EphemeralRunnerSetNamespace '%s' or EphemeralRunnerSetName '%s' is missing", config.EphemeralRunnerSetNamespace, config.EphemeralRunnerSetName)
|
||||
}
|
||||
|
||||
if config.RunnerScaleSetId == 0 {
|
||||
return fmt.Errorf("RunnerScaleSetId '%d' is missing", config.RunnerScaleSetId)
|
||||
}
|
||||
|
||||
if config.MaxRunners < config.MinRunners {
|
||||
return fmt.Errorf("MinRunners '%d' cannot be greater than MaxRunners '%d'", config.MinRunners, config.MaxRunners)
|
||||
}
|
||||
|
||||
hasToken := len(config.Token) > 0
|
||||
hasPrivateKeyConfig := config.AppID > 0 && config.AppPrivateKey != ""
|
||||
|
||||
if !hasToken && !hasPrivateKeyConfig {
|
||||
return fmt.Errorf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||
}
|
||||
|
||||
if hasToken && hasPrivateKeyConfig {
|
||||
return fmt.Errorf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
92
cmd/githubrunnerscalesetlistener/main_test.go
Normal file
92
cmd/githubrunnerscalesetlistener/main_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfigValidationMinMax(t *testing.T) {
|
||||
config := &RunnerScaleSetListenerConfig{
|
||||
ConfigureUrl: "github.com/some_org/some_repo",
|
||||
EphemeralRunnerSetNamespace: "namespace",
|
||||
EphemeralRunnerSetName: "deployment",
|
||||
RunnerScaleSetId: 1,
|
||||
MinRunners: 5,
|
||||
MaxRunners: 2,
|
||||
Token: "token",
|
||||
}
|
||||
err := validateConfig(config)
|
||||
assert.ErrorContains(t, err, "MinRunners '5' cannot be greater than MaxRunners '2", "Expected error about MinRunners > MaxRunners")
|
||||
}
|
||||
|
||||
func TestConfigValidationMissingToken(t *testing.T) {
|
||||
config := &RunnerScaleSetListenerConfig{
|
||||
ConfigureUrl: "github.com/some_org/some_repo",
|
||||
EphemeralRunnerSetNamespace: "namespace",
|
||||
EphemeralRunnerSetName: "deployment",
|
||||
RunnerScaleSetId: 1,
|
||||
}
|
||||
err := validateConfig(config)
|
||||
expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
|
||||
}
|
||||
|
||||
func TestConfigValidationAppKey(t *testing.T) {
|
||||
config := &RunnerScaleSetListenerConfig{
|
||||
AppID: 1,
|
||||
AppInstallationID: 10,
|
||||
ConfigureUrl: "github.com/some_org/some_repo",
|
||||
EphemeralRunnerSetNamespace: "namespace",
|
||||
EphemeralRunnerSetName: "deployment",
|
||||
RunnerScaleSetId: 1,
|
||||
}
|
||||
err := validateConfig(config)
|
||||
expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
|
||||
}
|
||||
|
||||
func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
|
||||
config := &RunnerScaleSetListenerConfig{
|
||||
AppID: 1,
|
||||
AppInstallationID: 10,
|
||||
AppPrivateKey: "asdf",
|
||||
Token: "asdf",
|
||||
ConfigureUrl: "github.com/some_org/some_repo",
|
||||
EphemeralRunnerSetNamespace: "namespace",
|
||||
EphemeralRunnerSetName: "deployment",
|
||||
RunnerScaleSetId: 1,
|
||||
}
|
||||
err := validateConfig(config)
|
||||
expectedError := fmt.Sprintf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
|
||||
}
|
||||
|
||||
func TestConfigValidation(t *testing.T) {
|
||||
config := &RunnerScaleSetListenerConfig{
|
||||
ConfigureUrl: "https://github.com/actions",
|
||||
EphemeralRunnerSetNamespace: "namespace",
|
||||
EphemeralRunnerSetName: "deployment",
|
||||
RunnerScaleSetId: 1,
|
||||
MinRunners: 1,
|
||||
MaxRunners: 5,
|
||||
Token: "asdf",
|
||||
}
|
||||
|
||||
err := validateConfig(config)
|
||||
|
||||
assert.NoError(t, err, "Expected no error")
|
||||
}
|
||||
|
||||
func TestConfigValidationConfigUrl(t *testing.T) {
|
||||
config := &RunnerScaleSetListenerConfig{
|
||||
EphemeralRunnerSetNamespace: "namespace",
|
||||
EphemeralRunnerSetName: "deployment",
|
||||
RunnerScaleSetId: 1,
|
||||
}
|
||||
|
||||
err := validateConfig(config)
|
||||
|
||||
assert.ErrorContains(t, err, "GitHubConfigUrl is not provided", "Expected error about missing ConfigureUrl")
|
||||
}
|
||||
13
cmd/githubrunnerscalesetlistener/messageListener.go
Normal file
13
cmd/githubrunnerscalesetlistener/messageListener.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/actions/actions-runner-controller/github/actions"
|
||||
)
|
||||
|
||||
//go:generate mockery --inpackage --name=RunnerScaleSetClient
|
||||
type RunnerScaleSetClient interface {
|
||||
GetRunnerScaleSetMessage(ctx context.Context, handler func(msg *actions.RunnerScaleSetMessage) error) error
|
||||
AcquireJobsForRunnerScaleSet(ctx context.Context, requestIds []int64) error
|
||||
}
|
||||
57
cmd/githubrunnerscalesetlistener/mock_KubernetesManager.go
Normal file
57
cmd/githubrunnerscalesetlistener/mock_KubernetesManager.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockKubernetesManager is an autogenerated mock type for the KubernetesManager type
|
||||
type MockKubernetesManager struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ScaleEphemeralRunnerSet provides a mock function with given fields: ctx, namespace, resourceName, runnerCount
|
||||
func (_m *MockKubernetesManager) ScaleEphemeralRunnerSet(ctx context.Context, namespace string, resourceName string, runnerCount int) error {
|
||||
ret := _m.Called(ctx, namespace, resourceName, runnerCount)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, int) error); ok {
|
||||
r0 = rf(ctx, namespace, resourceName, runnerCount)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateEphemeralRunnerWithJobInfo provides a mock function with given fields: ctx, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName, jobRequestId, workflowRunId
|
||||
func (_m *MockKubernetesManager) UpdateEphemeralRunnerWithJobInfo(ctx context.Context, namespace string, resourceName string, ownerName string, repositoryName string, jobWorkflowRef string, jobDisplayName string, jobRequestId int64, workflowRunId int64) error {
|
||||
ret := _m.Called(ctx, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName, jobRequestId, workflowRunId)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string, int64, int64) error); ok {
|
||||
r0 = rf(ctx, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName, jobRequestId, workflowRunId)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewMockKubernetesManager interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewMockKubernetesManager creates a new instance of MockKubernetesManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockKubernetesManager(t mockConstructorTestingTNewMockKubernetesManager) *MockKubernetesManager {
|
||||
mock := &MockKubernetesManager{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
actions "github.com/actions/actions-runner-controller/github/actions"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockRunnerScaleSetClient is an autogenerated mock type for the RunnerScaleSetClient type
|
||||
type MockRunnerScaleSetClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// AcquireJobsForRunnerScaleSet provides a mock function with given fields: ctx, requestIds
|
||||
func (_m *MockRunnerScaleSetClient) AcquireJobsForRunnerScaleSet(ctx context.Context, requestIds []int64) error {
|
||||
ret := _m.Called(ctx, requestIds)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []int64) error); ok {
|
||||
r0 = rf(ctx, requestIds)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetRunnerScaleSetMessage provides a mock function with given fields: ctx, handler
|
||||
func (_m *MockRunnerScaleSetClient) GetRunnerScaleSetMessage(ctx context.Context, handler func(*actions.RunnerScaleSetMessage) error) error {
|
||||
ret := _m.Called(ctx, handler)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, func(*actions.RunnerScaleSetMessage) error) error); ok {
|
||||
r0 = rf(ctx, handler)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewMockRunnerScaleSetClient interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewMockRunnerScaleSetClient creates a new instance of MockRunnerScaleSetClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockRunnerScaleSetClient(t mockConstructorTestingTNewMockRunnerScaleSetClient) *MockRunnerScaleSetClient {
|
||||
mock := &MockRunnerScaleSetClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
123
cmd/githubrunnerscalesetlistener/sessionrefreshingclient.go
Normal file
123
cmd/githubrunnerscalesetlistener/sessionrefreshingclient.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/actions/actions-runner-controller/github/actions"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SessionRefreshingClient struct {
|
||||
client actions.ActionsService
|
||||
logger logr.Logger
|
||||
session *actions.RunnerScaleSetSession
|
||||
}
|
||||
|
||||
func newSessionClient(client actions.ActionsService, logger *logr.Logger, session *actions.RunnerScaleSetSession) *SessionRefreshingClient {
|
||||
return &SessionRefreshingClient{
|
||||
client: client,
|
||||
session: session,
|
||||
logger: logger.WithName("refreshing_client"),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SessionRefreshingClient) GetMessage(ctx context.Context, lastMessageId int64) (*actions.RunnerScaleSetMessage, error) {
|
||||
message, err := m.client.GetMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, lastMessageId)
|
||||
if err == nil {
|
||||
return message, nil
|
||||
}
|
||||
|
||||
expiredError := &actions.MessageQueueTokenExpiredError{}
|
||||
if !errors.As(err, &expiredError) {
|
||||
return nil, fmt.Errorf("get message failed. %w", err)
|
||||
}
|
||||
|
||||
m.logger.Info("message queue token is expired during GetNextMessage, refreshing...")
|
||||
session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("refresh message session failed. %w", err)
|
||||
}
|
||||
|
||||
m.session = session
|
||||
message, err = m.client.GetMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, lastMessageId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("delete message failed after refresh message session. %w", err)
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func (m *SessionRefreshingClient) DeleteMessage(ctx context.Context, messageId int64) error {
|
||||
err := m.client.DeleteMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, messageId)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
expiredError := &actions.MessageQueueTokenExpiredError{}
|
||||
if !errors.As(err, &expiredError) {
|
||||
return fmt.Errorf("delete message failed. %w", err)
|
||||
}
|
||||
|
||||
m.logger.Info("message queue token is expired during DeleteMessage, refreshing...")
|
||||
session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("refresh message session failed. %w", err)
|
||||
}
|
||||
|
||||
m.session = session
|
||||
err = m.client.DeleteMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, messageId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete message failed after refresh message session. %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (m *SessionRefreshingClient) AcquireJobs(ctx context.Context, requestIds []int64) ([]int64, error) {
|
||||
ids, err := m.client.AcquireJobs(ctx, m.session.RunnerScaleSet.Id, m.session.MessageQueueAccessToken, requestIds)
|
||||
if err == nil {
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
expiredError := &actions.MessageQueueTokenExpiredError{}
|
||||
if !errors.As(err, &expiredError) {
|
||||
return nil, fmt.Errorf("acquire jobs failed. %w", err)
|
||||
}
|
||||
|
||||
m.logger.Info("message queue token is expired during AcquireJobs, refreshing...")
|
||||
session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("refresh message session failed. %w", err)
|
||||
}
|
||||
|
||||
m.session = session
|
||||
ids, err = m.client.AcquireJobs(ctx, m.session.RunnerScaleSet.Id, m.session.MessageQueueAccessToken, requestIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acquire jobs failed after refresh message session. %w", err)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (m *SessionRefreshingClient) Close() error {
|
||||
if m.session == nil {
|
||||
m.logger.Info("session is already deleted. (no-op)")
|
||||
return nil
|
||||
}
|
||||
|
||||
ctxWithTimeout, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
m.logger.Info("deleting session.")
|
||||
err := m.client.DeleteMessageSession(ctxWithTimeout, m.session.RunnerScaleSet.Id, m.session.SessionId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete message session failed. %w", err)
|
||||
}
|
||||
|
||||
m.session = nil
|
||||
return nil
|
||||
}
|
||||
421
cmd/githubrunnerscalesetlistener/sessionrefreshingclient_test.go
Normal file
421
cmd/githubrunnerscalesetlistener/sessionrefreshingclient_test.go
Normal file
@@ -0,0 +1,421 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/actions/actions-runner-controller/github/actions"
|
||||
"github.com/actions/actions-runner-controller/logging"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetMessage(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(nil, nil).Once()
|
||||
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(&actions.RunnerScaleSetMessage{MessageId: 1}, nil).Once()
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
|
||||
msg, err := client.GetMessage(ctx, 0)
|
||||
require.NoError(t, err, "GetMessage should not return an error")
|
||||
|
||||
assert.Nil(t, msg, "GetMessage should return nil message")
|
||||
|
||||
msg, err = client.GetMessage(ctx, 0)
|
||||
require.NoError(t, err, "GetMessage should not return an error")
|
||||
|
||||
assert.Equal(t, int64(1), msg.MessageId, "GetMessage should return a message with id 1")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||
}
|
||||
|
||||
func TestDeleteMessage(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(nil).Once()
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
|
||||
err := client.DeleteMessage(ctx, int64(1))
|
||||
assert.NoError(t, err, "DeleteMessage should not return an error")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||
}
|
||||
|
||||
func TestAcquireJobs(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, "token", mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return([]int64{1}, nil)
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
|
||||
ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3})
|
||||
assert.NoError(t, err, "AcquireJobs should not return an error")
|
||||
assert.Equal(t, []int64{1}, ids, "AcquireJobs should return a slice with one id")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
mockActionsClient.On("DeleteMessageSession", mock.Anything, 1, &sessionId).Return(nil).Once()
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
|
||||
err := client.Close()
|
||||
assert.NoError(t, err, "DeleteMessageSession should not return an error")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||
}
|
||||
|
||||
func TestGetMessage_Error(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(nil, fmt.Errorf("error")).Once()
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
|
||||
msg, err := client.GetMessage(ctx, 0)
|
||||
assert.ErrorContains(t, err, "get message failed. error", "GetMessage should return an error")
|
||||
assert.Nil(t, msg, "GetMessage should return nil message")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||
}
|
||||
|
||||
func TestDeleteMessage_SessionError(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(fmt.Errorf("error")).Once()
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
|
||||
err := client.DeleteMessage(ctx, int64(1))
|
||||
assert.ErrorContains(t, err, "delete message failed. error", "DeleteMessage should return an error")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||
}
|
||||
|
||||
func TestAcquireJobs_Error(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, "token", mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return(nil, fmt.Errorf("error")).Once()
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
|
||||
ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3})
|
||||
assert.ErrorContains(t, err, "acquire jobs failed. error", "AcquireJobs should return an error")
|
||||
assert.Nil(t, ids, "AcquireJobs should return nil ids")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||
}
|
||||
|
||||
func TestGetMessage_RefreshToken(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once()
|
||||
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, "token2", int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||
MessageId: 1,
|
||||
MessageType: "test",
|
||||
Body: "test",
|
||||
}, nil).Once()
|
||||
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(&actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token2",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
msg, err := client.GetMessage(ctx, 0)
|
||||
assert.NoError(t, err, "Error getting message")
|
||||
assert.Equal(t, int64(1), msg.MessageId, "message id should be updated")
|
||||
assert.Equal(t, "token2", client.session.MessageQueueAccessToken, "Message queue access token should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestDeleteMessage_RefreshSessionToken(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(&actions.MessageQueueTokenExpiredError{}).Once()
|
||||
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, "token2", int64(1)).Return(nil).Once()
|
||||
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(&actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token2",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
err := client.DeleteMessage(ctx, 1)
|
||||
assert.NoError(t, err, "Error delete message")
|
||||
assert.Equal(t, "token2", client.session.MessageQueueAccessToken, "Message queue access token should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestAcquireJobs_RefreshToken(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, session.MessageQueueAccessToken, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once()
|
||||
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, "token2", mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return([]int64{1, 2, 3}, nil)
|
||||
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(&actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token2",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3})
|
||||
assert.NoError(t, err, "Error acquiring jobs")
|
||||
assert.Equal(t, []int64{1, 2, 3}, ids, "Job ids should be returned")
|
||||
assert.Equal(t, "token2", client.session.MessageQueueAccessToken, "Message queue access token should be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestGetMessage_RefreshToken_Failed(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once()
|
||||
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(nil, fmt.Errorf("error"))
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
msg, err := client.GetMessage(ctx, 0)
|
||||
assert.ErrorContains(t, err, "refresh message session failed. error", "Error should be returned")
|
||||
assert.Nil(t, msg, "Message should be nil")
|
||||
assert.Equal(t, "token", client.session.MessageQueueAccessToken, "Message queue access token should not be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestDeleteMessage_RefreshToken_Failed(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(&actions.MessageQueueTokenExpiredError{}).Once()
|
||||
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(nil, fmt.Errorf("error"))
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
err := client.DeleteMessage(ctx, 1)
|
||||
|
||||
assert.ErrorContains(t, err, "refresh message session failed. error", "Error getting message")
|
||||
assert.Equal(t, "token", client.session.MessageQueueAccessToken, "Message queue access token should not be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestAcquireJobs_RefreshToken_Failed(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
ctx := context.Background()
|
||||
sessionId := uuid.New()
|
||||
session := &actions.RunnerScaleSetSession{
|
||||
SessionId: &sessionId,
|
||||
OwnerName: "owner",
|
||||
MessageQueueUrl: "https://github.com",
|
||||
MessageQueueAccessToken: "token",
|
||||
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, session.MessageQueueAccessToken, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once()
|
||||
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(nil, fmt.Errorf("error"))
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, session)
|
||||
ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3})
|
||||
assert.ErrorContains(t, err, "refresh message session failed. error", "Expect error refreshing message session")
|
||||
assert.Nil(t, ids, "Job ids should be nil")
|
||||
assert.Equal(t, "token", client.session.MessageQueueAccessToken, "Message queue access token should not be updated")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
|
||||
func TestClose_Skip(t *testing.T) {
|
||||
mockActionsClient := &actions.MockActionsService{}
|
||||
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||
logger = logger.WithName(t.Name())
|
||||
require.NoError(t, log_err, "Error creating logger")
|
||||
|
||||
client := newSessionClient(mockActionsClient, &logger, nil)
|
||||
err := client.Close()
|
||||
require.NoError(t, err, "Error closing session client")
|
||||
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||
}
|
||||
Reference in New Issue
Block a user