Allow update runner group for AutoScalingRunnerSet (#2216)

This commit is contained in:
Tingluo Huang 2023-01-27 09:27:52 -05:00 committed by GitHub
parent 219ba5b477
commit 803818162c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 196 additions and 7 deletions

View File

@ -21,6 +21,7 @@ import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/github/actions"
@ -47,6 +48,7 @@ const (
LabelKeyAutoScaleRunnerSetName = "auto-scale-runner-set-name"
autoscalingRunnerSetFinalizerName = "autoscalingrunnerset.actions.github.com/finalizer"
runnerScaleSetIdKey = "runner-scale-set-id"
runnerScaleSetRunnerGroupNameKey = "runner-scale-set-runner-group-name"
// scaleSetListenerLabel is the key of pod.meta.labels to label
// that the pod is a listener application
@ -150,6 +152,13 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
return r.createRunnerScaleSet(ctx, autoscalingRunnerSet, log)
}
// Make sure the runner group of the scale set is up to date
currentRunnerGroupName, ok := autoscalingRunnerSet.Annotations[runnerScaleSetRunnerGroupNameKey]
if !ok || !strings.EqualFold(currentRunnerGroupName, autoscalingRunnerSet.Spec.RunnerGroup) {
log.Info("AutoScalingRunnerSet runner group changed. Updating the runner scale set.")
return r.updateRunnerScaleSetRunnerGroup(ctx, autoscalingRunnerSet, log)
}
secret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingRunnerSet.Namespace, Name: autoscalingRunnerSet.Spec.GitHubConfigSecret}, secret); err != nil {
log.Error(err, "Failed to find GitHub config secret.",
@ -299,8 +308,8 @@ func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Contex
return ctrl.Result{}, err
}
runnerGroupId := 1
if runnerScaleSet == nil {
runnerGroupId := 1
if len(autoscalingRunnerSet.Spec.RunnerGroup) > 0 {
runnerGroup, err := actionsClient.GetRunnerGroupByName(ctx, autoscalingRunnerSet.Spec.RunnerGroup)
if err != nil {
@ -338,10 +347,12 @@ func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Contex
autoscalingRunnerSet.Annotations = map[string]string{}
}
autoscalingRunnerSet.Annotations[runnerScaleSetIdKey] = strconv.Itoa(runnerScaleSet.Id)
logger.Info("Adding runner scale set ID as an annotation")
if err := r.Update(ctx, autoscalingRunnerSet); err != nil {
logger.Error(err, "Failed to add runner scale set ID")
logger.Info("Adding runner scale set ID and runner group name as an annotation")
if err = patch(ctx, r.Client, autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
obj.Annotations[runnerScaleSetIdKey] = strconv.Itoa(runnerScaleSet.Id)
obj.Annotations[runnerScaleSetRunnerGroupNameKey] = runnerScaleSet.RunnerGroupName
}); err != nil {
logger.Error(err, "Failed to add runner scale set ID and runner group name as an annotation")
return ctrl.Result{}, err
}
@ -349,6 +360,43 @@ func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Contex
return ctrl.Result{}, nil
}
func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetRunnerGroup(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, logger logr.Logger) (ctrl.Result, error) {
runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdKey])
if err != nil {
logger.Error(err, "Failed to parse runner scale set ID")
return ctrl.Result{}, err
}
actionsClient, err := r.actionsClientFor(ctx, autoscalingRunnerSet)
if err != nil {
logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set")
return ctrl.Result{}, err
}
runnerGroup, err := actionsClient.GetRunnerGroupByName(ctx, autoscalingRunnerSet.Spec.RunnerGroup)
if err != nil {
logger.Error(err, "Failed to get runner group by name", "runnerGroup", autoscalingRunnerSet.Spec.RunnerGroup)
return ctrl.Result{}, err
}
updatedRunnerScaleSet, err := actionsClient.UpdateRunnerScaleSet(ctx, runnerScaleSetId, &actions.RunnerScaleSet{Name: autoscalingRunnerSet.Name, RunnerGroupId: int(runnerGroup.ID)})
if err != nil {
logger.Error(err, "Failed to update runner scale set", "runnerScaleSetId", runnerScaleSetId)
return ctrl.Result{}, err
}
logger.Info("Updating runner scale set runner group name as an annotation")
if err := patch(ctx, r.Client, autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
obj.Annotations[runnerScaleSetRunnerGroupNameKey] = updatedRunnerScaleSet.RunnerGroupName
}); err != nil {
logger.Error(err, "Failed to update runner group name annotation")
return ctrl.Result{}, err
}
logger.Info("Updated runner scale set with match runner group", "runnerGroup", updatedRunnerScaleSet.RunnerGroupName)
return ctrl.Result{}, nil
}
func (r *AutoscalingRunnerSetReconciler) createEphemeralRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, log logr.Logger) (ctrl.Result, error) {
desiredRunnerSet, err := r.resourceBuilder.newEphemeralRunnerSet(autoscalingRunnerSet)
if err != nil {

View File

@ -83,6 +83,7 @@ var _ = Describe("Test AutoScalingRunnerSet controller", func() {
GitHubConfigSecret: configSecret.Name,
MaxRunners: &max,
MinRunners: &min,
RunnerGroup: "testgroup",
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
@ -144,10 +145,14 @@ var _ = Describe("Test AutoScalingRunnerSet controller", func() {
return "", nil
}
return created.Annotations[runnerScaleSetIdKey], nil
if _, ok := created.Annotations[runnerScaleSetRunnerGroupNameKey]; !ok {
return "", nil
}
return fmt.Sprintf("%s_%s", created.Annotations[runnerScaleSetIdKey], created.Annotations[runnerScaleSetRunnerGroupNameKey]), nil
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval).Should(BeEquivalentTo("1"), "RunnerScaleSet should be created/fetched and update the AutoScalingRunnerSet's annotation")
autoscalingRunnerSetTestInterval).Should(BeEquivalentTo("1_testgroup"), "RunnerScaleSet should be created/fetched and update the AutoScalingRunnerSet's annotation")
// Check if ephemeral runner set is created
Eventually(

View File

@ -36,6 +36,7 @@ type ActionsService interface {
GetRunnerScaleSetById(ctx context.Context, runnerScaleSetId int) (*RunnerScaleSet, error)
GetRunnerGroupByName(ctx context.Context, runnerGroup string) (*RunnerGroup, error)
CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error)
UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetId int, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error)
CreateMessageSession(ctx context.Context, runnerScaleSetId int, owner string) (*RunnerScaleSetSession, error)
DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error
@ -350,6 +351,47 @@ func (c *Client) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *Runne
return createdRunnerScaleSet, nil
}
func (c *Client) UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetId int, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error) {
u := fmt.Sprintf("%s/%s/%d?api-version=6.0-preview", *c.ActionsServiceURL, scaleSetEndpoint, runnerScaleSetId)
if err := c.refreshTokenIfNeeded(ctx); err != nil {
return nil, fmt.Errorf("failed to refresh admin token if needed: %w", err)
}
body, err := json.Marshal(runnerScaleSet)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, u, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *c.ActionsServiceAdminToken))
if c.userAgent != "" {
req.Header.Set("User-Agent", c.userAgent)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, ParseActionsErrorFromResponse(resp)
}
var updatedRunnerScaleSet *RunnerScaleSet
err = unmarshalBody(resp, &updatedRunnerScaleSet)
if err != nil {
return nil, err
}
return updatedRunnerScaleSet, nil
}
func (c *Client) DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetId int) error {
u := fmt.Sprintf("%s/%s/%d?api-version=6.0-preview", *c.ActionsServiceURL, scaleSetEndpoint, runnerScaleSetId)

View File

@ -336,3 +336,53 @@ func TestCreateRunnerScaleSet(t *testing.T) {
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
})
}
func TestUpdateRunnerScaleSet(t *testing.T) {
ctx := context.Background()
auth := &actions.ActionsAuth{
Token: "token",
}
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: "ScaleSet", RunnerGroupId: 1, RunnerGroupName: "group", CreatedOn: scaleSetCreationDateTime, RunnerSetting: actions.RunnerSetting{}}
t.Run("Update runner scale set", func(t *testing.T) {
want := &runnerScaleSet
rsl, err := json.Marshal(want)
require.NoError(t, err)
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(rsl)
}))
client, err := actions.NewClient(ctx, server.configURLForOrg("my-org"), auth)
require.NoError(t, err)
got, err := client.UpdateRunnerScaleSet(ctx, 1, &actions.RunnerScaleSet{RunnerGroupId: 1})
require.NoError(t, err)
assert.Equal(t, want, got)
})
t.Run("UpdateRunnerScaleSet calls correct url", func(t *testing.T) {
rsl, err := json.Marshal(&runnerScaleSet)
require.NoError(t, err)
url := url.URL{}
method := ""
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(rsl)
url = *r.URL
method = r.Method
}))
client, err := actions.NewClient(ctx, server.configURLForOrg("my-org"), auth)
require.NoError(t, err)
_, err = client.UpdateRunnerScaleSet(ctx, 1, &runnerScaleSet)
require.NoError(t, err)
u := url.String()
expectedUrl := "/_apis/runtime/runnerscalesets/1?api-version=6.0-preview"
assert.Equal(t, expectedUrl, u)
assert.Equal(t, "PATCH", method)
})
}

View File

@ -36,6 +36,18 @@ var defaultRunnerScaleSet = &actions.RunnerScaleSet{
Statistics: nil,
}
var defaultUpdatedRunnerScaleSet = &actions.RunnerScaleSet{
Id: 1,
Name: "testset",
RunnerGroupId: 2,
RunnerGroupName: "testgroup",
Labels: []actions.Label{{Type: "test", Name: "test"}},
RunnerSetting: actions.RunnerSetting{},
CreatedOn: time.Now(),
RunnerJitConfigUrl: "test.test.test",
Statistics: nil,
}
var defaultRunnerGroup = &actions.RunnerGroup{
ID: 1,
Name: "testgroup",
@ -107,6 +119,10 @@ type FakeClient struct {
*actions.RunnerScaleSet
err error
}
updateRunnerScaleSetResult struct {
*actions.RunnerScaleSet
err error
}
createMessageSessionResult struct {
*actions.RunnerScaleSetSession
err error
@ -164,6 +180,7 @@ func (f *FakeClient) applyDefaults() {
f.getRunnerScaleSetByIdResult.RunnerScaleSet = defaultRunnerScaleSet
f.getRunnerGroupByNameResult.RunnerGroup = defaultRunnerGroup
f.createRunnerScaleSetResult.RunnerScaleSet = defaultRunnerScaleSet
f.updateRunnerScaleSetResult.RunnerScaleSet = defaultUpdatedRunnerScaleSet
f.createMessageSessionResult.RunnerScaleSetSession = defaultRunnerScaleSetSession
f.refreshMessageSessionResult.RunnerScaleSetSession = defaultRunnerScaleSetSession
f.acquireJobsResult.ids = []int64{1}
@ -190,6 +207,10 @@ func (f *FakeClient) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *a
return f.createRunnerScaleSetResult.RunnerScaleSet, f.createRunnerScaleSetResult.err
}
func (f *FakeClient) UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetId int, runnerScaleSet *actions.RunnerScaleSet) (*actions.RunnerScaleSet, error) {
return f.updateRunnerScaleSetResult.RunnerScaleSet, f.updateRunnerScaleSetResult.err
}
func (f *FakeClient) CreateMessageSession(ctx context.Context, runnerScaleSetId int, owner string) (*actions.RunnerScaleSetSession, error) {
return f.createMessageSessionResult.RunnerScaleSetSession, f.createMessageSessionResult.err
}

View File

@ -332,6 +332,29 @@ func (_m *MockActionsService) RemoveRunner(ctx context.Context, runnerId int64)
return r0
}
// UpdateRunnerScaleSet provides a mock function with given fields: ctx, runnerScaleSetId, runnerScaleSet
func (_m *MockActionsService) UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetId int, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error) {
ret := _m.Called(ctx, runnerScaleSetId, runnerScaleSet)
var r0 *RunnerScaleSet
if rf, ok := ret.Get(0).(func(context.Context, int, *RunnerScaleSet) *RunnerScaleSet); ok {
r0 = rf(ctx, runnerScaleSetId, runnerScaleSet)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerScaleSet)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int, *RunnerScaleSet) error); ok {
r1 = rf(ctx, runnerScaleSetId, runnerScaleSet)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewMockActionsService interface {
mock.TestingT
Cleanup(func())